Fix localStorage quota by caching images before saving; add part copy button
This commit is contained in:
+74
-8
File diff suppressed because one or more lines are too long
+4
-4
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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') {
|
||||||
@@ -30,6 +37,7 @@ function saveCarts(carts: Cart[]): string | null {
|
|||||||
return 'Failed to save carts.'
|
return 'Failed to save carts.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useCarts() {
|
export function useCarts() {
|
||||||
const [carts, setCarts] = useState<Cart[]>(() => loadCarts())
|
const [carts, setCarts] = useState<Cart[]>(() => loadCarts())
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user