Add guest/account auth flows, order tracking, and order history UI
- Wire up Better Auth (email/password + Google/Apple SSO) with a sign-in/sign-up UI, header account menu, and a basket that follows the signed-in-or-guest state. - Give the order status page a vertical timeline per preparation step (with a distinct customer-facing name separate from the machine tag, a done/in-progress/queued/pending look, and a sonar-ping highlight on the active step), a formatted ETA (relative under 3h, absolute clock time beyond that), and Buy again / Get help / Cancel actions. - Expand Pizza's recipe into 6 steps across a shared prep station and the existing oven/assembly stations, with matching schema, seed, and domain changes plus updated/added test coverage. - Add an Orders list (account orders via session, guest orders via a locally remembered access-token list) with a shared getOrdersForCustomer domain summary, and remember a guest's name/contact after their first order so checkout doesn't ask again. - Improve page transitions with the View Transitions API: a shared element morph between an order card and its detail page, a stable (non-jumping) header across navigations, and reduced-motion handling. - Add a Ghost Kitchen wordmark (Space Grotesk, faded "Ghost", ghost icon standing in for the "o") and shrink the basket icon to match the rest of the header controls. - Add a run-ghost-kitchen agent skill documenting local setup, the dev-server driver, and the gotchas hit along the way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
const STORAGE_KEY = 'ghost-kitchen-basket'
|
||||
|
||||
type BasketContextValue = {
|
||||
quantities: Record<number, number>
|
||||
itemCount: number
|
||||
add: (menuItemId: number) => void
|
||||
remove: (menuItemId: number) => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
const BasketContext = createContext<BasketContextValue | null>(null)
|
||||
|
||||
const readStoredQuantities = (): Record<number, number> => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||
return raw ? (JSON.parse(raw) as Record<number, number>) : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export const BasketProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [quantities, setQuantities] = useState<Record<number, number>>({})
|
||||
|
||||
// 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(() => {
|
||||
setQuantities(readStoredQuantities())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(quantities))
|
||||
} catch {
|
||||
// Unavailable (private mode, quota) — basket still works for this
|
||||
// page load, it just won't persist across a reload.
|
||||
}
|
||||
}, [quantities])
|
||||
|
||||
const add = (menuItemId: number) =>
|
||||
setQuantities((prev) => ({
|
||||
...prev,
|
||||
[menuItemId]: (prev[menuItemId] ?? 0) + 1,
|
||||
}))
|
||||
|
||||
const remove = (menuItemId: number) =>
|
||||
setQuantities((prev) => {
|
||||
const current = prev[menuItemId] ?? 0
|
||||
if (current <= 1) {
|
||||
const next = { ...prev }
|
||||
delete next[menuItemId]
|
||||
return next
|
||||
}
|
||||
return { ...prev, [menuItemId]: current - 1 }
|
||||
})
|
||||
|
||||
const clear = () => setQuantities({})
|
||||
|
||||
const itemCount = useMemo(
|
||||
() => Object.values(quantities).reduce((sum, quantity) => sum + quantity, 0),
|
||||
[quantities],
|
||||
)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ quantities, itemCount, add, remove, clear }),
|
||||
[quantities, itemCount],
|
||||
)
|
||||
|
||||
return (
|
||||
<BasketContext.Provider value={value}>{children}</BasketContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useBasket = () => {
|
||||
const context = useContext(BasketContext)
|
||||
if (!context) {
|
||||
throw new Error('useBasket must be used within a BasketProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user