Session 34: Design system Phase C — app shell, nav, routing, auth gate, footer, 404 (1818 tests)

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) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-15 23:27:58 -04:00
parent a74b5dd1ed
commit 907c7b17c1
19 changed files with 1020 additions and 430 deletions
+140
View File
@@ -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('<Ticker');
});
it('styles nav links in JetBrains Mono with grade-green active state', () => {
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('<AuthGate>');
expect(src).toContain('<Footer />');
expect(src).toContain('<HashRedirect />');
});
});
describe('Phase C — route stubs for not-yet-built screens', () => {
const stubs = ['terminal', 'compare', 'invite', 'help', 'about', 'notifications'];
it.each(stubs)('/%s has a page that uses the design-system RouteStub', (name) => {
expect(exists(`app/${name}/page.tsx`)).toBe(true);
expect(read(`app/${name}/page.tsx`)).toContain('RouteStub');
});
it('does NOT stub over existing real pages (ledger/tracker/blog/game)', () => {
// these predate Session 34 with real content — must not be RouteStubs
['app/ledger/page.tsx', 'app/tracker/page.tsx', 'app/blog/page.tsx'].forEach((p) => {
expect(read(p)).not.toContain('RouteStub');
});
});
it('RouteStub speaks the system language', () => {
const src = read('components/vyndr/RouteStub.tsx');
expect(src).toContain('ROUTE UNDER CONSTRUCTION');
expect(src).toContain('SectionHead');
});
});