canvas-website/src/shapes/EmbedShapeUtil.tsx

139 lines
4.8 KiB
TypeScript

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')) {
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]);
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>
);
}
}