Internal staff panel for Yarify. Next.js 16 App Router, React 19, Tailwind 4, shadcn/ui.
Specs: ../docs/06-CHECKLIST-Admin-UI.md (screens) and
../docs/05-CHECKLIST-Admin-API.md (contract).
Working rules: CLAUDE.md.
npm install
npm run dev # http://localhost:3000Sign in with either demo account (shown on the login screen, click a row to fill the form):
| Password | Role | |
|---|---|---|
bhaskar@yarify.app |
yarify-demo-2026 |
superadmin |
ops@yarify.app |
yarify-demo-2026 |
admin |
Sign in as the admin account to see the role gating: the Team section disappears from
the sidebar, and /team explains itself rather than 404ing.
Minimum window width is 1280px. Below 1024px the panel shows a notice instead of a broken table, which is deliberate (UI doc §0.4).
Every screen is complete and interactive, backed by an in-memory dataset in lib/mock/.
Nothing talks to ../api yet. What works end to end against the mocks:
- Suspending a user flips their row everywhere and writes an audit entry
- Approving a verification grants the badge and clears it from the queue
- Two "admins" cannot both claim one submission
- Sending a campaign walks a real progress bar and lands final delivery stats
- Deactivating a prompt or language persists; deleting an in-use one is refused
- Adding a team member reveals a generated password exactly once
A new verification submission is injected 45 seconds after load so the live-notification path (toast, queue prepend, sidebar badge) can be seen without a backend.
Data is deterministic, seeded from a fixed PRNG with a fixed "now" of 2026-08-06T10:00. The same screenshot reproduces on every reload and on any machine, so approved designs do not drift. State resets on reload.
One flag, then one branch per module. No dummy code is left behind, because the real transport already exists and is typed.
- Set
NEXT_PUBLIC_USE_MOCKS=falsein.env.local. - Generate types from the live contract and delete the hand-written ones:
Re-export the names from
npm run api:types # ../api/openapi.json -> lib/api/schema.d.tslib/api/types.tsoff the generated file, or keep that file as the single import surface and have it re-export. Nothing else imports the mock layer. - In each
lib/api/endpoints/*.ts, delete the code after theif (!USE_MOCKS)branch. The realhttp.get/http.postcalls are already written above it. - Delete
lib/mock/andlib/api/endpoints/session.ts(mock-only actor resolution). - Replace the polling in
lib/notifications/provider.tsxwith a Socket.IO connection to the/adminnamespace. The store, badges and toast throttling stay as they are; the polling path remains the documented fallback for when the socket is down. - The three placeholder image helpers in
lib/mock/media.tsbecome CloudFront and signed S3 URLs. Every consumer already treats them as opaque strings.
lib/api/http.ts is finished production code: single-flight refresh, one retry per 401,
in-memory access token, credentials: 'include' for the httpOnly refresh cookie, and the
error envelope parsed into ApiError.
app/
├── login/ login-01 block, adapted (no register, no reset, no social)
├── force-password/ unskippable first-login gate
└── (dash)/ sidebar-08 shell + auth boot + providers
├── page.tsx dashboard: 8 KPI tiles, 4 charts, 2 activity panels
├── users/ list (11 columns, keyset paging) + detail (6 tabs, suspend)
├── reviews/ split queue + selfie-vs-photos compare + signals
├── marketing/push/ list + composer + detail
├── content/ prompts + languages
├── team/ superadmin only
├── audit/ read-only, grouped filters
└── settings/
components/ 9 custom wrappers, everything else is shadcn
lib/
├── api/ types, config flag, http, token, errors, keys, endpoints/
├── mock/ the only place that knows the data is fake
├── auth/ AdminAuthProvider, usePermission, <Can>
├── notifications/ badge store + provider
└── hooks/ useCursorList, useLocalFlag
npm run dev
npm run build
npm run lint # eslint, including the React Compiler rules
npm run typecheck # tsc --noEmit
npm run api:types # regenerate lib/api/schema.d.ts from ../api/openapi.jsoncomponents/ui/** and hooks/use-mobile.ts are generated by shadcn add and are excluded
from lint. Do not hand-edit them.