The logo was much taller than the text columns and top-aligned,
making the footer look lopsided. Vertically centers the grid columns
and shrinks the logo so it's proportionate to the rest of the footer.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Replaces the plain text wordmark with the client-provided logo: a
compact icon+wordmark lockup in the nav, and the full crown/wordmark/
tagline mark (recolored for the dark footer) in the footer. Background
made transparent by color-keying the flat cream backdrop out of the
source artwork.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds hair length/type, scalp sensitivities, wet/dry state, desired
style, who provides extensions, guest count, and two optional
reference photos to the booking form. Photos upload to a new private
booking-photos storage bucket; admin can view intake details and
signed photo URLs per booking.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Shows a badge with the booking count on each day; clicking a date
filters the table below to that day's bookings, with a button to
clear the filter and see everything again.
Extended uploadImages() call to include the 4 image files referenced by
the site_content and testimonials migrations: about/mira-portrait.jpg
and testimonials/{amara,keisha,nadia}.jpg. These paths are seeded in
the migration but were not being uploaded to the salon-images bucket,
causing broken image links on deploy.
Verified files exist in public/ and match migration seed paths.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Two independent plan gaps, both surfaced only now because no prior task
in this plan ran a full tsc/lint pass (only scoped vitest per task):
1. Task 6 added required featured/featuredOrder fields to the Product
type but only updated products.test.ts's fixture. ProductCard.test.tsx
and InquiryModal.test.tsx also construct Product-typed fixtures and
were never updated, breaking `tsc -b`/`npm run build`.
2. Tasks 11-13's plan text called the extracted load*() function
directly from a bare `useEffect(() => { void load*() }, [])`, which
trips this repo's react-hooks/set-state-in-effect rule. This
project's established convention (already used in AdminSchedule.tsx)
wraps the call in an effect-local inner async function instead.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Apply the shared admin design system (adminShared.module.css) to the 4 remaining
legacy admin pages via CSS composition. All buttons in these 4 pages now use the
shared button classes (button, buttonSecondary, buttonDanger) for consistent styling.
- AdminServices: 3 buttons (Modifier, Supprimer, Enregistrer le service)
- AdminSchedule: 3 buttons (Enregistrer l'horaire, Retirer, Ajouter une exception)
- AdminBookings: 2 buttons (Annuler, Marquer comme terminée)
- AdminInquiries: 1 button (Marquer comme lue)
All tests pass unchanged (127 tests). No data-fetching or event-handler logic changed.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Implement new admin page for editing the site_content singleton table. Includes:
- Form with fields for hero headline/tagline, about bio, portrait image, contact email, and hours
- Image upload via uploadSalonImage with about/ path prefix
- Data persistence via supabase.from('site_content').update().eq('id', 'default')
- Composable styles from adminShared.module.css
- Full test coverage: loading content and saving edits
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
- Replace static rules import with async fetchRules() function
- Update RuleIcon prop from ruleId to iconKey
- Add error state handling for data loading failures
- Add .error CSS class for error message styling
Closes Task 5's deferred compilation breakage: Rules.tsx now imports fetchRules (async) instead of the deleted static rules export, and uses the renamed iconKey prop on RuleIcon.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
- Rewrite src/data/rules.ts: export async fetchRules() instead of static rules array
- Fetch from 'rules' table, map icon_key to iconKey in RuleSection interface
- Create src/data/rules.test.ts with 2 tests (fetch mapping, error handling)
- Rename RuleIcon prop from ruleId to iconKey for consistency with new data shape
- Update RuleIcon.test.tsx to use iconKey prop name
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Create AdminApp.module.css with admin-specific design tokens (--admin-bg, --admin-surface, --admin-border, --admin-text, --admin-text-muted, --admin-primary, --admin-primary-hover, --admin-danger, --admin-radius, --admin-shadow, --admin-font, --admin-space-xs/sm/md/lg/xl) scoped to .adminRoot wrapper class to avoid collisions with public site tokens.
Create adminShared.module.css with composable component classes (page, card, table, form, error, button, buttonSecondary, buttonDanger) that all later admin pages will build against.
Wrap AdminApp Routes in div with adminRoot class to establish token scope.
Redesign AdminLayout.module.css to use new admin tokens with improved sidebar styling (240px width, better spacing, hover states, active link highlighting).
Redesign AdminLogin.module.css to use new admin tokens with improved form styling (better card styling, focus states, proper typography).
No changes to AdminLayout.tsx or AdminLogin.tsx needed — existing JSX structure matches new CSS selectors.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
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
Task 3's implementer independently hit and fixed this exact bug
(a vi.fn() given a zero-arg implementation, then invoked via a spread
of unknown[] args inside a vi.mock factory, fails tsc -b with TS2556).
Applied the same fix (explicit vi.fn<...> generic type) to the other
occurrences of this pattern still written the old way in this plan's
Task 3 and Task 4 sections, so those implementers don't have to
rediscover it from scratch. Task 5, 10, and 11's occurrences were
checked and found to already use safe patterns (single literal-arg
calls, or bare vi.fn() with no implementation) that don't trigger this.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Task 3's review surfaced that the plan's original vi.importActual-based
mocking for Shop.test.tsx/Home.test.tsx/Book.test.tsx would execute the
real data-layer modules, which import the real Supabase client and
construct it at module-load time via createClient(VITE_SUPABASE_URL,
...) -- throwing "supabaseUrl is required" in a test env with no .env
file, before the test even runs.
Switched all three to mock ../lib/supabase directly instead, matching
the pattern Task 3's own products.test.ts already uses successfully.
Also fixed 3 occurrences of `err instanceof Error ? err.message :
String(err)` in the planned Shop.tsx/Home.tsx/Book.tsx error handling
-- fetchProducts/fetchServices throw the raw Postgrest error object
(not necessarily an Error instance), so this would have rendered
"[object Object]" instead of the real message in the mocked test
scenario.
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.
Without this, Vercel serves a 404 for any client-side route accessed
directly (e.g. /admin/login, /shop) since it has no server-side route
matching those paths — only index.html exists as a static file. React
Router then never gets a chance to handle the path client-side.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH