'use client'; import { useEffect, useState } from 'react'; import { GradeBadge, ArchetypeBadge, AccuracyBadge } from '@/components/vyndr'; /** * TopSignals (Session 55) — the landing hero's live intelligence preview. * * Pulls the top A-rated grades from tonight's REAL snapshot (not a mockup) and * shows them as mini grade cards, with the self-learning loop's live accuracy * line beneath. The product selling itself by working. Self-hides off-hours * (no A-rated grades) so the landing never shows an empty shell. */ interface SnapGrade { player?: string; player_name?: string; stat_type?: string; stat?: string; line?: number; direction?: string; grade?: string; confidence?: number; archetype?: string | null; // Session 60 (6.1) — settled outcome (yesterday's-proof fallback rows). outcome?: { result: string; actual?: number | null } | null; } const SPORTS = ['mlb', 'nba', 'wnba'] as const; const STAT_SHORT: Record = { total_bases: 'TB', home_runs: 'HR', hits: 'Hits', rbi: 'RBI', runs: 'Runs', strikeouts: 'Ks', hits_allowed: 'HA', earned_runs: 'ER', innings_pitched: 'IP', stolen_bases: 'SB', points: 'Pts', rebounds: 'Reb', assists: 'Ast', threes: '3PT', }; function shortStat(s?: string) { if (!s) return ''; return STAT_SHORT[s] || s.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); } const isTop = (g?: string) => g === 'A+' || g === 'A'; export default function TopSignals() { const [signals, setSignals] = useState(null); const [header, setHeader] = useState<'signals' | 'board' | 'proof'>('board'); useEffect(() => { let active = true; const load = async () => { try { const results = await Promise.all( SPORTS.map((sp) => fetch(`/api/snapshot/${sp}`, { cache: 'no-store' }) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), ), ); if (!active) return; const all: SnapGrade[] = []; const settled: SnapGrade[] = []; for (const res of results) { const grades = res && Array.isArray(res.grades) ? res.grades : []; for (const g of grades) { if (g.outcome) settled.push(g); else if (g.grade) all.push(g); } } all.sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0)); // Session 60 (6.1) — never oversell: top 3 by grade whatever they // are, but the TOP SIGNALS header only with ≥1 A-tier; otherwise // "TONIGHT'S BOARD". Nothing graded yet → yesterday's settled reads // WITH outcome chips (the proof strip carries the slate). if (all.length > 0) { setSignals(all.slice(0, 3)); setHeader(all.some((g) => isTop(g.grade)) ? 'signals' : 'board'); } else if (settled.length > 0) { setSignals(settled.slice(0, 3)); setHeader('proof'); } else { setSignals([]); } } catch { if (active) setSignals([]); } }; load(); const id = setInterval(load, 60_000); return () => { active = false; clearInterval(id); }; }, []); // Self-hide only when there's NOTHING real (no grades and no settles). if (!signals || signals.length === 0) return null; const headerText = header === 'signals' ? "TONIGHT'S TOP SIGNALS" : header === 'proof' ? "YESTERDAY'S SETTLED READS" : "TONIGHT'S BOARD"; const headerSub = header === 'proof' ? '· MISSES INCLUDED' : '· LIVE FROM THE SLATE'; return (
{headerText} {headerSub}
{signals.map((g, i) => { const player = g.player || g.player_name || ''; const side = String(g.direction || 'over').toLowerCase() === 'under' ? 'U' : 'O'; return (
{g.archetype ? : } {g.grade && }
{player}
{shortStat(g.stat_type || g.stat)} {side}{g.line} {/* Outcome chip on the proof-strip fallback — misses included. */} {g.outcome && ( {g.outcome.result === 'hit' ? '✓ HIT' : g.outcome.result === 'miss' ? '✕ MISS' : '– PUSH'} {g.outcome.actual != null ? ` (${g.outcome.actual})` : ''} )}
); })}
); }