From 0be7e77c1810b3747b4e7e0c92cb5484e25434e0 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:20:26 -0400 Subject: [PATCH] big mess of a commit --- index.html | 2 +- package.json | 1 + src/App.tsx | 83 +++++-- src/components/Board.tsx | 125 ++--------- src/components/Canvas.tsx | 2 +- src/{components => css}/ChatBoxStyles.css | 0 src/physics/simulation.ts | 2 +- ...{ChatBoxShape.tsx => ChatBoxShapeUtil.tsx} | 20 +- src/shapes/VideoChatShape.tsx | 93 -------- src/shapes/VideoChatShapeUtil.tsx | 79 +++++++ src/snapshot.json | 210 ++++++++++++++++++ src/tools/CardShapeTool.tsx | 20 -- src/tools/ChatBoxTool.ts | 3 +- src/tools/VideoChatTool.ts | 3 +- src/ui-overrides.tsx | 48 ++++ src/utils/ContainerShapeUtil.tsx | 0 src/utils/ElementShapeUtil.tsx | 0 src/{shapes => utils}/HTMLShapeUtil.tsx | 0 src/utils/card-shape-migrations.ts | 25 +++ src/utils/card-shape-props.ts | 11 + src/utils/card-shape-types.ts | 11 + src/utils/my-interactive-shape-util.tsx | 120 ++++++++++ src/{ => utils}/utils.tsx | 0 ui-overrides.tsx | 67 ------ worker/TldrawDurableObject.ts | 6 +- 25 files changed, 610 insertions(+), 321 deletions(-) rename src/{components => css}/ChatBoxStyles.css (100%) rename src/shapes/{ChatBoxShape.tsx => ChatBoxShapeUtil.tsx} (89%) delete mode 100644 src/shapes/VideoChatShape.tsx create mode 100644 src/shapes/VideoChatShapeUtil.tsx create mode 100644 src/snapshot.json delete mode 100644 src/tools/CardShapeTool.tsx create mode 100644 src/ui-overrides.tsx create mode 100644 src/utils/ContainerShapeUtil.tsx create mode 100644 src/utils/ElementShapeUtil.tsx rename src/{shapes => utils}/HTMLShapeUtil.tsx (100%) create mode 100644 src/utils/card-shape-migrations.ts create mode 100644 src/utils/card-shape-props.ts create mode 100644 src/utils/card-shape-types.ts create mode 100644 src/utils/my-interactive-shape-util.tsx rename src/{ => utils}/utils.tsx (100%) delete mode 100644 ui-overrides.tsx 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..6b0e345 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@dimforge/rapier2d": "^0.11.2", "@tldraw/sync": "^2.4.6", "@tldraw/sync-core": "latest", + "@tldraw/tldraw": "^3.3.1", "@tldraw/tlschema": "latest", "@types/markdown-it": "^14.1.1", "@vercel/analytics": "^1.2.2", diff --git a/src/App.tsx b/src/App.tsx index bb32041..c649b53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,37 +1,93 @@ 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 +114,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 b6d3574..7e82cb3 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -2,46 +2,28 @@ 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 { IChatBoxShape, ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' import { VideoChatTool } from '@/tools/VideoChatTool' -import { IVideoChatShape, VideoChatShape } from '@/shapes/VideoChatShape' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' // Adjusted path if necessary +import { IVideoChatShape, 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, useEffect, useRef } from 'react'; // Ensure useRef is imported -import { ChatBox } from '@/shapes/ChatBoxShape'; // Add this import - import { VideoChat } from '@/shapes/VideoChatShape'; +import React, { useState, useEffect, useRef } 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] const tools = [ChatBoxTool, VideoChatTool]; // Array of tools -// Function to register tools -const registerTools = (store: any, registeredToolsRef: React.MutableRefObject>) => { - const typedStore = store as unknown as { - registerTool: (tool: any) => void; - hasTool: (id: string) => boolean; - }; - - tools.forEach(tool => { - if (!registeredToolsRef.current.has(tool.id) && typedStore.hasTool && !typedStore.hasTool(tool.id)) { - typedStore.registerTool(tool); // Register the tool - registeredToolsRef.current.add(tool.id); // Mark this tool as registered - } - }); -}; - - export function Board() { + console.warn("HELLO FROM BOARD") const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component const roomId = slug || 'default-room'; // Declare roomId here @@ -49,48 +31,12 @@ export function Board() { uri: `${WORKER_URL}/connect/${roomId}`, assets: multiplayerAssetStore, shapeUtils: shapeUtils, - schema: customSchema + schema: customSchema, }); const [isChatBoxVisible, setChatBoxVisible] = useState(false); const [userName, setUserName] = useState(''); const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility - const registeredToolsRef = useRef(new Set()); // Ref to track registered tools - - // Call the function to register tools only once - useEffect(() => { - registerTools(store, registeredToolsRef); - }, [store]); // Ensure this effect runs when the store changes - -// const videoChatTool = { -// id: 'videoChatTool', // Ensure this ID is unique -// // ... other properties of the tool -// }; - - //const typedStore = store as unknown as { - // registerTool: (tool: any) => void; - // hasTool: (id: string) => boolean; // Ensure hasTool is included - //}; - - //if (typedStore.hasTool && !typedStore.hasTool(videoChatTool.id)) { // Check if the tool ID is unique - // typedStore.registerTool(videoChatTool); // Register the video chat tool - //} else { - // console.error(`Tool with id "${videoChatTool.id}" is already registered. Cannot register again.`); // Log an error - //} - -// useEffect(() => { -// tools.forEach(tool => { -// const typedStore = store as unknown as { -// registerTool: (tool: any) => void; -// hasTool: (id: string) => boolean; // Ensure hasTool is included -// }; -// if (typedStore.hasTool && !typedStore.hasTool(tool.id)) { // Check if hasTool exists -// typedStore.registerTool(tool); // Use the typed store -// } else { -// console.warn(`Tool with id "${tool.id}" is already registered. Cannot register again.`); // Log an error -// } -// }); -// }, [store]); // Run this effect when the store changes const handleNameChange = (event: React.ChangeEvent) => { setUserName(event.target.value); @@ -99,34 +45,13 @@ export function Board() { return (
{ editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) - editor.createShape({ - type: 'chatBox', - x: 0, - y: 0, - props: { - w: 200, - h: 200, - roomId: roomId, - }, - }); - //if (isVideoChatVisible) { - //editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) - editor.createShape({ - type: 'videoChat', - x: 300, // Adjust position as needed - y: 0, - props: { - roomUrl: 'https://whereby.com/default-room', // Default room URL - w: 640, - h: 480, - }, - }); - //} }} /> {isChatBoxVisible && ( @@ -137,12 +62,12 @@ export function Board() { onChange={handleNameChange} placeholder="Enter your name" /> - + w={200} // Set appropriate width + h={200} // Set appropriate height + />
)} {isVideoChatVisible && ( // Render the button to join video chat @@ -154,22 +79,6 @@ export function Board() { ) } -// 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 f657648..2ff1ec7 100644 --- a/src/physics/simulation.ts +++ b/src/physics/simulation.ts @@ -66,7 +66,7 @@ export class PhysicsWorld { this.createGroup(shape as TLGroupShape); break; // Add cases for any new shape types here - case "videoChat": + case "VideoChat": this.createShape (shape as TLGeoShape); break; } diff --git a/src/shapes/ChatBoxShape.tsx b/src/shapes/ChatBoxShapeUtil.tsx similarity index 89% rename from src/shapes/ChatBoxShape.tsx rename to src/shapes/ChatBoxShapeUtil.tsx index 99a47fe..b385ab6 100644 --- a/src/shapes/ChatBoxShape.tsx +++ b/src/shapes/ChatBoxShapeUtil.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from "react"; -import { BaseBoxShapeUtil, TLBaseBoxShape, TLBaseShape, TldrawBaseProps } from "tldraw"; +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; export type IChatBoxShape = TLBaseShape< - 'chatBox', + 'ChatBox', { w: number h: number @@ -12,7 +12,7 @@ export type IChatBoxShape = TLBaseShape< > export class ChatBoxShape extends BaseBoxShapeUtil { - static override type = 'chatBox' + static override type = 'ChatBox' getDefaultProps(): IChatBoxShape['props'] { return { @@ -29,7 +29,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil { component(shape: IChatBoxShape) { return ( - + ) } } @@ -41,15 +41,11 @@ interface Message { timestamp: Date; } -interface ChatBoxProps { - roomId: string; - width: number; - height: number; - userName: string; // Add this line -} + + // Update the ChatBox component to accept userName -export const ChatBox: React.FC = ({ roomId, width, height, userName }) => { +export const ChatBox: React.FC = ({ roomId, w, h, userName }) => { const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(""); const [username, setUsername] = useState(userName); @@ -98,7 +94,7 @@ export const ChatBox: React.FC = ({ roomId, width, height, userNam }; return ( -
+
{messages.map((msg) => (
diff --git a/src/shapes/VideoChatShape.tsx b/src/shapes/VideoChatShape.tsx deleted file mode 100644 index 5062981..0000000 --- a/src/shapes/VideoChatShape.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; -import React, { useEffect, useState } from "react"; // Updated import for React - -export type IVideoChatShape = TLBaseShape< - 'videoChat', - { - w: number; - h: number; - roomUrl: string; // Changed from roomId to roomUrl for Whereby - userName: string; - } ->; - -export class VideoChatShape extends BaseBoxShapeUtil { - static override type = 'videoChat'; - - getDefaultProps(): IVideoChatShape['props'] { - return { - roomUrl: 'https://whereby.com/default-room', // Default Whereby room URL - w: 640, - h: 480, - userName: '' - }; - } - - indicator(shape: IVideoChatShape) { - return ; - } - - component(shape: IVideoChatShape) { - return ; - } -} - -interface VideoChatProps { - roomUrl: string; - // Remove width and height as they are not used - // width: number; - // height: number; - // Remove userName as it is not used - // userName: string; -} - -// VideoChat component using Whereby -export const VideoChat: React.FC = () => { // Removed roomUrl from props - // Remove unused destructured props - // const [roomUrl, setRoomUrl] = useState(initialRoomUrl); // Use initialRoomUrl to avoid duplicate identifier - // const [roomUrl, setRoomUrl] = useState(roomId); // Use roomId instead - 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 () => { - setError(""); - setIsLoading(true); - try { - const response = await fetch('/api/get-or-create-room', { method: 'GET' }); - const data: { roomUrl?: string; error?: string } = await response.json(); // Explicitly type 'data' - if (data.roomUrl) { - // setRoomUrl(data.roomUrl); // Remove this line - setIsInRoom(true); - } else { - setError(data.error || "Failed to join room. Please try again."); - } - } catch (e) { - console.error("Error joining room:", e); - setError("An error occurred. Please try again."); - } - setIsLoading(false); - }; - - const leaveRoom = () => { - setIsInRoom(false); - // setRoomUrl(""); // Remove this line - }; - - return ( -
- {!isInRoom && ( // Show button if not in room - - )} - {/* Render Video Chat UI here when isInRoom is true */} - {isInRoom &&
Video Chat UI goes here
} -
- ); -} \ No newline at end of file diff --git a/src/shapes/VideoChatShapeUtil.tsx b/src/shapes/VideoChatShapeUtil.tsx new file mode 100644 index 0000000..b46127c --- /dev/null +++ b/src/shapes/VideoChatShapeUtil.tsx @@ -0,0 +1,79 @@ +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; +import { useEffect, useState } from "react"; + +export type IVideoChatShape = TLBaseShape< + 'VideoChat', + { + w: number; + h: number; + roomUrl: string; // Changed from roomId to roomUrl for Whereby + userName: string; + } +>; + +export class VideoChatShape extends BaseBoxShapeUtil { + static override type = 'VideoChat'; + + getDefaultProps(): IVideoChatShape['props'] { + return { + roomUrl: 'https://whereby.com/default-room', // Default Whereby room URL + w: 640, + h: 480, + userName: '' + }; + } + + indicator(shape: IVideoChatShape) { + return ; + } + + component(shape: IVideoChatShape) { + // Remove unused destructured props + // const [roomUrl, setRoomUrl] = useState(initialRoomUrl); // Use initialRoomUrl to avoid duplicate identifier + // const [roomUrl, setRoomUrl] = useState(roomId); // Use roomId instead + 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 () => { + setError(""); + setIsLoading(true); + try { + const response = await fetch('/api/get-or-create-room', { method: 'GET' }); + const data: { roomUrl?: string; error?: string } = await response.json(); // Explicitly type 'data' + if (data.roomUrl) { + // setRoomUrl(data.roomUrl); // Remove this line + setIsInRoom(true); + } else { + setError(data.error || "Failed to join room. Please try again."); + } + } catch (e) { + console.error("Error joining room:", e); + setError("An error occurred. Please try again."); + } + setIsLoading(false); + }; + + const leaveRoom = () => { + setIsInRoom(false); + // setRoomUrl(""); // Remove this line + }; + + return ( +
+ {!isInRoom && ( // Show button if not in room + + )} + {/* Render Video Chat UI here when isInRoom is true */} + {isInRoom &&
Video Chat UI goes here
} +
+ ); + } +} \ No newline at end of file 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 6dd5e40..795bfda 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'; + 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 index c3dde15..de0f6d0 100644 --- a/src/tools/VideoChatTool.ts +++ b/src/tools/VideoChatTool.ts @@ -1,7 +1,8 @@ import { BaseBoxShapeTool } from "tldraw"; export class VideoChatTool extends BaseBoxShapeTool { - shapeType = 'videoChat'; + static override id = 'VideoChat' + shapeType = 'VideoChat'; override initial = 'idle'; // Additional methods for handling video chat functionality can be added here diff --git a/src/ui-overrides.tsx b/src/ui-overrides.tsx new file mode 100644 index 0000000..0bddd0b --- /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 ed42779..b5e0274 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -10,12 +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 { VideoChatShape } from '@/shapes/VideoChatShape' +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, videoChat: VideoChatShape }, + shapes: { ...defaultShapeSchemas, ChatBox: ChatBoxShape, VideoChat: VideoChatShape }, // bindings: { ...defaultBindingSchemas }, })