Files
vyndr/web/src/components/TopSignals.tsx
T
builtbykev 1b4f2772d6 Session G (night2): Phase 6 — landing first-paint + content engine + OG
6.1 FIRST-PAINT ROOT CAUSE: the landing blocked its ENTIRE render on
    Supabase auth init ('loading || user') — anonymous visitors stared at
    'LOADING THE SLATE' for the whole auth roundtrip (~3-4s). Now a
    synchronous localStorage session check gates the suppression: only
    visitors who actually hold a session (and will redirect) wait;
    anonymous traffic paints the hero immediately. Full RSC conversion of
    the hero is deferred and logged — the blocker itself is dead.
    Proof Strip rules: top-3 by grade whatever they are; 'TONIGHT'S TOP
    SIGNALS' only with >=1 A-tier, else 'TONIGHT'S BOARD'; nothing graded
    yet → yesterday's SETTLED reads with outcome chips (misses included).
6.2 Content routes: /api/content/top-signals/:sport,
    /streak-watch/:sport (the zero-grade daily format off the aggregator),
    /daily-report/:sport — built but self-flagging do_not_post until the
    record clears n>=20. Flag, don't fake.
6.3 Per-player OG images: app/player/[name]/opengraph-image.tsx (Node
    runtime per the S53 rule) + server layout generateMetadata — every
    shared player link unfurls as an intelligence card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 02:26:45 -04:00

144 lines
6.4 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 { 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<string, string> = {
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<SnapGrade[] | null>(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 (
<section style={{ maxWidth: 960, margin: '0 auto', padding: '8px 16px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
<div className="mono" style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, letterSpacing: '0.1em', color: 'var(--text-secondary, #8A8A9A)' }}>
<span className="live-dot" aria-hidden style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--grade-a, #00D4A0)', display: 'inline-block' }} />
<span style={{ color: 'var(--grade-a, #00D4A0)', fontWeight: 700 }}>{headerText}</span>
<span style={{ color: 'var(--text-tertiary, #707080)' }}>{headerSub}</span>
</div>
<AccuracyBadge variant="inline" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 12 }}>
{signals.map((g, i) => {
const player = g.player || g.player_name || '';
const side = String(g.direction || 'over').toLowerCase() === 'under' ? 'U' : 'O';
return (
<a
key={`${player}-${i}`}
href="/signup"
className="mono"
style={{
display: 'block', textDecoration: 'none', color: 'inherit',
padding: 14, borderRadius: 12,
background: 'var(--bg-surface, #12121A)',
border: '1px solid var(--border, #1A1A24)',
borderLeft: '3px solid var(--grade-a, #00D4A0)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>
{g.archetype ? <ArchetypeBadge archetype={g.archetype} size="sm" variant="full" /> : <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }} />}
{g.grade && <GradeBadge grade={g.grade} size="sm" />}
</div>
<div style={{ fontWeight: 700, fontSize: 14, color: '#fff', fontFamily: 'var(--sans, sans-serif)', marginBottom: 4 }}>{player}</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary, #B8BCC8)' }}>
{shortStat(g.stat_type || g.stat)} {side}{g.line}
{/* Outcome chip on the proof-strip fallback — misses included. */}
{g.outcome && (
<span style={{ marginLeft: 8, fontWeight: 700, color: g.outcome.result === 'hit' ? 'var(--grade-a, #00D4A0)' : g.outcome.result === 'miss' ? 'var(--miss, #FF5252)' : 'var(--text-tertiary)' }}>
{g.outcome.result === 'hit' ? '✓ HIT' : g.outcome.result === 'miss' ? '✕ MISS' : ' PUSH'}
{g.outcome.actual != null ? ` (${g.outcome.actual})` : ''}
</span>
)}
</div>
</a>
);
})}
</div>
</section>
);
}