diff --git a/.gitignore b/.gitignore index 2da5402..66b82c9 100644 --- a/.gitignore +++ b/.gitignore @@ -171,4 +171,5 @@ dist .yarn/install-state.gz .pnp.\* -.wrangler/ \ No newline at end of file +.wrangler/ +.*.md \ No newline at end of file diff --git a/package.json b/package.json index 9e7a134..70423e7 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,6 @@ "vite-plugin-static-copy": "^1.0.6", "vite-plugin-top-level-await": "^1.3.1", "vite-plugin-wasm": "^3.2.2", - "wrangler": "^3.72.3" + "wrangler": "^3.88.0" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 740bd3b..832e943 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -98,6 +98,8 @@ function Home() { const shapes = createShapes(elementsInfo) const [isEditorMounted, setIsEditorMounted] = useState(false); + //console.log("THIS WORKS SO FAR") + useEffect(() => { const handleEditorDidMount = () => { setIsEditorMounted(true); diff --git a/src/components/Board copy.tsx b/src/components/Board copy.tsx new file mode 100644 index 0000000..b0c851d --- /dev/null +++ b/src/components/Board copy.tsx @@ -0,0 +1,123 @@ +import { useSync } from '@tldraw/sync' +import { + AssetRecordType, + getHashForString, + TLBookmarkAsset, + Tldraw, + // useLocalStorageState, +} from 'tldraw' +import { useParams } from 'react-router-dom' +import useLocalStorageState from 'use-local-storage-state' +import { ChatBoxTool } from '@/tools/ChatBoxTool' +import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' +import { VideoChatTool } from '@/tools/VideoChatTool' +import { VideoChatShape } from '@/shapes/VideoChatShapeUtil' +import { multiplayerAssetStore } from '../client/multiplayerAssetStore' +import { customSchema } from '../../worker/TldrawDurableObject' +import { EmbedShape } from '@/shapes/EmbedShapeUtil' +import { EmbedTool } from '@/tools/EmbedTool' + +import React, { useEffect, useState } from 'react'; +import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; +import { components, uiOverrides } from '@/ui-overrides' + +const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` + +const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] +const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools + +export function Board() { + const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component + const roomId = slug || 'default-room'; // Declare roomId here + + const store = useSync({ + uri: `${WORKER_URL}/connect/${roomId}`, + assets: multiplayerAssetStore, + shapeUtils: shapeUtils, + schema: customSchema, + }); + + const [isChatBoxVisible, setChatBoxVisible] = useState(false); + const [userName, setUserName] = useState(''); + const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility + + const handleNameChange = (event: React.ChangeEvent) => { + setUserName(event.target.value); + }; + + const [persistedStore, setPersistedStore] = useLocalStorageState('board-store', { defaultValue: store } + ) + + useEffect(() => { + setPersistedStore(store); + }, [store]); + + return ( +
+ { + editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) + editor.setCurrentTool('hand') + }} + /> + {isChatBoxVisible && ( +
+ + +
+ )} + {isVideoChatVisible && ( // Render the button to join video chat + + )} +
+ ) +} + +// How does our server handle bookmark unfurling? +async function unfurlBookmarkUrl({ url }: { url: string }): Promise { + const asset: TLBookmarkAsset = { + id: AssetRecordType.createId(getHashForString(url)), + typeName: 'asset', + type: 'bookmark', + meta: {}, + props: { + src: url, + description: '', + image: '', + favicon: '', + title: '', + }, + } + + try { + const response = await fetch(`${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`) + const data = await response.json() as { description: string, image: string, favicon: string, title: string } + + asset.props.description = data?.description ?? '' + asset.props.image = data?.image ?? '' + asset.props.favicon = data?.favicon ?? '' + asset.props.title = data?.title ?? '' + } catch (e) { + console.error(e) + } + + return asset +} diff --git a/src/shapes/EmbedShapeUtil copy.tsx b/src/shapes/EmbedShapeUtil copy.tsx new file mode 100644 index 0000000..0ddfbef --- /dev/null +++ b/src/shapes/EmbedShapeUtil copy.tsx @@ -0,0 +1,159 @@ +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; +import { useCallback, useState } from "react"; + +export type IEmbedShape = TLBaseShape< + 'Embed', + { + w: number; + h: number; + url: string | null; + } +>; + +export class EmbedShape extends BaseBoxShapeUtil { + static override type = 'Embed'; + + getDefaultProps(): IEmbedShape['props'] { + return { + url: null, + w: 640, + h: 480, + }; + } + + indicator(shape: IEmbedShape) { + return ( + + + + ); + } + + component(shape: IEmbedShape) { + const [inputUrl, setInputUrl] = useState(shape.props.url || ''); + const [error, setError] = useState(''); + + const handleSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + 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')) { + // Handle different types of Google Docs URLs + if (completedUrl.includes('/document/d/')) { + const docId = completedUrl.match(/\/document\/d\/([a-zA-Z0-9-_]+)/)?.[1]; + if (docId) { + completedUrl = `https://docs.google.com/document/d/${docId}/edit`; + } else { + setError('Invalid Google Docs URL'); + return; + } + } else if (completedUrl.includes('/spreadsheets/d/')) { + const docId = completedUrl.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/)?.[1]; + if (docId) { + completedUrl = `https://docs.google.com/spreadsheets/d/${docId}/edit`; + } else { + setError('Invalid Google Sheets URL'); + return; + } + } else if (completedUrl.includes('/presentation/d/')) { + const docId = completedUrl.match(/\/presentation\/d\/([a-zA-Z0-9-_]+)/)?.[1]; + if (docId) { + completedUrl = `https://docs.google.com/presentation/d/${docId}/embed`; + } else { + setError('Invalid Google Slides URL'); + return; + } + } + + // Add parameters for access + completedUrl += '?authuser=0'; // Allow Google authentication + } + + 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 ( +
+
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 ( +
+
+