docs: add Phase 2C public site live data design spec

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-12 19:28:12 +02:00
parent 9b85218a61
commit 3a64090a39

View File

@@ -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).