Phase 2
Base app architecture:
Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
This commit is contained in:
201 files changed
+31984
No files matched your search
@@ -0,0 +1,176 @@
|
||||
# NextChair — Phase 2 Summary
|
||||
### Project scaffold, design system, database schema, authentication foundation
|
||||
|
||||
Status: **Complete and verified.** Typecheck, lint, and `next build` are all clean. The full schema was migrated against a real Postgres instance and the entire auth flow (register → sign-in → sign-out → permission-gated routing) was exercised end-to-end in a browser, not just eyeballed. See §7 for exactly what was verified and how.
|
||||
|
||||
Per your instructions, this phase stops here: no booking, appointments, loyalty, or staff/owner functionality has been implemented. Where those areas needed a route to exist for the layout/nav/auth architecture to be provable, the route renders a labeled "Coming in Phase N" placeholder — see §3.
|
||||
|
||||
---
|
||||
|
||||
## 1. What was built
|
||||
|
||||
1. **Project scaffold** — Next.js 16 (App Router, Turbopack), React 19, TypeScript 6 (strict), Tailwind CSS v4, ESLint 9 (flat config), Prettier.
|
||||
2. **Architecture** — `src/` layout, five route groups (`(public)` reserved, `(auth)`, `(customer)`, `(staff)`, `(owner)`), a `proxy.ts` (Next 16's renamed `middleware.ts`) gating protected paths, and a `features/`/`lib/`/`services/`/`components/` separation matching the structure proposed in `docs/implementation-plan.md` §11.
|
||||
3. **Design system** — 20 reusable components under `components/ui`, `components/navigation`, and `components/feedback`, all built from the visual language in `/templates`, plus a `/style-guide` reference page showing every one with placeholder content.
|
||||
4. **Database schema** — a complete `prisma/schema.prisma` covering every entity your brief listed (and a few more identified in the Phase 1 domain model), migrated and seeded successfully against a real Postgres database.
|
||||
5. **Authentication foundation** — email/password (Credentials) and Google OAuth via Auth.js (NextAuth v5), a working register → auto-sign-in flow, sign-out, and real role/permission-based route gating (not just "is logged in").
|
||||
|
||||
---
|
||||
|
||||
## 2. Folder structure
|
||||
|
||||
```text
|
||||
NextChair/
|
||||
docs/
|
||||
implementation-plan.md # Phase 1 output
|
||||
phase-2-summary.md # this file
|
||||
prisma/
|
||||
schema.prisma
|
||||
seed.ts
|
||||
migrations/20260802161213_init/migration.sql
|
||||
templates/ # untouched — 35 UX Pilot exports + prototypes
|
||||
src/
|
||||
app/
|
||||
(auth)/ # sign-in, register — shared centered-card layout
|
||||
(customer)/ # home, appointments, settings — light bottom nav, session-gated
|
||||
(staff)/staff/ # calendar, current, clients, settings — dark bottom nav, StaffProfile-gated
|
||||
(owner)/owner/ # dashboard, team — same shell as (staff), MANAGE_STAFF-gated
|
||||
api/auth/[...nextauth]/route.ts
|
||||
style-guide/ # design-system showcase (reference only)
|
||||
page.tsx # Welcome/splash
|
||||
layout.tsx, globals.css
|
||||
auth.ts # full NextAuth config (adapter + real providers)
|
||||
auth.config.ts # DB-free subset, used by proxy.ts
|
||||
proxy.ts # Next 16's middleware convention — see §6
|
||||
components/
|
||||
ui/ # Button, Card, Input, Textarea, Select, Checkbox, Switch,
|
||||
# Modal, BottomSheet, Tabs, MonthCalendar, AppointmentCard,
|
||||
# ServiceCard, BarberCard, ProfileCard, LoyaltyCard, StatusPill
|
||||
navigation/ # BottomNav, TopNav
|
||||
feedback/ # EmptyState, ErrorState, LoadingState, Skeleton, ComingSoon
|
||||
features/auth/ # schemas, register server action, sign-in/up/out components
|
||||
lib/
|
||||
auth/permissions.ts # StaffProfile → Role → RolePermission lookup
|
||||
db/prisma.ts # the one file that imports the Postgres driver adapter
|
||||
navigation/staff-nav-items.ts
|
||||
utils/cn.ts
|
||||
types/
|
||||
domain.ts # UI-facing types, deliberately independent of the ORM
|
||||
next-auth.d.ts # session.user.id module augmentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. What each route actually does right now
|
||||
|
||||
| Route | Renders | Gate |
|
||||
|---|---|---|
|
||||
| `/` | Welcome/splash (matches template 20, minus "Continue as Guest" — see §8) | redirects to `/home` if already signed in |
|
||||
| `/sign-in`, `/register` | Real, working forms | none |
|
||||
| `/home`, `/appointments`, `/settings` | `/home` and `/settings` show real session data + a working sign-out; `/appointments` is a labeled placeholder | any authenticated session |
|
||||
| `/staff`, `/staff/current`, `/staff/clients` | Labeled placeholders | authenticated **and** has a `StaffProfile` |
|
||||
| `/staff/settings` | Real profile card + conditional "Switch to Owner Admin" link (only if the signed-in staff member's Role grants `MANAGE_STAFF`) + sign-out | same as above |
|
||||
| `/owner`, `/owner/team` | Labeled placeholders | authenticated, has a `StaffProfile`, **and** `MANAGE_STAFF` permission |
|
||||
|
||||
Every placeholder exists only to prove the layout/nav/auth architecture around it works — none contain business logic. This was a deliberate choice to satisfy "Create the architecture: Route groups, Shared layouts" (Phase 2 scope §3) without drifting into "Staff functionality, Owner functionality" (explicitly excluded).
|
||||
|
||||
---
|
||||
|
||||
## 4. Design system
|
||||
|
||||
All 20 components are in `components/ui`, `components/navigation`, `components/feedback`. Highlights and the judgment calls behind them:
|
||||
|
||||
- **Tokens** (`globals.css`) — Tailwind v4's CSS-based `@theme`, carrying forward the `ink`/`stone`/`surface`/`muted`/`line`/`accent` palette that dominated the templates (Phase 1 plan §8), *not* the salon-red or blue variants found on two outlier templates. Added a `--color-accent-loyalty` token for the gold loyalty-card accent instead of letting it leak into the base palette.
|
||||
- **Button** — `cva`-based variants (`primary`/`accent`/`secondary`/`ghost`/`destructive`), `asChild` via Radix `Slot` so it can wrap a Next `Link` and still look like a button.
|
||||
- **Switch, Tabs** — Radix primitives. These are the two places a hand-rolled implementation is genuinely easy to get subtly wrong (roving tabindex, `aria-checked`); the templates' own `switchTab()` DOM-manipulation script is exactly the kind of thing this replaces.
|
||||
- **Modal, BottomSheet** — built on the native `<dialog>` element rather than Radix Dialog or a hand-rolled focus trap. `showModal()` gives focus containment, Escape-to-close, and top-layer stacking from the browser for free. No template had a modal/sheet pattern at all (Phase 1 §8 flagged this as a gap); these are new.
|
||||
- **Select, Checkbox** — deliberately *native* `<select>`/`<input type="checkbox">`, not Radix — native select gets the OS picker on mobile for free, which no custom listbox matches.
|
||||
- **MonthCalendar** — real date math (no external date library), matches the templates' booking/reschedule calendar. The staff day/agenda calendar is **not** built — Phase 1 §14 found the templates disagree on its layout (grid-columns vs. vertical agenda) and it depends on the booking engine's availability model, which doesn't exist yet.
|
||||
- **LoyaltyCard** — ports the 3D flip card from template 11 faithfully, but fixes a real gap: the original had no actual flip trigger wired up (presentational only). Here it's a real `<button aria-pressed>`, and the flip transition respects `prefers-reduced-motion`.
|
||||
- **BottomNav** — one component serves both the customer (light) and staff/owner (dark) variants, replacing the four inconsistent nav markups found across the templates. It resolves its own "active" tab from `usePathname()` rather than requiring every layout to compute and pass it — see the RSC lesson in §6 for why its `icon` prop is a string key, not a component reference.
|
||||
- **EmptyState, ErrorState, LoadingState, Skeleton** — none of these existed in any template; every list screen needs one for the zero-items/failure/loading case.
|
||||
|
||||
---
|
||||
|
||||
## 5. Database schema
|
||||
|
||||
Full schema in `prisma/schema.prisma`, migrated cleanly (`prisma/migrations/20260802161213_init/migration.sql`) and seeded (`prisma/seed.ts`) against a real Postgres instance. Covers every entity from your brief:
|
||||
|
||||
Business, BusinessLocation, BusinessHours, BusinessSettings, BookingRule · User, Account, Session, VerificationToken (Auth.js standard shape) · CustomerProfile, StaffProfile, Role, RolePermission · ServiceCategory, Service, StaffService · WorkingHours, Break, BreakDay, TimeOff · Appointment, AppointmentService · LoyaltyProgramme, LoyaltyEligibleService, LoyaltyEligibleStaff, LoyaltyAccount, LoyaltyTransaction, LoyaltyReward · Notification, NotificationPreference · CustomerNote, AuditLog.
|
||||
|
||||
**Key decisions** (each is a comment in the schema itself, not just here):
|
||||
|
||||
- **Multi-service appointments.** `AppointmentService` is a proper join table with `priceAtBooking`/`durationAtBookingMinutes` snapshots — a later price change to a `Service` never rewrites historical bookings. Directly required by your "support multiple services within a single appointment" instruction, and matches the "add another service" step visible in the booking templates.
|
||||
- **Loyalty is a ledger, not a counter.** `LoyaltyAccount.currentStamps` is a cached total; `LoyaltyTransaction` is the append-only source of truth (`STAMP_ADDED`/`STAMP_REMOVED`/`STAMP_EXPIRED`/`REWARD_REDEEMED`), and `LoyaltyReward` records a completed redemption before the account's count resets — per your explicit instruction.
|
||||
- **Roles/Permissions.** `Role` is a real per-business table (seeded with "Owner" and "Barber"); `Permission` is a code-defined enum joined via `RolePermission`. A barber who is also an owner is **one** `User` with **one** `StaffProfile`, whose `Role` happens to grant more permissions — never two accounts. This directly resolves the role-conflict the Phase 1 review found in the templates (a "Switch to Admin View" button with no real permission model behind it).
|
||||
- **Guest-friendly by design.** `CustomerProfile.userId` is nullable and `email`/`phone` are **not** unique-constrained — a hard constraint would force guests to either collide with each other or be blocked from repeat bookings under a shared household email/phone. Guest deduplication stays an explicit open question (Phase 1 plan §15, Question 4), not something silently decided by a DB constraint.
|
||||
- **Portability (Postgres → MySQL).** No scalar-list columns anywhere — `Break`'s applicable days are a `BreakDay` join table, not a `Weekday[]` array, because MySQL has no native array type. Money fields use `Decimal`, long text uses `@db.Text`, dates use `@db.Date`, and `AuditLog.metadata` uses `Json` — all four map to native equivalent types on both providers. Recurring wall-clock times (working hours, breaks, business hours) are stored as `Int` minutes-from-midnight rather than a DB `TIME` type, so scheduling math lives in application code, not provider-specific SQL.
|
||||
- **`AuditLog`** exists in the schema but nothing writes to it yet — added now so Phase 4/6 features that need it don't require a migration to start.
|
||||
|
||||
### A real, current-generation Prisma detail worth flagging
|
||||
|
||||
Prisma 7 (what `npm install prisma` currently resolves to) changed two things that aren't yet reflected in most tutorials/training data, and I hit both empirically rather than assuming:
|
||||
|
||||
1. **The client generates to a custom folder** (`generated/prisma`, via the new `prisma-client` generator), not `node_modules/@prisma/client`. Connection config moved out of `schema.prisma` into `prisma.config.ts`.
|
||||
2. **`PrismaClient` now requires an explicit driver adapter** (`@prisma/adapter-pg` for Postgres, `@prisma/adapter-mariadb` for MySQL) instead of a bare connection string. `src/lib/db/prisma.ts` is the **only** file in the app that imports the adapter — switching providers later means changing that one file, `datasource.provider` in the schema, and re-running `prisma migrate dev`, not touching application code.
|
||||
|
||||
---
|
||||
|
||||
## 6. Architecture decisions worth knowing about (found by actually building and running this, not by inspection)
|
||||
|
||||
- **Next.js 16 renamed `middleware.ts` to `proxy.ts`**, and — significantly — proxy files now run on the **Node.js runtime by default**, not Edge. That alone would have resolved the "Prisma isn't Edge-compatible" warning I initially hit, but I still split a DB-free `auth.config.ts` from the full `auth.ts` (§ below) because spinning up a Postgres connection pool just to check "is there a valid session JWT" on every request is wasteful regardless of which runtime it's allowed to run on.
|
||||
- **RSC serialization boundary bit me twice, in the same way, and I want to flag the pattern rather than bury it in a diff.** A Server Component cannot pass a live function (an inline handler, or a React component reference like a Lucide icon) as a prop to a Client Component — both fail with slightly different error messages ("Event handlers cannot be passed to Client Component props" / "Only plain objects can be passed to Client Components from Server Components"). This hit `ErrorState`/`ProfileCard` demo handlers in the style guide (fixed by moving the interactive-with-real-handlers instances into an already-`"use client"` wrapper) and `BottomNav`'s `icon` prop (fixed by passing a string key, resolved to a component inside the Client Component itself, instead of the component reference). Both fixes are the general-purpose pattern for this class of bug, not one-off patches — worth knowing before Phase 3 adds more Server-Component pages that render interactive children.
|
||||
- **Auth split**: `auth.config.ts` (pages, providers: [], session strategy — no DB) vs. `auth.ts` (the full thing: Prisma adapter, Credentials `authorize()`, Google). `proxy.ts` builds its own lightweight `NextAuth(authConfig)` instance from the former; everything else (route handlers, Server Components, Server Actions) uses the latter.
|
||||
- **Permission enforcement happens in two places on purpose**: `proxy.ts` does a cheap "is there a session at all" check (Edge/Node-safe, no DB), and each protected layout does the real DB-backed check (`StaffProfile` exists? does the `Role` grant `MANAGE_STAFF`?). This matches the Phase 1 plan's explicit instruction not to trust route-group gating alone.
|
||||
- **Icons**: swapped the templates' Font Awesome CDN script for `lucide-react` tree-shaken imports, as recommended in the Phase 1 plan.
|
||||
|
||||
---
|
||||
|
||||
## 7. What was actually verified (not just built)
|
||||
|
||||
I don't consider "it compiles" sufficient proof for an auth system, so:
|
||||
|
||||
1. Ran `prisma migrate dev` against a real, throwaway local Postgres (Docker, torn down afterward — did not touch the two unrelated Postgres containers already running on this machine) — the full schema applies as real SQL, not just Prisma-schema-valid.
|
||||
2. Ran the seed script against that database — business, roles + permissions, services, two staff users (an Owner and a plain Barber), and a customer all created successfully.
|
||||
3. In an actual browser: registered a brand-new customer account end-to-end (form → server action → DB write → auto sign-in → redirect to `/home`).
|
||||
4. Signed in as the seeded Owner (`marcus@blakeheritage.co.uk` / `password123`) and confirmed `/owner` renders for them.
|
||||
5. Signed in as the seeded plain Barber (`leo@blakeheritage.co.uk` / `password123`) and confirmed **`/owner` redirects him to `/staff`** — the negative case, which is the one that actually matters for a permission system.
|
||||
6. Confirmed a customer with no `StaffProfile` is bounced from `/staff` to `/home`.
|
||||
7. Confirmed an unauthenticated request to any protected path (`/settings` while logged out) is redirected to `/sign-in` by `proxy.ts`.
|
||||
8. Confirmed sign-out actually clears the session (not just a UI state change).
|
||||
|
||||
`npm run typecheck`, `npm run lint`, and `npm run build` are all clean with zero warnings.
|
||||
|
||||
---
|
||||
|
||||
## 8. Assumptions made this phase
|
||||
|
||||
- Password reset ("Forgot Password", template 8) was **not** wired up, even as a dead link — it needs the Notifications system (real email delivery) to do anything honest, and that's out of scope. I'd rather have no link than a link that silently does nothing, which is exactly the kind of broken-link issue the Phase 1 review flagged in the original templates.
|
||||
- The Welcome/splash page's "Continue as Guest" button (template 20) isn't shown, for the same reason — it leads into the booking flow, which doesn't exist yet.
|
||||
- Registration auto-signs the user in immediately, with no email-verification gate (`emailVerified` is modeled on `User` but not enforced) — consistent with the "auto-confirm" default recommended in the Phase 1 plan (§15, Question 5) and with every template's own instant-success flow.
|
||||
- Seeded demo accounts (`marcus@blakeheritage.co.uk`, `leo@blakeheritage.co.uk`, `alex.chen@example.com`, all password `password123`) exist only for this verification — delete or change them before any real deployment.
|
||||
- Google OAuth is wired in code but untested end-to-end (needs real `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` from a Google Cloud project, which I don't have). The Credentials path is fully verified; the Google path follows the same, standard Auth.js provider pattern and should work once real credentials are supplied — flagging this as the one piece of Phase 2 I couldn't verify firsthand.
|
||||
|
||||
## 9. What should be reviewed before moving on
|
||||
|
||||
- **The Role/Permission seed set** (`prisma/seed.ts`) — I picked a reasonable permission split between "Owner" and "Barber" based on the Phase 1 domain model, but you may want different defaults before this becomes load-bearing.
|
||||
- **Whether email verification should gate anything** before Phase 2's account-creation flow is considered "final" — right now it's purely decorative on the `User` model.
|
||||
- **The route/URL naming** (`/staff`, `/owner`, `/home` — no `/customer` prefix) — chosen to match the Phase 1 route map, worth confirming before more pages accumulate under it.
|
||||
|
||||
## 10. Getting a real database running
|
||||
|
||||
```bash
|
||||
docker run -d --name nextchair-pg -e POSTGRES_USER=nextchair \
|
||||
-e POSTGRES_PASSWORD=nextchair -e POSTGRES_DB=nextchair \
|
||||
-p 5432:5432 postgres:16-alpine
|
||||
|
||||
npm run prisma:migrate # applies prisma/migrations/
|
||||
npm run prisma:seed # creates the demo business/staff/customer
|
||||
npm run dev
|
||||
```
|
||||
|
||||
`.env` already has a matching `DATABASE_URL` and a real `AUTH_SECRET`; only `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` need filling in to exercise Google sign-in.
|
||||
|
||||
---
|
||||
|
||||
Ready for direction on the next slice of work — per the phasing in `docs/implementation-plan.md` §13, that would be **Phase 2: Customer Accounts** (the real, data-backed home/appointments/loyalty pages, replacing the placeholders in §3 above). Note this is the implementation plan's own Phase 2, distinct from "Phase 2" as you've been using it in this conversation to mean the whole scaffold-and-foundation stage just completed. I won't start building it until you say so.
|
||||
Reference in new issue
Block a user