fix(lab): ouvrir le bon diagramme sans F5 (?id alias ?canvas + tri canvases)
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
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'
|
||||
|
||||
// Dynamic import with SSR disabled is REQUIRED for Excalidraw due to window dependencies
|
||||
const Excalidraw = dynamic(
|
||||
async () => (await import('@excalidraw/excalidraw')).Excalidraw,
|
||||
{ ssr: false }
|
||||
@@ -19,34 +20,106 @@ interface CanvasBoardProps {
|
||||
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 (
|
||||
<div className="absolute inset-0 h-full w-full flex flex-col items-center justify-center bg-white dark:bg-zinc-900">
|
||||
<div className="flex flex-col items-center gap-6 p-8">
|
||||
<div className="p-4 rounded-2xl bg-primary/10">
|
||||
<Presentation className="w-16 h-16 text-primary" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-foreground mb-1">{name}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
PowerPoint Presentation
|
||||
{data.slideCount ? ` • ${data.slideCount} slides` : ''}
|
||||
{data.theme ? ` • ${data.theme} theme` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 transition-colors font-medium shadow-sm"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
Download .pptx
|
||||
</button>
|
||||
<p className="text-xs text-muted-foreground max-w-sm text-center">
|
||||
This file can be opened in Microsoft PowerPoint, Google Slides, or Keynote.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
||||
const [isDarkMode, setIsDarkMode] = useState(false)
|
||||
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'error'>('saved')
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const excalidrawAPIRef = useRef<ExcalidrawImperativeAPI | null>(null)
|
||||
const filesRef = useRef<BinaryFiles>({})
|
||||
|
||||
// Parse initial state safely (ONLY ON MOUNT to prevent Next.js revalidation infinite loops)
|
||||
const [elements] = useState<readonly ExcalidrawElement[]>(() => {
|
||||
if (initialData) {
|
||||
try {
|
||||
const parsed = JSON.parse(initialData)
|
||||
if (parsed && Array.isArray(parsed)) {
|
||||
return parsed
|
||||
} else if (parsed && parsed.elements) {
|
||||
// Restore binary files if present
|
||||
if (parsed.files && typeof parsed.files === 'object') {
|
||||
filesRef.current = parsed.files
|
||||
}
|
||||
return parsed.elements
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[CanvasBoard] Failed to parse initial Excalidraw data:", e)
|
||||
}
|
||||
}
|
||||
return []
|
||||
})
|
||||
const scene = useMemo(
|
||||
() => parseCanvasScene(initialData),
|
||||
[canvasId, initialData]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
filesRef.current = scene.files
|
||||
}, [scene])
|
||||
|
||||
// Detect dark mode from html class
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const isDark = document.documentElement.classList.contains('dark')
|
||||
@@ -55,7 +128,6 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
||||
|
||||
checkDarkMode()
|
||||
|
||||
// Observer for theme changes
|
||||
const observer = new MutationObserver(checkDarkMode)
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||
|
||||
@@ -64,10 +136,9 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
||||
|
||||
const handleChange = (
|
||||
excalidrawElements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
_appState: AppState,
|
||||
files: BinaryFiles
|
||||
) => {
|
||||
// Keep files ref up to date so we can include them in the save payload
|
||||
if (files) filesRef.current = files
|
||||
|
||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
||||
@@ -75,7 +146,6 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
||||
setSaveStatus('saving')
|
||||
saveTimeoutRef.current = setTimeout(async () => {
|
||||
try {
|
||||
// Save both elements and binary files so images persist across page changes
|
||||
const snapshot = JSON.stringify({ elements: excalidrawElements, files: filesRef.current })
|
||||
await fetch('/api/canvas', {
|
||||
method: 'POST',
|
||||
@@ -84,26 +154,35 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
||||
})
|
||||
setSaveStatus('saved')
|
||||
} catch (e) {
|
||||
console.error("[CanvasBoard] Save failure:", e)
|
||||
console.error('[CanvasBoard] Save failure:', e)
|
||||
setSaveStatus('error')
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
if (scene.pptx) {
|
||||
return <PptxViewer data={scene.pptx} name={name} />
|
||||
}
|
||||
|
||||
const excalKey = canvasId ? `excal-${canvasId}` : 'excal-new'
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 h-full w-full bg-slate-50 dark:bg-[#121212]" dir="ltr">
|
||||
<Excalidraw
|
||||
initialData={{ elements, files: filesRef.current }}
|
||||
theme={isDarkMode ? "dark" : "light"}
|
||||
key={excalKey}
|
||||
excalidrawAPI={(api) => { excalidrawAPIRef.current = api }}
|
||||
initialData={{ elements: scene.elements, files: scene.files }}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
onChange={handleChange}
|
||||
libraryReturnUrl={typeof window !== 'undefined' ? window.location.origin + window.location.pathname + window.location.search : undefined}
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
loadScene: true,
|
||||
saveToActiveFile: false,
|
||||
clearCanvas: true
|
||||
clearCanvas: true,
|
||||
}
|
||||
}}
|
||||
validateEmbeddable={false}
|
||||
renderTopRightUI={() => null}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user