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:
@@ -0,0 +1,13 @@
|
||||
import RouteStub from '@/components/vyndr/RouteStub';
|
||||
|
||||
export const metadata = { title: 'About' };
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<RouteStub
|
||||
title="About"
|
||||
arriving="SESSION 36"
|
||||
blurb="The books have every advantage. We built this to give it back. Built by Kevon Butler · Detroit."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import RouteStub from '@/components/vyndr/RouteStub';
|
||||
|
||||
export const metadata = { title: 'Compare' };
|
||||
|
||||
export default function ComparePage() {
|
||||
return (
|
||||
<RouteStub
|
||||
title="Compare"
|
||||
arriving="SESSION 36"
|
||||
blurb="Two players head-to-head — markets, form, and the verdict on who has the edge tonight."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import RouteStub from '@/components/vyndr/RouteStub';
|
||||
|
||||
export const metadata = { title: 'Help' };
|
||||
|
||||
export default function HelpPage() {
|
||||
return (
|
||||
<RouteStub
|
||||
title="Help & FAQ"
|
||||
arriving="SESSION 36"
|
||||
blurb="Searchable answers on grades, reads, plans, and how to read the signal."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import RouteStub from '@/components/vyndr/RouteStub';
|
||||
|
||||
export const metadata = { title: 'Invite' };
|
||||
|
||||
export default function InvitePage() {
|
||||
return (
|
||||
<RouteStub
|
||||
title="Invite"
|
||||
arriving="SESSION 36"
|
||||
blurb="Bring three friends onto the signal and your Analyst plan is on the house."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import AuthProvider from '@/contexts/AuthContext';
|
||||
import ParlayProvider from '@/contexts/ParlayContext';
|
||||
import ExplainModeProvider from '@/contexts/ExplainModeContext';
|
||||
import Nav from '@/components/Nav';
|
||||
import Footer from '@/components/Footer';
|
||||
import AuthGate from '@/components/AuthGate';
|
||||
import HashRedirect from '@/components/vyndr/HashRedirect';
|
||||
import ParlayTray from '@/components/ParlayTray';
|
||||
import BottomTabBar from '@/components/BottomTabBar';
|
||||
import InstallPrompt from '@/components/InstallPrompt';
|
||||
@@ -120,8 +123,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<AuthProvider>
|
||||
<ExplainModeProvider>
|
||||
<ParlayProvider>
|
||||
<HashRedirect />
|
||||
<Nav />
|
||||
<main style={{ paddingTop: 64, minHeight: '100vh', paddingBottom: 80 }}>{children}</main>
|
||||
{/* Header = 60px nav + 32px ticker; offset main so content clears it. */}
|
||||
<AuthGate>
|
||||
<main style={{ paddingTop: 96, minHeight: '100vh', paddingBottom: 80 }}>{children}</main>
|
||||
</AuthGate>
|
||||
<Footer />
|
||||
<ParlayTray />
|
||||
<BottomTabBar />
|
||||
<InstallPrompt />
|
||||
|
||||
+43
-25
@@ -1,50 +1,68 @@
|
||||
import Link from 'next/link';
|
||||
import Wordmark from '@/components/Wordmark';
|
||||
import { Wordmark } from '@/components/vyndr';
|
||||
import NotFoundActions from '@/components/vyndr/NotFoundActions';
|
||||
|
||||
export const metadata = {
|
||||
title: '404 — Signal Lost',
|
||||
};
|
||||
|
||||
/**
|
||||
* The north star (§3). The brand distilled: full-page scanlines, the glitch
|
||||
* wordmark, system language ("TRANSMISSION INTERRUPTED"), a giant glowing amber
|
||||
* 404, and a CRT sweep on load. Every page should aspire to this intentionality.
|
||||
*/
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<section
|
||||
className="tex-scan"
|
||||
className="scanlines"
|
||||
style={{
|
||||
minHeight: 'calc(100vh - 144px)',
|
||||
minHeight: 'calc(100vh - 140px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 24,
|
||||
padding: '32px 16px',
|
||||
gap: 0,
|
||||
padding: '40px 24px',
|
||||
textAlign: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Wordmark size={28} />
|
||||
<p className="lbl" style={{ color: 'var(--grade-c)' }}>TRANSMISSION INTERRUPTED</p>
|
||||
<h1
|
||||
className="num"
|
||||
{/* CRT sweep — fires its 0.7s animation once on mount */}
|
||||
<div className="crt-sweep" />
|
||||
|
||||
<div style={{ marginBottom: 22 }}>
|
||||
<Wordmark size="lg" cursor beta />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mono amber-glow"
|
||||
style={{ fontSize: 13, letterSpacing: '0.28em', color: 'var(--amber)', marginBottom: 22 }}
|
||||
>
|
||||
TRANSMISSION INTERRUPTED
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="wm"
|
||||
data-text="404"
|
||||
style={{
|
||||
fontSize: 'clamp(80px, 16vw, 160px)',
|
||||
fontSize: 'clamp(96px, 16vw, 150px)',
|
||||
fontWeight: 800,
|
||||
color: 'var(--grade-c)',
|
||||
textShadow: '0 0 24px rgba(255, 179, 71, 0.6), 0 0 48px rgba(255, 179, 71, 0.25)',
|
||||
lineHeight: 0.9,
|
||||
letterSpacing: '-0.04em',
|
||||
color: 'var(--amber)',
|
||||
textShadow: '0 0 24px rgba(255,179,71,.6), 0 0 60px rgba(255,179,71,.3)',
|
||||
lineHeight: 1,
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
404
|
||||
</h1>
|
||||
<p style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-0)', maxWidth: 480 }}>
|
||||
This page doesn't exist. The signal was lost.
|
||||
</p>
|
||||
<p style={{ color: 'var(--text-1)', fontSize: 14, maxWidth: 480 }}>
|
||||
Check the URL, head back to the slate, or open the Ledger to review past grades.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<Link href="/dashboard" className="btn-primary">Back to Dashboard →</Link>
|
||||
<Link href="/ledger" className="btn-ghost">Open the Ledger</Link>
|
||||
</div>
|
||||
|
||||
<h1 style={{ fontSize: 26, fontWeight: 800, letterSpacing: '-0.02em', margin: '26px 0 0', color: 'var(--text-0)' }}>
|
||||
This page doesn't exist.
|
||||
</h1>
|
||||
<p className="mono" style={{ fontSize: 13.5, color: 'var(--text-1)', marginTop: 10 }}>
|
||||
The signal was lost.
|
||||
</p>
|
||||
|
||||
<NotFoundActions />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import RouteStub from '@/components/vyndr/RouteStub';
|
||||
|
||||
export const metadata = { title: 'Notifications' };
|
||||
|
||||
// Gated route (lib/routes.js) — AuthGate bounces signed-out visitors to /login.
|
||||
export default function NotificationsPage() {
|
||||
return (
|
||||
<RouteStub
|
||||
title="Notifications"
|
||||
arriving="SESSION 36"
|
||||
blurb="Your inbox of A+ signals, injury cascades, and graded results."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import Features from '@/components/Features';
|
||||
import HowItWorks from '@/components/HowItWorks';
|
||||
import Pricing from '@/components/Pricing';
|
||||
import FAQ from '@/components/FAQ';
|
||||
import Footer from '@/components/Footer';
|
||||
// Footer is mounted globally in the root layout (Session 34) — no per-page import.
|
||||
|
||||
export default function Home() {
|
||||
const { user, loading } = useAuth();
|
||||
@@ -54,7 +54,6 @@ export default function Home() {
|
||||
<HowItWorks />
|
||||
<Pricing />
|
||||
<FAQ />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import RouteStub from '@/components/vyndr/RouteStub';
|
||||
|
||||
export const metadata = { title: 'Terminal' };
|
||||
|
||||
export default function TerminalPage() {
|
||||
return (
|
||||
<RouteStub
|
||||
title="The Terminal"
|
||||
arriving="SESSION 35"
|
||||
blurb="League intelligence: injury cascades, game-impact scores, gradeable leaders, factor pulse, matchup exploits."
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user