checkpoint before google auth
This commit is contained in:
parent
4719128d40
commit
66b59b2fea
|
|
@ -171,4 +171,5 @@ dist
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.\*
|
.pnp.\*
|
||||||
|
|
||||||
.wrangler/
|
.wrangler/
|
||||||
|
.*.md
|
||||||
|
|
@ -53,6 +53,6 @@
|
||||||
"vite-plugin-static-copy": "^1.0.6",
|
"vite-plugin-static-copy": "^1.0.6",
|
||||||
"vite-plugin-top-level-await": "^1.3.1",
|
"vite-plugin-top-level-await": "^1.3.1",
|
||||||
"vite-plugin-wasm": "^3.2.2",
|
"vite-plugin-wasm": "^3.2.2",
|
||||||
"wrangler": "^3.72.3"
|
"wrangler": "^3.88.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ function Home() {
|
||||||
const shapes = createShapes(elementsInfo)
|
const shapes = createShapes(elementsInfo)
|
||||||
const [isEditorMounted, setIsEditorMounted] = useState(false);
|
const [isEditorMounted, setIsEditorMounted] = useState(false);
|
||||||
|
|
||||||
|
//console.log("THIS WORKS SO FAR")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEditorDidMount = () => {
|
const handleEditorDidMount = () => {
|
||||||
setIsEditorMounted(true);
|
setIsEditorMounted(true);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue