Files
braidsbymira/docs/superpowers/specs/2026-08-12-phase2c-public-site-live-data-design.md

8.7 KiB

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