big mess of a commit

This commit is contained in:
Jeff Emmett 2024-10-16 11:20:26 -04:00
parent 4b901ed5bd
commit 0be7e77c18
25 changed files with 610 additions and 321 deletions

View File

@ -26,7 +26,7 @@
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="jeffemmett.com">
<meta property="twitter:url" content="https://jeffemmett.com">
<meta name="twitter:title" content="Orion Reed">
<meta name="twitter:title" content="Jeff Emmett">
<meta name="twitter:description"
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
<meta name="twitter:image" content="/website-embed.png">

View File

@ -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",

View File

@ -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 (
<div className="tldraw__editor">
<Tldraw
shapeUtils={customShapeUtils} // Use custom shape utils
tools={customTools} // Pass in the array of custom tool classes
overrides={uiOverrides}
components={components}
onMount={(editor) => {
editor.createShape({ type: 'my-interactive-shape', x: 100, y: 100 });
}}
/>
</div>
);
}
// ... existing code ...
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
function App() {
return (
// <React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/card/contact" element={<Contact />} />
<Route path="/posts/:slug" element={<Post />} />
<Route path="/board/:slug" element={<Board />} />
<Route path="/inbox" element={<Inbox />} />
<Route path="/books" element={<Books />} />
</Routes>
</BrowserRouter>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/card/contact" element={<Contact />} />
<Route path="/posts/:slug" element={<Post />} />
<Route path="/board/:slug" element={<Board />} />
<Route path="/inbox" element={<Inbox />} />
<Route path="/books" element={<Books />} />
</Routes>
</BrowserRouter>
// </React.StrictMode>
);
};
@ -58,5 +114,6 @@ function Home() {
<div style={{ zIndex: 999999 }} className={`${isCanvasEnabled && isEditorMounted ? 'transparent' : ''}`}>
{<Default />}
</div>
{isCanvasEnabled && elementsInfo.length > 0 ? <Canvas shapes={shapes} /> : null}</>)
{isCanvasEnabled && elementsInfo.length > 0 ? <Canvas shapes={shapes} /> : null}</>
)
}

View File

@ -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<Set<string>>) => {
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<string>()); // 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<HTMLInputElement>) => {
setUserName(event.target.value);
@ -99,34 +45,13 @@ export function Board() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
store={store}
store={store}
shapeUtils={shapeUtils}
overrides={uiOverrides}
components={components}
tools={tools}
onMount={(editor) => {
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
editor.createShape<IChatBoxShape>({
type: 'chatBox',
x: 0,
y: 0,
props: {
w: 200,
h: 200,
roomId: roomId,
},
});
//if (isVideoChatVisible) {
//editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
editor.createShape<IVideoChatShape>({
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"
/>
<ChatBox
userName={userName}
<ChatBox
userName={userName}
roomId={roomId} // Added roomId
width={200} // Set appropriate width
height={200} // Set appropriate height
/>
w={200} // Set appropriate width
h={200} // Set appropriate height
/>
</div>
)}
{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 (
<div className={message.isUser ? 'user-message' : 'other-message'}>
{message.text}
</div>
)
}
// How does our server handle bookmark unfurling?
async function unfurlBookmarkUrl({ url }: { url: string }): Promise<TLBookmarkAsset> {
const asset: TLBookmarkAsset = {

View File

@ -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,

View File

@ -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;
}

View File

@ -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<IChatBoxShape> {
static override type = 'chatBox'
static override type = 'ChatBox'
getDefaultProps(): IChatBoxShape['props'] {
return {
@ -29,7 +29,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
component(shape: IChatBoxShape) {
return (
<ChatBox roomId={shape.props.roomId} width={shape.props.w} height={shape.props.h} userName="" />
<ChatBox roomId={shape.props.roomId} w={shape.props.w} h={shape.props.h} userName="" />
)
}
}
@ -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<ChatBoxProps> = ({ roomId, width, height, userName }) => {
export const ChatBox: React.FC<IChatBoxShape['props']> = ({ roomId, w, h, userName }) => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputMessage, setInputMessage] = useState("");
const [username, setUsername] = useState(userName);
@ -98,7 +94,7 @@ export const ChatBox: React.FC<ChatBoxProps> = ({ roomId, width, height, userNam
};
return (
<div className="chat-container" style={{ pointerEvents: 'all', width: `${width}px`, height: `${height}px`, overflow: 'auto' }}>
<div className="chat-container" style={{ pointerEvents: 'all', width: `${w}px`, height: `${h}px`, overflow: 'auto' }}>
<div className="messages-container">
{messages.map((msg) => (
<div key={msg.id} className={`message ${msg.username === username ? 'own-message' : ''}`}>

View File

@ -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 <IVideoChatShape> {
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 <rect x={0} y={0} width={shape.props.w} height={shape.props.h} />;
}
component(shape: IVideoChatShape) {
return <VideoChat roomUrl={shape.props.roomUrl} />;
}
}
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<VideoChatProps> = () => { // 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 (
<div>
{!isInRoom && ( // Show button if not in room
<button onClick={() => setIsInRoom(true)} className="bg-green-500 text-white px-4 py-2 rounded">
Join Video Chat
</button>
)}
{/* Render Video Chat UI here when isInRoom is true */}
{isInRoom && <div>Video Chat UI goes here</div>}
</div>
);
}

View File

@ -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<IVideoChatShape> {
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 <rect x={0} y={0} width={shape.props.w} height={shape.props.h} />;
}
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 (
<div style={{ border: '5px solid red' }}>
{!isInRoom && ( // Show button if not in room
<button onClick={() => setIsInRoom(true)} className="bg-green-500 text-white px-4 py-2 rounded">
Join Video Chat
</button>
)}
{/* Render Video Chat UI here when isInRoom is true */}
{isInRoom && <div>Video Chat UI goes here</div>}
</div>
);
}
}

210
src/snapshot.json Normal file
View File

@ -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
}
}
}

View File

@ -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.
*/

View File

@ -1,6 +1,7 @@
import { BaseBoxShapeTool } from "tldraw";
export class ChatBoxTool extends BaseBoxShapeTool {
shapeType = 'chatBox';
static override id = 'ChatBox'
shapeType = 'ChatBox';
override initial = 'idle';
}

View File

@ -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

48
src/ui-overrides.tsx Normal file
View File

@ -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 (
<DefaultToolbar {...props}>
<TldrawUiMenuItem {...tools['VideoChat']} isSelected={isVideoSelected} />
<TldrawUiMenuItem {...tools['ChatBox']} isSelected={isChatBoxSelected} />
<DefaultToolbarContent />
</DefaultToolbar>
)
},
}

View File

View File

View File

@ -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
},
},
],
})

View File

@ -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<ICardShape> = {
w: T.number,
h: T.number,
color: DefaultColorStyle,
}
// To generate your own custom styles, check out the custom styles example.

View File

@ -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
}
>

View File

@ -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<IMyInteractiveShape> {
static override type = 'my-interactive-shape' as const
static override props: RecordProps<IMyInteractiveShape> = {
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 (
<HTMLContainer
style={{
padding: 16,
height: shape.props.h,
width: shape.props.w,
// [a] This is where we allow pointer events on our shape
pointerEvents: 'all',
backgroundColor: '#efefef',
overflow: 'hidden',
}}
>
<input
type="checkbox"
checked={shape.props.checked}
onChange={() =>
this.editor.updateShape<IMyInteractiveShape>({
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()}
/>
<input
type="text"
placeholder="Enter a todo..."
readOnly={shape.props.checked}
value={shape.props.text}
onChange={(e) =>
this.editor.updateShape<IMyInteractiveShape>({
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()
}
}}
/>
</HTMLContainer>
)
}
// [5]
indicator(shape: IMyInteractiveShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}
/*
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.
*/

View File

@ -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 (
<DefaultToolbar {...props}>
<TldrawUiMenuItem {...tools['card']} isSelected={isCardSelected} />
<DefaultToolbarContent />
</DefaultToolbar>
)
},
KeyboardShortcutsDialog: (props) => {
const tools = useTools()
return (
<DefaultKeyboardShortcutsDialog {...props}>
<TldrawUiMenuItem {...tools['card']} />
<DefaultKeyboardShortcutsDialogContent />
</DefaultKeyboardShortcutsDialog>
)
},
}
/*
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.
*/

View File

@ -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 },
})