Files
vyndr/web/src/app/page.tsx
T
builtbykev 76c289d4c1 Wave 5A: /u house-mode public profile (D2)
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>
2026-07-13 15:26:48 -04:00

124 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 34s "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>
</>
);
}