Follow-ups to the booking work in #256.
Public booking flow spacing. The admin availability editor got a spacing pass on a single scale; the customer-facing flow at /grab-a-coffee and /book-a-podcast did not. Time-slot radios also need accessible names.
Editable blackout dates. availability_blackouts exists, is read by the availability endpoint, and is displayed read-only in the admin editor. There is no UI to add or remove a range. This matters before the holidays — later hours and days off both need it.
Confirmation and receipts. Neither the contact nor the quote form sends the submitter a receipt, and the quote API returns an id (quote.js:160) that the client discards. Success banners render above the form and are never focused or scrolled to, so on mobile the three-step quote form appears to blank itself.
Input caps. contactSchema.message is z.string().min(1) with no .max() (api/_lib/validation.js:7), and the same across the quote fields — unbounded payloads into Supabase and Resend.
Follow-ups to the booking work in #256.
Public booking flow spacing. The admin availability editor got a spacing pass on a single scale; the customer-facing flow at
/grab-a-coffeeand/book-a-podcastdid not. Time-slot radios also need accessible names.Editable blackout dates.
availability_blackoutsexists, is read by the availability endpoint, and is displayed read-only in the admin editor. There is no UI to add or remove a range. This matters before the holidays — later hours and days off both need it.Confirmation and receipts. Neither the contact nor the quote form sends the submitter a receipt, and the quote API returns an
id(quote.js:160) that the client discards. Success banners render above the form and are never focused or scrolled to, so on mobile the three-step quote form appears to blank itself.Input caps.
contactSchema.messageisz.string().min(1)with no.max()(api/_lib/validation.js:7), and the same across the quote fields — unbounded payloads into Supabase and Resend.