'use client'; import { useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { RotateCcw } from 'lucide-react'; import { useWhiteboardHistoryStore } from '@/lib/store/whiteboard-history'; import { useStageStore } from '@/lib/store'; import { useCanvasStore } from '@/lib/store/canvas'; import { createStageAPI } from '@/lib/api/stage-api'; import { elementFingerprint } from '@/lib/utils/element-fingerprint'; import { toast } from 'sonner'; import { useI18n } from '@/lib/hooks/use-i18n'; interface WhiteboardHistoryProps { readonly isOpen: boolean; readonly onClose: () => void; } /** * Whiteboard history dropdown panel. * Shows a list of saved whiteboard snapshots with timestamps and element counts. * Clicking "Restore" replaces the current whiteboard content with the snapshot. */ export function WhiteboardHistory({ isOpen, onClose }: WhiteboardHistoryProps) { const { t } = useI18n(); const snapshots = useWhiteboardHistoryStore((s) => s.snapshots); const isClearing = useCanvasStore.use.whiteboardClearing(); const panelRef = useRef(null); // Close on outside click useEffect(() => { if (!isOpen) return; const handler = (e: MouseEvent) => { if (panelRef.current && !panelRef.current.contains(e.target as Node)) { onClose(); } }; // Delay listener so the click that opens the panel doesn't immediately close it const id = setTimeout(() => document.addEventListener('mousedown', handler), 0); return () => { clearTimeout(id); document.removeEventListener('mousedown', handler); }; }, [isOpen, onClose]); const handleRestore = (index: number) => { // P1: Block restore while a clear animation is in flight — the pending // delete/update would overwrite the restored content moments later. if (isClearing) { toast.error(t('whiteboard.restoreError')); return; } const snapshot = useWhiteboardHistoryStore.getState().getSnapshot(index); if (!snapshot) return; const stageStore = useStageStore; const stageAPI = createStageAPI(stageStore); // Get or create whiteboard const wbResult = stageAPI.whiteboard.get(); if (!wbResult.success || !wbResult.data) { return; } const whiteboardId = wbResult.data.id; // P2a: Skip no-op restores — if the snapshot matches what's already // on screen, restoring would be a no-op. const restoredElementsKey = snapshot.fingerprint; const currentKey = elementFingerprint(wbResult.data.elements ?? []); if (restoredElementsKey === currentKey) { toast.success(t('whiteboard.restored')); onClose(); return; } // Save current content before overwriting so the user can undo the restore const currentElements = wbResult.data.elements ?? []; if (currentElements.length > 0) { useWhiteboardHistoryStore.getState().pushSnapshot(currentElements); } // Transactional restore: replace all elements in one update() call // instead of looping delete/add which produces intermediate states. const result = stageAPI.whiteboard.update({ elements: snapshot.elements }, whiteboardId); if (!result.success) { console.error('Failed to restore whiteboard snapshot:', result.error); // P3: Dedicated restoreError key (not clearError) toast.error(t('whiteboard.restoreError') + (result.error ?? '')); return; } toast.success(t('whiteboard.restored')); onClose(); }; const formatTime = (ts: number) => { const d = new Date(ts); return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}:${d.getSeconds().toString().padStart(2, '0')}`; }; return ( {isOpen && ( {/* Header */}
{t('whiteboard.history')} {snapshots.length > 0 ? `${snapshots.length}` : ''}
{/* Snapshot list */}
{snapshots.length === 0 ? (
{t('whiteboard.noHistory')}
) : (
{[...snapshots].reverse().map((snap, reverseIdx) => { const realIdx = snapshots.length - 1 - reverseIdx; return (
{`#${realIdx + 1}`}
{formatTime(snap.timestamp)} ·{' '} {t('whiteboard.elementCount', { count: snap.elements.length })}
); })}
)}
)}
); }