fixed a bunch of stuff

This commit is contained in:
Jeff Emmett 2024-10-19 23:21:42 -04:00
parent 4e2103aab2
commit 4eff918bd3
6 changed files with 194 additions and 55 deletions

View File

@ -4,6 +4,8 @@ import {
getHashForString, getHashForString,
TLBookmarkAsset, TLBookmarkAsset,
Tldraw, Tldraw,
TLUiMenuGroup,
TLUiOverrides,
} from 'tldraw' } from 'tldraw'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { ChatBoxTool } from '@/tools/ChatBoxTool' import { ChatBoxTool } from '@/tools/ChatBoxTool'
@ -43,16 +45,59 @@ export function Board() {
setUserName(event.target.value); setUserName(event.target.value);
}; };
const customUiOverrides: TLUiOverrides = {
...uiOverrides,
contextMenu: (editor, contextMenuSchema, helpers) => {
const defaultContextMenu = uiOverrides.contextMenu ? uiOverrides.contextMenu(editor, contextMenuSchema, helpers) : contextMenuSchema
const newContextMenu: TLUiMenuGroup[] = [
...defaultContextMenu,
{
id: 'external-link',
type: 'group',
checkbox: false,
disabled: false,
readonlyOk: true,
children: [
{
id: 'add-external-link',
type: 'item',
readonlyOk: true,
label: 'Add External Link',
icon: 'link',
onSelect: () => {
const selectedShapes = editor.getSelectedShapes()
if (selectedShapes.length === 1) {
const shape = selectedShapes[0]
const externalUrl = `${window.location.origin}/board/${roomId}?shapeId=${shape.id}`
// Here you can implement the logic to copy the link to clipboard or show it to the user
console.log('External link:', externalUrl)
// For example, to copy to clipboard:
navigator.clipboard.writeText(externalUrl).then(() => {
editor.setToast({ id: 'external-link-copied', title: 'External link copied to clipboard' })
})
}
},
},
],
},
]
return newContextMenu
},
}
return ( return (
<div style={{ position: 'fixed', inset: 0 }}> <div style={{ position: 'fixed', inset: 0 }}>
<Tldraw <Tldraw
store={store} store={store}
shapeUtils={shapeUtils} shapeUtils={shapeUtils}
overrides={uiOverrides} overrides={customUiOverrides}
components={components} components={components}
tools={tools} tools={tools}
onMount={(editor) => { onMount={(editor) => {
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
editor.setCurrentTool('hand')
}} }}
/> />
{isChatBoxVisible && ( {isChatBoxVisible && (

View File

@ -29,7 +29,7 @@ export function Default() {
<h2>Get in touch</h2> <h2>Get in touch</h2>
<p> <p>
I am on Twitter <a href="https://twitter.com/OrionReedOne">@OrionReedOne</a>, I am on Twitter <a href="https://twitter.com/jeffemmett">@jeffemmett</a>,
Mastodon <a href="https://hci.social/@orion">@orion@hci.social</a> and GitHub <a href="https://github.com/orionreed">@orionreed</a>. You can also shoot me an email <a href="mailto:me@orionreed.com">me@orionreed.com</a> Mastodon <a href="https://hci.social/@orion">@orion@hci.social</a> and GitHub <a href="https://github.com/orionreed">@orionreed</a>. You can also shoot me an email <a href="mailto:me@orionreed.com">me@orionreed.com</a>
</p> </p>

View File

@ -94,7 +94,7 @@ export const ChatBox: React.FC<IChatBoxShape['props']> = ({ roomId, w, h, userNa
}; };
return ( return (
<div className="chat-container" style={{ pointerEvents: 'all', width: `${w}px`, height: `${h}px`, overflow: 'auto' }}> <div className="chat-container" style={{ pointerEvents: 'all', width: `${w}px`, height: `${h}px`, overflow: 'auto', touchAction: 'auto' }}>
<div className="messages-container"> <div className="messages-container">
{messages.map((msg) => ( {messages.map((msg) => (
<div key={msg.id} className={`message ${msg.username === username ? 'own-message' : ''}`}> <div key={msg.id} className={`message ${msg.username === username ? 'own-message' : ''}`}>
@ -114,8 +114,17 @@ export const ChatBox: React.FC<IChatBoxShape['props']> = ({ roomId, w, h, userNa
onChange={(e) => setInputMessage(e.target.value)} onChange={(e) => setInputMessage(e.target.value)}
placeholder="Type a message..." placeholder="Type a message..."
className="message-input" className="message-input"
style={{ touchAction: 'manipulation' }}
/> />
<button type="submit" style={{ pointerEvents: 'all', }} onPointerDown={(e) => e.stopPropagation()} className="send-button">Send</button> <button
type="submit"
style={{ pointerEvents: 'all', touchAction: 'manipulation' }}
onPointerDown={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
className="send-button"
>
Send
</button>
</form> </form>
</div> </div>
); );

View File

@ -25,39 +25,113 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
return ( return (
<g> <g>
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} /> <rect x={0} y={0} width={shape.props.w} height={shape.props.h} />
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} style={{ stroke: 'black', strokeWidth: 2, fill: 'none' }} />
</g> </g>
); );
} }
component(shape: IEmbedShape) { component(shape: IEmbedShape) {
const [inputUrl, setInputUrl] = useState(shape.props.url || ''); const [inputUrl, setInputUrl] = useState(shape.props.url || '');
const [error, setError] = useState('');
const handleSubmit = useCallback((e: React.FormEvent) => { const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
this.editor.updateShape<IEmbedShape>({ id: shape.id, type: 'Embed', props: { ...shape.props, url: inputUrl } }); 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')) {
const docId = completedUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1];
if (docId) {
completedUrl = `https://docs.google.com/document/d/${docId}/preview`;
} else {
setError('Invalid Google Docs URL');
return;
}
}
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]); }, [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) { if (!shape.props.url) {
return ( return (
<div style={{ pointerEvents: 'all', border: '1px solid #000', borderRadius: '5px', padding: '5px' }}> <div style={wrapperStyle}>
<form onSubmit={handleSubmit}> <div style={contentStyle} onClick={() => document.querySelector('input')?.focus()}>
<form onSubmit={handleSubmit} style={{ width: '100%', height: '100%', padding: '10px' }}>
<input <input
type="text" type="text"
value={inputUrl} value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)} onChange={(e) => setInputUrl(e.target.value)}
placeholder="Enter URL" placeholder="Enter URL"
style={{ width: shape.props.w, height: shape.props.h }} style={{ width: '100%', height: '100%', border: 'none', padding: '10px' }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSubmit(e);
}
}}
/> />
<button type="submit" onTouchStart={handleSubmit} onClick={handleSubmit}>Load</button> {error && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
</form> </form>
</div> </div>
</div>
); );
} }
return ( return (
<div style={{ pointerEvents: 'all' }}> <div style={wrapperStyle}>
<iframe src={shape.props.url} width={shape.props.w} height={shape.props.h} /> <div style={contentStyle}>
<iframe
src={shape.props.url}
width="100%"
height="100%"
style={{ border: 'none' }}
allowFullScreen
/>
</div>
</div> </div>
); );
} }

View File

@ -14,7 +14,6 @@ export type IVideoChatShape = TLBaseShape<
>; >;
const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key
// const ROOM_PREFIX = 'test'
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> { export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
static override type = 'VideoChat'; static override type = 'VideoChat';
@ -34,8 +33,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
async ensureRoomExists(shape: IVideoChatShape) { async ensureRoomExists(shape: IVideoChatShape) {
console.log('This is your roomUrl 1:', shape.props.roomUrl);
if (shape.props.roomUrl !== null) { if (shape.props.roomUrl !== null) {
return; return;
} }
@ -45,14 +42,12 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
const response = await fetch(`${CORS_PROXY}https://api.whereby.dev/v1/meetings`, { const response = await fetch(`${CORS_PROXY}https://api.whereby.dev/v1/meetings`, {
method: 'POST', method: 'POST',
headers: { headers: {
// 'Access-Control-Allow-Origin': 'https://jeffemmett.com/',
'Authorization': `Bearer ${WHEREBY_API_KEY}`, 'Authorization': `Bearer ${WHEREBY_API_KEY}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', // Required by some CORS proxies 'X-Requested-With': 'XMLHttpRequest', // Required by some CORS proxies
}, },
body: JSON.stringify({ body: JSON.stringify({
isLocked: false, isLocked: false,
// roomNamePrefix: ROOM_PREFIX,
roomMode: 'normal', roomMode: 'normal',
endDate: expiryDate.toISOString(), endDate: expiryDate.toISOString(),
fields: ['hostRoomUrl'], fields: ['hostRoomUrl'],
@ -62,10 +57,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
throw error; throw error;
}); });
console.log('This is your response:', response);
console.log('This is your roomUrl 2:', shape.props.roomUrl);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
console.error('Whereby API error:', errorData); console.error('Whereby API error:', errorData);
@ -85,8 +76,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
roomUrl roomUrl
} }
}) })
} }
component(shape: IVideoChatShape) { component(shape: IVideoChatShape) {
@ -107,7 +96,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
}, []); }, []);
const joinRoom = async () => { const joinRoom = async () => {
// this.ensureRoomExists(shape);
setError(""); setError("");
setIsLoading(true); setIsLoading(true);
try { try {
@ -126,11 +114,34 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
}; };
return ( return (
<div className="p-4" style={{ pointerEvents: 'all', width: '100%', height: '100%' }}> <div style={{
pointerEvents: 'all',
width: `${shape.props.w}px`,
height: `${shape.props.h}px`,
position: 'absolute',
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
}}>
<div style={{
width: '100%',
height: '100%',
border: '1px solid #D3D3D3',
backgroundColor: '#FFFFFF',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
}}>
{isLoading ? ( {isLoading ? (
<p>Joining room...</p> <p>Joining room...</p>
) : isInRoom && shape.props.roomUrl && typeof window !== 'undefined' ? ( ) : isInRoom && shape.props.roomUrl && typeof window !== 'undefined' ? (
<div className="mb-4" style={{ width: '100%', height: '100%' }}> <div className="mb-4" style={{ width: '100%', height: '100%', objectFit: 'contain' }}>
<whereby-embed <whereby-embed
room={shape.props.roomUrl} room={shape.props.roomUrl}
background="off" background="off"
@ -150,6 +161,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
</div> </div>
)} )}
</div> </div>
</div>
); );
} }
} }

View File

@ -43,7 +43,6 @@ export class TldrawDurableObject {
private readonly ctx: DurableObjectState, private readonly ctx: DurableObjectState,
env: Environment env: Environment
) { ) {
console.log("hello from durable object")
this.r2 = env.TLDRAW_BUCKET this.r2 = env.TLDRAW_BUCKET
ctx.blockConcurrencyWhile(async () => { ctx.blockConcurrencyWhile(async () => {