12 KiB
Phase 2D: Site Content Management & Admin Panel Redesign — Design
Purpose
Two problems, addressed together because they both live in the admin panel:
- 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.
- 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_contentsingleton table (hero, about, contact/hours) + one admin edit page. - New
testimonialstable (full CRUD + reorder) + one admin page. - New
rulestable (full CRUD + reorder, icon picked from the existing 6-icon set) replacing the hardcodedsrc/data/rules.ts, and its own admin page. featured/featured_ordercolumns added to the existingproductstable, with featured toggle + reorder controls added to the existing AdminProducts page.- Public site changes:
Home.tsx,Footer.tsx, andRules.tsxread 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
TESTIMONIALSarray withfetchTestimonials(), loaded alongsidefetchServices()/fetchProducts()in the existingPromise.allin 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/errorstate covering the services+products fetch — the two new fetches join the samePromise.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.