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
+91
View File
@@ -0,0 +1,91 @@
/* ============================================================
VYNDR 2.0 — app-shell routing config (§6, §12).
Plain CommonJS so the client AuthGate/HashRedirect import it
(allowJs) AND the Jest suite requires it directly (no transform).
AUTH NOTE: our auth is client-side Supabase (session in
localStorage via @supabase/supabase-js), and the existing
middleware.ts is locale-only. A Next server middleware can't read
that session, so the gate is enforced CLIENT-side by <AuthGate>.
This file is the single source of truth for which routes gate.
============================================================ */
/* Gated — require an authenticated user. Deliberately narrower than the
prototype's GATED set: the prototype also gated dashboard + scan, but those
are OUR free-scan acquisition funnel (anon/free users get 5 reads), so
gating them would be a monetization regression. We gate only the genuinely
personal surfaces (a user's own ledger, bets, account, alerts). */
const GATED_ROUTES = [
'/ledger',
'/tracker',
'/account',
'/profile',
'/settings',
'/notifications',
'/invite',
];
/* Open — reachable without auth (marketing + the free funnel + auth itself). */
const OPEN_ROUTES = [
'/',
'/dashboard',
'/slate',
'/scan',
'/terminal',
'/compare',
'/game',
'/pricing',
'/blog',
'/article',
'/responsible-gambling',
'/help',
'/about',
'/terms',
'/privacy',
'/login',
'/signup',
'/auth',
'/forgot-password',
'/verify',
'/welcome',
'/offline',
'/upgrade',
];
/* Hash deep-link aliases (§C.3.4). The prototype was a single HTML file using
#scan / #terminal; we keep Next file-based routing and treat these hashes as
redirects so old share links and PWA shortcuts still resolve. */
const HASH_ALIASES = {
'#slate': '/dashboard',
'#dashboard': '/dashboard',
'#scan': '/scan',
'#terminal': '/terminal',
'#compare': '/compare',
'#ledger': '/ledger',
'#tracker': '/tracker',
'#account': '/account',
'#pricing': '/pricing',
'#blog': '/blog',
'#invite': '/invite',
'#notifications': '/notifications',
};
/** True when `pathname` falls under a gated route (exact or nested). */
function isGatedRoute(pathname) {
if (!pathname) return false;
return GATED_ROUTES.some((r) => pathname === r || pathname.startsWith(r + '/'));
}
/** Resolve a window.location.hash (e.g. "#scan") to a real route, or null. */
function resolveHashAlias(hash) {
if (!hash) return null;
return HASH_ALIASES[hash] || null;
}
module.exports = {
GATED_ROUTES,
OPEN_ROUTES,
HASH_ALIASES,
isGatedRoute,
resolveHashAlias,
};