'use client' import dynamic from 'next/dynamic' import { useState, useEffect, useRef, useMemo } from 'react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' import { Download, Presentation } from 'lucide-react' import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types' import type { AppState, BinaryFiles } from '@excalidraw/excalidraw/types' import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types' import '@excalidraw/excalidraw/index.css' const Excalidraw = dynamic( async () => (await import('@excalidraw/excalidraw')).Excalidraw, { ssr: false } ) interface CanvasBoardProps { initialData?: string canvasId?: string name: string } type PptxPayload = { type: string; filename: string; base64: string; slideCount?: number; theme?: string } function parseCanvasScene(initialData?: string): { pptx: PptxPayload | null elements: readonly ExcalidrawElement[] files: BinaryFiles } { if (!initialData) { return { pptx: null, elements: [], files: {} } } try { const parsed = JSON.parse(initialData) if (parsed && parsed.type === 'pptx' && parsed.base64) { return { pptx: parsed as PptxPayload, elements: [], files: {} } } if (parsed && Array.isArray(parsed)) { return { pptx: null, elements: parsed as ExcalidrawElement[], files: {} } } if (parsed && parsed.elements) { const files: BinaryFiles = parsed.files && typeof parsed.files === 'object' ? parsed.files : {} return { pptx: null, elements: parsed.elements as readonly ExcalidrawElement[], files } } } catch (e) { console.error('[CanvasBoard] Failed to parse canvas data:', e) } return { pptx: null, elements: [], files: {} } } function PptxViewer({ data, name }: { data: PptxPayload; name: string }) { const handleDownload = () => { try { const byteCharacters = atob(data.base64) const byteNumbers = new Array(byteCharacters.length) for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i) } const byteArray = new Uint8Array(byteNumbers) const blob = new Blob([byteArray], { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = data.filename || `${name}.pptx` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } catch (e) { console.error('[PptxViewer] Download failed:', e) toast.error('Failed to download presentation') } } return (

{name}

PowerPoint Presentation {data.slideCount ? ` • ${data.slideCount} slides` : ''} {data.theme ? ` • ${data.theme} theme` : ''}

This file can be opened in Microsoft PowerPoint, Google Slides, or Keynote.

) } export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) { const [isDarkMode, setIsDarkMode] = useState(false) const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'error'>('saved') const [localId, setLocalId] = useState(canvasId || null) const router = useRouter() const saveTimeoutRef = useRef(null) const excalidrawAPIRef = useRef(null) const filesRef = useRef({}) const scene = useMemo( () => parseCanvasScene(initialData), [canvasId, initialData] ) useEffect(() => { filesRef.current = scene.files }, [scene]) useEffect(() => { const checkDarkMode = () => { const isDark = document.documentElement.classList.contains('dark') setIsDarkMode(isDark) } checkDarkMode() const observer = new MutationObserver(checkDarkMode) observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) return () => observer.disconnect() }, []) const handleChange = ( excalidrawElements: readonly ExcalidrawElement[], _appState: AppState, files: BinaryFiles ) => { if (files) filesRef.current = files if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current) setSaveStatus('saving') saveTimeoutRef.current = setTimeout(async () => { try { const snapshot = JSON.stringify({ elements: excalidrawElements, files: filesRef.current }) const res = await fetch('/api/canvas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: localId || null, name, data: snapshot }) }) const data = await res.json() if (data.success && data.canvas?.id) { if (!localId) { setLocalId(data.canvas.id) router.replace(`/lab?id=${data.canvas.id}`, { scroll: false }) } setSaveStatus('saved') } else { throw new Error(data.error || 'Failed to save') } } catch (e) { console.error('[CanvasBoard] Save failure:', e) setSaveStatus('error') } }, 2000) } if (scene.pptx) { return } const excalKey = localId ? `excal-${localId}` : 'excal-new' return (
{ excalidrawAPIRef.current = api }} initialData={{ elements: scene.elements, files: scene.files }} theme={isDarkMode ? 'dark' : 'light'} onChange={handleChange} UIOptions={{ canvasActions: { loadScene: true, saveToActiveFile: false, clearCanvas: true, } }} validateEmbeddable={false} />
) }