notify_booking_created() and notify_inquiry_created() each held their own
literal copy of the local Edge Functions base URL. Introduce
public.edge_functions_base_url() as the single source of truth so Task 9's
production deploy only needs to update one function definition instead of
two independent literals.
Wires pg_net-based database triggers on bookings/inquiries to invoke the two
Edge Functions on insert. Two empirically-discovered fixes over the brief's
literal SQL were required to make this work locally:
- ALTER DATABASE ... SET for a custom GUC needs superuser, but the local
migration role (postgres) isn't superuser in this Supabase CLI stack (only
supabase_admin is) - inlined the base URL as a literal constant in each
trigger function instead.
- pg_net's http_post lives in schema `net`, not `extensions.net` - fixed the
function calls accordingly.
Also disabled verify_jwt for both functions in config.toml since they're
invoked by pg_net (no Authorization header), not by end-user clients.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds supabase/functions/ (deno.json, _shared/email.ts) with pure
buildBookingNotificationEmail/buildInquiryNotificationEmail functions
for Task 6's Edge Functions, tested via Deno's test runner. Excludes
supabase/functions/** from the root Vitest config, which otherwise
tried to resolve the jsr: import and failed.
Replace the blanket select/insert/update/delete grant to anon/authenticated/
service_role with privilege-scoped grants: anon/authenticated get select-only
on the public-read tables plus insert-only on bookings/inquiries; full CRUD
is reserved for authenticated (narrowed to the admin UID by RLS) and
service_role. Table grants are now a real second layer of defense instead of
mirroring RLS. Updates two pgTAP assertions whose blocking mechanism moved
from RLS-filtered-empty to grant-level permission-denied, and adds a new
assertion proving anon cannot UPDATE bookings.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Includes explicit GRANTs for anon/authenticated/service_role on all
public tables: the Supabase CLI's current default no longer
auto-exposes new tables, so RLS policies from the initial schema were
unreachable without them (every request failed at the privilege check
before RLS was evaluated).
Initializes the local Supabase project (supabase/config.toml) and adds
a thin src/lib/supabase.ts wrapper around @supabase/supabase-js that
reads connection details from Vite env vars, tested with a mocked
createClient.
Adds emulator-backed rules-unit tests verifying the Firestore rules
committed in Task 3, including the global constraint that
bookings/inquiries are never publicly readable. Excludes
firebase-tests/ from the default vitest run since it needs the
emulator (via `npm run test:rules`) rather than jsdom.
The old flex layout with justify-content: space-between pushed its
two children to the far edges on wide screens, leaving a large dead
void in the middle. Replaced with a centered, max-width three-column
grid (brand, contact, navigate) with a script wordmark, gold section
labels, a divider separating it from the banner above, and a
copyright line.
Sourced from Pexels (free for commercial use, no attribution
required) to complete the image wiring added in the visual overhaul.
Fills the 18 image slots that were previously deferred (products,
signature styles, About Mira portrait, testimonial avatars).
Kept in French to match how Mira actually messages clients. Updated
RuleIcon's icon set to match the real policy categories (deposit,
hair prep, extensions, lateness, guests, payment) in place of the
placeholder categories.
- Lazy-load Home in App.tsx so three/@react-three/fiber (~238 kB gzip) only ships to the
Home route, not to Shop/Book/Rules.
- Add max-width: 260px to ProductCard/ServiceCard so real images won't collapse Home/Book
grid layouts.
- Restore AnimatePresence mode="wait" in Book.tsx to eliminate the exit/enter double-stack
during step transitions, updating the three Book.test.tsx assertions that raced the
transition to findBy* queries.
- Hero3DScene: drop the meaningless "static" className string in favor of a dedicated
data-testid, add aria-hidden to the reduced-motion fallback branch, and remove the
unused @react-three/drei dependency (adding @types/three directly since it was only
present transitively via drei).
Adds StepProgress component showing 4 labeled booking steps (Service,
Date & Time, Contact, Confirmed) and wires it into Book.tsx alongside
framer-motion AnimatePresence/motion.div step transitions.
Deviates from literal brief in two ways, both to avoid known issues:
- Guards the transition with useReducedMotion() (duration: 0 when
prefers-reduced-motion is set), matching the fix pattern already
applied to Home.tsx and Shop.tsx after prior review round-trips.
- Drops AnimatePresence's mode="wait" in favor of the default (sync)
mode: with mode="wait", the incoming step's DOM doesn't mount until
the outgoing step's exit animation finishes, which broke existing
synchronous Testing Library queries in Book.test.tsx (real 250ms
exit animations in jsdom, no fake timers). Sync mode mounts the new
step immediately while both fade, preserving test synchronicity.
Also updates Book.test.tsx's confirmation assertion since the old
"You're booked!" heading is gone (page now always shows the shared
"Book an Appointment" heading and StepProgress; only the body swaps).
The About section's portrait image now checks useReducedMotion() and
disables parallax scroll motion when the user has set OS-level reduced
motion preferences.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds Hero3DScene, a procedurally-built rotating braid mesh rendered via
@react-three/fiber, replacing the flat CSS-circle Hero3D component. Falls
back to a static gradient circle under prefers-reduced-motion, matching the
old component's fallback behavior.
Also adds a ResizeObserver mock to the test setup, required by
@react-three/fiber's Canvas (via react-use-measure) in jsdom, which doesn't
implement it natively.
Replace 'global.IntersectionObserver' with 'globalThis.IntersectionObserver' to fix
TS2304 "Cannot find name 'global'" error. globalThis is the standard environment-agnostic
reference that type-checks cleanly in browser-targeted tsconfig. Runtime behavior
identical to global in Node/jsdom contexts.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Implements a modal component for collecting product inquiries with name, email, and notes fields. Validates name (required) and email (must contain @) client-side, displays inline error messages for invalid input, and shows a confirmation message on valid submit.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Implement ProductCard component with:
- Renders product details (name, price, description)
- "Ask about this" button disabled when out of stock
- Shows "Currently unavailable" badge when out of stock
- Calls onInquire callback when clicked
Implement ServiceCard component with:
- Renders service details (name, formatted duration, price)
- Clickable button card that calls onSelect with service id
- formatDuration helper for human-readable time format
Both components include CSS modules with hover effects and proper styling.
All 4 tests pass (2 for each component).
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
framer-motion's useReducedMotion() hook caches its result at the module level
and never re-reads window.matchMedia on subsequent calls. This caused the second
test to fail because the cache was locked to the first test's matchMedia mock.
Fixed by using vi.doMock() with vi.resetModules() to mock useReducedMotion
directly with the desired value for each test, ensuring each test runs with a
fresh module context where the hook returns the expected value.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH