docs: add Phase 2D design spec (site content CMS + admin redesign)
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user