swapped in daily.co video and removed whereby sdk, finished zoom and copylink except for context menu display
This commit is contained in:
parent
db3205f97a
commit
d1a8407a9b
|
|
@ -0,0 +1,19 @@
|
|||
# Google API Credentials
|
||||
VITE_GOOGLE_CLIENT_ID='your_google_client_id'
|
||||
VITE_GOOGLE_API_KEY='your_google_api_key'
|
||||
|
||||
# Cloudflare Worker
|
||||
CLOUDFLARE_API_TOKEN='your_cloudflare_token'
|
||||
CLOUDFLARE_ACCOUNT_ID='your_account_id'
|
||||
CLOUDFLARE_ZONE_ID='your_zone_id'
|
||||
|
||||
# Worker URL
|
||||
TLDRAW_WORKER_URL='your_worker_url'
|
||||
|
||||
# R2 Bucket Configuration
|
||||
R2_BUCKET_NAME='your_bucket_name'
|
||||
R2_PREVIEW_BUCKET_NAME='your_preview_bucket_name'
|
||||
|
||||
# Daily.co Configuration
|
||||
DAILY_API_KEY='your_daily_api_key'
|
||||
DAILY_DOMAIN='your_daily_domain'
|
||||
|
|
@ -174,3 +174,12 @@ dist
|
|||
.wrangler/
|
||||
.*.md
|
||||
.vercel
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.dev.vars
|
||||
|
||||
# Keep example file
|
||||
!.env.example
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"name": "jeffemmett",
|
||||
"version": "1.0.0",
|
||||
"description": "Jeff Emmett's personal website",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others --names client,worker --prefix-colors blue,red \"yarn dev:client\" \"yarn dev:worker\"",
|
||||
"dev:client": "vite --host --port 5173",
|
||||
"dev:worker": "wrangler dev --local --port 5172 --ip 0.0.0.0",
|
||||
"build": "tsc && vite build && wrangler deploy",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"deploy": "yarn build && vercel deploy --prod"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Jeff Emmett",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier2d": "^0.11.2",
|
||||
"@tldraw/assets": "^2.0.0",
|
||||
"@tldraw/tldraw": "^3.4.1",
|
||||
"@tldraw/sync": "^2.4.6",
|
||||
"@tldraw/sync-core": "^2.4.6",
|
||||
"@tldraw/tlschema": "^2.4.6",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"@whereby.com/browser-sdk": "^3.9.2",
|
||||
"cloudflare-workers-unfurl": "^0.0.7",
|
||||
"crdts": "^0.2.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"itty-router": "^5.0.17",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-latex2img": "^0.0.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"tldraw": "^2.4.6",
|
||||
"use-local-storage-state": "^19.5.0",
|
||||
"vercel": "^39.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.4.1",
|
||||
"@cloudflare/types": "^6.29.1",
|
||||
"@cloudflare/workers-types": "^4.20240821.1",
|
||||
"@types/lodash.throttle": "^4",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||
"@typescript-eslint/parser": "^5.59.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.3.4",
|
||||
"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.88.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"react": "^18.2.0",
|
||||
"@types/react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
11
package.json
11
package.json
|
|
@ -17,13 +17,12 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier2d": "^0.11.2",
|
||||
"@tldraw/sync": "^2.4.6",
|
||||
"@tldraw/sync-core": "^2.4.6",
|
||||
"@tldraw/sync": "^3.4.1",
|
||||
"@tldraw/sync-core": "^3.4.1",
|
||||
"@tldraw/tldraw": "^3.4.1",
|
||||
"@tldraw/tlschema": "^2.4.6",
|
||||
"@tldraw/tlschema": "^3.4.1",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"@whereby.com/browser-sdk": "^3.9.2",
|
||||
"cloudflare-workers-unfurl": "^0.0.7",
|
||||
"crdts": "^0.2.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
|
|
@ -35,13 +34,13 @@
|
|||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"tldraw": "^2.4.6",
|
||||
"tldraw": "^3.4.1",
|
||||
"use-local-storage-state": "^19.5.0",
|
||||
"vercel": "^39.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.4.1",
|
||||
"@cloudflare/types": "^6.29.1",
|
||||
"@cloudflare/types": "^6.0.0",
|
||||
"@cloudflare/workers-types": "^4.20240821.1",
|
||||
"@types/lodash.throttle": "^4",
|
||||
"@types/react": "^18.2.15",
|
||||
|
|
|
|||
44
src/App.tsx
44
src/App.tsx
|
|
@ -16,10 +16,12 @@ import { Inbox } from './components/Inbox';
|
|||
import { Books } from './components/Books';
|
||||
import {
|
||||
BindingUtil,
|
||||
Editor,
|
||||
IndexKey,
|
||||
TLBaseBinding,
|
||||
TLBaseShape,
|
||||
Tldraw,
|
||||
TLShapeId,
|
||||
} from 'tldraw';
|
||||
import { components, uiOverrides } from './ui-overrides';
|
||||
import { ChatBoxShape } from './shapes/ChatBoxShapeUtil';
|
||||
|
|
@ -58,12 +60,12 @@ 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
|
||||
shapeUtils={customShapeUtils}
|
||||
tools={customTools}
|
||||
overrides={uiOverrides}
|
||||
components={components}
|
||||
onMount={(editor) => {
|
||||
handleInitialShapeLoad(editor);
|
||||
editor.createShape({ type: 'my-interactive-shape', x: 100, y: 100 });
|
||||
}}
|
||||
/>
|
||||
|
|
@ -71,7 +73,41 @@ export default function InteractiveShapeExample() {
|
|||
);
|
||||
}
|
||||
|
||||
// ... existing code ...
|
||||
// Add this function before or after InteractiveShapeExample
|
||||
const handleInitialShapeLoad = (editor: Editor) => {
|
||||
const url = new URL(window.location.href);
|
||||
const shapeId = url.searchParams.get('shapeId') || url.searchParams.get('frameId');
|
||||
const x = url.searchParams.get('x');
|
||||
const y = url.searchParams.get('y');
|
||||
const zoom = url.searchParams.get('zoom');
|
||||
|
||||
if (shapeId) {
|
||||
console.log('Found shapeId in URL:', shapeId);
|
||||
const shape = editor.getShape(shapeId as TLShapeId);
|
||||
|
||||
if (shape) {
|
||||
console.log('Found shape:', shape);
|
||||
if (x && y && zoom) {
|
||||
console.log('Setting camera to:', { x, y, zoom });
|
||||
editor.setCamera({
|
||||
x: parseFloat(x),
|
||||
y: parseFloat(y),
|
||||
z: parseFloat(zoom)
|
||||
});
|
||||
} else {
|
||||
console.log('Zooming to shape bounds');
|
||||
editor.zoomToBounds(editor.getShapeGeometry(shape).bounds, {
|
||||
targetZoom: 1,
|
||||
//padding: 32
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn('Shape not found in the editor');
|
||||
}
|
||||
} else {
|
||||
console.warn('No shapeId found in the URL');
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import {
|
|||
TLBookmarkAsset,
|
||||
TLRecord,
|
||||
Tldraw,
|
||||
Editor,
|
||||
TLFrameShape,
|
||||
TLUiEventSource,
|
||||
} from 'tldraw'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import useLocalStorageState from 'use-local-storage-state'
|
||||
|
|
@ -20,6 +23,7 @@ import { EmbedTool } from '@/tools/EmbedTool'
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { ChatBox } from '@/shapes/ChatBoxShapeUtil';
|
||||
import { components, uiOverrides } from '@/ui-overrides'
|
||||
import { useCameraControls } from '@/hooks/useCameraControls'
|
||||
|
||||
//const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev`
|
||||
export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev';
|
||||
|
|
@ -37,16 +41,44 @@ export function Board() {
|
|||
const { slug } = useParams<{ slug: string }>();
|
||||
const roomId = slug || 'default-room';
|
||||
const { store } = usePersistentBoard(roomId);
|
||||
const [editor, setEditor] = useState<Editor | null>(null)
|
||||
const { zoomToFrame, copyFrameLink, copyLocationLink } = useCameraControls(editor)
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw
|
||||
store={store}
|
||||
shapeUtils={shapeUtils}
|
||||
overrides={uiOverrides}
|
||||
overrides={{
|
||||
...uiOverrides,
|
||||
tools: (_editor, baseTools) => ({
|
||||
...baseTools,
|
||||
frame: {
|
||||
...baseTools.frame,
|
||||
contextMenu: (shape: TLFrameShape) => [
|
||||
{
|
||||
id: 'copy-frame-link',
|
||||
label: 'Copy Frame Link',
|
||||
onSelect: () => copyFrameLink(shape.id),
|
||||
},
|
||||
{
|
||||
id: 'zoom-to-frame',
|
||||
label: 'Zoom to Frame',
|
||||
onSelect: () => zoomToFrame(shape.id),
|
||||
},
|
||||
{
|
||||
id: 'copy-location-link',
|
||||
label: 'Copy Location Link',
|
||||
onSelect: () => copyLocationLink(),
|
||||
}
|
||||
]
|
||||
},
|
||||
})
|
||||
}}
|
||||
components={components}
|
||||
tools={tools}
|
||||
onMount={(editor) => {
|
||||
setEditor(editor)
|
||||
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
|
||||
editor.setCurrentTool('hand')
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const components: TLUiComponents = {
|
|||
PageMenu: null,
|
||||
NavigationPanel: null,
|
||||
DebugMenu: null,
|
||||
ContextMenu: null,
|
||||
//ContextMenu: null,
|
||||
ActionsMenu: null,
|
||||
QuickActions: null,
|
||||
MainMenu: null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Editor, TLFrameShape, TLParentId } from 'tldraw';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export function useCameraControls(editor: Editor | null) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const frameId = searchParams.get('frameId');
|
||||
const x = searchParams.get('x');
|
||||
const y = searchParams.get('y');
|
||||
const zoom = searchParams.get('zoom');
|
||||
|
||||
console.log('Loading camera position:', { frameId, x, y, zoom });
|
||||
|
||||
if (x && y && zoom) {
|
||||
editor.setCamera({
|
||||
x: parseFloat(x),
|
||||
y: parseFloat(y),
|
||||
z: parseFloat(zoom)
|
||||
});
|
||||
console.log('Camera position set from URL params');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frameId) return;
|
||||
|
||||
const frame = editor.getShape(frameId as TLParentId) as TLFrameShape;
|
||||
if (!frame) {
|
||||
console.warn('Frame not found:', frameId);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.zoomToBounds(
|
||||
editor.getShapePageBounds(frame)!,
|
||||
{
|
||||
inset: 32,
|
||||
targetZoom: editor.getCamera().z,
|
||||
}
|
||||
);
|
||||
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.set('frameId', frameId);
|
||||
window.history.replaceState(null, '', newUrl.toString());
|
||||
}, [editor, searchParams]);
|
||||
|
||||
const copyLocationLink = () => {
|
||||
if (!editor) return;
|
||||
const camera = editor.getCamera();
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('x', camera.x.toString());
|
||||
url.searchParams.set('y', camera.y.toString());
|
||||
url.searchParams.set('zoom', camera.z.toString());
|
||||
console.log('Copying location link:', url.toString());
|
||||
navigator.clipboard.writeText(url.toString());
|
||||
};
|
||||
|
||||
const zoomToFrame = (frameId: string) => {
|
||||
if (!editor) return;
|
||||
|
||||
const frame = editor.getShape(frameId as TLParentId) as TLFrameShape;
|
||||
if (!frame) {
|
||||
console.warn('Frame not found:', frameId);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.zoomToBounds(
|
||||
editor.getShapePageBounds(frame)!,
|
||||
{
|
||||
inset: 32,
|
||||
targetZoom: editor.getCamera().z,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const copyFrameLink = (frameId: string) => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('frameId', frameId);
|
||||
console.log('Copying frame link:', url.toString());
|
||||
navigator.clipboard.writeText(url.toString());
|
||||
};
|
||||
|
||||
return {
|
||||
zoomToFrame,
|
||||
copyFrameLink,
|
||||
copyLocationLink
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import { TLRecord, createTLStore, SerializedStore, Editor, StoreSchema, TLStoreProps } from '@tldraw/tldraw';
|
||||
import { customSchema } from '../../worker/TldrawDurableObject';
|
||||
import { useMemo, useCallback, useEffect, useState } from 'react';
|
||||
import { useSync } from '@tldraw/sync';
|
||||
import { WORKER_URL } from '../components/Board';
|
||||
import { TLRecord as TLSchemaRecord } from '@tldraw/tlschema'
|
||||
import { defaultAssetUrls } from '@tldraw/assets'
|
||||
|
||||
const CACHE_VERSION = '1.0';
|
||||
|
||||
export function useLocalStorageRoom(roomId: string) {
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
const storageKey = `tldraw_board_${roomId}_v${CACHE_VERSION}`;
|
||||
|
||||
const [records, setRecords] = useLocalStorageState<SerializedStore<TLRecord>>(storageKey, {
|
||||
defaultValue: createTLStore({
|
||||
schema: customSchema as unknown as StoreSchema<TLRecord, unknown>
|
||||
}).serialize()
|
||||
});
|
||||
|
||||
// Create a persistent store
|
||||
const baseStore = useMemo(() => {
|
||||
return createTLStore({
|
||||
schema: customSchema as unknown as StoreSchema<TLRecord, unknown>,
|
||||
initialData: records,
|
||||
})
|
||||
}, [records]);
|
||||
|
||||
// Use sync with the base store
|
||||
const syncedStore = useSync({
|
||||
uri: `${WORKER_URL.replace('https://', 'wss://')}/connect/${roomId}`,
|
||||
schema: customSchema,
|
||||
store: baseStore,
|
||||
assets: defaultAssetUrls
|
||||
});
|
||||
|
||||
// Handle online/offline transitions
|
||||
useEffect(() => {
|
||||
const handleOnline = () => {
|
||||
setIsOnline(true);
|
||||
if (syncedStore?.store) {
|
||||
const filteredRecords = filterNonCameraRecords(records);
|
||||
syncedStore.store.mergeRemoteChanges(() => {
|
||||
Object.values(filteredRecords).forEach(record => {
|
||||
syncedStore.store.put([record as unknown as TLSchemaRecord]);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOffline = () => setIsOnline(false);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, [records, syncedStore?.store]);
|
||||
|
||||
const filterNonCameraRecords = (data: SerializedStore<TLRecord>) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(data).filter(([_, record]) => {
|
||||
return (record as TLRecord).typeName !== 'camera' &&
|
||||
(record as TLRecord).typeName !== 'instance_page_state' &&
|
||||
(record as TLRecord).typeName !== 'instance_presence';
|
||||
})
|
||||
) as SerializedStore<TLRecord>;
|
||||
};
|
||||
|
||||
// Sync with server store when online
|
||||
useEffect(() => {
|
||||
if (!isOnline || !syncedStore?.store) return;
|
||||
|
||||
const syncInterval = setInterval(() => {
|
||||
const serverRecords = syncedStore.store.allRecords();
|
||||
if (Object.keys(serverRecords).length > 0) {
|
||||
setRecords(syncedStore.store.serialize() as typeof records);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(syncInterval);
|
||||
}, [isOnline, syncedStore?.store, setRecords]);
|
||||
|
||||
const store = useMemo(() => {
|
||||
if (isOnline && syncedStore?.store) {
|
||||
return syncedStore.store;
|
||||
}
|
||||
return createTLStore({
|
||||
schema: customSchema as unknown as StoreSchema<TLRecord, unknown>,
|
||||
initialData: records,
|
||||
});
|
||||
}, [isOnline, syncedStore?.store, records]);
|
||||
|
||||
return {
|
||||
store,
|
||||
records,
|
||||
setRecords,
|
||||
isOnline
|
||||
};
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/';
|
||||
import { WORKER_URL } from '../components/Board';
|
||||
|
||||
export type IVideoChatShape = TLBaseShape<
|
||||
'VideoChat',
|
||||
|
|
@ -13,11 +12,13 @@ export type IVideoChatShape = TLBaseShape<
|
|||
}
|
||||
>;
|
||||
|
||||
const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key
|
||||
|
||||
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||
static override type = 'VideoChat';
|
||||
|
||||
indicator(_shape: IVideoChatShape) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDefaultProps(): IVideoChatShape['props'] {
|
||||
return {
|
||||
roomUrl: null,
|
||||
|
|
@ -27,55 +28,34 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
};
|
||||
}
|
||||
|
||||
indicator(shape: IVideoChatShape) {
|
||||
return <rect x={0} y={0} width={shape.props.w} height={shape.props.h} />;
|
||||
}
|
||||
|
||||
async ensureRoomExists(shape: IVideoChatShape) {
|
||||
|
||||
if (shape.props.roomUrl !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiryDate = new Date(Date.now() + 1000 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const response = await fetch(`${CORS_PROXY}https://api.whereby.dev/v1/meetings`, {
|
||||
const response = await fetch(`${WORKER_URL}/daily/rooms`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WHEREBY_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest', // Required by some CORS proxies
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
isLocked: false,
|
||||
roomMode: 'normal',
|
||||
endDate: expiryDate.toISOString(),
|
||||
fields: ['hostRoomUrl'],
|
||||
}),
|
||||
}).catch((error) => {
|
||||
console.error('Failed to create meeting:', error);
|
||||
throw error;
|
||||
properties: {
|
||||
enable_recording: true,
|
||||
max_participants: 8
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
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<IVideoChatShape>({
|
||||
id: shape.id,
|
||||
type: 'VideoChat',
|
||||
props: {
|
||||
...shape.props,
|
||||
roomUrl
|
||||
roomUrl: (data as any).url
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
component(shape: IVideoChatShape) {
|
||||
|
|
@ -84,34 +64,26 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Load the Whereby SDK only in the browser
|
||||
if (typeof window !== 'undefined') {
|
||||
import("@whereby.com/browser-sdk/embed").then(() => {
|
||||
joinRoom();
|
||||
}).catch(err => {
|
||||
console.error("Error loading Whereby SDK:", err);
|
||||
setError("Failed to load video chat component.");
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
if (isInRoom && shape.props.roomUrl) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://www.daily.co/static/call-machine.js';
|
||||
document.body.appendChild(script);
|
||||
|
||||
const joinRoom = async () => {
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await this.ensureRoomExists(shape);
|
||||
setIsInRoom(true);
|
||||
} catch (e) {
|
||||
console.error("Error joining room:", e);
|
||||
setError("An error occurred. Please try again.");
|
||||
script.onload = () => {
|
||||
// @ts-ignore
|
||||
window.DailyIframe.createFrame({
|
||||
iframeStyle: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: '0',
|
||||
borderRadius: '4px'
|
||||
},
|
||||
showLeaveButton: true,
|
||||
showFullscreenButton: true
|
||||
}).join({ url: shape.props.roomUrl });
|
||||
};
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const leaveRoom = () => {
|
||||
setIsInRoom(false);
|
||||
// setRoomUrl(""); // Clear the room URL
|
||||
};
|
||||
}, [isInRoom, shape.props.roomUrl]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
|
|
@ -122,45 +94,25 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
top: '10px',
|
||||
left: '10px',
|
||||
zIndex: 9999,
|
||||
padding: '15px', // Increased padding by 5px
|
||||
margin: 0,
|
||||
backgroundColor: '#F0F0F0', // Light gray background
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', // Added drop shadow
|
||||
borderRadius: '4px', // Slight border radius for softer look
|
||||
padding: '15px',
|
||||
backgroundColor: '#F0F0F0',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
borderRadius: '4px',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: '1px solid #D3D3D3',
|
||||
backgroundColor: '#FFFFFF',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{isLoading ? (
|
||||
<p>Joining room...</p>
|
||||
) : isInRoom && shape.props.roomUrl && typeof window !== 'undefined' ? (
|
||||
<div className="mb-4" style={{ width: '100%', height: '100%', objectFit: 'contain' }}>
|
||||
<whereby-embed
|
||||
room={shape.props.roomUrl}
|
||||
background="off"
|
||||
logo="off"
|
||||
chat="off"
|
||||
screenshare="on"
|
||||
people="on"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
></whereby-embed>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<button onClick={joinRoom} className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||
Join Room
|
||||
</button>
|
||||
{error && <p className="text-red-500 mt-2">{error}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isInRoom ? (
|
||||
<button
|
||||
onClick={() => setIsInRoom(true)}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Join Room
|
||||
</button>
|
||||
) : (
|
||||
<div id="daily-call-iframe-container" style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}} />
|
||||
)}
|
||||
{error && <p className="text-red-500 mt-2">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,277 @@ import {
|
|||
TLComponents,
|
||||
TLUiOverrides,
|
||||
TldrawUiMenuItem,
|
||||
useIsToolSelected,
|
||||
useEditor,
|
||||
useTools,
|
||||
TLShapeId,
|
||||
DefaultContextMenu,
|
||||
DefaultContextMenuContent,
|
||||
TLUiContextMenuProps,
|
||||
TldrawUiMenuGroup,
|
||||
TLShape,
|
||||
} from 'tldraw'
|
||||
import { CustomMainMenu } from './components/CustomMainMenu'
|
||||
import { EmbedShape } from './shapes/EmbedShapeUtil'
|
||||
import { Editor } from 'tldraw'
|
||||
|
||||
let cameraHistory: { x: number; y: number; z: number }[] = [];
|
||||
const MAX_HISTORY = 10; // Keep last 10 camera positions
|
||||
|
||||
// Helper function to store camera position
|
||||
const storeCameraPosition = (editor: Editor) => {
|
||||
const currentCamera = editor.getCamera();
|
||||
// Only store if there's a meaningful change from the last position
|
||||
const lastPosition = cameraHistory[cameraHistory.length - 1];
|
||||
if (!lastPosition ||
|
||||
Math.abs(lastPosition.x - currentCamera.x) > 1 ||
|
||||
Math.abs(lastPosition.y - currentCamera.y) > 1 ||
|
||||
Math.abs(lastPosition.z - currentCamera.z) > 0.1) {
|
||||
|
||||
cameraHistory.push({ ...currentCamera });
|
||||
if (cameraHistory.length > MAX_HISTORY) {
|
||||
cameraHistory.shift();
|
||||
}
|
||||
console.log('Stored camera position:', currentCamera);
|
||||
}
|
||||
};
|
||||
|
||||
const copyFrameLink = async (editor: Editor, frameId: string) => {
|
||||
console.log('Starting copyFrameLink with frameId:', frameId);
|
||||
|
||||
if (!editor.store.getSnapshot()) {
|
||||
console.warn('Store not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = `${window.location.origin}${window.location.pathname}`;
|
||||
console.log('Base URL:', baseUrl);
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
url.searchParams.set('frameId', frameId);
|
||||
|
||||
const frame = editor.getShape(frameId as TLShapeId);
|
||||
console.log('Found frame:', frame);
|
||||
|
||||
if (frame) {
|
||||
const camera = editor.getCamera();
|
||||
console.log('Camera position:', { x: camera.x, y: camera.y, zoom: camera.z });
|
||||
|
||||
url.searchParams.set('x', camera.x.toString());
|
||||
url.searchParams.set('y', camera.y.toString());
|
||||
url.searchParams.set('zoom', camera.z.toString());
|
||||
}
|
||||
|
||||
const finalUrl = url.toString();
|
||||
console.log('Final URL to copy:', finalUrl);
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
console.log('Using modern clipboard API...');
|
||||
await navigator.clipboard.writeText(finalUrl);
|
||||
console.log('URL copied successfully using clipboard API');
|
||||
} else {
|
||||
console.log('Falling back to legacy clipboard method...');
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = finalUrl;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
console.log('URL copied successfully using fallback method');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to copy to clipboard:', error);
|
||||
alert('Failed to copy link. Please check clipboard permissions.');
|
||||
}
|
||||
};
|
||||
|
||||
const zoomToShape = (editor: Editor) => {
|
||||
// Store camera position before zooming
|
||||
storeCameraPosition(editor);
|
||||
|
||||
// Get all selected shape IDs
|
||||
const selectedIds = editor.getSelectedShapeIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
// Get the common bounds that encompass all selected shapes
|
||||
const commonBounds = editor.getSelectionPageBounds();
|
||||
if (!commonBounds) return;
|
||||
|
||||
// Calculate viewport dimensions
|
||||
const viewportPageBounds = editor.getViewportPageBounds();
|
||||
|
||||
// Calculate the ratio of selection size to viewport size
|
||||
const widthRatio = commonBounds.width / viewportPageBounds.width;
|
||||
const heightRatio = commonBounds.height / viewportPageBounds.height;
|
||||
|
||||
// Calculate target zoom based on selection size
|
||||
let targetZoom;
|
||||
if (widthRatio < 0.1 || heightRatio < 0.1) {
|
||||
// For very small selections, zoom in up to 8x
|
||||
targetZoom = Math.min(
|
||||
(viewportPageBounds.width * 0.8) / commonBounds.width,
|
||||
(viewportPageBounds.height * 0.8) / commonBounds.height,
|
||||
8 // Max zoom of 8x for small selections
|
||||
);
|
||||
} else if (widthRatio > 1 || heightRatio > 1) {
|
||||
// For selections larger than viewport, zoom out more
|
||||
targetZoom = Math.min(
|
||||
(viewportPageBounds.width * 0.7) / commonBounds.width,
|
||||
(viewportPageBounds.height * 0.7) / commonBounds.height,
|
||||
0.125 // Min zoom of 1/8x for large selections (reciprocal of 8)
|
||||
);
|
||||
} else {
|
||||
// For medium-sized selections, allow up to 4x zoom
|
||||
targetZoom = Math.min(
|
||||
(viewportPageBounds.width * 0.8) / commonBounds.width,
|
||||
(viewportPageBounds.height * 0.8) / commonBounds.height,
|
||||
4 // Medium zoom level
|
||||
);
|
||||
}
|
||||
|
||||
// Zoom to the common bounds
|
||||
editor.zoomToBounds(commonBounds, {
|
||||
targetZoom,
|
||||
inset: widthRatio > 1 || heightRatio > 1 ? 20 : 50, // Less padding for large selections
|
||||
animation: {
|
||||
duration: 400,
|
||||
easing: (t) => t * (2 - t)
|
||||
}
|
||||
});
|
||||
|
||||
// Update URL with new camera position and first selected shape ID
|
||||
const newCamera = editor.getCamera();
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('shapeId', selectedIds[0].toString());
|
||||
url.searchParams.set('x', newCamera.x.toString());
|
||||
url.searchParams.set('y', newCamera.y.toString());
|
||||
url.searchParams.set('zoom', newCamera.z.toString());
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
};
|
||||
|
||||
const copyLinkToCurrentView = async (editor: Editor) => {
|
||||
console.log('Starting copyLinkToCurrentView');
|
||||
|
||||
if (!editor.store.getSnapshot()) {
|
||||
console.warn('Store not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = `${window.location.origin}${window.location.pathname}`;
|
||||
console.log('Base URL:', baseUrl);
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
const camera = editor.getCamera();
|
||||
console.log('Current camera position:', { x: camera.x, y: camera.y, zoom: camera.z });
|
||||
|
||||
// Set camera parameters
|
||||
url.searchParams.set('x', camera.x.toString());
|
||||
url.searchParams.set('y', camera.y.toString());
|
||||
url.searchParams.set('zoom', camera.z.toString());
|
||||
|
||||
const finalUrl = url.toString();
|
||||
console.log('Final URL to copy:', finalUrl);
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
console.log('Using modern clipboard API...');
|
||||
await navigator.clipboard.writeText(finalUrl);
|
||||
console.log('URL copied successfully using clipboard API');
|
||||
} else {
|
||||
console.log('Falling back to legacy clipboard method...');
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = finalUrl;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
console.log('URL copied successfully using fallback method');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to copy to clipboard:', error);
|
||||
alert('Failed to copy link. Please check clipboard permissions.');
|
||||
}
|
||||
};
|
||||
|
||||
const revertCamera = (editor: Editor) => {
|
||||
if (cameraHistory.length > 0) {
|
||||
const previousCamera = cameraHistory.pop();
|
||||
if (previousCamera) {
|
||||
// Get current viewport bounds
|
||||
const viewportPageBounds = editor.getViewportPageBounds();
|
||||
|
||||
// Create bounds that center on the previous camera position
|
||||
const targetBounds = {
|
||||
x: previousCamera.x - (viewportPageBounds.width / 2) / previousCamera.z,
|
||||
y: previousCamera.y - (viewportPageBounds.height / 2) / previousCamera.z,
|
||||
w: viewportPageBounds.width / previousCamera.z,
|
||||
h: viewportPageBounds.height / previousCamera.z,
|
||||
};
|
||||
|
||||
// Use the same zoom animation as zoomToShape
|
||||
editor.zoomToBounds(targetBounds, {
|
||||
targetZoom: previousCamera.z,
|
||||
animation: {
|
||||
duration: 400,
|
||||
easing: (t) => t * (2 - t)
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Reverted to camera position:', previousCamera);
|
||||
}
|
||||
} else {
|
||||
console.log('No camera history available');
|
||||
}
|
||||
};
|
||||
|
||||
function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||
const editor = useEditor()
|
||||
const hasSelection = editor.getSelectedShapeIds().length > 0
|
||||
const selectedShape = editor.getSelectedShapes()[0]
|
||||
const hasCameraHistory = cameraHistory.length > 0
|
||||
|
||||
return (
|
||||
<DefaultContextMenu {...props}>
|
||||
<TldrawUiMenuGroup id="camera-actions">
|
||||
<TldrawUiMenuItem
|
||||
id="revert-camera"
|
||||
label="Revert Camera"
|
||||
icon="undo"
|
||||
kbd="b"
|
||||
readonlyOk
|
||||
disabled={!hasCameraHistory}
|
||||
onSelect={() => {
|
||||
console.log('Reverting camera');
|
||||
revertCamera(editor);
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="zoom-to-shape"
|
||||
label="Zoom to Selection"
|
||||
icon="zoom-in"
|
||||
kbd="z"
|
||||
readonlyOk
|
||||
disabled={!hasSelection}
|
||||
onSelect={() => {
|
||||
console.log('Zoom to Selection clicked');
|
||||
zoomToShape(editor);
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="copy-link-to-current-view"
|
||||
label="Copy Link to Current View"
|
||||
icon="link"
|
||||
kbd="c"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
console.log('Copy Link to Current View clicked');
|
||||
copyLinkToCurrentView(editor);
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultContextMenuContent />
|
||||
</DefaultContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const uiOverrides: TLUiOverrides = {
|
||||
tools(editor, tools) {
|
||||
|
|
@ -17,6 +283,7 @@ export const uiOverrides: TLUiOverrides = {
|
|||
icon: 'color',
|
||||
label: 'Video',
|
||||
kbd: 'x',
|
||||
meta: {},
|
||||
onSelect: () => {
|
||||
editor.setCurrentTool('VideoChat')
|
||||
},
|
||||
|
|
@ -26,6 +293,7 @@ export const uiOverrides: TLUiOverrides = {
|
|||
icon: 'color',
|
||||
label: 'Chat',
|
||||
kbd: 'x',
|
||||
meta: {},
|
||||
onSelect: () => {
|
||||
editor.setCurrentTool('ChatBox')
|
||||
},
|
||||
|
|
@ -35,28 +303,140 @@ export const uiOverrides: TLUiOverrides = {
|
|||
icon: 'embed',
|
||||
label: 'Embed',
|
||||
kbd: 'e',
|
||||
meta: {},
|
||||
onSelect: () => {
|
||||
editor.setCurrentTool('Embed')
|
||||
},
|
||||
}
|
||||
return tools
|
||||
},
|
||||
actions(editor, actions) {
|
||||
actions['copyFrameLink'] = {
|
||||
id: 'copy-frame-link',
|
||||
label: 'Copy Frame Link',
|
||||
onSelect: () => {
|
||||
const shape = editor.getSelectedShapes()[0]
|
||||
if (shape && shape.type === 'frame') {
|
||||
copyFrameLink(editor, shape.id)
|
||||
}
|
||||
},
|
||||
readonlyOk: true,
|
||||
}
|
||||
|
||||
actions['zoomToFrame'] = {
|
||||
id: 'zoom-to-frame',
|
||||
label: 'Zoom to Frame',
|
||||
onSelect: () => {
|
||||
const shape = editor.getSelectedShapes()[0]
|
||||
if (shape && shape.type === 'frame') {
|
||||
zoomToShape(editor)
|
||||
}
|
||||
},
|
||||
readonlyOk: true,
|
||||
}
|
||||
|
||||
actions['copyLinkToCurrentView'] = {
|
||||
id: 'copy-link-to-current-view',
|
||||
label: 'Copy Link to Current View',
|
||||
kbd: 'c',
|
||||
onSelect: () => {
|
||||
console.log('Creating link to current view');
|
||||
copyLinkToCurrentView(editor);
|
||||
},
|
||||
readonlyOk: true,
|
||||
}
|
||||
|
||||
actions['zoomToShape'] = {
|
||||
id: 'zoom-to-shape',
|
||||
label: 'Zoom to Selection',
|
||||
kbd: 'z',
|
||||
onSelect: () => {
|
||||
if (editor.getSelectedShapeIds().length > 0) {
|
||||
console.log('Zooming to selection');
|
||||
zoomToShape(editor);
|
||||
}
|
||||
},
|
||||
readonlyOk: true,
|
||||
}
|
||||
|
||||
actions['revertCamera'] = {
|
||||
id: 'revert-camera',
|
||||
label: 'Revert Camera',
|
||||
kbd: 'b',
|
||||
onSelect: () => {
|
||||
console.log('Reverting camera position');
|
||||
revertCamera(editor);
|
||||
},
|
||||
readonlyOk: true,
|
||||
}
|
||||
|
||||
return actions
|
||||
},
|
||||
}
|
||||
|
||||
export const components: TLComponents = {
|
||||
Toolbar: (props) => {
|
||||
Toolbar: function Toolbar() {
|
||||
const editor = useEditor()
|
||||
const tools = useTools()
|
||||
const isChatBoxSelected = useIsToolSelected(tools['ChatBox'])
|
||||
const isVideoSelected = useIsToolSelected(tools['VideoChat'])
|
||||
const isEmbedSelected = useIsToolSelected(tools['Embed'])
|
||||
return (
|
||||
<DefaultToolbar {...props}>
|
||||
<TldrawUiMenuItem {...tools['VideoChat']} isSelected={isVideoSelected} />
|
||||
<TldrawUiMenuItem {...tools['ChatBox']} isSelected={isChatBoxSelected} />
|
||||
<TldrawUiMenuItem {...tools['Embed']} isSelected={isEmbedSelected} />
|
||||
<DefaultToolbar>
|
||||
{tools['VideoChat'] && (
|
||||
<TldrawUiMenuItem
|
||||
{...tools['VideoChat']}
|
||||
isSelected={tools['VideoChat'].id === editor.getCurrentToolId()}
|
||||
/>
|
||||
)}
|
||||
{tools['ChatBox'] && (
|
||||
<TldrawUiMenuItem
|
||||
{...tools['ChatBox']}
|
||||
isSelected={tools['ChatBox'].id === editor.getCurrentToolId()}
|
||||
/>
|
||||
)}
|
||||
{tools['Embed'] && (
|
||||
<TldrawUiMenuItem
|
||||
{...tools['Embed']}
|
||||
isSelected={tools['Embed'].id === editor.getCurrentToolId()}
|
||||
/>
|
||||
)}
|
||||
<DefaultToolbarContent />
|
||||
</DefaultToolbar>
|
||||
)
|
||||
},
|
||||
MainMenu: CustomMainMenu,
|
||||
}
|
||||
ContextMenu: CustomContextMenu,
|
||||
}
|
||||
|
||||
const handleInitialShapeLoad = (editor: Editor) => {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// Check for both shapeId and legacy frameId (for backwards compatibility)
|
||||
const shapeId = url.searchParams.get('shapeId') || url.searchParams.get('frameId');
|
||||
const x = url.searchParams.get('x');
|
||||
const y = url.searchParams.get('y');
|
||||
const zoom = url.searchParams.get('zoom');
|
||||
|
||||
if (shapeId) {
|
||||
console.log('Found shapeId in URL:', shapeId);
|
||||
const shape = editor.getShape(shapeId as TLShapeId);
|
||||
|
||||
if (shape) {
|
||||
console.log('Found shape:', shape);
|
||||
if (x && y && zoom) {
|
||||
console.log('Setting camera to:', { x, y, zoom });
|
||||
editor.setCamera({
|
||||
x: parseFloat(x),
|
||||
y: parseFloat(y),
|
||||
z: parseFloat(zoom)
|
||||
});
|
||||
} else {
|
||||
console.log('Zooming to shape bounds');
|
||||
editor.zoomToBounds(editor.getShapeGeometry(shape).bounds, {
|
||||
targetZoom: 1,
|
||||
//padding: 32
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn('Shape not found:', shapeId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -5,4 +5,6 @@
|
|||
export interface Environment {
|
||||
TLDRAW_BUCKET: R2Bucket
|
||||
TLDRAW_DURABLE_OBJECT: DurableObjectNamespace
|
||||
DAILY_API_KEY: string;
|
||||
DAILY_DOMAIN: string;
|
||||
}
|
||||
|
|
@ -109,5 +109,24 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
|||
})
|
||||
})
|
||||
|
||||
.post('/daily/rooms', async (request, env) => {
|
||||
const response = await fetch('https://api.daily.co/v1/rooms', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${env.DAILY_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: await request.text()
|
||||
});
|
||||
|
||||
const data = await response.json() as Record<string, unknown>;
|
||||
return new Response(JSON.stringify({
|
||||
...data,
|
||||
url: `https://${env.DAILY_DOMAIN}/${data.name}`
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
})
|
||||
|
||||
// export our router for cloudflare
|
||||
export default router
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
main = "worker/worker.ts"
|
||||
compatibility_date = "2024-07-01"
|
||||
name = "jeffemmett-canvas"
|
||||
account_id = "0e7b3338d5278ed1b148e6456b940913"
|
||||
zone_id = "45c200f8dc2a01852e41b9bb09eb7359"
|
||||
account_id = "${CLOUDFLARE_ACCOUNT_ID}"
|
||||
zone_id = "${CLOUDFLARE_ZONE_ID}"
|
||||
|
||||
[vars]
|
||||
TLDRAW_WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
||||
DAILY_API_KEY = "${DAILY_API_KEY}"
|
||||
DAILY_DOMAIN = "${DAILY_DOMAIN}"
|
||||
TLDRAW_WORKER_URL = "${TLDRAW_WORKER_URL}"
|
||||
|
||||
[dev]
|
||||
port = 5172
|
||||
|
|
@ -13,24 +15,20 @@ ip = "0.0.0.0"
|
|||
local_protocol = "http"
|
||||
upstream_protocol = "https"
|
||||
|
||||
# Set up the durable object used for each tldraw room
|
||||
[durable_objects]
|
||||
bindings = [
|
||||
{ name = "TLDRAW_DURABLE_OBJECT", class_name = "TldrawDurableObject" },
|
||||
]
|
||||
|
||||
# Durable objects require migrations to create/modify/delete them
|
||||
[[migrations]]
|
||||
tag = "v1"
|
||||
new_classes = ["TldrawDurableObject"]
|
||||
|
||||
# We store rooms and asset uploads in an R2 bucket
|
||||
[[r2_buckets]]
|
||||
binding = 'TLDRAW_BUCKET'
|
||||
bucket_name = 'jeffemmett-canvas'
|
||||
preview_bucket_name = 'jeffemmett-canvas-preview'
|
||||
bucket_name = '${R2_BUCKET_NAME}'
|
||||
preview_bucket_name = '${R2_PREVIEW_BUCKET_NAME}'
|
||||
|
||||
# wrangler.toml (wrangler v3.79.0^)
|
||||
[observability]
|
||||
enabled = true
|
||||
head_sampling_rate = 1
|
||||
head_sampling_rate = 1
|
||||
Loading…
Reference in New Issue