docs: add Phase 2C public site live data design spec
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
# Phase 2C: Public Site Live Data — Design
|
||||
|
||||
**Status:** Approved
|
||||
**Supersedes:** N/A. Builds on the Phase 2A Supabase backend
|
||||
(`docs/superpowers/specs/2026-08-08-phase2-backend-foundation-supabase-design.md`)
|
||||
and the Phase 2B admin panel
|
||||
(`docs/superpowers/specs/2026-08-12-phase2b-admin-panel-design.md`), both
|
||||
already live and deployed.
|
||||
|
||||
## Context
|
||||
|
||||
Phase 2A built the backend; Phase 2B gave Mira an admin panel to manage it.
|
||||
But the public site — the pages actual customers visit — still reads from
|
||||
static mock-data files (`src/data/products.ts`, `services.ts`,
|
||||
`availability.ts`) and never persists anything a visitor submits. The result:
|
||||
anything Mira changes in the admin panel has zero effect on what customers
|
||||
see, and no real booking or inquiry a customer submits is actually saved
|
||||
anywhere. This spec closes that gap — the last piece needed for the whole
|
||||
system to function end-to-end, referred to earlier in this project as
|
||||
"Sub-project C."
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
- `Shop.tsx` and `Home.tsx` read `products`/`services` from Supabase instead
|
||||
of the static mock-data files
|
||||
- `Book.tsx` computes real available time slots from `schedule_template` +
|
||||
`schedule_exceptions` + `booked_slots`, instead of `src/data/availability.ts`'s
|
||||
pseudo-random mock
|
||||
- Booking submission (`useBooking.ts`) actually persists to the `bookings`
|
||||
table
|
||||
- Inquiry submission (`InquiryModal.tsx`) actually persists to the
|
||||
`inquiries` table
|
||||
- Product/service images resolve to real Supabase Storage URLs; the
|
||||
existing local image files are uploaded to Storage so real photos render
|
||||
- Loading and error states for every page now doing a live fetch
|
||||
|
||||
Out of scope (no schema exists for these, not part of the original Phase 2A
|
||||
data model):
|
||||
- The Rules page's content — stays static, matches how it was deliberately
|
||||
kept static and French-only from Phase 1
|
||||
- Homepage testimonials — stays a static hardcoded array
|
||||
- The "About Mira" portrait and other purely decorative static images
|
||||
|
||||
## Architecture
|
||||
|
||||
The public site switches from importing static mock-data files to querying
|
||||
Supabase directly, using the same `useState`/`useEffect` + `supabase-js`
|
||||
pattern already established throughout the admin panel (Phase 2B) — no new
|
||||
data-fetching library, matching this project's existing conventions
|
||||
throughout.
|
||||
|
||||
All RLS policies this spec needs already exist from Phase 2A:
|
||||
`products_public_read`, `services_public_read`,
|
||||
`schedule_template_public_read`, `schedule_exceptions_public_read`,
|
||||
`booked_slots_public_read` (all `select`), and `bookings_public_insert`,
|
||||
`inquiries_public_insert` (both `insert`). No new migrations are needed for
|
||||
RLS — this phase is purely a frontend change consuming an already-complete
|
||||
backend.
|
||||
|
||||
## Availability computation
|
||||
|
||||
A new pure function, `computeAvailableSlots(template, exceptions,
|
||||
bookedSlots, date)`, replaces `getSlotsForDate`. Logic:
|
||||
|
||||
1. If an exception row exists for `date` with `closed: true`, return no
|
||||
slots (the schema's `schedule_exceptions` table only supports fully
|
||||
closing a day, not overriding hours — there's no start/end-time override
|
||||
column, so that's the full extent of what "exception" can express today).
|
||||
2. Otherwise, if `date`'s day-of-week (`0`=Sunday..`6`=Saturday, matching
|
||||
`Date.getDay()`) isn't in `template.working_days`, return no slots.
|
||||
3. Otherwise, generate slots from `template.start_time` to
|
||||
`template.end_time` stepped by `template.slot_minutes`, formatted to
|
||||
match the existing display convention used throughout this project
|
||||
(`"9:00 AM"` — no leading zero on the hour, uppercase AM/PM, matching the
|
||||
literal strings already stored in `bookings.time`, which is a `text`
|
||||
column, not a real time type).
|
||||
4. Mark any generated slot present in `bookedSlots` for that date as
|
||||
`available: false`.
|
||||
|
||||
`getNextAvailableDate` is reimplemented the same way it works today (search
|
||||
forward day-by-day for the first date with any available slot), just backed
|
||||
by the real computation instead of the mock.
|
||||
|
||||
`Book.tsx` fetches `schedule_template` once on mount (single row) and
|
||||
`schedule_exceptions` once on mount (small table, unlikely to ever be large
|
||||
enough to need date-range filtering), then fetches `booked_slots` filtered
|
||||
by the selected date each time the date changes — mirroring how
|
||||
`getSlotsForDate` is already called reactively today, just with real data
|
||||
underneath.
|
||||
|
||||
## Booking & inquiry submission
|
||||
|
||||
`useBooking.ts`'s `submit()` becomes async: after existing client-side
|
||||
validation passes, it calls
|
||||
`supabase.from('bookings').insert({ service_id, date, time, contact_name,
|
||||
contact_phone, contact_email, contact_notes })` via the anon client (already
|
||||
permitted by RLS). On success, transitions to the `'confirmed'` step exactly
|
||||
as today. On failure — most likely SQLSTATE `23505` (unique-constraint
|
||||
violation on `bookings_active_slot_idx`) if another visitor books the same
|
||||
slot in the gap between the user selecting it and submitting — the hook
|
||||
surfaces an error and returns the user to slot selection instead of
|
||||
advancing, since the slot they picked is no longer real. Any other failure
|
||||
(network, RLS misconfiguration) surfaces the raw Postgres error message,
|
||||
matching the admin panel's existing unmapped-error fallback pattern (no
|
||||
`friendlyError`-style mapping needed here — a customer hitting an obscure
|
||||
Postgres error is already a rare edge case, and the existing "please try
|
||||
again" framing is enough without inventing new copy for cases that don't
|
||||
realistically occur on the public flow).
|
||||
|
||||
`InquiryModal.tsx`'s `handleSubmit` becomes async: after existing
|
||||
validation passes, it calls
|
||||
`supabase.from('inquiries').insert({ product_id, contact_name,
|
||||
contact_email, contact_notes })`. On success, shows the existing "submitted"
|
||||
thank-you state. On failure, shows an inline error and keeps the form open
|
||||
so the visitor can retry.
|
||||
|
||||
## Images
|
||||
|
||||
Product/service `image` columns store storage-relative paths (e.g.
|
||||
`products/edge-control.jpg`) — this was already fixed in Phase 2B's final
|
||||
review, which changed `uploadSalonImage()` to return the path rather than a
|
||||
public URL, but nothing yet derives a public URL from that path at render
|
||||
time. This phase adds:
|
||||
|
||||
- `src/lib/resolveImageUrl.ts` exporting `resolveImageUrl(path: string):
|
||||
string`, wrapping `supabase.storage.from('salon-images').getPublicUrl(path).data.publicUrl`
|
||||
- `ProductCard.tsx` and `ServiceCard.tsx` call this on the `image` field
|
||||
from a live-fetched product/service before rendering `<img src=...>`
|
||||
|
||||
Since the actual photo files today only exist locally under
|
||||
`public/products/*.jpg` and `public/styles/*.jpg` (referenced directly by
|
||||
the old mock-data imports, never uploaded anywhere), this phase also
|
||||
extends the seed step to upload each file to the `salon-images` Storage
|
||||
bucket under the same relative key already stored in the `image` column
|
||||
(`products/edge-control.jpg`, `styles/knotless.jpg`, etc.) — so
|
||||
`resolveImageUrl()` resolves to a real, already-uploaded photo rather than a
|
||||
404. This is a one-time upload step added to `scripts/seed.ts`, using the
|
||||
same service-role client the script already holds.
|
||||
|
||||
## Loading & error states
|
||||
|
||||
Every page doing a live fetch (`Shop`, `Home`, `Book`) gets a loading state
|
||||
for the initial fetch (data is no longer synchronously available at render,
|
||||
unlike the old static imports) and a plain-language inline error on fetch
|
||||
failure — matching the admin panel's existing pattern
|
||||
(`setError(error.message)`, rendered as `{error && <p>...</p>}`). No global
|
||||
loading spinner or skeleton library; simple conditional rendering
|
||||
(`{loading && <p>Chargement…</p>}`), consistent with how the rest of this
|
||||
codebase stays deliberately simple.
|
||||
|
||||
## Testing
|
||||
|
||||
- New unit tests for `computeAvailableSlots` covering: a normal working
|
||||
day's slots, a fully-closed exception day, a non-working weekday (per
|
||||
`working_days`), and a slot correctly excluded because it already appears
|
||||
in `booked_slots`.
|
||||
- New unit tests for `resolveImageUrl`.
|
||||
- Existing `Shop`/`Home`/`Book`/`InquiryModal` component tests are updated
|
||||
to mock `supabase-js` calls (matching the pattern already used throughout
|
||||
every admin-panel test) instead of importing the static mock-data files
|
||||
directly.
|
||||
- `useBooking`'s existing tests are extended to cover the async `submit()`
|
||||
path: successful insert, and a `23505` failure returning the user to slot
|
||||
selection with an error shown.
|
||||
|
||||
No new pgTAP tests are needed — this phase doesn't touch RLS policies or
|
||||
add new database behavior; it only consumes what Phase 2A already tested
|
||||
and shipped.
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
None outstanding — the one architecture decision (client-side vs.
|
||||
server-side availability computation) was resolved during brainstorming in
|
||||
favor of client-side, matching this project's established pattern of a thin
|
||||
client with no custom SQL functions beyond the private-schema helpers
|
||||
Phase 2A already has for unrelated purposes (Edge Function auth).
|
||||
Reference in New Issue
Block a user