76c289d4c1
Reserved house handle (default 'vyndr', env HOUSE_HANDLE) resolves to the PUBLIC model record — getModelAggregate() with no userId (user_id=NULL rows) — WITHOUT a public_profiles row. It is the ONLY special case; every other handle keeps the private-by-default, byte-identical-404 no-existence-leak contract. The house profile is always public and never 404s (a fetch failure degrades to an honest building state). - src/routes/profiles.js: house short-circuit + sendHouseProfile (public aggregate + by_tier + public settled entries), reserved before the publish lookup so a user claim is shadowed. - PublicProfile.tsx: house label 'VYNDR MODEL · PUBLIC RECORD' + hero/subtitle off data.house; keeps the CLV-VERIFIED record hero + TierRecord calibration + recent settled reads (misses included). - opengraph-image.tsx (1200x630): house-branded eyebrow/heading. - portrait/route.tsx: new 1080x1350 share crop (real aggregate or tagline fallback, never a fabricated number). - Discoverability: 'VIEW AS PUBLIC PAGE ->' on the ledger MODEL header + 'VIEW PUBLIC RECORD ->' under the landing ModelRecord, both to /u/vyndr. - tests/unit/houseProfile.test.js: house resolves to user_id=NULL aggregate (no public_profiles row) + by_tier; unknown/unpublished user handles stay byte-identical 404; page renders house label + TierRecord + portrait crop. 3019 tests green (3012 -> 3019); next build EXIT=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.7 KiB
TypeScript
124 lines
5.7 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import Link from 'next/link';
|
||
import { useRouter } from 'next/navigation';
|
||
import { useAuth } from '@/contexts/AuthContext';
|
||
import Hero from '@/components/Hero';
|
||
import { ClaimMeter, ModelRecord, Skeleton, SkeletonList } from '@/components/vyndr';
|
||
// Session 55 — live top A-rated grades pulled from tonight's real snapshot,
|
||
// with the self-learning loop's accuracy line. The product shown, not described.
|
||
import TopSignals from '@/components/TopSignals';
|
||
// Session 17 — game-count strip mounted between the hero and the
|
||
// existing LivePropsStrip. Shows "X NBA · Y WNBA · Z MLB games
|
||
// being graded right now" with a signup CTA. Hides itself when
|
||
// every sport returns zero (off-hours / upstream outages).
|
||
import TonightsSlate from '@/components/TonightsSlate';
|
||
import LivePropsStrip from '@/components/LivePropsStrip';
|
||
// Session 23 — all-day intelligence teasers. Free/cheap content that
|
||
// keeps the landing page alive even when odds-api props are empty.
|
||
// Both self-hide when there's nothing to show.
|
||
import StreaksPanel from '@/components/StreaksPanel';
|
||
import HotListPanel from '@/components/HotListPanel';
|
||
import Features from '@/components/Features';
|
||
import HowItWorks from '@/components/HowItWorks';
|
||
import Pricing from '@/components/Pricing';
|
||
import FAQ from '@/components/FAQ';
|
||
// Session S7 (a1) — THE VYNDR REPORT capture (double opt-in via Listmonk).
|
||
import NewsletterCapture from '@/components/NewsletterCapture';
|
||
// Footer is mounted globally in the root layout (Session 34) — no per-page import.
|
||
|
||
// Session 60 (6.1) — the 3–4s "LOADING THE SLATE" first paint was THIS
|
||
// page blocking its entire render on Supabase auth initialization
|
||
// (`loading || user`), even for anonymous visitors who were never going to
|
||
// redirect. Synchronous localStorage check instead: only a visitor who
|
||
// actually HAS a stored session (and will redirect to /dashboard) sees the
|
||
// suppressed render; anonymous traffic paints the hero immediately.
|
||
function hasStoredSession(): boolean {
|
||
if (typeof window === 'undefined') return false;
|
||
try {
|
||
for (let i = 0; i < window.localStorage.length; i += 1) {
|
||
const k = window.localStorage.key(i) || '';
|
||
if (/^sb-.*-auth-token$/.test(k) || k === 'sb-token') return true;
|
||
}
|
||
} catch { /* storage blocked → treat as anonymous */ }
|
||
return false;
|
||
}
|
||
|
||
export default function Home() {
|
||
const { user, loading } = useAuth();
|
||
const router = useRouter();
|
||
// DS1 (DESIGN-SPEC §4, audit #5) — React #418 hydration fix. Reading
|
||
// localStorage inside a useState INITIALIZER runs during render: the server
|
||
// (no `window`) computes `false` and emits the marketing tree, but the first
|
||
// CLIENT render of a signed-in visitor reads the stored session, computes
|
||
// `true`, and emits the "Loading" placeholder instead — a server/client HTML
|
||
// mismatch that made React discard and re-render the whole page (#418). The
|
||
// client-only value is now deferred behind a mounted flag: the first client
|
||
// render matches the server (both treat it as `false`), then the stored-
|
||
// session check flips it post-mount. SSR HTML is never discarded.
|
||
const [mounted, setMounted] = useState(false);
|
||
useEffect(() => { setMounted(true); }, []);
|
||
const maybeSignedIn = mounted && hasStoredSession();
|
||
|
||
useEffect(() => {
|
||
if (!loading && user) router.replace('/dashboard');
|
||
}, [user, loading, router]);
|
||
|
||
// Suppress the marketing render ONLY for visitors with a stored session
|
||
// (they're about to redirect) — anonymous first paint is instant.
|
||
if (user || (loading && maybeSignedIn)) {
|
||
// Signed-in visitor mid-redirect to /dashboard — a layout-matched skeleton,
|
||
// never a text wall (§4).
|
||
return (
|
||
<section style={{ maxWidth: 1100, margin: '0 auto', padding: '32px 16px' }} aria-busy="true">
|
||
<Skeleton height={40} width={280} radius={10} style={{ marginBottom: 24 }} />
|
||
<SkeletonList count={4} height={96} />
|
||
</section>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Hero />
|
||
{/* Session 55 — tonight's real top signals + live accuracy (the system works). */}
|
||
<TopSignals />
|
||
{/* Session 58 (work-order 1.4) — the public model record (proof-strip
|
||
footer). Deferred-render: "RECORD BUILDING" until 20 settles, then
|
||
the real hit% + beat-close%. Self-hides with no data. */}
|
||
<div style={{ padding: '4px 16px 12px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
|
||
<ModelRecord />
|
||
{/* Wave 5A (D2) — the shareable public model record (the house profile). */}
|
||
<Link
|
||
href="/u/vyndr"
|
||
className="mono"
|
||
style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-tertiary)', textDecoration: 'none' }}
|
||
>
|
||
VIEW PUBLIC RECORD →
|
||
</Link>
|
||
</div>
|
||
{/* Founder-seat scarcity meter (§12) */}
|
||
<div style={{ padding: '0 16px 8px' }}>
|
||
<ClaimMeter />
|
||
</div>
|
||
<TonightsSlate />
|
||
<LivePropsStrip />
|
||
<div style={{ maxWidth: 960, margin: '0 auto', padding: '0 16px' }}>
|
||
{/* Session 60 (night2/C) — the free hook: top-3 REAL streaks through
|
||
the lens. MLB is the in-season board (was nba — off-season = the
|
||
panel self-hid and the teaser never showed). */}
|
||
<StreaksPanel sport="mlb" tier="free" limit={3} />
|
||
<HotListPanel sport="mlb" tier="free" limit={3} />
|
||
</div>
|
||
<Features />
|
||
<HowItWorks />
|
||
<Pricing />
|
||
<FAQ />
|
||
{/* Session S7 (a1) — the daily wire, by email. Sits above the global footer. */}
|
||
<div style={{ maxWidth: 640, margin: '0 auto', padding: '8px 16px 40px' }}>
|
||
<NewsletterCapture />
|
||
</div>
|
||
</>
|
||
);
|
||
}
|