booked_slots previously recorded only (date, time), so availability
checking excluded an exact time match but not a real overlap — a
4-hour service booked at 9:00 AM didn't block 11:00 AM or 1:00 PM.
Add duration_minutes to booked_slots (populated by sync_booked_slot()
from the booked service's own duration) and switch client-side
computeAvailableSlots to a half-open-interval overlap check, also
rejecting candidates that wouldn't finish before closing time.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Critical: computeAvailableSlots looped forever when slot_minutes was
0 or negative -- reachable in production, since AdminSchedule's
slot-length input has no validation and clearing it yields Number('')
=== 0. Guarded the computation, added min="5"/required to the admin
input as defense in depth, and added a regression test.
Important fixes:
- Removed the one eslint-disable that slipped into the branch
(InquiryModal.test.tsx), replacing it with the explicit vi.fn<...>
generic type pattern already used everywhere else. Also cleaned up
a similar unused-var workaround in useBooking.test.ts for
consistency.
- Added a submitting/in-flight guard to both booking and inquiry
submission, so a double-click can no longer produce a false
"slot already taken" error after a booking actually succeeded (or
a duplicate inquiry row).
- Added loading states to Home.tsx (services/products) and Book.tsx
(services list, date/slot step) -- previously only Shop.tsx had
one, though the spec named all three pages.
- Restored the full date -> slot -> contact -> confirmation ->
persisted-booking test in Book.test.tsx, which had been replaced
rather than supplemented by a narrower slot-fetching test.
Deferred to a follow-up (explicit decision, not silently dropped):
duration-aware slot conflicts. The current model only blocks an exact
(date, time) match, so a 4-hour service booked at 9:00 AM doesn't
block 11:00 AM even though they'd physically overlap. Inherited from
the Phase 2A data model; this branch is what makes it reachable by
real customers, so it needs a recorded decision before go-live rather
than silent inheritance.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Make useBooking's submit() function async and integrate with Supabase bookings table.
Add submitError field for displaying conflict/error messages to users.
Handle SQLSTATE 23505 (duplicate key) by returning to slot selection.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Replaces static services import and mock slot/date functions with
fetchServices() (Task 4) and useAvailability() (Task 5).
Fixes two real bugs surfaced by this task's cross-task integration:
1. Book.test.tsx's useAvailability mock returned fresh inline-wrapped
functions every render, causing Book.tsx's exhaustive-deps-correct
effect to re-fire every render (infinite loop, hung the test
indefinitely). Fixed by returning the already-stable vi.fn()
references directly.
2. useAvailability.ts's fire-and-forget `void load()` had no
try/catch, so a synchronous throw from a mocked/unexpected
supabase.from() call became an unhandled promise rejection
(surfaced via Home.test.tsx rendering Book with an unmocked
schedule_template/schedule_exceptions query). Added try/catch;
pure robustness fix, no production behavior change since the real
client never throws synchronously.
Also fixed a react-hooks/set-state-in-effect lint error (moved the
slot effect's guard clause inside its async closure, same pattern
used throughout Phase 2B) and the resulting exhaustive-deps warning,
by memoizing useAvailability's return value so Book.tsx can safely
depend on the whole `availability` object without risking the same
re-render-loop class of bug this task started with.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
The fromMock in the test uses vi.fn's explicit generic type parameter
instead of inferring from a zero-arg implementation, since the brief's
literal (...args) => fromMock(...args) wrapper otherwise fails to
typecheck (TS2556) against tsc -b.
Translates all user-visible text in the admin panel (login, nav,
products, services, schedule, bookings, inquiries, and the shared
friendlyError helper) to plain, direct French for Mira's internal
use. Database CHECK-constraint enum values (product category,
booking/inquiry status) and column names are left untouched in code;
French display labels are added via lookup maps instead. Corresponding
test assertions updated to match.
Translates all user-visible English text on the public-facing pages
(Home, Shop, Book, Rules, Nav, Footer, InquiryModal, StepProgress,
ServiceCard, ProductCard) plus product and service data to French,
matching the tone/register of the already-French rules.ts. Category
enum values, IDs, route paths, and admin pages are left untouched.
Updates matching test assertions to the new French copy.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Consolidated fix wave for 1 Critical + 6 Important findings from the final
review before merge:
- Critical: marking a booking completed no longer frees its booked_slots
mirror row or the unique-slot index, closing a double-booking hole
(new migration 20260812110508, with regression pgTAP tests added first
and confirmed failing against the old trigger/index).
- uploadSalonImage() now returns the storage-relative path instead of an
absolute public URL, so products/services.image stays one consistent
format.
- The product/service Id field is disabled while editing, preventing
accidental duplicate rows from an upsert keyed on a changed id.
- Cancel / Mark completed in AdminBookings now require window.confirm,
matching the existing delete-confirmation pattern.
- /admin and any unmatched /admin/* path now redirect to /admin/products
instead of rendering blank.
- Public signup is disabled in supabase/config.toml, and ProtectedRoute
adds a defense-in-depth check that the session belongs to the admin
user id, not just that a session exists.
- Product/service delete now surfaces a friendly message for FK-violation
errors (23503) via a new friendlyError() helper, instead of a raw
Postgres error string.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds AdminApp as a lazy-loaded route subtree in App.tsx, mounting
ProtectedRoute, AdminLogin, AdminLayout, and all five CRUD pages built
in Tasks 3-11. Mounted as a sibling of the public Layout route (not
nested under it), since the admin panel has its own AdminLayout
sidebar rather than the public nav/footer.
Manually verified end-to-end against the local Supabase stack: sign-in
redirect guard, all five pages rendering real seeded data, creating and
cancelling a real booking through the UI (confirming the booked_slots
cleanup trigger from the merged Phase 2A follow-up fixes correctly
frees the slot), and sign-out. No console errors across the flow.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Replace variadic spread pattern with properly-typed single-argument mock.
storage.from() only takes bucketId string argument, so fromMock type
accurately reflects the actual API. No suppression needed - TypeScript
and ESLint both pass cleanly.
Use @ts-expect-error on vi.mock line only - vitest mock function calls
require spreading unknown[] which TypeScript can't verify as type-safe
without explicit suppression or use of 'any' type. This is a known
limitation of TypeScript with generic mock frameworks.
Renamed useAdminAuthMock to adminAuthMock in ProtectedRoute.test.tsx to prevent
ESLint from flagging it as an invalid hook call. The 'use' prefix naming heuristic
was causing a react-hooks/rules-of-hooks rule violation, though it was not actually
a hook call context. This change maintains all test behavior while passing lint.
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.
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.
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.