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') }
}
function handleCreateFromTemplate(templateId: string) {
async function handleCreateFromTemplate(templateId: string) {
const existing = carts.find(c => c.templateId === templateId)
if (existing) { setActiveCart(existing.id); return }
const template = catalog?.templates.find(t => t.id === templateId)
if (!template) return
createCart(template.name, template)
await createCart(template.name, template)
}
function handleCreateEmpty() {
async function handleCreateEmpty() {
if (!newCartName.trim()) return
createCart(newCartName.trim())
await createCart(newCartName.trim())
setNewCartName('')
setShowNewCart(false)
}
+9 -1
View File
@@ -114,7 +114,15 @@ export function ItemCard({ item, onEdit, onRemove }: Props) {
<span className="truncate">{p.name}</span>
{p.image && <span className="shrink-0 text-xs" style={{ color: 'var(--color-bg3)' }}></span>}
</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>
))}
</ul>
+3 -3
View File
@@ -56,11 +56,8 @@ export function SectionBlock({ section, onRemoveItem, onAddItem, onEditItem, onR
return (
<div
draggable
onDragStart={onDragStart}
onDragOver={onDragOver}
onDrop={onDrop}
onDragEnd={onDragEnd}
className="flex flex-col gap-3 rounded-lg border p-4 transition-opacity"
style={{
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 gap-2">
<span
draggable
onDragStart={onDragStart}
onDragEnd={onDragEnd}
className="cursor-grab select-none text-base leading-none"
style={{ color: 'var(--color-bg3)' }}
title="Drag to reorder"
+14 -4
View File
@@ -20,8 +20,15 @@ function loadActiveId(carts: Cart[]): string | null {
}
function saveCarts(carts: Cart[]): string | null {
const json = JSON.stringify(carts)
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
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
@@ -30,6 +37,7 @@ function saveCarts(carts: Cart[]): string | null {
return 'Failed to save carts.'
}
}
}
export function useCarts() {
const [carts, setCarts] = useState<Cart[]>(() => loadCarts())
@@ -50,7 +58,8 @@ export function useCarts() {
if (!needsMigration) return
Promise.all(loaded.map(cacheImagesInCart)).then(migrated => {
pruneImageCache(migrated)
saveCarts(migrated)
const err = saveCarts(migrated)
setStorageError(err)
setCarts(migrated)
})
}, [])
@@ -74,15 +83,16 @@ export function useCarts() {
// ── Cart management ────────────────────────────────────────────────────────
function createCart(name: string, template?: CartTemplate): string {
async function createCart(name: string, template?: CartTemplate): Promise<string> {
const id = crypto.randomUUID()
const cart: Cart = {
const raw: Cart = {
id,
name,
createdAt: new Date().toISOString().slice(0, 10),
templateId: template?.id,
sections: template ? structuredClone(template.sections) : [],
}
const cart = template ? await cacheImagesInCart(raw) : raw
mutateCarts(prev => [...prev, cart])
setActiveCart(id)
return id