Files
braidsbymira/docs/superpowers/specs/2026-08-13-phase2d-site-content-and-admin-redesign-design.md

309 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.