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:
+101
-12
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user