diff --git a/client/src/App.jsx b/client/src/App.jsx index 5b205d6..7bb8b82 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -13,16 +13,45 @@ function App() { deleteElement, selectElement, deselectAll, + undo, + redo, + canUndo, + canRedo, + initializeHistory, } = useDesignEditor(); const selectedElement = elements.find((el) => el.id === selectedId); - // Keyboard shortcut: Delete/Backspace removes selected element + // Initialize history on mount + useEffect(() => { + initializeHistory(); + }, [initializeHistory]); + + // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } + + // Undo: Ctrl/Cmd + Z + if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { + e.preventDefault(); + if (canUndo) undo(); + return; + } + + // Redo: Ctrl/Cmd + Shift + Z or Ctrl/Cmd + Y + if ((e.ctrlKey || e.metaKey) && ( + (e.key === 'z' && e.shiftKey) || + e.key === 'y' + )) { + e.preventDefault(); + if (canRedo) redo(); + return; + } + + // Delete/Backspace removes selected element if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedId) { deleteElement(selectedId); @@ -32,7 +61,7 @@ function App() { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedId, deleteElement]); + }, [selectedId, deleteElement, undo, redo, canUndo, canRedo]); const handleUpload = (data) => { if (data.preview?.url) { @@ -82,8 +111,30 @@ function App() { {/* Center Canvas */}
-

Apparel Designer

-

T-shirt customization editor

+
+
+

Apparel Designer

+

T-shirt customization editor

+
+
+ + +
+
{ + // 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++; + } + }, []); + + const canUndo = historyIndexRef.current > 0; + const canRedo = historyIndexRef.current < historyRef.current.length - 1; + const addElement = useCallback((element) => { const newElement = { ...element, id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, }; - setElements((prev) => [...prev, newElement]); + + setElements((prev) => { + const newElements = [...prev, newElement]; + saveToHistory(newElements); + return newElements; + }); + setSelectedId(newElement.id); return newElement.id; - }, []); + }, [saveToHistory]); const updateElement = useCallback((id, attrs) => { - setElements((prev) => - prev.map((el) => (el.id === id ? { ...el, ...attrs } : el)) - ); - }, []); + setElements((prev) => { + const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el)); + saveToHistory(newElements); + return newElements; + }); + }, [saveToHistory]); const deleteElement = useCallback((id) => { - setElements((prev) => prev.filter((el) => el.id !== id)); + setElements((prev) => { + const newElements = prev.filter((el) => el.id !== id); + saveToHistory(newElements); + return newElements; + }); + if (selectedId === id) { setSelectedId(null); } - }, [selectedId]); + }, [selectedId, saveToHistory]); const selectElement = useCallback((id) => { setSelectedId(id); @@ -39,11 +78,38 @@ export function useDesignEditor() { 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; }); + }, [saveToHistory]); + + 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 { @@ -55,5 +121,10 @@ export function useDesignEditor() { selectElement, deselectAll, reorderElement, + undo, + redo, + canUndo, + canRedo, + initializeHistory, }; } diff --git a/client/src/index.css b/client/src/index.css index af023d6..e2513c5 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -102,16 +102,57 @@ input, textarea, select { .app-title { font-size: 1.5rem; font-weight: 600; - margin: 0 0 0.5rem 0; + margin: 0 0 0.25rem 0; color: var(--text-primary); } .app-subtitle { color: var(--text-secondary); - margin: 0 0 1.5rem 0; + margin: 0; font-size: 0.875rem; } +.canvas-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + width: 100%; + max-width: 400px; +} + +.undo-redo-buttons { + display: flex; + gap: 0.5rem; +} + +.icon-btn { + padding: 0.5rem 0.75rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; + color: var(--text-secondary); +} + +.icon-btn:hover:not(:disabled) { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.icon-btn.disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.icon-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + .canvas-wrapper { margin-bottom: 1rem; }