import { useEffect, useState } from 'react' import type { Cart, CartTemplate, Section, Product, Drone } from '../types' import { cacheImagesInCart, resolveImagesInCart, pruneImageCache } from '../utils/imageCache' const CARTS_KEY = 'carts' const ACTIVE_KEY = 'active-cart-id' function loadCarts(): Cart[] { try { const raw = localStorage.getItem(CARTS_KEY) if (raw) return JSON.parse(raw) as Cart[] } catch {} return [] } function loadActiveId(carts: Cart[]): string | null { const stored = localStorage.getItem(ACTIVE_KEY) if (stored && carts.find(c => c.id === stored)) return stored return carts[0]?.id ?? null } function saveCarts(carts: Cart[]): string | null { try { localStorage.setItem(CARTS_KEY, JSON.stringify(carts)) return null } catch (e) { if (e instanceof DOMException && e.name === 'QuotaExceededError') { return 'Storage quota exceeded — images may be too large. Try removing unused images.' } return 'Failed to save carts.' } } export function useCarts() { const [carts, setCarts] = useState(() => loadCarts()) const [activeId, setActiveId] = useState(() => { const c = loadCarts() return loadActiveId(c) }) // Migrate any inline base64 images to IndexedDB on first load useEffect(() => { const loaded = loadCarts() const needsMigration = loaded.some(cart => cart.sections.some(s => s.items.some(item => (item.image && !item.image.startsWith('img:')) || ('parts' in item && item.parts.some(p => p.image && !p.image.startsWith('img:'))) )) ) if (!needsMigration) return Promise.all(loaded.map(cacheImagesInCart)).then(migrated => { pruneImageCache(migrated) saveCarts(migrated) setCarts(migrated) }) }, []) const [storageError, setStorageError] = useState(null) const activeCart = carts.find(c => c.id === activeId) ?? null function mutateCarts(fn: (carts: Cart[]) => Cart[]) { setCarts(prev => { const next = fn(prev) pruneImageCache(next) // fire-and-forget async const err = saveCarts(next) setStorageError(err) return next }) } function mutateActiveCart(fn: (cart: Cart) => Cart) { mutateCarts(carts => carts.map(c => c.id === activeId ? fn(c) : c)) } // ── Cart management ──────────────────────────────────────────────────────── function createCart(name: string, template?: CartTemplate): string { const id = crypto.randomUUID() const cart: Cart = { id, name, createdAt: new Date().toISOString().slice(0, 10), templateId: template?.id, sections: template ? structuredClone(template.sections) : [], } mutateCarts(prev => [...prev, cart]) setActiveCart(id) return id } function deleteCart(id: string) { mutateCarts(prev => { const next = prev.filter(c => c.id !== id) if (activeId === id) { const newActive = next[0]?.id ?? null setActiveId(newActive) if (newActive) localStorage.setItem(ACTIVE_KEY, newActive) else localStorage.removeItem(ACTIVE_KEY) } return next }) } function renameCart(id: string, name: string) { mutateCarts(carts => carts.map(c => c.id === id ? { ...c, name } : c)) } function setActiveCart(id: string) { setActiveId(id) localStorage.setItem(ACTIVE_KEY, id) } async function importCart(json: string): Promise { try { const cart = JSON.parse(json) as Cart if (!cart.id || !cart.name || !Array.isArray(cart.sections)) return false const imported = await cacheImagesInCart({ ...cart, id: crypto.randomUUID() }) mutateCarts(prev => [...prev, imported]) setActiveCart(imported.id) return true } catch { return false } } async function exportCart() { if (!activeCart) return const resolved = await resolveImagesInCart(activeCart) const blob = new Blob([JSON.stringify(resolved, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${activeCart.name.toLowerCase().replace(/\s+/g, '-')}.json` a.click() URL.revokeObjectURL(url) } // ── Section management ──────────────────────────────────────────────────── function addSection(section: Omit) { mutateActiveCart(cart => ({ ...cart, sections: [...cart.sections, { ...section, items: [] }], })) } function removeSection(sectionId: string) { mutateActiveCart(cart => ({ ...cart, sections: cart.sections.filter(s => s.id !== sectionId), })) } function renameSection(sectionId: string, label: string) { mutateActiveCart(cart => ({ ...cart, sections: cart.sections.map(s => s.id === sectionId ? { ...s, label } : s), })) } function reorderSections(fromId: string, toId: string) { mutateActiveCart(cart => { const sections = [...cart.sections] const fromIdx = sections.findIndex(s => s.id === fromId) const toIdx = sections.findIndex(s => s.id === toId) if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return cart const [moved] = sections.splice(fromIdx, 1) sections.splice(toIdx, 0, moved) return { ...cart, sections } }) } // ── Item management ─────────────────────────────────────────────────────── function addItem(sectionId: string, item: Product | Drone) { mutateActiveCart(cart => ({ ...cart, sections: cart.sections.map(s => s.id === sectionId ? { ...s, items: [...s.items, item] } : s ), })) } function updateItem(sectionId: string, item: Product | Drone) { mutateActiveCart(cart => ({ ...cart, sections: cart.sections.map(s => s.id === sectionId ? { ...s, items: s.items.map(i => i.id === item.id ? item : i) } : s ), })) } function removeItem(sectionId: string, itemId: string) { mutateActiveCart(cart => ({ ...cart, sections: cart.sections.map(s => s.id === sectionId ? { ...s, items: s.items.filter(i => i.id !== itemId) } : s ), })) } function copyItemToCart(item: Product | Drone, targetCartId: string, targetSectionId: string) { const copy = { ...structuredClone(item), id: crypto.randomUUID() } mutateCarts(carts => carts.map(c => { if (c.id !== targetCartId) return c const hasSection = c.sections.some(s => s.id === targetSectionId) if (!hasSection) return c return { ...c, sections: c.sections.map(s => s.id === targetSectionId ? { ...s, items: [...s.items, copy] } : s ), } })) } return { carts, activeCart, activeId, storageError, setActiveCart, createCart, deleteCart, renameCart, importCart, exportCart, addSection, removeSection, renameSection, reorderSections, addItem, updateItem, removeItem, copyItemToCart, } }