Session 28: Parlay builder, line movement tracker, book comparison — 3 features, zero credits (1623 tests)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* BookComparison (Session 28).
|
||||
*
|
||||
* Presentational book-by-book grid for a single prop with the best line
|
||||
* highlighted. Fed by /api/books/:sport/:player/:stat (or the prop grid
|
||||
* from a parent). Pure render — no fetching here, so it drops cleanly into
|
||||
* a modal or an expanded prop row.
|
||||
*/
|
||||
|
||||
export interface BookRow {
|
||||
book: string;
|
||||
line?: number | null;
|
||||
over_odds?: number | null;
|
||||
under_odds?: number | null;
|
||||
isBest?: boolean;
|
||||
}
|
||||
|
||||
export interface BookComparisonProps {
|
||||
player: string;
|
||||
stat: string;
|
||||
line?: number | null;
|
||||
side?: 'over' | 'under';
|
||||
books: BookRow[];
|
||||
savings?: number;
|
||||
}
|
||||
|
||||
function fmt(o?: number | null) {
|
||||
if (o == null) return '—';
|
||||
return o > 0 ? `+${o}` : `${o}`;
|
||||
}
|
||||
|
||||
export default function BookComparison({ player, stat, line, side = 'over', books, savings }: BookComparisonProps) {
|
||||
if (!books || books.length === 0) return null;
|
||||
return (
|
||||
<div className="book-comparison" style={{ display: 'grid', gap: 8 }}>
|
||||
<div className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary, #6B6B7B)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
{player} · {stat.replace(/_/g, ' ')}{line != null ? ` ${line}` : ''} · {side}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
{books.map((b) => (
|
||||
<div
|
||||
key={b.book}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto auto',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
padding: '6px 10px',
|
||||
borderRadius: 8,
|
||||
background: b.isBest ? 'rgba(0,212,160,0.10)' : 'var(--bg-2, #12121A)',
|
||||
border: `1px solid ${b.isBest ? 'var(--grade-a, #00D4A0)' : 'var(--border, #1A1A24)'}`,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, textTransform: 'capitalize', color: 'var(--text-0, #F0F0F5)' }}>{b.book}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--text-secondary, #8A8A9A)' }}>
|
||||
{fmt(b.over_odds)} / {fmt(b.under_odds)}
|
||||
</span>
|
||||
{b.isBest ? (
|
||||
<span className="mono" style={{ fontSize: 9, fontWeight: 800, letterSpacing: '0.08em', color: '#06060B', background: 'var(--grade-a, #00D4A0)', padding: '2px 6px', borderRadius: 4 }}>BEST</span>
|
||||
) : <span />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{savings != null && savings > 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--grade-a, #00D4A0)' }}>
|
||||
Betting the best line saves ~${savings.toFixed(2)} per $100.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user