import { useState, useCallback, useRef, useEffect } from 'react'; const MAX_HISTORY = 50; const DEBOUNCE_DELAY_MS = 300; export function useDesignEditor() { const [elements, setElements] = useState([]); const [selectedId, setSelectedId] = useState(null); // History for undo/redo const historyRef = useRef([]); const historyIndexRef = useRef(-1); // Debounce timer for rapid changes (drag/transform) const historyTimerRef = useRef(null); const pendingChangesRef = useRef(null); const saveToHistory = useCallback((newElements) => { // Remove any future history if we're in the middle of the stack if (historyIndexRef.current < historyRef.current.length - 1) { historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1); } // Add new state to history historyRef.current.push(JSON.stringify(newElements)); // Limit history size if (historyRef.current.length > MAX_HISTORY) { historyRef.current.shift(); } else { historyIndexRef.current++; } }, []); // Flush pending changes to history const flushPendingChanges = useCallback(() => { if (pendingChangesRef.current) { saveToHistory(pendingChangesRef.current); pendingChangesRef.current = null; } if (historyTimerRef.current) { clearTimeout(historyTimerRef.current); historyTimerRef.current = null; } }, [saveToHistory]); // Cleanup timer on unmount useEffect(() => { return () => { if (historyTimerRef.current) { clearTimeout(historyTimerRef.current); } }; }, []); const canUndo = historyIndexRef.current > 0; const canRedo = historyIndexRef.current < historyRef.current.length - 1; const addElement = useCallback((element) => { // Flush any pending debounced changes first flushPendingChanges(); const newElement = { ...element, id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, }; setElements((prev) => { const newElements = [...prev, newElement]; saveToHistory(newElements); return newElements; }); setSelectedId(newElement.id); return newElement.id; }, [flushPendingChanges, saveToHistory]); const updateElement = useCallback((id, attrs) => { setElements((prev) => { const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el)); // Debounce history commits for rapid changes (drag/transform) // Store pending changes but don't commit yet pendingChangesRef.current = newElements; // Clear existing timer if (historyTimerRef.current) { clearTimeout(historyTimerRef.current); } // Set timer to commit changes after delay historyTimerRef.current = setTimeout(() => { flushPendingChanges(); }, DEBOUNCE_DELAY_MS); return newElements; }); }, [flushPendingChanges]); const deleteElement = useCallback((id) => { // Flush any pending debounced changes first flushPendingChanges(); setElements((prev) => { const newElements = prev.filter((el) => el.id !== id); saveToHistory(newElements); return newElements; }); if (selectedId === id) { setSelectedId(null); } }, [selectedId, flushPendingChanges, saveToHistory]); const selectElement = useCallback((id) => { setSelectedId(id); }, []); const deselectAll = useCallback(() => { setSelectedId(null); }, []); const reorderElement = useCallback((id, newOrder) => { // Flush any pending debounced changes first flushPendingChanges(); setElements((prev) => { const index = prev.findIndex((el) => el.id === id); if (index === -1 || index === newOrder) return prev; const newElements = [...prev]; const [removed] = newElements.splice(index, 1); newElements.splice(newOrder, 0, removed); saveToHistory(newElements); return newElements; }); }, [flushPendingChanges, saveToHistory]); // Commit history immediately (called on dragEnd/transformEnd) const commitHistory = useCallback(() => { flushPendingChanges(); }, [flushPendingChanges]); const undo = useCallback(() => { if (historyIndexRef.current > 0) { historyIndexRef.current--; const prevState = JSON.parse(historyRef.current[historyIndexRef.current]); setElements(prevState); setSelectedId(null); } }, []); const redo = useCallback(() => { if (historyIndexRef.current < historyRef.current.length - 1) { historyIndexRef.current++; const nextState = JSON.parse(historyRef.current[historyIndexRef.current]); setElements(nextState); setSelectedId(null); } }, []); // Initialize history with empty state const initializeHistory = useCallback(() => { historyRef.current = [JSON.stringify([])]; historyIndexRef.current = 0; }, []); return { elements, selectedId, addElement, updateElement, deleteElement, selectElement, deselectAll, reorderElement, commitHistory, // Call this on dragEnd/transformEnd to commit debounced changes undo, redo, canUndo, canRedo, initializeHistory, }; }