From 907c7b17c106abcfd19fca641f3cf7b23170404c Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 15 Jun 2026 23:27:58 -0400 Subject: [PATCH] =?UTF-8?q?Session=2034:=20Design=20system=20Phase=20C=20?= =?UTF-8?q?=E2=80=94=20app=20shell,=20nav,=20routing,=20auth=20gate,=20foo?= =?UTF-8?q?ter,=20404=20(1818=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VYNDR 2.0 conversion, Phase C (the frame every page sits inside). Frontend-only; zero backend changes. - Nav rewritten: new .wm Wordmark, mono uppercase links, More dropdown, search/ bell/read-meter/avatar, Ticker under the bar. layout main paddingTop 64 -> 96. - Routing: web/src/lib/routes.js (GATED/OPEN/HASH_ALIASES, isGatedRoute, resolveHashAlias). Client AuthGate bounces signed-out users off personal routes to /login?next=. HashRedirect maps #scan/#terminal to real routes. - Footer rewritten to system voice + Detroit signature; mounted globally in layout (removed per-page dup). - 404 converted to the north star (scanlines, crt-sweep, glitch wordmark, amber). - Stub pages for terminal/compare/invite/help/about/notifications via RouteStub. Honest reconciliations: auth gate is client-side (no auth-helpers pkg; session is client-side Supabase); GATED narrowed to protect the free-scan funnel; did not stub over existing real pages; redirect param is ?next= (what /login reads). 26 new tests. Backend 1792 -> 1818, 142 suites, zero regressions. Web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- BUILD-STATE.md | 92 ++- CLAUDE.md | 32 + tests/unit/vyndrAppShell.test.js | 140 +++++ web/src/app/about/page.tsx | 13 + web/src/app/compare/page.tsx | 13 + web/src/app/help/page.tsx | 13 + web/src/app/invite/page.tsx | 13 + web/src/app/layout.tsx | 10 +- web/src/app/not-found.tsx | 68 +- web/src/app/notifications/page.tsx | 14 + web/src/app/page.tsx | 3 +- web/src/app/terminal/page.tsx | 13 + web/src/components/AuthGate.tsx | 33 + web/src/components/Footer.tsx | 201 +++--- web/src/components/Nav.tsx | 618 +++++++++++-------- web/src/components/vyndr/HashRedirect.tsx | 23 + web/src/components/vyndr/NotFoundActions.tsx | 15 + web/src/components/vyndr/RouteStub.tsx | 45 ++ web/src/lib/routes.js | 91 +++ 19 files changed, 1020 insertions(+), 430 deletions(-) create mode 100644 tests/unit/vyndrAppShell.test.js create mode 100644 web/src/app/about/page.tsx create mode 100644 web/src/app/compare/page.tsx create mode 100644 web/src/app/help/page.tsx create mode 100644 web/src/app/invite/page.tsx create mode 100644 web/src/app/notifications/page.tsx create mode 100644 web/src/app/terminal/page.tsx create mode 100644 web/src/components/AuthGate.tsx create mode 100644 web/src/components/vyndr/HashRedirect.tsx create mode 100644 web/src/components/vyndr/NotFoundActions.tsx create mode 100644 web/src/components/vyndr/RouteStub.tsx create mode 100644 web/src/lib/routes.js diff --git a/BUILD-STATE.md b/BUILD-STATE.md index 10e9280..9d3f301 100755 --- a/BUILD-STATE.md +++ b/BUILD-STATE.md @@ -4,8 +4,96 @@ 2026-06-15 ## Current Phase -SHIP BUILD v33.0 — VYNDR 2.0 design system, Phase A+B: tokens + global CSS + -fonts + glitch keyframes + shared components (Session 33) +SHIP BUILD v34.0 — VYNDR 2.0 design system, Phase C: app shell — nav, routing, +auth gate, footer, 404 (Session 34) + +## Session 34 (2026-06-15) — SHIPPED + +Phase C of the VYNDR 2.0 conversion: the app shell every page sits inside. +Frontend-only; ZERO backend changes. Converted the nav/footer/404 to the design +and added the routing + auth-gate frame. Backend 1792 → **1818 tests** (+26), +142 suites, zero regressions. Web build clean (exit 0, 36 routes). + +### Honest reconciliations (spec ↔ our reality — same discipline as the S32 NFL-key fix) +- **Auth gate is CLIENT-side, not server middleware.** The prompt's example used + `createMiddlewareClient` from `@supabase/auth-helpers-nextjs` — that package + isn't installed, our auth is client-side Supabase (session in localStorage via + `@supabase/supabase-js`), and the existing `middleware.ts` is locale-only. A + server middleware physically can't read that session. So the gate runs in + `` (client) on top of the existing Supabase auth (rule 5 honored). + Locale middleware left untouched. +- **Narrowed the GATED set.** The prototype gated dashboard + scan; those are OUR + free-scan acquisition funnel (anon/free get 5 reads), so gating them is a + monetization regression. GATED = ledger/tracker/account/profile/settings/ + notifications/invite (genuinely personal surfaces). dashboard/scan stay OPEN. +- **Did NOT stub over existing real pages.** The prompt's stub list assumed + ledger/tracker/blog/game/responsible/offline didn't exist — they do, with real + content. Stubs created ONLY for genuinely-missing routes. +- **Redirect param is `?next=`,** not the prompt's `?redirect=` — that's the + param our `/login` page already reads (`router.replace(next)`). + +### C.1 — Traced the shell +Nav used legacy `@/components/Wordmark` (`.wordmark`). Auth = `AuthContext`/ +`useAuth` (user/tier/scansRemaining/loading/signOut). `middleware.ts` = locale +only. Footer existed but was mounted only on the landing page. 28 route pages. + +### C.2 — Nav (`components/Nav.tsx` rewritten) +- New `@/components/vyndr` Wordmark (size md, cursor, beta — `.wm` markup). +- Primary links SLATE/TERMINAL/SCAN/LEDGER + a **More** dropdown (Compare, + Tracker, The Report, Invite, Pricing, Settings). All nav chrome JetBrains Mono, + uppercase, 11px, 0.08em; active = `--g-a`, hover `--text-0`, default `--text-1`. +- Right cluster: `›` Query search trigger, NotificationBell (signed-in), read + meter (`n/5 · MO`, free + scan/dashboard only) / `∞ TIER` plan badge, avatar + menu (Account/Settings/Upgrade/Log out). Mobile toggle kept (full mobile shell + is Session 37). +- **Ticker** mounted under the bar (sample items; real feed = Session 38). Header + is now a fixed wrapper = 60px nav + 32px ticker → `layout` main `paddingTop` + 64 → 96. + +### C.3 — Routing + auth gate +- `web/src/lib/routes.js` (CommonJS, unit-testable): `GATED_ROUTES`, + `OPEN_ROUTES`, `HASH_ALIASES`, `isGatedRoute(path)`, `resolveHashAlias(hash)`. +- `components/AuthGate.tsx` — waits for `loading`, then bounces signed-out users + off gated routes to `/login?next=`. Mounted around `
` in layout. +- `components/vyndr/HashRedirect.tsx` — maps `#scan`/`#terminal`/… to real routes + once on mount (keeps Next file-based routing; honors old share links). Mounted + in layout. +- Stub pages (design-system `RouteStub`, system language "ROUTE UNDER + CONSTRUCTION · SESSION xx"): `/terminal`, `/compare`, `/invite`, `/help`, + `/about`, `/notifications`. + +### C.4 — Footer (`components/Footer.tsx` rewritten) +System voice: Wordmark sm+beta, mono columns PRODUCT/COMPANY/LEGAL, legal/21+ +line with `--amber` 1-800-522-4700, and **BUILT BY KEVON BUTLER · DETROIT · +© 2026 VYNDR**. Now mounted GLOBALLY in the layout (removed the per-page import +from the landing page to avoid a double footer). + +### C.5 — 404 north star (`app/not-found.tsx` rewritten) +Full-page `.scanlines`, `.crt-sweep` on load, glitch Wordmark (lg), amber-glow +"TRANSMISSION INTERRUPTED", giant `--amber` 404 (`.wm` data-text), "This page +doesn't exist. / The signal was lost.", VBtn CTAs (client `NotFoundActions` +so the page stays a server component for metadata). + +### Files created +- `web/src/lib/routes.js` +- `web/src/components/AuthGate.tsx` +- `web/src/components/vyndr/{HashRedirect,NotFoundActions,RouteStub}.tsx` +- `web/src/app/{terminal,compare,invite,help,about,notifications}/page.tsx` +- `tests/unit/vyndrAppShell.test.js` (26 tests: routing logic, gate, nav/footer/ + 404 contracts, layout wiring, stubs-don't-clobber-real-pages) + +### Files modified +- `web/src/components/Nav.tsx`, `web/src/components/Footer.tsx` +- `web/src/app/layout.tsx` (mount HashRedirect + AuthGate + global Footer, + paddingTop 96), `web/src/app/not-found.tsx`, `web/src/app/page.tsx` (drop dup + footer) + +### Deferred (Sessions 35+) +- Real screens for the stubbed routes (D/E), mobile shell (F), living-layer + ticker/heartbeat data + nav-string i18n (G). The Nav search `›` currently links + to `/scan`; the ⌘K command palette is Phase G. + +--- ## Session 33 (2026-06-15) — SHIPPED diff --git a/CLAUDE.md b/CLAUDE.md index bb7fe44..be6700d 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -165,6 +165,38 @@ Multi-session frontend conversion of the claude.ai/design "VYNDR 2.0" handoff data-motion>` overrides in globals.css. Wiring the toggles to these attrs is Phase G (Session 38). +## VYNDR 2.0 App Shell (Session 34 — Phase C) +The frame every page sits in. Frontend-only. +- **Routing config** = `web/src/lib/routes.js` (CommonJS so it's unit-testable): + `GATED_ROUTES`, `OPEN_ROUTES`, `HASH_ALIASES`, `isGatedRoute()`, + `resolveHashAlias()`. GATED is deliberately narrow — only personal surfaces + (ledger/tracker/account/profile/settings/notifications/invite). dashboard + + scan stay OPEN (the free-scan funnel); gating them would be a monetization + regression. +- **Auth gate = CLIENT-side** (`components/AuthGate.tsx`, mounted around `
` + in layout). Our Supabase session lives in localStorage, not an httpOnly cookie, + and `middleware.ts` is locale-only — a server middleware can't read it. The + gate uses `useAuth().loading/user` + `isGatedRoute()` and redirects to + `/login?next=` (the param `/login` already consumes). Do NOT try to move + this to middleware without first adding `@supabase/auth-helpers-nextjs` + + cookie sessions (a separate, larger change). +- **Hash deep-links**: `components/vyndr/HashRedirect.tsx` translates + `#scan`/`#terminal`/… → real Next routes once on mount. We keep file-based + routing; hashes are just redirect aliases for old share links / PWA shortcuts. +- **Nav** (`components/Nav.tsx`): uses the new `@/components/vyndr` Wordmark + (`.wm`), mono uppercase links, active = `--g-a`, a More dropdown, and a + **Ticker** under the bar. The fixed header is 60px nav + 32px ticker, so layout + `main` paddingTop is **96** (not 64) — keep that in sync if the header height + changes. +- **Footer** (`components/Footer.tsx`) is mounted GLOBALLY in the layout (not + per-page). System voice + "BUILT BY KEVON BUTLER · DETROIT". +- **404** (`app/not-found.tsx`) is the north star — scanlines, crt-sweep, glitch + wordmark, amber 404. Interactive CTAs live in client `NotFoundActions` so the + page stays a server component (keeps its metadata export). +- **RouteStub** (`components/vyndr/RouteStub.tsx`) backs not-yet-built routes + (terminal/compare/invite/help/about/notifications). Never stub over a route + that already has real content. + ## Active Skills - vyndr-voice (all user-facing output) - prop-analysis (grading methodology) diff --git a/tests/unit/vyndrAppShell.test.js b/tests/unit/vyndrAppShell.test.js new file mode 100644 index 0000000..0adf559 --- /dev/null +++ b/tests/unit/vyndrAppShell.test.js @@ -0,0 +1,140 @@ +// VYNDR 2.0 — Phase C app shell (Session 34): routing config + auth gate, +// nav, footer, 404, route stubs. Routing LOGIC is exercised directly via the +// CommonJS routes module; the .tsx shell is asserted against its source text +// (the plain-JS Jest config has no TS/Babel transform). + +const fs = require('fs'); +const path = require('path'); + +const WEB = path.join(__dirname, '..', '..', 'web', 'src'); +const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8'); +const exists = (rel) => fs.existsSync(path.join(WEB, rel)); + +const routes = require('../../web/src/lib/routes'); + +describe('Phase C — routing config (lib/routes)', () => { + it('gates the personal-data routes', () => { + ['/ledger', '/tracker', '/account', '/profile', '/notifications', '/invite'].forEach((r) => { + expect(routes.isGatedRoute(r)).toBe(true); + }); + }); + + it('gates nested paths under a gated route', () => { + expect(routes.isGatedRoute('/ledger/2026-06')).toBe(true); + expect(routes.isGatedRoute('/settings/security')).toBe(true); + }); + + it('leaves the landing + free funnel open (dashboard, scan stay public)', () => { + ['/', '/dashboard', '/scan', '/pricing', '/blog', '/terminal', '/login'].forEach((r) => { + expect(routes.isGatedRoute(r)).toBe(false); + }); + }); + + it('handles empty/undefined pathnames without throwing', () => { + expect(routes.isGatedRoute('')).toBe(false); + expect(routes.isGatedRoute(undefined)).toBe(false); + }); + + it('resolves hash deep-link aliases to real routes', () => { + expect(routes.resolveHashAlias('#scan')).toBe('/scan'); + expect(routes.resolveHashAlias('#terminal')).toBe('/terminal'); + expect(routes.resolveHashAlias('#slate')).toBe('/dashboard'); + expect(routes.resolveHashAlias('#nope')).toBeNull(); + expect(routes.resolveHashAlias('')).toBeNull(); + }); + + it('keeps GATED and OPEN route lists disjoint', () => { + const overlap = routes.GATED_ROUTES.filter((r) => routes.OPEN_ROUTES.includes(r)); + expect(overlap).toEqual([]); + }); +}); + +describe('Phase C — AuthGate (client-side gate, keeps Supabase auth)', () => { + const src = read('components/AuthGate.tsx'); + it('decides gating from lib/routes, not a server middleware', () => { + expect(src).toContain("from '@/lib/routes'"); + expect(src).toContain('isGatedRoute'); + expect(src).toContain('useAuth'); + }); + it('redirects to /login carrying the intended path via ?next=', () => { + expect(src).toContain('/login?next='); + }); + it('waits for auth to finish loading before redirecting', () => { + expect(src).toMatch(/if \(loading\) return/); + }); +}); + +describe('Phase C — Nav conversion', () => { + const src = read('components/Nav.tsx'); + it('uses the new VYNDR 2.0 Wordmark (.wm), not the legacy one', () => { + expect(src).toContain("from '@/components/vyndr'"); + expect(src).not.toContain("from '@/components/Wordmark'"); + }); + it('renders the Ticker under the bar', () => { + expect(src).toContain(' { + expect(src).toContain("fontFamily: 'var(--mono)'"); + expect(src).toContain("'var(--g-a)'"); + }); + it('exposes the primary Slate/Terminal/Scan/Ledger routes', () => { + ['/dashboard', '/terminal', '/scan', '/ledger'].forEach((href) => { + expect(src).toContain(href); + }); + }); +}); + +describe('Phase C — Footer (system voice)', () => { + const src = read('components/Footer.tsx'); + it('uses the new Wordmark and mono type', () => { + expect(src).toContain("from '@/components/vyndr'"); + expect(src).toContain('var(--mono)'); + }); + it('carries the Detroit signature + legal/21+ system language', () => { + expect(src).toContain('DETROIT'); + expect(src).toContain('21+'); + expect(src).toContain('not a sportsbook'); + expect(src).toContain('Not financial advice'); + }); +}); + +describe('Phase C — 404 north star', () => { + const src = read('app/not-found.tsx'); + it('renders the amber 404 with system language', () => { + expect(src).toContain('TRANSMISSION INTERRUPTED'); + expect(src).toContain('404'); + expect(src).toContain('var(--amber)'); + }); + it('uses the scanline texture and a CRT sweep on load', () => { + expect(src).toContain('scanlines'); + expect(src).toContain('crt-sweep'); + }); +}); + +describe('Phase C — layout wiring', () => { + const src = read('app/layout.tsx'); + it('mounts AuthGate, global Footer, and the hash bridge', () => { + expect(src).toContain(''); + expect(src).toContain('