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

12 KiB
Raw Blame History

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)

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)

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)

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)

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)

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)

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:

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.