Files
apparel-designer/client/src/hooks/useDesignEditor.js
Khalid A 4ca7910465 Phases 7-10: Complete remaining features and optimizations
Phase 7.2 - Debounce undo/redo history:
- Add 300ms debounce timer for rapid drag/transform changes
- Commit history on dragEnd/transformEnd events only
- Prevents history bloat during continuous interactions

Phase 8.3 - Template-aware export:
- Render template background layer first
- Apply slot crop regions for image elements
- Render template overlay layer last
- Support nonPrintable flag for guides/watermarks

Phase 9 - PWA icons:
- Add pwa-192x192.svg and pwa-512x512.svg icons
- Update vite.config.js manifest configuration

Phase 10.3 - Performance optimizations:
- Add React.memo to canvas components (ImageElement, TextElement, DesignCanvas)
- Add React.memo to panel components (LayersPanel, PropertiesPanel)
- Prevent unnecessary re-renders during canvas interactions

Phase 10.6 - Template documentation:
- Document template JSON schema in docs/template-schema.md
- Include element properties, slot definitions, and examples
- Describe background/overlay layer structure
2026-04-21 21:50:33 -05:00

186 lines
5.1 KiB
JavaScript

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,
};
}