907c7b17c1
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>
150 lines
5.6 KiB
TypeScript
150 lines
5.6 KiB
TypeScript
import type { Metadata, Viewport } from 'next';
|
|
import PostHogProvider from '@/components/PostHogProvider';
|
|
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';
|
|
import PushPrompt from '@/components/PushPrompt';
|
|
import MFAPrompt from '@/components/MFAPrompt';
|
|
import MFAChallenge from '@/components/MFAChallenge';
|
|
import CookieConsent from '@/components/CookieConsent';
|
|
import SentryInit from '@/components/SentryInit';
|
|
import { LocaleProvider } from '@/contexts/LocaleContext';
|
|
import { headers } from 'next/headers';
|
|
import { LOCALE_HEADER, COUNTRY_HEADER, isLocale, DEFAULT_LOCALE, LOCALE_META } from '@/lib/locales';
|
|
import './globals.css';
|
|
|
|
export const metadata: Metadata = {
|
|
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://vyndr.app'),
|
|
title: {
|
|
default: 'VYNDR — Sports Prop Intelligence',
|
|
template: '%s · VYNDR',
|
|
},
|
|
description:
|
|
"Grade your props across every sport with intelligence the books don't want you to have. NBA, MLB, WNBA, and soccer today — NFL and more through 2026. World Cup 2026 intelligence: xG regression, altitude, referee, penalty taker. Built in Detroit.",
|
|
applicationName: 'VYNDR',
|
|
authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }],
|
|
manifest: '/manifest.json',
|
|
keywords: [
|
|
'sports prop grading',
|
|
'NBA prop bet analysis',
|
|
'MLB prop intelligence',
|
|
'WNBA prop grading',
|
|
'soccer prop intelligence',
|
|
'World Cup 2026 props',
|
|
'xG regression analysis',
|
|
'parlay correlation analysis',
|
|
'prop betting tools',
|
|
],
|
|
openGraph: {
|
|
title: "VYNDR — Intelligence the books don't want you to have",
|
|
description:
|
|
'Read player props with Bayesian intelligence. See the factors. Know the kill conditions. Take the edge back.',
|
|
url: 'https://vyndr.app',
|
|
siteName: 'VYNDR',
|
|
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'VYNDR — Sports Prop Intelligence' }],
|
|
type: 'website',
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: 'VYNDR',
|
|
description: 'The books have every advantage. We built this to give it back.',
|
|
images: ['/og-image.png'],
|
|
creator: '@getvyndr',
|
|
},
|
|
icons: {
|
|
icon: [
|
|
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
|
{ url: '/favicon.ico', sizes: '32x32' },
|
|
{ url: '/favicon-32.png', sizes: '32x32', type: 'image/png' },
|
|
{ url: '/favicon-16.png', sizes: '16x16', type: 'image/png' },
|
|
],
|
|
apple: '/apple-touch-icon.png',
|
|
},
|
|
appleWebApp: {
|
|
capable: true,
|
|
statusBarStyle: 'black-translucent',
|
|
title: 'VYNDR',
|
|
},
|
|
robots: {
|
|
index: true,
|
|
follow: true,
|
|
googleBot: {
|
|
index: true,
|
|
follow: true,
|
|
'max-image-preview': 'large',
|
|
'max-snippet': -1,
|
|
},
|
|
},
|
|
};
|
|
|
|
export const viewport: Viewport = {
|
|
themeColor: '#06060B',
|
|
width: 'device-width',
|
|
initialScale: 1,
|
|
maximumScale: 5,
|
|
};
|
|
|
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
// Session 12 — resolve locale from the middleware-stamped request
|
|
// header so server components render with the right translations
|
|
// and the <html> dir attribute is set before paint (no FOUC of
|
|
// mis-directional text).
|
|
const hdrs = await headers();
|
|
const localeHeader = hdrs.get(LOCALE_HEADER);
|
|
const locale = isLocale(localeHeader) ? localeHeader : DEFAULT_LOCALE;
|
|
const dir = LOCALE_META[locale].dir;
|
|
// Session 13 — country from CF-IPCountry (set by middleware).
|
|
// Empty string when traffic bypasses Cloudflare (local dev, direct
|
|
// origin hits). The Africa-tier gate degrades closed on empty.
|
|
const country = hdrs.get(COUNTRY_HEADER) || '';
|
|
|
|
return (
|
|
<html lang={locale} dir={dir} className="dark">
|
|
<head>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
{/* VYNDR 2.0 (§2): Inter for chrome/UI, JetBrains Mono for ALL data.
|
|
IBM Plex Mono + Instrument Sans kept while pages migrate session-by-session. */}
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600;700;800&family=Instrument+Sans:wght@400;500;600;700&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
</head>
|
|
<body className="antialiased tex-grain">
|
|
<LocaleProvider locale={locale} country={country}>
|
|
<PostHogProvider>
|
|
<AuthProvider>
|
|
<ExplainModeProvider>
|
|
<ParlayProvider>
|
|
<HashRedirect />
|
|
<Nav />
|
|
{/* 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 />
|
|
<PushPrompt />
|
|
<MFAPrompt />
|
|
<MFAChallenge />
|
|
<CookieConsent />
|
|
<SentryInit />
|
|
</ParlayProvider>
|
|
</ExplainModeProvider>
|
|
</AuthProvider>
|
|
</PostHogProvider>
|
|
</LocaleProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|