106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { getVisibleCount, getHiddenCount, type Tier } from '@/lib/tierGate';
|
|
|
|
/**
|
|
* BestLinesPanel (Session 28).
|
|
*
|
|
* "Best lines tonight" — the prop + sportsbook offering the highest payout,
|
|
* with the dollars-per-$100 a user saves by line-shopping. Reads
|
|
* /api/books/:sport (cached odds, zero credits). Self-hides when empty.
|
|
*/
|
|
|
|
interface BestLine {
|
|
player: string;
|
|
stat: string;
|
|
line: number | null;
|
|
bestBook: string;
|
|
bestOdds: number;
|
|
savings: number;
|
|
}
|
|
|
|
export interface BestLinesPanelProps {
|
|
sport: string;
|
|
tier?: Tier;
|
|
limit?: number;
|
|
}
|
|
|
|
export default function BestLinesPanel({ sport, tier = 'free', limit }: BestLinesPanelProps) {
|
|
const [lines, setLines] = useState<BestLine[] | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function load() {
|
|
try {
|
|
const res = await fetch(`/api/books/${sport}`);
|
|
if (!res.ok) { if (!cancelled) setLines([]); return; }
|
|
const data = await res.json();
|
|
if (!cancelled) setLines(Array.isArray(data?.bestLines) ? data.bestLines : []);
|
|
} catch {
|
|
if (!cancelled) setLines([]);
|
|
}
|
|
}
|
|
load();
|
|
return () => { cancelled = true; };
|
|
}, [sport]);
|
|
|
|
if (!lines || lines.length === 0) return null;
|
|
|
|
const tierCount = getVisibleCount(tier, lines.length);
|
|
const cap = limit && limit > 0 ? Math.min(limit, tierCount) : tierCount;
|
|
const visible = lines.slice(0, cap);
|
|
const hidden = limit ? lines.length - visible.length : getHiddenCount(tier, lines.length);
|
|
|
|
const fmtOdds = (o: number) => (o > 0 ? `+${o}` : `${o}`);
|
|
|
|
return (
|
|
<section className="best-lines-panel" style={{ margin: '16px 0' }}>
|
|
<h3 style={heading}>💰 BEST LINES TONIGHT</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
{visible.map((bl) => (
|
|
<div key={`${bl.player}-${bl.stat}`} style={row}>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={playerName}>{bl.player}</div>
|
|
<div style={propLine}>
|
|
{bl.stat.replace(/_/g, ' ')}{bl.line != null ? ` ${bl.line}` : ''}
|
|
</div>
|
|
</div>
|
|
<span style={book}>
|
|
{bl.bestBook} <span style={{ color: 'var(--grade-a, #00D4A0)' }}>{fmtOdds(bl.bestOdds)}</span>
|
|
</span>
|
|
{bl.savings > 0 && <span style={savings}>save ${bl.savings.toFixed(2)}</span>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
{hidden > 0 && (
|
|
<a href="/pricing" style={upsell}>{hidden} more — upgrade to compare every book →</a>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const heading: React.CSSProperties = {
|
|
fontSize: 12, letterSpacing: '0.12em', textTransform: 'uppercase',
|
|
color: 'var(--text-tertiary, #6B6B7B)', margin: '0 0 10px',
|
|
};
|
|
const row: React.CSSProperties = {
|
|
display: 'flex', alignItems: 'center', gap: 12,
|
|
padding: '8px 10px', borderRadius: 10,
|
|
background: 'var(--bg-2, #12121A)', border: '1px solid var(--border, #1A1A24)',
|
|
};
|
|
const playerName: React.CSSProperties = {
|
|
fontSize: 14, fontWeight: 700, color: 'var(--text-0, #F0F0F5)',
|
|
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
};
|
|
const propLine: React.CSSProperties = { fontSize: 12, color: 'var(--text-secondary, #8A8A9A)', textTransform: 'capitalize' };
|
|
const book: React.CSSProperties = { flex: '0 0 auto', fontSize: 12, fontWeight: 700, color: 'var(--text-0, #F0F0F5)', textTransform: 'capitalize' };
|
|
const savings: React.CSSProperties = {
|
|
flex: '0 0 auto', fontSize: 11, fontWeight: 700, padding: '2px 7px', borderRadius: 6,
|
|
background: 'rgba(0,212,160,0.12)', color: 'var(--grade-a, #00D4A0)', whiteSpace: 'nowrap',
|
|
};
|
|
const upsell: React.CSSProperties = {
|
|
display: 'inline-block', marginTop: 10, fontSize: 12, fontWeight: 600,
|
|
color: 'var(--grade-a, #00D4A0)', textDecoration: 'none',
|
|
};
|