diff --git a/index.html b/index.html index c5c131b..063435a 100644 --- a/index.html +++ b/index.html @@ -26,7 +26,7 @@ - + diff --git a/package.json b/package.json index b4f7def..090d692 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,13 @@ "preview": "vite preview" }, "keywords": [], - "author": "Orion Reed", + "author": "Jeff Emmett", "license": "ISC", "dependencies": { "@dimforge/rapier2d": "^0.11.2", "@tldraw/sync": "^2.4.6", - "@tldraw/sync-core": "latest", - "@tldraw/tlschema": "latest", + "@tldraw/sync-core": "^2.4.6", + "@tldraw/tlschema": "^2.4.6", "@types/markdown-it": "^14.1.1", "@vercel/analytics": "^1.2.2", "cloudflare-workers-unfurl": "^0.0.7", @@ -47,11 +47,11 @@ "eslint": "^8.38.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.3.4", - "typescript": "^5.0.2", + "typescript": "^5.6.3", "vite": "^5.3.3", "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" } -} +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index bb32041..740bd3b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,37 +1,94 @@ import { inject } from '@vercel/analytics'; import "tldraw/tldraw.css"; import "@/css/style.css" -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; import { Default } from "@/components/Default"; import { Canvas } from "@/components/Canvas"; import { Toggle } from "@/components/Toggle"; import { useCanvas } from "@/hooks/useCanvas" -import { createShapes } from "@/utils"; +import { createShapes } from "@/utils/utils"; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { Contact } from "@/components/Contact"; import { Post } from '@/components/Post'; import { Board } from './components/Board'; import { Inbox } from './components/Inbox'; import { Books } from './components/Books'; +import { + BindingUtil, + IndexKey, + TLBaseBinding, + TLBaseShape, + Tldraw, +} from 'tldraw'; +import { components, uiOverrides } from './ui-overrides'; +import { ChatBoxShape } from './shapes/ChatBoxShapeUtil'; +import { VideoChatShape } from './shapes/VideoChatShapeUtil'; +import { ChatBoxTool } from './tools/ChatBoxTool'; +import { VideoChatTool } from './tools/VideoChatTool'; + inject(); +// The container shapes that can contain element shapes +const CONTAINER_PADDING = 24; + +type ContainerShape = TLBaseShape<'element', { height: number; width: number }>; + +// ... existing code for ContainerShapeUtil ... + +// The element shapes that can be placed inside the container shapes +type ElementShape = TLBaseShape<'element', { color: string }>; + +// ... existing code for ElementShapeUtil ... + +// The binding between the element shapes and the container shapes +type LayoutBinding = TLBaseBinding< + 'layout', + { + index: IndexKey; + placeholder: boolean; + } +>; + +const customShapeUtils = [ChatBoxShape, VideoChatShape]; +const customTools = [ChatBoxTool, VideoChatTool]; + +// [2] +export default function InteractiveShapeExample() { + return ( +
+ { + editor.createShape({ type: 'my-interactive-shape', x: 100, y: 100 }); + }} + /> +
+ ); +} + +// ... existing code ... + ReactDOM.createRoot(document.getElementById("root")!).render(); function App() { return ( // - - - } /> - } /> - } /> - } /> - } /> - } /> - - + + + } /> + } /> + } /> + } /> + } /> + } /> + + // ); }; @@ -58,5 +115,6 @@ function Home() {
{}
- {isCanvasEnabled && elementsInfo.length > 0 ? : null}) + {isCanvasEnabled && elementsInfo.length > 0 ? : null} + ) } \ No newline at end of file diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 6d28e36..80da60f 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -2,83 +2,82 @@ import { useSync } from '@tldraw/sync' import { AssetRecordType, getHashForString, - TLAssetStore, TLBookmarkAsset, Tldraw, - uniqueId, } from 'tldraw' -import { useParams } from 'react-router-dom' // Add this import +import { useParams } from 'react-router-dom' import { ChatBoxTool } from '@/tools/ChatBoxTool' -import { IChatBoxShape, ChatBoxShape } from '@/shapes/ChatBoxShape' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' // Adjusted path if necessary +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 './ChatBoxStyles.css' // Add a CSS file for styles + +import React, { 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] -const tools = [ChatBoxTool] +const shapeUtils = [ChatBoxShape, VideoChatShape] +const tools = [ChatBoxTool, VideoChatTool]; // Array of tools export function Board() { - // Extract the slug from the URL - const { slug } = useParams<{ slug: string }>() - - // Use the slug as the roomId, or fallback to 'default-room' if not provided - const roomId = slug || 'default-room' + const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component + const roomId = slug || 'default-room'; // Declare roomId here - // Create a store connected to multiplayer. const store = useSync({ - // Use the dynamic roomId in the URI uri: `${WORKER_URL}/connect/${roomId}`, - // ...and how to handle static assets like images & videos assets: multiplayerAssetStore, - shapeUtils: shapeUtils, - schema: customSchema - }) + 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); + }; return (
{ - // when the editor is ready, we need to register out bookmark unfurling service editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) - editor.createShape({ - type: 'chatBox', - x: 0, - y: 0, - props: { - w: 200, - h: 200, - roomId: roomId, - }, - }) }} /> + {isChatBoxVisible && ( +
+ + +
+ )} + {isVideoChatVisible && ( // Render the button to join video chat + + )}
) } -// Assuming you have a message structure like this -interface ChatMessage { - id: string; - text: string; - isUser: boolean; // New property to identify the sender -} - -// Example rendering function for messages -function renderMessage(message: ChatMessage) { - return ( -
- {message.text} -
- ) -} - // How does our server handle bookmark unfurling? async function unfurlBookmarkUrl({ url }: { url: string }): Promise { const asset: TLBookmarkAsset = { diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index bdf7457..34f2a2d 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -1,6 +1,6 @@ import { Editor, Tldraw, TLShape, TLUiComponents } from "tldraw"; import { SimController } from "@/physics/PhysicsControls"; -import { HTMLShapeUtil } from "@/shapes/HTMLShapeUtil"; +import { HTMLShapeUtil } from "@/utils/HTMLShapeUtil"; const components: TLUiComponents = { HelpMenu: null, diff --git a/src/components/ChatBoxStyles.css b/src/css/ChatBoxStyles.css similarity index 100% rename from src/components/ChatBoxStyles.css rename to src/css/ChatBoxStyles.css diff --git a/src/physics/simulation.ts b/src/physics/simulation.ts index b67a8e5..2ff1ec7 100644 --- a/src/physics/simulation.ts +++ b/src/physics/simulation.ts @@ -66,6 +66,9 @@ export class PhysicsWorld { this.createGroup(shape as TLGroupShape); break; // Add cases for any new shape types here + case "VideoChat": + this.createShape (shape as TLGeoShape); + break; } } } diff --git a/src/shapes/ChatBoxShape.tsx b/src/shapes/ChatBoxShapeUtil.tsx similarity index 75% rename from src/shapes/ChatBoxShape.tsx rename to src/shapes/ChatBoxShapeUtil.tsx index b1b80fa..e94ebcd 100644 --- a/src/shapes/ChatBoxShape.tsx +++ b/src/shapes/ChatBoxShapeUtil.tsx @@ -1,13 +1,14 @@ import { useEffect, useRef, useState } from "react"; -import { BaseBoxShapeUtil, TLBaseBoxShape, TLBaseShape, TldrawBaseProps } from "tldraw"; +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; export type IChatBoxShape = TLBaseShape< - 'chatBox', - { - w: number - h: number + 'chatBox', + { + w: number + h: number roomId: string - } + userName: string + } > export class ChatBoxShape extends BaseBoxShapeUtil { @@ -18,6 +19,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil { roomId: 'default-room', w: 100, h: 100, + userName: '', } } @@ -27,7 +29,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil { component(shape: IChatBoxShape) { return ( - + ) } } @@ -39,11 +41,14 @@ interface Message { timestamp: Date; } -// Add this new component after the ChatBoxShape class -function ChatBox({ roomId, width, height }: { roomId: string, width: number, height: number }) { + + + +// Update the chatBox component to accept userName +export const chatBox: React.FC = ({ roomId, w, h, userName }) => { const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(""); - const [username, setUsername] = useState("jeff"); + const [username, setUsername] = useState(userName); const messagesEndRef = useRef(null); useEffect(() => { @@ -89,12 +94,12 @@ function ChatBox({ roomId, width, height }: { roomId: string, width: number, hei }; return ( -
+
{messages.map((msg) => (
- {msg.username} + {msg.username} {new Date(msg.timestamp).toLocaleTimeString()}
{msg.content}
@@ -110,7 +115,7 @@ function ChatBox({ roomId, width, height }: { roomId: string, width: number, hei placeholder="Type a message..." className="message-input" /> - +
); @@ -118,24 +123,24 @@ function ChatBox({ roomId, width, height }: { roomId: string, width: number, hei async function sendMessageToChat(roomId: string, username: string, content: string): Promise { const apiUrl = 'https://jeffemmett-realtimechatappwithpolling.web.val.run'; // Replace with your actual Val Town URL - + try { - const response = await fetch(`${apiUrl}?action=sendMessage`, { - method: 'POST', - mode: 'no-cors', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - roomId, - username, - content, - }), - }); - - const result = await response.text(); - console.log('Message sent successfully:', result); + const response = await fetch(`${apiUrl}?action=sendMessage`, { + method: 'POST', + mode: 'no-cors', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + roomId, + username, + content, + }), + }); + + const result = await response.text(); + console.log('Message sent successfully:', result); } catch (error) { - console.error('Error sending message:', error); + console.error('Error sending message:', error); } - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/shapes/VideoChatShapeUtil.tsx b/src/shapes/VideoChatShapeUtil.tsx new file mode 100644 index 0000000..afe2eba --- /dev/null +++ b/src/shapes/VideoChatShapeUtil.tsx @@ -0,0 +1,181 @@ +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; +import { useEffect, useState } from "react"; + +export type IVideoChatShape = TLBaseShape< + 'VideoChat', + { + w: number; + h: number; + roomUrl: string | null; + userName: string; + } +>; + +const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key +const ROOM_PREFIX = 'test' + +export class VideoChatShape extends BaseBoxShapeUtil { + static override type = 'VideoChat'; + + getDefaultProps(): IVideoChatShape['props'] { + return { + roomUrl: null, + w: 640, + h: 480, + userName: '' + }; + } + + indicator(shape: IVideoChatShape) { + return ; + } + + async ensureRoomExists(shape: IVideoChatShape) { + + console.log('This is your roomUrl 1:', shape.props.roomUrl); + + if (shape.props.roomUrl !== null) { + return + } + + + const expiryDate = new Date(Date.now() + 1000 * 24 * 60 * 60 * 1000) + + const response = await fetch('https://api.whereby.dev/v1/meetings', { + method: 'POST', + headers: { + // 'Access-Control-Allow-Origin': 'http://localhost:5173/', + 'Authorization': `Bearer ${WHEREBY_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + isLocked: false, + roomNamePrefix: ROOM_PREFIX, + roomMode: 'normal', + endDate: expiryDate.toISOString(), + fields: ['hostRoomUrl'], + }), + }).catch((error) => { + console.error('Failed to create meeting:', error); + throw error; + }); + + console.log('This is your response:', response); + + console.log('This is your roomUrl 2:', shape.props.roomUrl); + + if (!response.ok) { + const errorData = await response.json(); + console.error('Whereby API error:', errorData); + throw new Error(`Whereby API error: ${(errorData as any).message || 'Unknown error'}`); + } + + const data = await response.json(); + const roomUrl = (data as any).roomUrl; + + console.log('This is your roomUrl 3:', roomUrl); + + this.editor.updateShape({ + id: shape.id, + type: 'VideoChat', + props: { + ...shape.props, + roomUrl + } + }) + + + } + + component(shape: IVideoChatShape) { + const [roomUrl, setRoomUrl] = useState(""); // Added roomUrl state + const [isInRoom, setIsInRoom] = useState(false); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + // Automatically show the button on load + useEffect(() => { + joinRoom(); + }, []); + + const joinRoom = async () => { + //console.log("HI IM A CONSOLE TEST") + this.ensureRoomExists(shape); + setError(""); + setIsLoading(true); + try { + // Generate a room name based on a default slug or any logic you prefer + // const roomNamePrefix = 'default-room'; // You can modify this logic as needed + + // const response = await fetch('https://cors-anywhere.herokuapp.com/https://api.whereby.dev/v1/meetings', { + const response = await fetch('https://api.whereby.dev/v1/meetings', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${WHEREBY_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + isLocked: false, + roomNamePrefix: ROOM_PREFIX, + roomMode: 'normal', + endDate: new Date(Date.now() + 1000 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now + fields: ['hostRoomUrl'], + }), + }); + + if (!response.ok) { + const errorData: { message?: string } = await response.json(); // Explicitly type errorData + console.error('Whereby API error:', errorData); + throw new Error(`Whereby API error: ${errorData.message || 'Unknown error'}`); + } + + const data: { roomUrl: string } = await response.json(); // Explicitly type the response + setRoomUrl(data.roomUrl); // Set the room URL + setIsInRoom(true); + } catch (e) { + console.error("Error joining room:", e); + setError("An error occurred. Please try again."); + } + setIsLoading(false); + }; + + const leaveRoom = () => { + setIsInRoom(false); + setRoomUrl(""); // Clear the room URL + }; + + + + return ( +
+

Whereby Video Chat Room

+ {isLoading ? ( +

Joining room...

+ ) : isInRoom ? ( +
+ +
+ +
+
+ ) : ( +
+ + {error &&

{error}

} +
+ )} +

+ View source: Val Town +

+
+ ); + } +} diff --git a/src/snapshot.json b/src/snapshot.json new file mode 100644 index 0000000..1b80b16 --- /dev/null +++ b/src/snapshot.json @@ -0,0 +1,210 @@ +{ + "store": { + "document:document": { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + "page:page": { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + "shape:f4LKGB_8M2qsyWGpHR5Dq": { + "x": 30.9375, + "y": 69.48828125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:f4LKGB_8M2qsyWGpHR5Dq", + "type": "container", + "parentId": "page:page", + "index": "a1", + "props": { + "width": 644, + "height": 148 + }, + "typeName": "shape" + }, + "shape:2oThF4kJ4v31xqKN5lvq2": { + "x": 550.9375, + "y": 93.48828125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:2oThF4kJ4v31xqKN5lvq2", + "type": "element", + "props": { + "color": "#5BCEFA" + }, + "parentId": "page:page", + "index": "a2", + "typeName": "shape" + }, + "shape:K2vk_VTaNh-ANaRNOAvgY": { + "x": 426.9375, + "y": 93.48828125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:K2vk_VTaNh-ANaRNOAvgY", + "type": "element", + "props": { + "color": "#F5A9B8" + }, + "parentId": "page:page", + "index": "a3", + "typeName": "shape" + }, + "shape:6uouhIK7PvyIRNQHACf-d": { + "x": 302.9375, + "y": 93.48828125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:6uouhIK7PvyIRNQHACf-d", + "type": "element", + "props": { + "color": "#FFFFFF" + }, + "parentId": "page:page", + "index": "a4", + "typeName": "shape" + }, + "shape:GTQq2qxkWPHEK7KMIRtsh": { + "x": 54.9375, + "y": 93.48828125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:GTQq2qxkWPHEK7KMIRtsh", + "type": "element", + "props": { + "color": "#5BCEFA" + }, + "parentId": "page:page", + "index": "a5", + "typeName": "shape" + }, + "shape:05jMujN6A0sIp6zzHMpbV": { + "x": 178.9375, + "y": 93.48828125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:05jMujN6A0sIp6zzHMpbV", + "type": "element", + "props": { + "color": "#F5A9B8" + }, + "parentId": "page:page", + "index": "a6", + "typeName": "shape" + }, + "binding:iOBENBUHvzD8N7mBdIM5l": { + "meta": {}, + "id": "binding:iOBENBUHvzD8N7mBdIM5l", + "type": "layout", + "fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq", + "toId": "shape:05jMujN6A0sIp6zzHMpbV", + "props": { + "index": "a2", + "placeholder": false + }, + "typeName": "binding" + }, + "binding:YTIeOALEmHJk6dczRpQmE": { + "meta": {}, + "id": "binding:YTIeOALEmHJk6dczRpQmE", + "type": "layout", + "fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq", + "toId": "shape:GTQq2qxkWPHEK7KMIRtsh", + "props": { + "index": "a1", + "placeholder": false + }, + "typeName": "binding" + }, + "binding:n4LY_pVuLfjV1qpOTZX-U": { + "meta": {}, + "id": "binding:n4LY_pVuLfjV1qpOTZX-U", + "type": "layout", + "fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq", + "toId": "shape:6uouhIK7PvyIRNQHACf-d", + "props": { + "index": "a3", + "placeholder": false + }, + "typeName": "binding" + }, + "binding:8XayRsWB_nxAH2833SYg1": { + "meta": {}, + "id": "binding:8XayRsWB_nxAH2833SYg1", + "type": "layout", + "fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq", + "toId": "shape:2oThF4kJ4v31xqKN5lvq2", + "props": { + "index": "a5", + "placeholder": false + }, + "typeName": "binding" + }, + "binding:MTYuIRiEVTn2DyVChthry": { + "meta": {}, + "id": "binding:MTYuIRiEVTn2DyVChthry", + "type": "layout", + "fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq", + "toId": "shape:K2vk_VTaNh-ANaRNOAvgY", + "props": { + "index": "a4", + "placeholder": false + }, + "typeName": "binding" + } + }, + "schema": { + "schemaVersion": 2, + "sequences": { + "com.tldraw.store": 4, + "com.tldraw.asset": 1, + "com.tldraw.camera": 1, + "com.tldraw.document": 2, + "com.tldraw.instance": 25, + "com.tldraw.instance_page_state": 5, + "com.tldraw.page": 1, + "com.tldraw.instance_presence": 5, + "com.tldraw.pointer": 1, + "com.tldraw.shape": 4, + "com.tldraw.asset.bookmark": 2, + "com.tldraw.asset.image": 4, + "com.tldraw.asset.video": 4, + "com.tldraw.shape.group": 0, + "com.tldraw.shape.text": 2, + "com.tldraw.shape.bookmark": 2, + "com.tldraw.shape.draw": 2, + "com.tldraw.shape.geo": 9, + "com.tldraw.shape.note": 7, + "com.tldraw.shape.line": 5, + "com.tldraw.shape.frame": 0, + "com.tldraw.shape.arrow": 5, + "com.tldraw.shape.highlight": 1, + "com.tldraw.shape.embed": 4, + "com.tldraw.shape.image": 3, + "com.tldraw.shape.video": 2, + "com.tldraw.shape.container": 0, + "com.tldraw.shape.element": 0, + "com.tldraw.binding.arrow": 0, + "com.tldraw.binding.layout": 0 + } + } +} diff --git a/src/tools/CardShapeTool.tsx b/src/tools/CardShapeTool.tsx deleted file mode 100644 index dcd0b72..0000000 --- a/src/tools/CardShapeTool.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { BaseBoxShapeTool, TLClickEvent } from 'tldraw' -export class CardShapeTool extends BaseBoxShapeTool { - static override id = 'card' - static override initial = 'idle' - override shapeType = 'card' - - override onDoubleClick: TLClickEvent = (_info) => { - // you can handle events in handlers like this one; - // check the BaseBoxShapeTool source as an example - } -} - -/* -This file contains our custom tool. The tool is a StateNode with the `id` "card". - -We get a lot of functionality for free by extending the BaseBoxShapeTool. but we can -handle events in out own way by overriding methods like onDoubleClick. For an example -of a tool with more custom functionality, check out the screenshot-tool example. - -*/ \ No newline at end of file diff --git a/src/tools/ChatBoxTool.ts b/src/tools/ChatBoxTool.ts index ce64403..ec8970b 100644 --- a/src/tools/ChatBoxTool.ts +++ b/src/tools/ChatBoxTool.ts @@ -1,6 +1,7 @@ import { BaseBoxShapeTool } from "tldraw"; export class ChatBoxTool extends BaseBoxShapeTool { - shapeType = 'chatBox' - override initial = 'idle' + static override id = 'chatBox' + shapeType = 'chatBox'; + override initial = 'idle'; } \ No newline at end of file diff --git a/src/tools/VideoChatTool.ts b/src/tools/VideoChatTool.ts new file mode 100644 index 0000000..de0f6d0 --- /dev/null +++ b/src/tools/VideoChatTool.ts @@ -0,0 +1,9 @@ +import { BaseBoxShapeTool } from "tldraw"; + +export class VideoChatTool extends BaseBoxShapeTool { + static override id = 'VideoChat' + shapeType = 'VideoChat'; + override initial = 'idle'; + + // Additional methods for handling video chat functionality can be added here +} \ No newline at end of file diff --git a/src/ui-overrides.tsx b/src/ui-overrides.tsx new file mode 100644 index 0000000..6710c2f --- /dev/null +++ b/src/ui-overrides.tsx @@ -0,0 +1,48 @@ +import { + DefaultToolbar, + DefaultToolbarContent, + TLComponents, + TLUiOverrides, + TldrawUiMenuItem, + useIsToolSelected, + useTools, +} from 'tldraw' + +export const uiOverrides: TLUiOverrides = { + tools(editor, tools) { + tools.VideoChat = { + id: 'VideoChat', + icon: 'color', + label: 'Video', + kbd: 'x', + onSelect: () => { + editor.setCurrentTool('VideoChat') + }, + } + tools.chatBox = { + id: 'chatBox', + icon: 'color', + label: 'Chat', + kbd: 'x', + onSelect: () => { + editor.setCurrentTool('chatBox') + }, + } + return tools + }, +} + +export const components: TLComponents = { + Toolbar: (props) => { + const tools = useTools() + const isChatBoxSelected = useIsToolSelected(tools['chatBox']) + const isVideoSelected = useIsToolSelected(tools['VideoChat']) + return ( + + + + + + ) + }, +} \ No newline at end of file diff --git a/src/utils/ContainerShapeUtil.tsx b/src/utils/ContainerShapeUtil.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/ElementShapeUtil.tsx b/src/utils/ElementShapeUtil.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/shapes/HTMLShapeUtil.tsx b/src/utils/HTMLShapeUtil.tsx similarity index 100% rename from src/shapes/HTMLShapeUtil.tsx rename to src/utils/HTMLShapeUtil.tsx diff --git a/src/utils/card-shape-migrations.ts b/src/utils/card-shape-migrations.ts new file mode 100644 index 0000000..6021ec8 --- /dev/null +++ b/src/utils/card-shape-migrations.ts @@ -0,0 +1,25 @@ +import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from 'tldraw' + +const versions = createShapePropsMigrationIds( + // this must match the shape type in the shape definition + 'card', + { + AddSomeProperty: 1, + } +) + +// Migrations for the custom card shape (optional but very helpful) +export const cardShapeMigrations = createShapePropsMigrationSequence({ + sequence: [ + { + id: versions.AddSomeProperty, + up(props) { + // it is safe to mutate the props object here + props.someProperty = 'some value' + }, + down(props) { + delete props.someProperty + }, + }, + ], +}) \ No newline at end of file diff --git a/src/utils/card-shape-props.ts b/src/utils/card-shape-props.ts new file mode 100644 index 0000000..bead2fb --- /dev/null +++ b/src/utils/card-shape-props.ts @@ -0,0 +1,11 @@ +import { DefaultColorStyle, RecordProps, T } from 'tldraw' +import { ICardShape } from './card-shape-types' + +// Validation for our custom card shape's props, using one of tldraw's default styles +export const cardShapeProps: RecordProps = { + w: T.number, + h: T.number, + color: DefaultColorStyle, +} + +// To generate your own custom styles, check out the custom styles example. \ No newline at end of file diff --git a/src/utils/card-shape-types.ts b/src/utils/card-shape-types.ts new file mode 100644 index 0000000..0ec08e0 --- /dev/null +++ b/src/utils/card-shape-types.ts @@ -0,0 +1,11 @@ +import { TLBaseShape, TLDefaultColorStyle } from 'tldraw' + +// A type for our custom card shape +export type ICardShape = TLBaseShape< + 'card', + { + w: number + h: number + color: TLDefaultColorStyle + } +> \ No newline at end of file diff --git a/src/utils/my-interactive-shape-util.tsx b/src/utils/my-interactive-shape-util.tsx new file mode 100644 index 0000000..ec3ecc7 --- /dev/null +++ b/src/utils/my-interactive-shape-util.tsx @@ -0,0 +1,120 @@ +import { BaseBoxShapeUtil, HTMLContainer, RecordProps, T, TLBaseShape } from 'tldraw' + +// There's a guide at the bottom of this file! + +type IMyInteractiveShape = TLBaseShape< + 'my-interactive-shape', + { + w: number + h: number + checked: boolean + text: string + } +> + +export class myInteractiveShape extends BaseBoxShapeUtil { + static override type = 'my-interactive-shape' as const + static override props: RecordProps = { + w: T.number, + h: T.number, + checked: T.boolean, + text: T.string, + } + + getDefaultProps(): IMyInteractiveShape['props'] { + return { + w: 230, + h: 230, + checked: false, + text: '', + } + } + + // [1] + component(shape: IMyInteractiveShape) { + return ( + + + this.editor.updateShape({ + id: shape.id, + type: 'my-interactive-shape', + props: { checked: !shape.props.checked }, + }) + } + // [b] This is where we stop event propagation + onPointerDown={(e) => e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} + onTouchEnd={(e) => e.stopPropagation()} + /> + + this.editor.updateShape({ + id: shape.id, + type: 'my-interactive-shape', + props: { text: e.currentTarget.value }, + }) + } + // [c] + onPointerDown={(e) => { + if (!shape.props.checked) { + e.stopPropagation() + } + }} + onTouchStart={(e) => { + if (!shape.props.checked) { + e.stopPropagation() + } + }} + onTouchEnd={(e) => { + if (!shape.props.checked) { + e.stopPropagation() + } + }} + /> + + ) + } + + // [5] + indicator(shape: IMyInteractiveShape) { + return + } +} + +/* +This is a custom shape, for a more in-depth look at how to create a custom shape, +see our custom shape example. + +[1] +This is where we describe how our shape will render + + [a] We need to set pointer-events to all so that we can interact with our shape. This CSS property is + set to "none" off by default. We need to manually opt-in to accepting pointer events by setting it to + 'all' or 'auto'. + + [b] We need to stop event propagation so that the editor doesn't select the shape + when we click on the checkbox. The 'canvas container' forwards events that it receives + on to the editor, so stopping propagation here prevents the event from reaching the canvas. + + [c] If the shape is not checked, we stop event propagation so that the editor doesn't + select the shape when we click on the input. If the shape is checked then we allow that event to + propagate to the canvas and then get sent to the editor, triggering clicks or drags as usual. + +*/ diff --git a/src/utils.tsx b/src/utils/utils.tsx similarity index 100% rename from src/utils.tsx rename to src/utils/utils.tsx diff --git a/ui-overrides.tsx b/ui-overrides.tsx deleted file mode 100644 index b709669..0000000 --- a/ui-overrides.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react' -import { - DefaultKeyboardShortcutsDialog, - DefaultKeyboardShortcutsDialogContent, - DefaultToolbar, - DefaultToolbarContent, - TLComponents, - TLUiOverrides, - TldrawUiMenuItem, - useIsToolSelected, - useTools, -} from 'tldraw' - -// There's a guide at the bottom of this file! - -export const uiOverrides: TLUiOverrides = { - tools(editor, tools) { - // Create a tool item in the ui's context. - tools.card = { - id: 'card', - icon: 'color', - label: 'Card', - kbd: 'c', - onSelect: () => { - editor.setCurrentTool('card') - }, - } - return tools - }, -} - -export const components: TLComponents = { - Toolbar: (props) => { - const tools = useTools() - const isCardSelected = useIsToolSelected(tools['card']) - return ( - - - - - ) - }, - KeyboardShortcutsDialog: (props) => { - const tools = useTools() - return ( - - - - - ) - }, -} - -/* - -This file contains overrides for the Tldraw UI. These overrides are used to add your custom tools to -the toolbar and the keyboard shortcuts menu. - -First we have to add our new tool to the tools object in the tools override. This is where we define -all the basic information about our new tool - its icon, label, keyboard shortcut, what happens when -we select it, etc. - -Then, we replace the UI components for the toolbar and keyboard shortcut dialog with our own, that -add our new tool to the existing default content. Ideally, we'd interleave our new tool into the -ideal place among the default tools, but for now we're just adding it at the start to keep things -simple. -*/ \ No newline at end of file diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index f878c83..4ee489d 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -10,11 +10,12 @@ import { import { AutoRouter, IRequest, error } from 'itty-router' import throttle from 'lodash.throttle' import { Environment } from './types' -import { ChatBoxShape } from '@/shapes/ChatBoxShape' +import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' +import { VideoChatShape } from '@/shapes/VideoChatShapeUtil' // add custom shapes and bindings here if needed: export const customSchema = createTLSchema({ - shapes: { ...defaultShapeSchemas, chatBox: ChatBoxShape }, + shapes: { ...defaultShapeSchemas, chatBox: ChatBoxShape, VideoChat: VideoChatShape }, // bindings: { ...defaultBindingSchemas }, })