'use client'; import { useEffect, useMemo, useState } from 'react'; import SportBadge from '@/components/vyndr/SportBadge'; import GradeBadge from '@/components/vyndr/GradeBadge'; import { playerHref } from '@/lib/playerHref'; import { dedupeLeaders } from '@/lib/playerGrouping'; import { statAbbrev } from '@/lib/statAbbrev'; /** * ExploreHub (Session 42 leaders; Session 60 night2/C — THE AGGREGATOR). * The "entire picture" page: leaders + full streaks + hot lists, one sport * selector, everything free and interpreted through the lens. The page shell * (app/explore/page.tsx) is a server component carrying the SEO metadata. */ import StreaksPanel from '@/components/StreaksPanel'; import HotListPanel from '@/components/HotListPanel'; import NewsWire from '@/components/vyndr/NewsWire'; import FuturesBoard from '@/components/vyndr/FuturesBoard'; import { useAuth } from '@/contexts/AuthContext'; import { nextRunLabelET } from '@/lib/pipelineSchedule'; import { OFF_SEASON } from '@/lib/emptyState'; interface Leader { player: string; team: string; stat: string; line: number | string; side: string; grade: string; confidence: number; } const SPORTS = [ { key: 'nba', label: 'NBA' }, { key: 'mlb', label: 'MLB' }, { key: 'wnba', label: 'WNBA' }, ]; export default function ExploreHub() { const { tier } = useAuth(); const [sport, setSport] = useState('mlb'); const [leaders, setLeaders] = useState([]); const [state, setState] = useState<'loading' | 'ready' | 'error'>('loading'); const [query, setQuery] = useState(''); useEffect(() => { let active = true; setState('loading'); fetch(`/api/stats/leaders?sport=${sport}&limit=25`) .then((r) => r.json()) .then((d) => { if (active) { setLeaders(Array.isArray(d.leaders) ? d.leaders : []); setState('ready'); } }) .catch(() => { if (active) setState('error'); }); return () => { active = false; }; }, [sport]); const rows = useMemo(() => { const q = query.trim().toLowerCase(); const filtered = q ? leaders.filter((l) => (l.player || '').toLowerCase().includes(q)) : leaders; // P0-3 — ONE row per player+market family + a per-market cap, so a player's // alt-line ladder (Bohm ×3) collapses to one entry and no single prop type // (9× stolen_bases) can flood the ranked board. return dedupeLeaders(filtered, 4) as Leader[]; }, [leaders, query]); // Wave 2B — never-dark hub. When the selected sport is in its OFF-SEASON, the // hub LEADS with futures + the wire (real, always-available data) instead of // a dark leaderboard; in-season those sections COMPLEMENT the live board // below. Each self-hides independently on an empty feed — no empty boxes. const off = OFF_SEASON[sport as keyof typeof OFF_SEASON]; const isOffseason = !!(off && off.months.includes(new Date().getMonth())); // Both sections self-hide (return null) when their feeds are empty. const hubSections = ( <> ); return (
STATS · /EXPLORE

Stats Explorer

Tonight's league leaderboard — every graded prop, ranked by VYNDR confidence, with the grade and archetype context only VYNDR has.

{SPORTS.map((s) => ( ))}
{/* OFF-SEASON LEAD — futures + wire come FIRST when the board is dark. */} {isOffseason && hubSections} {/* FILTER BAR */}
setQuery(e.target.value)} placeholder="Search players" style={{ appearance: 'none', background: 'transparent', border: 'none', outline: 'none', fontFamily: 'var(--sans)', fontSize: 13, color: '#fff', flex: 1, minWidth: 140 }} /> {rows.length} graded
{/* LEADERBOARD */}
LEAGUE LEADERBOARD
#
PLAYER
PROP
CONF
GRD
{state === 'loading' &&
Loading tonight's slate…
} {state === 'error' &&
Could not load the leaderboard. Try again.
} {state === 'ready' && rows.length === 0 && (
No graded props for {sport.toUpperCase()} yet. Grades post {nextRunLabelET() || 'on the next pipeline run'}.
)} {state === 'ready' && rows.map((r, i) => (
{i + 1}
{r.player} {r.team && {r.team}}
{statAbbrev(r.stat)} {r.side}{r.line}
{r.confidence != null ? `${r.confidence}%` : '—'}
))}
{/* Session 60 (night2/C) — THE AGGREGATOR. Full streaks + hot lists for the selected sport, every row through the lens. The picture is free; the grade is the paid layer. Panels self-hide when cold. */}
{/* IN-SEASON — futures + wire COMPLEMENT the live board below it. */} {!isOffseason && hubSections}
); }