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 (
)
}
-// 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"
/>
-
e.stopPropagation()} className="send-button">Send
+
e.stopPropagation()} className="send-button">Send
);
@@ -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 ? (
+
+ ) : (
+
+
+ Join Room
+
+ {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 },
})