- After closing, the box button stays disabled as "Box N closed" for
5 minutes, timed from a new picked_up_at so it survives a reload
- Picked-up items keep showing their pickup box number
- Hide the steps toggle on ready and picked-up items
- Move "Buy again" and "Get help" into a dots menu in the item header
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shackle now morphs instead of lifting: unlocked, the left leg stays
on the body and the right leg shrinks to a stub. The icon may draw past
its viewBox so the raised arc isn't clipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a reusable HoldButton that fires after an 800ms hold (pointer or
Space/Enter) with a fill showing progress, and cancels on early release.
The pickup box button now uses it, with extra space above it on mobile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ready Order Item now gets the lowest-numbered free Pickup Box (or waits
for one to free up), shown as "Pickup box N" on its status. The customer
opens the box from their order and closes it again; closing marks the
item picked up and hands the box to the next waiting item.
On the order card the box button comes first, full width on mobile, with
a padlock that lifts on open and snaps shut before the button collapses
away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The order status view now carries each item's menu item id and the ids of
its current Options matching the ordered ones by name, since editing a
Menu Item recreates its Options under new ids. Options no longer on the
menu are left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Slide the "Ready in about" value when it changes (new EtaMessage)
- Drop the "Running late" banner; delays only move the ETA now
- Remove the cancel-info tooltip and its icon button
- Shorten the steps toggle to "Steps" / "Hide"
- Add icons and more padding to the order card action buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Order cards derive their label from item progress (Not Started,
Preparing, Partly Ready, Ready, Picked Up, Cancelled) instead of the
order's own status; final states are coloured
- Order page: animated status banner per item (ready/picked up check,
cancelled cross, running-late clock), steps collapse once an item is
settled with a Show/Hide steps toggle, done steps are faded instead
of struck through, and the per-item status badge is gone
- New Delay concept: machines can report extra time on a claimed step
(preparation_step_instances.delay_seconds), which counts toward the ETA
- The kitchen can cancel an item at any point before it's ready, with a
refund (order_items.cancelled_by records who cancelled)
- Dev kitchen simulation now runs server-side across every order, so a
step nobody is watching no longer blocks the queue; each settled step
may randomly be delayed or cancelled
- Humanize snake_case statuses in the UI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- "Simulate kitchen (dev)" button advances each placed/paid item's
Preparation Steps as a Machine would, polling fresh status per item
- Each simulated step takes 10–30s, scaled from its estimated duration
- Timeline connector fills in with a Framer Motion animation when a
step completes; finished steps and the line now use the success green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New /account route: edit name/email, change password, delete account
(detaches the user's orders instead of leaving a dangling reference,
since orders.customerId has no DB-level FK to better-auth's user table)
- Better Auth: allow email changes without a verification email (no
email sending is wired up anywhere in this app) and enable account
deletion
- Header avatar: fall back to the initials placeholder if the profile
image fails to load, not just when there's no image URL
- Fix low-contrast "Continue with Google" button in dark mode (it used
the page background color and a hardcoded dark-gray label, so it
nearly disappeared against a dark background)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The +/- glyphs were wrapped in a flex-centered div, but only the +
button had been converted — the - button still used a plain span and
rendered off-center. Apply the same wrapping to both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Menu items can now list allergens (shared catalog + join table),
shown on the menu grid and item detail pages when applicable, and
editable from the admin menu item form.
- Removed the top-nav "Help" link now that the footer links to it.
- The item customization page's price now uses the sliding
AnimatedNumber, matching the basket line price.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a Footer with the brand, quick links (Menu, Basket, Your orders,
Help), and a copyright line, rendered on every page. Body is now a
full-height flex column so the footer sits at the bottom of the
viewport on short pages instead of floating right under the content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a /help route with an animated FAQ accordion and a contact form
(persisted via a new contact_messages table). The per-order-item "Get
help" link now points here and passes the order/item/status as search
params, which the page uses to show a short, state-specific note and
prefill the message field.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reflow remaining basket lines with framer-motion when an item is
removed, slide the per-line price when it changes, and tween the
basket total up/down to its new amount instead of snapping to it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flexbox centering of a bare text node inside a <button> is a known
WebKit quirk — Safari (including iOS) doesn't always vertically center
it the same way it centers an element child. Wrapping the +/- glyph in
its own <span aria-hidden="true"> (the button's own aria-label already
conveys the meaning) is the standard workaround.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvYdJ3os7tumimsLdLu2j9
.stepperButton relied on the browser's default button content
centering, which the appearance: none reset in the previous commit
took away. Explicitly flex-center it (and zero out padding/line-height)
across all three copies (MenuItemCustomizer, MenuList, basket).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvYdJ3os7tumimsLdLu2j9
globals.scss's button reset only set font: inherit — no appearance
reset, no color fallback. Several buttons only set a background and
relied on inherited text color, which iOS Safari doesn't honor for
unstyled <button> elements, so their labels/icons rendered in Safari's
native blue instead of the theme color:
- .stepperButton in MenuItemCustomizer, MenuList, and basket
- .refreshButton in admin/kitchen
- .addButton in admin/MenuItemForm (color was only set on :hover)
Added explicit `color` to each, and hardened globals.scss's button
reset with `color: inherit` plus `-webkit-appearance: none` /
`appearance: none` as a fallback for any future button that forgets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvYdJ3os7tumimsLdLu2j9
Basket's "Browse the menu" button still had its original plain style
(flat opacity hover, no arrow) from before that button was reworked on
the orders page. Bring it in line: shadow, hover lift, and the shifting
arrow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvYdJ3os7tumimsLdLu2j9
Drop MenuBackdrop entirely and put the icon, title, text, and CTA
directly back on the empty state's own solid background — no separate
overlay card needed once there's no pattern behind it to cover up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvYdJ3os7tumimsLdLu2j9
Replace the per-icon DOM elements (with client-side randomized
placement) with a single small SVG tile rendered once and set as a
repeating CSS background-image. Icons sit in a 4x4 grid within the
tile, evenly spaced and the same size, cycling through the food-icon
shapes and color palette.
Animates via background-position instead of per-element transforms:
moving by exactly one tile size per loop (linear, infinite) scrolls
the whole pattern diagonally forever with a perfectly seamless wrap,
which per-icon placements couldn't do without quadruplicating the DOM.
Also tried a grayscale pass (filter: grayscale(1)) alongside the blur,
now bumped from 2px to 4px.
This also sidesteps the hydration-mismatch concern entirely — there's
no more client-only randomization step, so no server/client mismatch
risk to guard against.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvYdJ3os7tumimsLdLu2j9
The .empty svg rule (styling the OrdersIcon/BasketIcon) was a plain
descendant selector, so it also matched — and, with higher specificity
than inherited color, overrode — the backdrop's own food-icon svgs
nested inside the same .empty box. Scope it to a direct child (> svg)
so it only targets the actual empty-state icon.
Also expands the backdrop palette from 3 to 6 colors (adds basil,
berry, and cocoa) for more variety across the icons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cycle a small warm palette (terracotta, golden-brown, golden-yellow)
across the icons via a CSS custom property instead of one flat tint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop the tinted circle badge around the icon and use the same plain,
muted-gray sizing as basket.module.scss's empty state instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confine MenuBackdrop to whatever card renders it instead of the whole
viewport: it's now position: absolute inset:0 within its container
(previously position: fixed, spanning the page) with a gentle floating
animation per icon (staggered delay, respects prefers-reduced-motion).
Wired it into the empty basket and empty orders states instead of the
menu list page.
Also gives .empty on both pages position: relative + z-index: 0 so it
forms its own stacking context — without an explicit z-index, the
backdrop's z-index: -1 child was escaping to the document root and
painting either fully behind everything or (on the basket page, where
motion.div meant `.empty` never picked up a context on its own) above
the card's own text, depending on the surrounding markup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MenuBackdrop renders pizza, burger, and fries stroke icons at fixed,
hand-placed positions (no Math.random(), to avoid an SSR/client
hydration mismatch) as a fixed, pointer-events-none layer behind the
page content. Also tweaks the intro line to call out "your tasty menu".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fades/slides the empty state in on mount, gives the icon a tinted
circle badge instead of a bare stroke icon, and gives the "Browse the
menu" button a shadow, hover lift, and a shifting arrow instead of a
flat opacity hover.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The prior attempt wrapped the sign-out router.invalidate() in
document.startViewTransition(), but that had two problems: invalidate()
does its own network refetch inside the transition's update callback,
which freezes the page for the round-trip instead of animating
anything, and TanStack Router's invalidate() explicitly opts itself out
of view transitions (sets shouldViewTransition = false) for exactly
that reason. On top of that, OrdersRoute briefly returns null between
the account loader clearing and the guest-orders lookup resolving,
unmounting OrdersListView and wiping any exit-animation state anyway.
Replace it with a plain, self-contained approach:
- OrdersRoute now keeps rendering the last known non-null orders list
through that brief gap (a lastKnownOrders ref) instead of unmounting
OrdersListView, so its local animation state survives.
- OrdersListView holds onto the previous non-empty list for
EXIT_DURATION_MS after it goes empty, applying an `orderExiting`
class that fades/slides each card out via a plain CSS transition,
then swaps to the actual empty state once the transition finishes.
This has no dependency on browser view-transition support, network
timing, or router internals.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wraps the sign-out-triggered router.invalidate() in
document.startViewTransition() so the cleared order cards cross-fade
out (and the empty state fades in) instead of popping away instantly.
Each order card already carries its own view-transition-name (see
OrdersListView), so this reuses that per-card exit; the existing
::view-transition-group(*) timing in globals.scss applies automatically.
Falls back to a plain invalidate() where the API is unsupported, and
swallows rejection on the transition's ready/finished promises since
the browser can abort it in some states (e.g. a backgrounded tab) —
router.invalidate() still runs regardless.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signing out only clears the auth session cookie; a route already
loaded (e.g. /orders) keeps its stale loader data until something
triggers a reload. Call router.invalidate() once signOut succeeds so
the current route's loader reruns against the now-signed-out session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Guest order refs and the basket both live in localStorage independent
of the auth session, so they'd otherwise linger for the next person to
use the browser. Add clearGuestOrderRefs (mirroring clearGuestIdentity)
and call it alongside the existing basket clear() from the sign-out
handler.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move the color palette into CSS custom properties (:root in
globals.scss) with a prefers-color-scheme: dark override, and point the
existing $color-* Sass variables at them so every component picks up
dark mode automatically. Replaced the handful of rgba($color, alpha)
call sites with color-mix(), since Sass can't alpha-blend a color that's
now a CSS variable at compile time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a backdrop blur and semi-transparent background/border so page
content shows through the sticky header while scrolling.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The admin layout's .layout used align-items: flex-start, correct for
the desktop row (sidebar + content) but wrong once it stacks to a
column at mobile width — align-items controls the cross axis, which
flips from height to width, so children were shrinking to fit their
own content instead of stretching to the viewport. This is what caused
the sidebar's nav links to render squashed/tall and the menu list to
overflow horizontally. Same root cause on .sidebar's own flex-basis,
which fixed its height instead of width once stacked.
Also stacks the menu item form's option/step rows to a column below
the tablet breakpoint (they didn't fit and forced horizontal scroll),
and removes AdminOrdersView's leftover standalone-page padding/max-width
— it was still wrapping its content in the styling meant for a
top-level page, giving "All orders" visibly more padding than Menu/
Kitchen, which just use the admin layout's own padding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduces a role-gated /admin section (sidebar layout, requireAdmin
checked both in route loaders and independently in every admin server
function) with three sections:
- Orders: every order across all customers, and an admin-only order
detail view nested under /admin so the sidebar persists and admin
access bypasses the guest-token check.
- Menu: create/edit/delete menu items, their options, and their
preparation steps; deleting an item with past orders is refused
(protected by the orderItems FK) instead of crashing.
- Kitchen: read-only live view of every machine's busy/idle status and
per-tag queue depth, with a manual refresh.
Also:
- Adds a `role` field to the user table (Better Auth additionalFields,
input:false so it can't be self-assigned) and a db:seed-admin script
to create/promote an admin account.
- Fixes a real bug found while building this: src/server/auth.ts's
shared session helpers were plain exported functions using a
server-only import, which the framework couldn't tree-shake out of
the client bundle once shared across multiple server/*.ts modules —
this silently broke hydration app-wide. Fixed with createServerOnlyFn.
- Removes the now-fully-unused @tanstack/devtools-vite plugin.
- Disables view transitions for in-admin navigation so the persistent
sidebar doesn't cross-fade/shift on every click.
- Gives the "your orders" empty state the same visual treatment as the
basket page's.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both loaders threw a plain Error that the router doesn't treat as a
route-level not-found, so visiting /items/<bad-id> or /orders/<bad-id>
(or a valid order with a missing/wrong ?token) crashed instead of
showing the NotFound page. Menu item lookups now throw notFound()
directly; the order loader catches the server function's error and
maps both "order not found" and "wrong access token" to notFound(),
so a guessed id can't be used to probe which orders exist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Configures a notFoundComponent on the root route so unmatched paths get a
branded empty state instead of TanStack Router's generic "Not Found" text,
and drops the always-on devtools panel from the root layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets customers pick options on a menu item detail page before adding to the
basket, and makes displayed prices respect a configurable currency/locale
via VITE_CURRENCY/VITE_LOCALE. Also crossfades the basket's empty/filled
states with AnimatePresence in wait mode instead of animating both at once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add an AnimatedNumber component that slides a quantity stepper's
count horizontally — left on increment, right on decrement — and
wire it into both the menu and basket steppers.
- Make the top nav bar position: sticky, with an explicit background
so scrolled content doesn't show through it.
- Reserve the scrollbar's gutter on every page (scrollable or not) so
the sticky header doesn't shift width when navigating between short
and tall pages.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>
Installs @better-auth/cli as a devDependency instead of fetching it via
npx each time, and updates the README to use the new script.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Convert getRouter, OrderingPage, and OrderStatusRoute from function
declarations to arrow functions
- Migrate header-user.tsx off raw Tailwind classes onto a co-located
SCSS module, and remove Tailwind entirely (deps, Vite plugin,
styles.css) since it was the only consumer and duplicated the
documented SCSS Modules system
- Fix CLAUDE.md's stale ~/ alias example and structure diagram to
match the project's actual #/ and @/ aliases and directories
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds docker-compose.yml (Postgres on host port 5433, to avoid clashing
with another project's container already using 5432 on this machine)
and .env.example so `docker compose up -d` + `cp .env.example .env.local`
gets a working local DB. Adds a db:seed script alongside the existing
db:* scripts.
Rewrites README's "Adding a Database" section, which still described
Better Auth's DB as optional/hypothetical even though this project
already requires Postgres for both its own data and Better Auth's
tables. Fills in CLAUDE.md's Data layer connection-details placeholder
and fixes two more stale pnpm references and a wrong migrations path
(src/db/migrations/ instead of the actual drizzle/) found nearby.
Also fixes a real bug the new docs surfaced: `npm run db:seed` didn't
load .env.local the way drizzle-kit does, so DATABASE_URL was silently
unset when run as documented. seed.ts now loads env the same way
drizzle.config.ts already does.
Verified the full documented flow end-to-end against a fresh container:
migrate, better-auth migrate, seed, dev server, real page render.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Builds the main customer-facing flow: browse the Brand's menu, add
items to a cart, and check out either as a guest (name + contact) or,
if signed in, as the account customer — both call the existing
placeOrder server function, which already distinguishes the two. On
success, navigates to a new /orders/$orderId status page showing each
Order Item's Preparation Steps, queue position, ETA, and (per the
domain rules already established) a Cancel button while cancellable
and a pickup-confirmation button once ready.
Styled with SCSS Modules per CLAUDE.md; added the sass dependency and
the src/styles/ structure it describes (globals.scss imported once in
__root.tsx, alongside the existing Tailwind stylesheet the not-yet-
mounted header-user.tsx component depends on).
Verified end-to-end against a throwaway local Postgres: menu renders
with seeded items, cart quantities update, guest checkout places a
real order, and the status page reflects a real cancellation.
Also fixes CLAUDE.md's Commands section, which documented pnpm even
though this repo only has npm (package-lock.json, no pnpm installed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both placeOrder and cancelOrderItem call a compensating payment action
(refund / re-charge) after catching a failure, then rethrow. If that
compensating call itself threw, its error replaced the original one,
hiding the actual root cause behind whatever the payment provider
happened to fail with on the second attempt. Wrap each compensation in
its own try/catch, log the compensation failure, and always rethrow
the original error.
Also fixes a stale comment left over from renaming
createFirstStepForOrderItem to initializeOrderItemRecipe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cancelOrderItem committed the cancellation (freeing the Machine slot,
marking the item cancelled) before calling the payment provider's
refund — so a refund failure (gateway timeout, provider outage) left
the item cancelled with no refund ever issued, and no record that one
was owed. placeOrder already avoids the equivalent problem by charging
before its DB transaction and refunding if the transaction then fails;
apply the same pattern here for consistency.
Refund now happens first; the DB transaction's atomic queued-status
check (added for the earlier cancellation race) doubles as the signal
that the refund needs to be undone if this transaction loses the race
or otherwise fails, so the catch block re-charges to compensate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Price was already snapshotted per Order Item so later menu changes
don't rewrite history, but the Recipe (Step list) wasn't: advancing an
Order Item looked up its next Step from the live menuItemSteps table,
so an operator editing a Menu Item's Recipe mid-cook could silently
reshape an Order already in flight (different Machine Tag, duration,
or Step order than what the customer paid for).
Adds order_item_steps as a per-Order-Item copy of the Recipe, taken at
placement time; placeOrder, advanceOrderItemStep, and getOrderStatus
now all read from that snapshot instead of the live menu. Recorded as
docs/adr/0005 and added Recipe as a formal CONTEXT.md term.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CONTEXT.md described Order Item cancellability and Order completion
in ways the code didn't actually implement (cancellability only
covered the first Preparation Step; completion required pickup, not
any terminal state). Corrected the glossary to match intended
behavior, added Guest Access Token and Charge as formal terms, and
recorded the single-Charge-per-Order design as an ADR.
Also closes a real race in cancelOrderItem: a Machine could claim a
queued Step between the cancellability check and the delete, letting
an already-cooking item get cancelled and refunded. The delete now
atomically re-checks status = 'queued'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scaffolds the TanStack Start + Drizzle/Postgres + Better Auth app and
implements the core ordering domain: menu browsing, guest/account order
placement with online payment, per-Order-Item independent cooking and
cancellation, dynamic Machine-Tag pool assignment, and multi-step recipes
(see docs/adr/0001-0003). Adds a guest-access-token / session-based
ownership check on every read/mutation so an order can't be viewed,
cancelled, or picked up by anyone but its owner.
Covered by 14 tests running the domain layer directly against a real,
ephemeral Postgres (testcontainers) — createServerFn couldn't be called
outside a real request context, so tests exercise the domain functions the
server functions thinly wrap instead of the server functions themselves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>