Add multi-cart UI, image caching, copy/paste, and mobile layout fixes
- Multi-cart system with create, rename, delete, import/export - Sections with copy/paste items via clipboard JSON - IndexedDB image cache with deduplication and unused-image pruning - Migrate existing localStorage base64 images to IndexedDB on load - Mobile-responsive header and cart bar - Template row with pill buttons replacing dropdown select - Cart rename via double-click inline edit - Storage quota error handling with user-facing banner - Updated README and page title/description
This commit is contained in:
+39
-8
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
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'
|
||||
@@ -18,8 +19,16 @@ function loadActiveId(carts: Cart[]): string | null {
|
||||
return carts[0]?.id ?? null
|
||||
}
|
||||
|
||||
function saveCarts(carts: Cart[]) {
|
||||
localStorage.setItem(CARTS_KEY, JSON.stringify(carts))
|
||||
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() {
|
||||
@@ -29,12 +38,32 @@ export function useCarts() {
|
||||
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<string | null>(null)
|
||||
|
||||
const activeCart = carts.find(c => c.id === activeId) ?? null
|
||||
|
||||
function mutateCarts(fn: (carts: Cart[]) => Cart[]) {
|
||||
setCarts(prev => {
|
||||
const next = fn(prev)
|
||||
saveCarts(next)
|
||||
pruneImageCache(next) // fire-and-forget async
|
||||
const err = saveCarts(next)
|
||||
setStorageError(err)
|
||||
return next
|
||||
})
|
||||
}
|
||||
@@ -81,11 +110,11 @@ export function useCarts() {
|
||||
localStorage.setItem(ACTIVE_KEY, id)
|
||||
}
|
||||
|
||||
function importCart(json: string): boolean {
|
||||
async function importCart(json: string): Promise<boolean> {
|
||||
try {
|
||||
const cart = JSON.parse(json) as Cart
|
||||
if (!cart.id || !cart.name || !Array.isArray(cart.sections)) return false
|
||||
const imported = { ...cart, id: crypto.randomUUID() }
|
||||
const imported = await cacheImagesInCart({ ...cart, id: crypto.randomUUID() })
|
||||
mutateCarts(prev => [...prev, imported])
|
||||
setActiveCart(imported.id)
|
||||
return true
|
||||
@@ -94,9 +123,10 @@ export function useCarts() {
|
||||
}
|
||||
}
|
||||
|
||||
function exportCart() {
|
||||
async function exportCart() {
|
||||
if (!activeCart) return
|
||||
const blob = new Blob([JSON.stringify(activeCart, null, 2)], { type: 'application/json' })
|
||||
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
|
||||
@@ -180,6 +210,7 @@ export function useCarts() {
|
||||
carts,
|
||||
activeCart,
|
||||
activeId,
|
||||
storageError,
|
||||
setActiveCart,
|
||||
createCart,
|
||||
deleteCart,
|
||||
|
||||
Reference in New Issue
Block a user