import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; import { persist } from 'zustand/middleware'; import { v4 as uuidv4 } from 'uuid'; import type { Snap, CanvasElement, CodeElement, TextElement, ArrowElement, Background, CanvasMeta, } from '../types'; interface HistoryState { past: Snap[]; future: Snap[]; } interface CanvasState { snap: Snap; selectedElementId: string | null; zoom: number; showGrid: boolean; tool: 'select' | 'code' | 'text' | 'arrow'; history: HistoryState; // Actions setSnap: (snap: Snap) => void; updateMeta: (meta: Partial) => void; setBackground: (background: Partial) => void; addElement: (element: CanvasElement) => void; updateElement: (id: string, updates: Partial) => void; deleteElement: (id: string) => void; duplicateElement: (id: string) => void; selectElement: (id: string | null) => void; setZoom: (zoom: number) => void; setShowGrid: (show: boolean) => void; setTool: (tool: 'select' | 'code' | 'text' | 'arrow') => void; moveElementUp: (id: string) => void; moveElementDown: (id: string) => void; undo: () => void; redo: () => void; saveToHistory: () => void; newSnap: (meta: CanvasMeta) => void; exportSnap: () => string; importSnap: (json: string) => void; } const defaultSnap: Snap = { version: '1.0.0', meta: { title: 'Untitled', aspect: '16:9', width: 1920, height: 1080, }, background: { type: 'gradient', solid: { color: '#101022' }, gradient: { from: '#101022', to: '#1f1f3a', angle: 135 }, }, elements: [], }; export const useCanvasStore = create()( persist( immer((set, get) => ({ snap: defaultSnap, selectedElementId: null, zoom: 0.5, showGrid: false, tool: 'select', history: { past: [], future: [] }, setSnap: (snap) => set((state) => { state.snap = snap; }), updateMeta: (meta) => set((state) => { state.snap.meta = { ...state.snap.meta, ...meta }; }), setBackground: (background) => set((state) => { state.snap.background = { ...state.snap.background, ...background }; }), addElement: (element) => set((state) => { get().saveToHistory(); state.snap.elements.push(element); state.selectedElementId = element.id; state.tool = 'select'; }), updateElement: (id, updates) => set((state) => { const index = state.snap.elements.findIndex((el) => el.id === id); if (index !== -1) { state.snap.elements[index] = { ...state.snap.elements[index], ...updates } as CanvasElement; } }), deleteElement: (id) => set((state) => { get().saveToHistory(); state.snap.elements = state.snap.elements.filter((el) => el.id !== id); if (state.selectedElementId === id) { state.selectedElementId = null; } }), duplicateElement: (id) => set((state) => { const element = state.snap.elements.find((el) => el.id === id); if (element) { get().saveToHistory(); const newElement = { ...JSON.parse(JSON.stringify(element)), id: uuidv4(), x: element.x + 20, y: element.y + 20, }; state.snap.elements.push(newElement); state.selectedElementId = newElement.id; } }), selectElement: (id) => set((state) => { state.selectedElementId = id; }), setZoom: (zoom) => set((state) => { state.zoom = Math.max(0.25, Math.min(4, zoom)); }), setShowGrid: (show) => set((state) => { state.showGrid = show; }), setTool: (tool) => set((state) => { state.tool = tool; if (tool !== 'select') { state.selectedElementId = null; } }), moveElementUp: (id) => set((state) => { const index = state.snap.elements.findIndex((el) => el.id === id); if (index < state.snap.elements.length - 1) { const temp = state.snap.elements[index]; state.snap.elements[index] = state.snap.elements[index + 1]; state.snap.elements[index + 1] = temp; } }), moveElementDown: (id) => set((state) => { const index = state.snap.elements.findIndex((el) => el.id === id); if (index > 0) { const temp = state.snap.elements[index]; state.snap.elements[index] = state.snap.elements[index - 1]; state.snap.elements[index - 1] = temp; } }), saveToHistory: () => set((state) => { state.history.past.push(JSON.parse(JSON.stringify(state.snap))); state.history.future = []; if (state.history.past.length > 50) { state.history.past.shift(); } }), undo: () => set((state) => { if (state.history.past.length > 0) { const previous = state.history.past.pop()!; state.history.future.push(JSON.parse(JSON.stringify(state.snap))); state.snap = previous; state.selectedElementId = null; } }), redo: () => set((state) => { if (state.history.future.length > 0) { const next = state.history.future.pop()!; state.history.past.push(JSON.parse(JSON.stringify(state.snap))); state.snap = next; state.selectedElementId = null; } }), newSnap: (meta) => set((state) => { state.snap = { ...defaultSnap, meta: { ...defaultSnap.meta, ...meta }, }; state.selectedElementId = null; state.history = { past: [], future: [] }; }), exportSnap: () => { return JSON.stringify(get().snap, null, 2); }, importSnap: (json) => { try { const snap = JSON.parse(json) as Snap; set((state) => { state.snap = snap; state.selectedElementId = null; state.history = { past: [], future: [] }; }); } catch (e) { console.error('Failed to import snap:', e); } }, })), { name: 'code-canvas-storage', partialize: (state) => ({ snap: state.snap }), } ) ); // Helper functions to create elements export const createCodeElement = (x: number, y: number): CodeElement => ({ id: uuidv4(), type: 'code', x, y, width: 600, height: 300, rotation: 0, locked: false, visible: true, props: { code: '// Your code here\nconsole.log("Hello, World!");', language: 'javascript', theme: 'github-dark', fontFamily: 'JetBrains Mono', fontSize: 14, lineHeight: 1.5, lineNumbers: true, highlights: [], padding: 24, cornerRadius: 12, shadow: { blur: 24, spread: 0, color: 'rgba(0,0,0,0.4)' }, }, }); export const createTextElement = (x: number, y: number): TextElement => ({ id: uuidv4(), type: 'text', x, y, rotation: 0, locked: false, visible: true, props: { text: 'Your text here', fontFamily: 'Inter', fontSize: 24, color: '#ffffff', bold: false, italic: false, underline: false, align: 'left', background: null, padding: 8, cornerRadius: 4, }, }); export const createArrowElement = (x: number, y: number): ArrowElement => ({ id: uuidv4(), type: 'arrow', x: 0, y: 0, rotation: 0, locked: false, visible: true, points: [ { x, y }, { x: x + 150, y: y + 80 }, ], props: { style: 'straight', color: '#60a5fa', thickness: 3, head: 'filled', }, });