Session 24: Connect everything — Slate wired to all sources, copy fixed, nav fixed, startup prefetch, language button removed (1571 tests)

This commit is contained in:
Kev
2026-06-12 15:45:19 -04:00
parent 0538205fab
commit 433e827103
15 changed files with 586 additions and 99 deletions
+101 -12
View File
@@ -34,6 +34,24 @@ const SPORT_ACCENT: Record<SlateSport, string> = {
soccer: '#00D4A0',
};
// Session 24 — game-level book-by-book lines from Tank01. One row per
// sportsbook (bet365 / betmgm / caesars …). All fields optional/null —
// books publish lines independently, so we render only what exists.
export interface GameLineBook {
homeML?: string | null;
awayML?: string | null;
total?: string | null;
homeSpread?: string | null;
awaySpread?: string | null;
}
export interface GameLines {
homeTeam?: string | null;
awayTeam?: string | null;
books: Record<string, GameLineBook>;
}
export type GameStatus = 'pre' | 'in' | 'post';
export interface GameCardProps {
sport: SlateSport;
homeTeam: string;
@@ -49,6 +67,19 @@ export interface GameCardProps {
onGrade: (prop: PropRowProp) => void;
onUpgrade?: () => void;
defaultVisible?: number; // how many props to show before "+ N more"
// Session 24 — all-day intelligence layers, all optional. A game card
// shows whatever exists: schedule status/score (ESPN), game lines
// (Tank01), and props (odds-api) — nothing replaces anything else.
status?: GameStatus;
score?: { home: number; away: number } | null;
gameLines?: GameLines | null;
}
// Map ESPN status → a compact, human badge. 'in' is live; 'post' is final.
function statusBadge(status?: GameStatus, score?: { home: number; away: number } | null) {
if (status === 'in') return { label: 'LIVE', color: '#FF4D4D', score };
if (status === 'post') return { label: 'FINAL', color: '#6B6B7B', score };
return null; // 'pre' or unknown → no badge; the tip-off time carries it
}
function formatTime(iso?: string) {
@@ -142,8 +173,11 @@ export default function GameCard(props: GameCardProps) {
props: propList, gradedProps, loadingKey, errorByKey,
tier = 'free', onGrade, onUpgrade,
defaultVisible = 4,
status, score, gameLines,
} = props;
const [expanded, setExpanded] = useState(false);
const badge = statusBadge(status, score);
const bookRows = gameLines?.books ? Object.entries(gameLines.books) : [];
// Session 19 — visibility budget now applies to PLAYERS, not raw
// props. Showing the first 4 prop rows that all belonged to the
@@ -244,21 +278,76 @@ export default function GameCard(props: GameCardProps) {
{[formatTime(gameTime), venue, context].filter(Boolean).join(' · ') || ' '}
</div>
</div>
<div
className="mono"
style={{
fontSize: 10,
color: 'var(--text-tertiary, #6B6B7B)',
background: 'rgba(255,255,255,0.04)',
padding: '4px 8px',
borderRadius: 999,
whiteSpace: 'nowrap',
}}
>
{propList.length} prop{propList.length === 1 ? '' : 's'}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 }}>
{/* Session 24 — live/final status + score (ESPN schedule). */}
{badge && (
<span
className="mono"
style={{
fontSize: 10,
fontWeight: 800,
letterSpacing: '0.08em',
color: '#0A0A0F',
background: badge.color,
padding: '3px 8px',
borderRadius: 4,
whiteSpace: 'nowrap',
}}
>
{badge.label}{badge.score ? ` · ${badge.score.away}${badge.score.home}` : ''}
</span>
)}
<div
className="mono"
style={{
fontSize: 10,
color: 'var(--text-tertiary, #6B6B7B)',
background: 'rgba(255,255,255,0.04)',
padding: '4px 8px',
borderRadius: 999,
whiteSpace: 'nowrap',
}}
>
{propList.length} prop{propList.length === 1 ? '' : 's'}
</div>
</div>
</header>
{/* Session 24 — game lines strip (Tank01). Book-by-book moneyline,
spread, total. Renders only when lines exist; never blocks the
card. The brand edge is props, but lines give immediate action
even when props haven't been published. */}
{bookRows.length > 0 && (
<div
style={{
padding: '10px 16px',
borderTop: '1px solid var(--border, #1A1A24)',
display: 'grid',
gap: 6,
background: 'rgba(255,255,255,0.015)',
}}
>
<div
className="mono"
style={{ fontSize: 10, letterSpacing: '0.08em', color: 'var(--text-tertiary, #6B6B7B)', textTransform: 'uppercase' }}
>
Game Lines
</div>
{bookRows.map(([book, line]) => (
<div
key={book}
className="mono"
style={{ display: 'flex', gap: 12, fontSize: 11, color: 'var(--text-secondary, #8A8A9A)', flexWrap: 'wrap' }}
>
<span style={{ minWidth: 64, color: 'var(--text-0, #F0F0F5)', fontWeight: 700, textTransform: 'capitalize' }}>{book}</span>
{line.awayML && <span>{teamAbbr(awayTeam, sport)} {line.awayML}</span>}
{line.homeML && <span>{teamAbbr(homeTeam, sport)} {line.homeML}</span>}
{line.total && <span>O/U {line.total}</span>}
</div>
))}
</div>
)}
{propList.length === 0 ? (
<p
style={{