From 4eff918bd3f2759b6db30dbb250f95b8ef87e238 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:21:42 -0400 Subject: [PATCH] fixed a bunch of stuff --- src/components/Board.tsx | 47 +++++++++++++- src/components/Default.tsx | 2 +- src/shapes/ChatBoxShapeUtil.tsx | 13 +++- src/shapes/EmbedShapeUtil.tsx | 104 +++++++++++++++++++++++++----- src/shapes/VideoChatShapeUtil.tsx | 82 +++++++++++++---------- worker/TldrawDurableObject.ts | 1 - 6 files changed, 194 insertions(+), 55 deletions(-) diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 30009c3..b014085 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -4,6 +4,8 @@ import { getHashForString, TLBookmarkAsset, Tldraw, + TLUiMenuGroup, + TLUiOverrides, } from 'tldraw' import { useParams } from 'react-router-dom' import { ChatBoxTool } from '@/tools/ChatBoxTool' @@ -43,16 +45,59 @@ export function Board() { setUserName(event.target.value); }; + const customUiOverrides: TLUiOverrides = { + ...uiOverrides, + contextMenu: (editor, contextMenuSchema, helpers) => { + const defaultContextMenu = uiOverrides.contextMenu ? uiOverrides.contextMenu(editor, contextMenuSchema, helpers) : contextMenuSchema + + const newContextMenu: TLUiMenuGroup[] = [ + ...defaultContextMenu, + { + id: 'external-link', + type: 'group', + checkbox: false, + disabled: false, + readonlyOk: true, + children: [ + { + id: 'add-external-link', + type: 'item', + readonlyOk: true, + label: 'Add External Link', + icon: 'link', + onSelect: () => { + const selectedShapes = editor.getSelectedShapes() + if (selectedShapes.length === 1) { + const shape = selectedShapes[0] + const externalUrl = `${window.location.origin}/board/${roomId}?shapeId=${shape.id}` + // Here you can implement the logic to copy the link to clipboard or show it to the user + console.log('External link:', externalUrl) + // For example, to copy to clipboard: + navigator.clipboard.writeText(externalUrl).then(() => { + editor.setToast({ id: 'external-link-copied', title: 'External link copied to clipboard' }) + }) + } + }, + }, + ], + }, + ] + + return newContextMenu + }, + } + return (
{ editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) + editor.setCurrentTool('hand') }} /> {isChatBoxVisible && ( diff --git a/src/components/Default.tsx b/src/components/Default.tsx index 181730a..3827ab5 100644 --- a/src/components/Default.tsx +++ b/src/components/Default.tsx @@ -29,7 +29,7 @@ export function Default() {

Get in touch

- I am on Twitter @OrionReedOne, + I am on Twitter @jeffemmett, Mastodon @orion@hci.social and GitHub @orionreed. You can also shoot me an email me@orionreed.com

diff --git a/src/shapes/ChatBoxShapeUtil.tsx b/src/shapes/ChatBoxShapeUtil.tsx index 36ba0a6..8d11f90 100644 --- a/src/shapes/ChatBoxShapeUtil.tsx +++ b/src/shapes/ChatBoxShapeUtil.tsx @@ -94,7 +94,7 @@ export const ChatBox: React.FC = ({ roomId, w, h, userNa }; return ( -
+
{messages.map((msg) => (
@@ -114,8 +114,17 @@ export const ChatBox: React.FC = ({ roomId, w, h, userNa onChange={(e) => setInputMessage(e.target.value)} placeholder="Type a message..." className="message-input" + style={{ touchAction: 'manipulation' }} /> - +
); diff --git a/src/shapes/EmbedShapeUtil.tsx b/src/shapes/EmbedShapeUtil.tsx index 2dcda48..ae404d3 100644 --- a/src/shapes/EmbedShapeUtil.tsx +++ b/src/shapes/EmbedShapeUtil.tsx @@ -25,39 +25,113 @@ export class EmbedShape extends BaseBoxShapeUtil { return ( - ); } component(shape: IEmbedShape) { const [inputUrl, setInputUrl] = useState(shape.props.url || ''); + const [error, setError] = useState(''); const handleSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); - this.editor.updateShape({ id: shape.id, type: 'Embed', props: { ...shape.props, url: inputUrl } }); + let completedUrl = inputUrl.startsWith('http://') || inputUrl.startsWith('https://') ? inputUrl : `https://${inputUrl}`; + + // Handle YouTube links + if (completedUrl.includes('youtube.com') || completedUrl.includes('youtu.be')) { + const videoId = extractYouTubeVideoId(completedUrl); + if (videoId) { + completedUrl = `https://www.youtube.com/embed/${videoId}`; + } else { + setError('Invalid YouTube URL'); + return; + } + } + + // Handle Google Docs links + if (completedUrl.includes('docs.google.com')) { + const docId = completedUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1]; + if (docId) { + completedUrl = `https://docs.google.com/document/d/${docId}/preview`; + } else { + setError('Invalid Google Docs URL'); + return; + } + } + + this.editor.updateShape({ id: shape.id, type: 'Embed', props: { ...shape.props, url: completedUrl } }); + + // Check if the URL is valid + const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//); + if (!isValidUrl) { + setError('Invalid website URL'); + } else { + setError(''); + } }, [inputUrl]); + const extractYouTubeVideoId = (url: string): string | null => { + const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; + const match = url.match(regExp); + return (match && match[2].length === 11) ? match[2] : null; + }; + + const wrapperStyle = { + width: `${shape.props.w}px`, + height: `${shape.props.h}px`, + padding: '15px', + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', + backgroundColor: '#F0F0F0', + borderRadius: '4px', + }; + + const contentStyle = { + pointerEvents: 'all' as const, + width: '100%', + height: '100%', + border: '1px solid #D3D3D3', + backgroundColor: '#FFFFFF', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + }; + if (!shape.props.url) { return ( -
-
- setInputUrl(e.target.value)} - placeholder="Enter URL" - style={{ width: shape.props.w, height: shape.props.h }} - /> - -
+
+
document.querySelector('input')?.focus()}> +
+ setInputUrl(e.target.value)} + placeholder="Enter URL" + style={{ width: '100%', height: '100%', border: 'none', padding: '10px' }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSubmit(e); + } + }} + /> + {error &&
{error}
} +
+
); } return ( -
-