import { createContext, useContext, useEffect, useMemo, useState, } from 'react' import type { ReactNode } from 'react' const STORAGE_KEY = 'ghost-kitchen-basket' export type BasketLine = { menuItemId: number optionIds: Array quantity: number } type StoredLines = Partial> // Two lines with the same Menu Item and the same set of selected Options // are the same line (their quantities combine) — order of selection // shouldn't matter, so the key sorts the option ids first. const lineKey = (menuItemId: number, optionIds: Array) => `${menuItemId}:${[...optionIds].sort((a, b) => a - b).join(',')}` const isBasketLine = (value: unknown): value is BasketLine => typeof value === 'object' && value !== null && typeof (value as BasketLine).menuItemId === 'number' && Array.isArray((value as BasketLine).optionIds) && typeof (value as BasketLine).quantity === 'number' type BasketContextValue = { lines: Array itemCount: number add: (menuItemId: number, optionIds?: Array) => void remove: (menuItemId: number, optionIds?: Array) => void clear: () => void } const BasketContext = createContext(null) const readStoredLines = (): StoredLines => { try { const raw = window.localStorage.getItem(STORAGE_KEY) if (!raw) return {} const parsed: unknown = JSON.parse(raw) if (typeof parsed !== 'object' || parsed === null) return {} const result: StoredLines = {} for (const [key, value] of Object.entries(parsed)) { // Earlier versions stored a flat `Record` with // no customization — read a leftover value like that as a plain, // option-free line rather than dropping it. if (typeof value === 'number') { const menuItemId = Number(key) if (!Number.isFinite(menuItemId) || value <= 0) continue result[lineKey(menuItemId, [])] = { menuItemId, optionIds: [], quantity: value, } } else if (isBasketLine(value)) { result[key] = value } } return result } catch { return {} } } export const BasketProvider = ({ children }: { children: ReactNode }) => { const [storedLines, setStoredLines] = useState({}) // Read from localStorage after mount, not during initial render, so the // server-rendered and first client-rendered HTML match (no localStorage on // the server) and React doesn't flag a hydration mismatch. useEffect(() => { setStoredLines(readStoredLines()) }, []) useEffect(() => { try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(storedLines)) } catch { // Unavailable (private mode, quota) — basket still works for this // page load, it just won't persist across a reload. } }, [storedLines]) const add = (menuItemId: number, optionIds: Array = []) => { const key = lineKey(menuItemId, optionIds) setStoredLines((prev) => ({ ...prev, [key]: { menuItemId, optionIds, quantity: (prev[key]?.quantity ?? 0) + 1, }, })) } const remove = (menuItemId: number, optionIds: Array = []) => { const key = lineKey(menuItemId, optionIds) setStoredLines((prev) => { const current = prev[key] if (!current) return prev if (current.quantity <= 1) { const next = { ...prev } delete next[key] return next } return { ...prev, [key]: { ...current, quantity: current.quantity - 1 } } }) } const clear = () => setStoredLines({}) const lines = useMemo( () => Object.values(storedLines).filter((line) => line !== undefined), [storedLines], ) const itemCount = useMemo( () => lines.reduce((sum, line) => sum + line.quantity, 0), [lines], ) const value = useMemo( () => ({ lines, itemCount, add, remove, clear }), [lines, itemCount], ) return ( {children} ) } export const useBasket = () => { const context = useContext(BasketContext) if (!context) { throw new Error('useBasket must be used within a BasketProvider') } return context }