Fix localStorage quota by caching images before saving; add part copy button

This commit is contained in:
2026-04-20 01:30:36 +02:00
parent bcda0e7315
commit a771f3993e
5 changed files with 108 additions and 24 deletions
+74 -8
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -67,17 +67,17 @@ export default function App() {
else { localStorage.removeItem(THEME_KEY); document.documentElement.removeAttribute('data-theme') } else { localStorage.removeItem(THEME_KEY); document.documentElement.removeAttribute('data-theme') }
} }
function handleCreateFromTemplate(templateId: string) { async function handleCreateFromTemplate(templateId: string) {
const existing = carts.find(c => c.templateId === templateId) const existing = carts.find(c => c.templateId === templateId)
if (existing) { setActiveCart(existing.id); return } if (existing) { setActiveCart(existing.id); return }
const template = catalog?.templates.find(t => t.id === templateId) const template = catalog?.templates.find(t => t.id === templateId)
if (!template) return if (!template) return
createCart(template.name, template) await createCart(template.name, template)
} }
function handleCreateEmpty() { async function handleCreateEmpty() {
if (!newCartName.trim()) return if (!newCartName.trim()) return
createCart(newCartName.trim()) await createCart(newCartName.trim())
setNewCartName('') setNewCartName('')
setShowNewCart(false) setShowNewCart(false)
} }
+9 -1
View File
@@ -114,7 +114,15 @@ export function ItemCard({ item, onEdit, onRemove }: Props) {
<span className="truncate">{p.name}</span> <span className="truncate">{p.name}</span>
{p.image && <span className="shrink-0 text-xs" style={{ color: 'var(--color-bg3)' }}></span>} {p.image && <span className="shrink-0 text-xs" style={{ color: 'var(--color-bg3)' }}></span>}
</div> </div>
<span className="shrink-0">{fmt(p.price.amount, p.price.currency)}</span> <div className="flex shrink-0 items-center gap-2">
<span>{fmt(p.price.amount, p.price.currency)}</span>
<button
onClick={() => navigator.clipboard.writeText(JSON.stringify(p))}
className="text-xs opacity-50 hover:opacity-100"
style={{ color: 'var(--color-grey)' }}
title="Copy part to clipboard"
></button>
</div>
</li> </li>
))} ))}
</ul> </ul>
+3 -3
View File
@@ -56,11 +56,8 @@ export function SectionBlock({ section, onRemoveItem, onAddItem, onEditItem, onR
return ( return (
<div <div
draggable
onDragStart={onDragStart}
onDragOver={onDragOver} onDragOver={onDragOver}
onDrop={onDrop} onDrop={onDrop}
onDragEnd={onDragEnd}
className="flex flex-col gap-3 rounded-lg border p-4 transition-opacity" className="flex flex-col gap-3 rounded-lg border p-4 transition-opacity"
style={{ style={{
background: 'var(--color-bg1)', background: 'var(--color-bg1)',
@@ -72,6 +69,9 @@ export function SectionBlock({ section, onRemoveItem, onAddItem, onEditItem, onR
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
draggable
onDragStart={onDragStart}
onDragEnd={onDragEnd}
className="cursor-grab select-none text-base leading-none" className="cursor-grab select-none text-base leading-none"
style={{ color: 'var(--color-bg3)' }} style={{ color: 'var(--color-bg3)' }}
title="Drag to reorder" title="Drag to reorder"
+14 -4
View File
@@ -20,8 +20,15 @@ function loadActiveId(carts: Cart[]): string | null {
} }
function saveCarts(carts: Cart[]): string | null { function saveCarts(carts: Cart[]): string | null {
const json = JSON.stringify(carts)
try { try {
localStorage.setItem(CARTS_KEY, JSON.stringify(carts)) localStorage.setItem(CARTS_KEY, json)
return null
} catch {
// Some browsers can't replace a large value in one step — free it first, then retry
try {
localStorage.removeItem(CARTS_KEY)
localStorage.setItem(CARTS_KEY, json)
return null return null
} catch (e) { } catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') { if (e instanceof DOMException && e.name === 'QuotaExceededError') {
@@ -29,6 +36,7 @@ function saveCarts(carts: Cart[]): string | null {
} }
return 'Failed to save carts.' return 'Failed to save carts.'
} }
}
} }
export function useCarts() { export function useCarts() {
@@ -50,7 +58,8 @@ export function useCarts() {
if (!needsMigration) return if (!needsMigration) return
Promise.all(loaded.map(cacheImagesInCart)).then(migrated => { Promise.all(loaded.map(cacheImagesInCart)).then(migrated => {
pruneImageCache(migrated) pruneImageCache(migrated)
saveCarts(migrated) const err = saveCarts(migrated)
setStorageError(err)
setCarts(migrated) setCarts(migrated)
}) })
}, []) }, [])
@@ -74,15 +83,16 @@ export function useCarts() {
// ── Cart management ──────────────────────────────────────────────────────── // ── Cart management ────────────────────────────────────────────────────────
function createCart(name: string, template?: CartTemplate): string { async function createCart(name: string, template?: CartTemplate): Promise<string> {
const id = crypto.randomUUID() const id = crypto.randomUUID()
const cart: Cart = { const raw: Cart = {
id, id,
name, name,
createdAt: new Date().toISOString().slice(0, 10), createdAt: new Date().toISOString().slice(0, 10),
templateId: template?.id, templateId: template?.id,
sections: template ? structuredClone(template.sections) : [], sections: template ? structuredClone(template.sections) : [],
} }
const cart = template ? await cacheImagesInCart(raw) : raw
mutateCarts(prev => [...prev, cart]) mutateCarts(prev => [...prev, cart])
setActiveCart(id) setActiveCart(id)
return id return id