docs: add Phase 2D design spec (site content CMS + admin redesign)

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-13 08:12:37 +02:00
parent f41c6f5812
commit 510847eac2

View File

@@ -0,0 +1,308 @@
# Phase 2D: Site Content Management & Admin Panel Redesign — Design
## Purpose
Two problems, addressed together because they both live in the admin panel:
1. **Missing editability.** Several pieces of public-site content are hardcoded in
the frontend and can't be changed without a code deploy: the homepage hero
headline/tagline, the "About Mira" bio and portrait, testimonials, the
featured-products selection on the homepage, footer contact info/hours, and
the salon rules page. Mira needs to edit all of these from the admin panel.
2. **Unprofessional admin UI.** The Phase 2B admin panel was built with zero
design attention. It needs a visual pass: sidebar, tables, forms, buttons —
consistent spacing, typography, and color, reading as a professional
back-office tool.
## Scope
**In scope:**
- New `site_content` singleton table (hero, about, contact/hours) + one admin
edit page.
- New `testimonials` table (full CRUD + reorder) + one admin page.
- New `rules` table (full CRUD + reorder, icon picked from the existing
6-icon set) replacing the hardcoded `src/data/rules.ts`, and its own admin
page.
- `featured` / `featured_order` columns added to the existing `products`
table, with featured toggle + reorder controls added to the existing
AdminProducts page.
- Public site changes: `Home.tsx`, `Footer.tsx`, and `Rules.tsx` read from
Supabase instead of hardcoded values/imports.
- Visual redesign of the admin panel: `AdminLayout` (sidebar/topbar), shared
table and form styling, buttons — applied to all 8 admin pages (5 existing
+ 3 new).
**Out of scope:**
- Uploadable/custom rule icons (stays a fixed 6-icon dropdown).
- Redesigning the public-facing site (already has design attention from an
earlier phase; only the 3 pages above get new *data*, not new *look*).
- Multi-admin support (unchanged from the single fixed admin UUID model).
- Any change to the booking/inquiry/availability logic from Phase 2C.
## Data Model
Four schema changes, one migration, following the project's established
public-read/admin-write RLS pattern (`(select auth.uid()) = '<admin-uuid>'`,
using the current admin UUID `2b88676b-f901-459a-9147-b914190c4f81`).
### `site_content` (new, singleton — same pattern as `schedule_template`)
```sql
create table public.site_content (
id text primary key default 'default',
hero_headline text not null,
hero_tagline text not null,
about_bio text not null,
about_portrait_image text not null,
contact_email text not null,
contact_hours text not null
);
```
One row, `id = 'default'`. `about_portrait_image` and any avatar/logo-style
image fields store a Storage path, resolved client-side via the existing
`resolveImageUrl` helper — same convention as `products.image` and
`services.image`.
RLS: `site_content_public_read` (select, `true`), `site_content_admin_write`
(all, admin UUID check).
Seeded with the current hardcoded values (from `Home.tsx` and `Footer.tsx`)
so the migration doesn't blank the live site.
### `testimonials` (new)
```sql
create table public.testimonials (
id uuid primary key default gen_random_uuid(),
quote text not null,
author text not null,
avatar_image text not null,
sort_order integer not null
);
```
RLS: `testimonials_public_read` (select, `true`),
`testimonials_admin_write` (all, admin UUID check).
Seeded with the 3 current hardcoded testimonials (Amara T., Keisha R., Nadia
F.), `sort_order` 0/1/2.
### `rules` (new, replaces `src/data/rules.ts`)
```sql
create table public.rules (
id uuid primary key default gen_random_uuid(),
title text not null,
body text not null,
icon_key text not null,
sort_order integer not null
);
```
`icon_key` is one of the 6 existing keys in `RuleIcon.tsx`
(`deposit`, `hair-prep`, `extensions`, `lateness`, `guests`, `payment`) — the
admin form offers these 6 as a dropdown, no free text. `RuleIcon` changes
from taking a `ruleId` prop to an `iconKey` prop and falls back to
`deposit`'s icon for any unrecognized value (matches its current fallback
behavior).
RLS: `rules_public_read` (select, `true`), `rules_admin_write` (all, admin
UUID check).
Seeded with the current 6 hardcoded rules and their existing icon mapping,
`sort_order` 0–5.
### `products` (altered)
```sql
alter table public.products add column featured boolean not null default false;
alter table public.products add column featured_order integer;
```
`featured_order` is `null` when `featured = false`. When `featured = true`,
it holds the display position among featured products (0-based, no gaps
enforced at the DB level — the admin UI is responsible for keeping it dense,
same as the existing `sort_order` columns above). No RLS change needed —
`products` already has public-read/admin-write policies covering all
columns.
## Frontend: Public Site
### `src/data/siteContent.ts` (new)
```ts
export interface SiteContent {
heroHeadline: string
heroTagline: string
aboutBio: string
aboutPortraitImage: string
contactEmail: string
contactHours: string
}
export async function fetchSiteContent(): Promise<SiteContent>
```
Queries `site_content` where `id = 'default'`, maps snake_case → camelCase,
resolves `about_portrait_image` via `resolveImageUrl`. Matches the existing
`fetchProducts`/`fetchServices` shape and error-handling convention (throws
on Supabase error; callers already have a `try/catch` + `error` state
pattern established in `Home.tsx`/`Book.tsx`).
### `src/data/testimonials.ts` (new)
```ts
export interface Testimonial {
id: string
quote: string
author: string
avatarImage: string
}
export async function fetchTestimonials(): Promise<Testimonial[]>
```
Queries `testimonials` ordered by `sort_order`, resolves `avatar_image` via
`resolveImageUrl`.
### `src/data/rules.ts` (rewritten)
Currently exports a static `rules` array. Becomes:
```ts
export interface RuleSection {
id: string
title: string
body: string
iconKey: string
}
export async function fetchRules(): Promise<RuleSection[]>
```
Queries `rules` ordered by `sort_order`.
### `src/data/products.ts` (extended)
`fetchProducts()`'s existing row mapping gains `featured: boolean` and
`featuredOrder: number | null` on the returned `Product` type. No new
function — `Home.tsx` filters/sorts the existing `fetchProducts()` result
client-side (`products.filter(p => p.featured).sort((a, b) => a.featuredOrder! - b.featuredOrder!)`),
replacing today's `products.filter((p) => p.inStock).slice(0, 3)`.
### `Home.tsx`
- Replace the hardcoded `TESTIMONIALS` array with `fetchTestimonials()`,
loaded alongside `fetchServices()`/`fetchProducts()` in the existing
`Promise.all` in the load effect.
- Replace the hardcoded hero headline/tagline and About-Mira bio/portrait
with fields from `fetchSiteContent()`, loaded in the same effect.
- Replace `products.filter((p) => p.inStock).slice(0, 3)` with the
featured-products filter/sort described above.
- Loading/error states extend the existing pattern (already has
`loading`/`error` state covering the services+products fetch — the two
new fetches join the same `Promise.all`).
### `Footer.tsx`
Currently a static component with no data fetching. Gains its own
`fetchSiteContent()` call (own `useEffect`/`useState`, matching this
project's established no-shared-cache convention — every page/component
that needs Supabase data fetches it independently; there's no existing
context/cache layer to hook into, and introducing one is out of scope) to
render `contact_email` and `contact_hours` instead of the hardcoded values.
### `Rules.tsx`
Replaces the static `import { rules } from '../data/rules'` with a
`fetchRules()` call in a `useEffect`, following the same
loading/error/render pattern as `Shop.tsx`. `RuleIcon` takes `iconKey`
instead of `ruleId`.
## Frontend: Admin Panel
### New page: `AdminContent` (`/admin/content`, nav label "Contenu")
Single form editing the `site_content` singleton — same structural pattern
as `AdminSchedule`'s template-editing half (fetch on mount, controlled
inputs, one "Enregistrer" button, `saving`/`error` state). Fields: hero
headline, hero tagline, about bio (textarea), about portrait (image
upload, reusing the existing image-upload component/pattern from
`AdminProducts`/`AdminServices`), contact email, contact hours.
### New page: `AdminTestimonials` (`/admin/testimonials`, nav label "Témoignages")
Table + create/edit form, same shape as `AdminServices`: list existing
testimonials (quote, author, avatar thumbnail), create/edit/delete, plus
up/down buttons per row that swap `sort_order` with the adjacent row and
persist both rows in one update. (Deliberately not drag-and-drop — up/down
buttons are simpler to build, test, and use correctly for a handful of
rows, and this project has no existing drag-and-drop dependency.)
### New page: `AdminRules` (`/admin/rules`, nav label "Règlements")
Same shape as `AdminTestimonials`: table + create/edit form (title, body
textarea, icon dropdown showing all 6 icons rendered via `RuleIcon`),
up/down reorder.
### `AdminProducts` (extended)
Each row gains a "Vedette" checkbox/toggle. When checked, the row also
shows up/down reorder controls (same swap-and-persist mechanism as
testimonials/rules) scoped to just the featured subset. Unchecking clears
`featured_order` to `null`.
### `AdminLayout` / `AdminApp` (extended)
`NAV_ITEMS` gains 3 entries (Contenu, Témoignages, Règlements) alongside
the existing 5. `AdminApp`'s route table gains 3 matching `<Route>` entries.
## Redesign
Scope: **CSS, layout, and shared component structure only** — no page's
data-fetching logic changes as part of the redesign (only as part of the
content-editability work described above, where a page is touched for that
reason anyway).
- **`AdminLayout`**: proper sidebar (fixed width, clear active-state
styling, grouped nav), consistent page-content padding/max-width, a
simple topbar or page-title convention so every admin page has a clear
heading area.
- **Shared table styling**: consistent row height, borders, hover state,
action-button alignment — applied to every admin page with a table
(Produits, Services, Réservations, Demandes, and the 2 new list pages).
- **Shared form styling**: consistent label/input spacing, button styling
(primary "Enregistrer"/"Ajouter" actions vs. secondary "Annuler"/
"Supprimer"), validation-error display — applied to every admin page with
a form.
- **Color/type system**: a small, deliberate palette and type scale distinct
from the public site's editorial fonts/colors — functional and
professional rather than decorative. Reuses CSS custom properties/module
pattern already established in this codebase (CSS Modules throughout);
introduces admin-specific tokens rather than reusing the public site's.
This does not introduce a new CSS framework or design-system dependency —
it's a styling pass within the project's existing CSS Modules convention.
## Testing
Same conventions as every prior phase: Vitest + React Testing Library for
components/hooks/data functions (mocking `../lib/supabase` directly, not
`vi.importActual`), pgTAP for the new RLS policies (add
`site_content`/`testimonials`/`rules` read/write cases and the `products`
featured-column cases to `supabase/tests/database/rls.test.sql`, matching
the existing per-table pattern in that file).
## Migration & Rollout
One new migration file adds the 3 tables + 2 `products` columns + RLS
policies + seed data (seeding directly in the migration, since these are
fixed initial values the site currently already shows — not sample/test
data, so `scripts/seed.ts` is not the right place; `scripts/seed.ts`
already only owns products/services/schedule).
Local verification (`supabase db reset` + `supabase test db`) before
pushing to the live project, then `supabase db push`, matching every prior
phase's rollout process. Frontend changes go through the same
build/lint/test/deploy verification as Phase 2C.