d3637e7abd
- specs/ROW-GRAMMAR.md: the row grammar law (slot order, color law, mark
law, mobile stacking, no-truncation), locked by tests/unit/rowGrammar.
StatStrip violations fixed: MovementChip before the grade (market
context before model output); ViabilityChips after the archetype
(identity is one contiguous run).
- Line-movement sparklines: intradayRefreshService.trackHistory captures
real {t,line} points per grade (seeded with the lock, deduped when
flat, capped 24) inside the snapshot write-back; StatStrip.LineSparkline
renders at >=3 points (green toward / amber against / dim flat).
- Last-10 dot strips: services/last10Dots (streaksService accessors) ->
/api/snapshot/:sport attaches last10_dots from rosterlogs:{sport};
StatStrip.DotStrip renders vs the LOCKED line, newest first.
- CLV distribution: getModelAggregate emits clv_distribution (7 signed
buckets, outliers clamped) only past the centralized n>=20 gate;
ledger MODEL header renders the bar strip.
- Global search: SearchModal (cmd-K via GlobalHosts + window.__search),
players via /api/players/search per sport + static lib/teams.js
(soccer deliberately absent); Nav search icon + Search first in the
mobile More sheet. Explore tab untouched.
- Landing LCP: fade-up floored at opacity .6 (hero h1 contentful on
first frame, S33 visible-floor rule) + IBM Plex Mono preload:false
(4 decorative font files off the slow-4G critical path).
2654 -> 2698 tests (226 suites) green; web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
177 lines
7.2 KiB
TypeScript
177 lines
7.2 KiB
TypeScript
import type { Metadata, Viewport } from 'next';
|
|
import { Inter, JetBrains_Mono, IBM_Plex_Mono } from 'next/font/google';
|
|
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 GlobalHosts from '@/components/vyndr/GlobalHosts';
|
|
import PartnerRefCapture from '@/components/vyndr/PartnerRefCapture';
|
|
// Session 50 — the Parlay Lab supersedes the legacy ParlayTray.
|
|
import ParlayPanel from '@/components/vyndr/ParlayPanel';
|
|
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';
|
|
|
|
// Session 41 — self-hosted via next/font (downloaded at build time, served
|
|
// from our origin). Replaces the runtime fonts.googleapis.com <link>, which
|
|
// returned 503 in production and added ~4s to page load. §2 brand fonts:
|
|
// Inter for chrome/UI (--font-sans), JetBrains Mono for ALL data (--font-mono).
|
|
// IBM Plex Mono (--font-ibm) kept for the wordmark + legacy mono surfaces while
|
|
// pages migrate. globals.css :root maps --sans/--mono onto these variables.
|
|
const inter = Inter({ subsets: ['latin'], variable: '--font-sans', display: 'swap' });
|
|
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono', display: 'swap' });
|
|
const ibmPlexMono = IBM_Plex_Mono({
|
|
subsets: ['latin'],
|
|
weight: ['400', '500', '600', '700'],
|
|
variable: '--font-ibm',
|
|
display: 'swap',
|
|
// S6 (A1 board, LCP) — IBM Plex is the legacy/decorative mono (wordmark,
|
|
// hero sport chips); its FOUR weight files were <link rel=preload>'d ahead
|
|
// of the render-critical CSS + Inter (the hero h1's face) on slow 4G.
|
|
// Not preloading them frees that bandwidth; display:swap still swaps them
|
|
// in when ready. Inter + JetBrains (the data face) stay preloaded.
|
|
preload: false,
|
|
});
|
|
const fontVars = `${inter.variable} ${jetbrainsMono.variable} ${ibmPlexMono.variable}`;
|
|
|
|
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:
|
|
'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. NBA, MLB, WNBA, and soccer. 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 — The edge the books don't want you to have",
|
|
description:
|
|
'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. Built in Detroit.',
|
|
url: 'https://vyndr.app',
|
|
siteName: 'VYNDR',
|
|
locale: 'en_US',
|
|
type: 'website',
|
|
// og:image is supplied by the file-based opengraph-image.tsx convention.
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: "VYNDR — The edge the books don't want you to have",
|
|
description: 'Pre-graded player props. Proprietary archetypes. Parlay Lab. Built in Detroit.',
|
|
creator: '@getvyndr',
|
|
// twitter:image is supplied by the file-based twitter-image.tsx convention.
|
|
},
|
|
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,
|
|
// Session 37 — extend under the iOS notch / home indicator so the
|
|
// mobile tab bar's env(safe-area-inset-*) padding has room to work.
|
|
viewportFit: 'cover',
|
|
};
|
|
|
|
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 ${fontVars}`}>
|
|
<body className="antialiased tex-grain">
|
|
<LocaleProvider locale={locale} country={country}>
|
|
<PostHogProvider>
|
|
<AuthProvider>
|
|
<ExplainModeProvider>
|
|
<ParlayProvider>
|
|
<HashRedirect />
|
|
<Nav />
|
|
{/* Header = 60px nav + 30px heartbeat (+2 slack) + a DYNAMIC
|
|
ticker: the Ticker publishes --ticker-h (32px with ≥4 real
|
|
items, else 0px — spec §3) so main's offset collapses with
|
|
it. Keep the 92px base in sync with Nav/HeartbeatBar. */}
|
|
<AuthGate>
|
|
<main style={{ paddingTop: 'calc(92px + var(--ticker-h, 0px))', minHeight: '100vh', paddingBottom: 80 }}>{children}</main>
|
|
</AuthGate>
|
|
<Footer />
|
|
<ParlayPanel />
|
|
<BottomTabBar />
|
|
<InstallPrompt />
|
|
<PushPrompt />
|
|
<MFAPrompt />
|
|
<MFAChallenge />
|
|
<CookieConsent />
|
|
<SentryInit />
|
|
{/* Session 38 — prefs apply + paywall/checkout/prefs globals (§9/§10/§12) */}
|
|
<GlobalHosts />
|
|
{/* A1 S3 — first-visit ?ref=CODE → vyndr_ref cookie (90d, first-touch) */}
|
|
<PartnerRefCapture />
|
|
</ParlayProvider>
|
|
</ExplainModeProvider>
|
|
</AuthProvider>
|
|
</PostHogProvider>
|
|
</LocaleProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|