checkpoint before google auth

This commit is contained in:
Jeff Emmett 2024-11-21 17:00:46 +07:00
parent 4719128d40
commit 66b59b2fea
5 changed files with 288 additions and 3 deletions

3
.gitignore vendored
View File

@ -171,4 +171,5 @@ dist
.yarn/install-state.gz
.pnp.\*
.wrangler/
.wrangler/
.*.md

View File

@ -53,6 +53,6 @@
"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"
"wrangler": "^3.88.0"
}
}
}

View File

@ -98,6 +98,8 @@ function Home() {
const shapes = createShapes(elementsInfo)
const [isEditorMounted, setIsEditorMounted] = useState(false);
//console.log("THIS WORKS SO FAR")
useEffect(() => {
const handleEditorDidMount = () => {
setIsEditorMounted(true);

View File

@ -0,0 +1,123 @@
import { useSync } from '@tldraw/sync'
import {
AssetRecordType,
getHashForString,
TLBookmarkAsset,
Tldraw,
// useLocalStorageState,
} from 'tldraw'
import { useParams } from 'react-router-dom'
import useLocalStorageState from 'use-local-storage-state'
import { ChatBoxTool } from '@/tools/ChatBoxTool'
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 { EmbedShape } from '@/shapes/EmbedShapeUtil'
import { EmbedTool } from '@/tools/EmbedTool'
import React, { useEffect, 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, VideoChatShape, EmbedShape]
const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools
export function Board() {
const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component
const roomId = slug || 'default-room'; // Declare roomId here
const store = useSync({
uri: `${WORKER_URL}/connect/${roomId}`,
assets: multiplayerAssetStore,
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<HTMLInputElement>) => {
setUserName(event.target.value);
};
const [persistedStore, setPersistedStore] = useLocalStorageState('board-store', { defaultValue: store }
)
useEffect(() => {
setPersistedStore(store);
}, [store]);
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
//store={persistedStore}
store={store}
shapeUtils={shapeUtils}
overrides={uiOverrides}
components={components}
tools={tools}
onMount={(editor) => {
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
editor.setCurrentTool('hand')
}}
/>
{isChatBoxVisible && (
<div>
<input
type="text"
value={userName}
onChange={handleNameChange}
placeholder="Enter your name"
/>
<ChatBox
userName={userName}
roomId={roomId} // Added roomId
w={200} // Set appropriate width
h={200} // Set appropriate height
/>
</div>
)}
{isVideoChatVisible && ( // Render the button to join video chat
<button onClick={() => setVideoChatVisible(false)} className="bg-green-500 text-white px-4 py-2 rounded">
Join Video Call
</button>
)}
</div>
)
}
// How does our server handle bookmark unfurling?
async function unfurlBookmarkUrl({ url }: { url: string }): Promise<TLBookmarkAsset> {
const asset: TLBookmarkAsset = {
id: AssetRecordType.createId(getHashForString(url)),
typeName: 'asset',
type: 'bookmark',
meta: {},
props: {
src: url,
description: '',
image: '',
favicon: '',
title: '',
},
}
try {
const response = await fetch(`${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`)
const data = await response.json() as { description: string, image: string, favicon: string, title: string }
asset.props.description = data?.description ?? ''
asset.props.image = data?.image ?? ''
asset.props.favicon = data?.favicon ?? ''
asset.props.title = data?.title ?? ''
} catch (e) {
console.error(e)
}
return asset
}

View File

@ -0,0 +1,159 @@
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw";
import { useCallback, useState } from "react";
export type IEmbedShape = TLBaseShape<
'Embed',
{
w: number;
h: number;
url: string | null;
}
>;
export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
static override type = 'Embed';
getDefaultProps(): IEmbedShape['props'] {
return {
url: null,
w: 640,
h: 480,
};
}
indicator(shape: IEmbedShape) {
return (
<g>
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} />
</g>
);
}
component(shape: IEmbedShape) {
const [inputUrl, setInputUrl] = useState(shape.props.url || '');
const [error, setError] = useState('');
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
let completedUrl = inputUrl.startsWith('http://') || inputUrl.startsWith('https://') ? inputUrl : `https://${inputUrl}`;
// Handle YouTube links
if (completedUrl.includes('youtube.com') || completedUrl.includes('youtu.be')) {
const videoId = extractYouTubeVideoId(completedUrl);
if (videoId) {
completedUrl = `https://www.youtube.com/embed/${videoId}`;
} else {
setError('Invalid YouTube URL');
return;
}
}
// Handle Google Docs links
if (completedUrl.includes('docs.google.com')) {
// Handle different types of Google Docs URLs
if (completedUrl.includes('/document/d/')) {
const docId = completedUrl.match(/\/document\/d\/([a-zA-Z0-9-_]+)/)?.[1];
if (docId) {
completedUrl = `https://docs.google.com/document/d/${docId}/edit`;
} else {
setError('Invalid Google Docs URL');
return;
}
} else if (completedUrl.includes('/spreadsheets/d/')) {
const docId = completedUrl.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/)?.[1];
if (docId) {
completedUrl = `https://docs.google.com/spreadsheets/d/${docId}/edit`;
} else {
setError('Invalid Google Sheets URL');
return;
}
} else if (completedUrl.includes('/presentation/d/')) {
const docId = completedUrl.match(/\/presentation\/d\/([a-zA-Z0-9-_]+)/)?.[1];
if (docId) {
completedUrl = `https://docs.google.com/presentation/d/${docId}/embed`;
} else {
setError('Invalid Google Slides URL');
return;
}
}
// Add parameters for access
completedUrl += '?authuser=0'; // Allow Google authentication
}
this.editor.updateShape<IEmbedShape>({ id: shape.id, type: 'Embed', props: { ...shape.props, url: completedUrl } });
// Check if the URL is valid
const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//);
if (!isValidUrl) {
setError('Invalid website URL');
} else {
setError('');
}
}, [inputUrl]);
const extractYouTubeVideoId = (url: string): string | null => {
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
const match = url.match(regExp);
return (match && match[2].length === 11) ? match[2] : null;
};
const wrapperStyle = {
width: `${shape.props.w}px`,
height: `${shape.props.h}px`,
padding: '15px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
backgroundColor: '#F0F0F0',
borderRadius: '4px',
};
const contentStyle = {
pointerEvents: 'all' as const,
width: '100%',
height: '100%',
border: '1px solid #D3D3D3',
backgroundColor: '#FFFFFF',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
};
if (!shape.props.url) {
return (
<div style={wrapperStyle}>
<div style={contentStyle} onClick={() => document.querySelector('input')?.focus()}>
<form onSubmit={handleSubmit} style={{ width: '100%', height: '100%', padding: '10px' }}>
<input
type="text"
value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)}
placeholder="Enter URL"
style={{ width: '100%', height: '100%', border: 'none', padding: '10px' }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSubmit(e);
}
}}
/>
{error && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
</form>
</div>
</div>
);
}
return (
<div style={wrapperStyle}>
<div style={contentStyle}>
<iframe
src={shape.props.url}
width="100%"
height="100%"
style={{ border: 'none' }}
allowFullScreen
/>
</div>
</div>
);
}
}