Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
+227
-214
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
type Period = 'weekly' | 'monthly' | 'all_time';
|
||||
|
||||
@@ -18,271 +20,282 @@ interface Bet {
|
||||
potential_payout: number;
|
||||
book: string;
|
||||
bet_type: string;
|
||||
status: string;
|
||||
slip_data: { legs: any[]; total_odds?: number; scan_session_id?: string };
|
||||
submission_method: string;
|
||||
status: 'pending' | 'won' | 'lost' | 'push' | 'void';
|
||||
slip_data: { legs: { player?: string; stat_type?: string; line?: number; direction?: string }[]; total_odds?: number };
|
||||
placed_at: string;
|
||||
settled_at: string | null;
|
||||
}
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
||||
return token
|
||||
? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
|
||||
: { 'Content-Type': 'application/json' };
|
||||
}
|
||||
|
||||
export default function TrackerPage() {
|
||||
const router = useRouter();
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const [period, setPeriod] = useState<Period>('monthly');
|
||||
const [performance, setPerformance] = useState<Record<Period, PerfStats | null>>({ weekly: null, monthly: null, all_time: null });
|
||||
const [performance, setPerformance] = useState<Record<Period, PerfStats | null>>({
|
||||
weekly: null, monthly: null, all_time: null,
|
||||
});
|
||||
const [bets, setBets] = useState<Bet[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [settlingId, setSettlingId] = useState<string | null>(null);
|
||||
const [settleStatus, setSettleStatus] = useState('won');
|
||||
const [showQuickSlip, setShowQuickSlip] = useState(false);
|
||||
|
||||
// Quick slip state
|
||||
const [showQuickSlip, setShowQuickSlip] = useState(false);
|
||||
const [slipPlayer, setSlipPlayer] = useState('');
|
||||
const [slipStat, setSlipStat] = useState('points');
|
||||
const [slipLine, setSlipLine] = useState('');
|
||||
const [slipDirection, setSlipDirection] = useState('over');
|
||||
const [slipOdds, setSlipOdds] = useState('-110');
|
||||
const [slipAmount, setSlipAmount] = useState('');
|
||||
const [slipBook, setSlipBook] = useState('draftkings');
|
||||
const [slip, setSlip] = useState({
|
||||
player: '', stat: 'points', line: '', direction: 'over',
|
||||
odds: '-110', amount: '', book: 'draftkings',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) router.replace('/login?next=/tracker');
|
||||
}, [authLoading, user, router]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ limit: '20', offset: '0' });
|
||||
const params = new URLSearchParams({ limit: '30' });
|
||||
if (statusFilter) params.set('status', statusFilter);
|
||||
|
||||
const [perfRes, betsRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/api/bets/performance`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API_BASE}/api/bets?${params}`, { headers: getAuthHeaders() }),
|
||||
]);
|
||||
|
||||
if (perfRes.ok) {
|
||||
const perfData = await perfRes.json();
|
||||
setPerformance(perfData);
|
||||
}
|
||||
if (perfRes.ok) setPerformance(await perfRes.json());
|
||||
if (betsRes.ok) {
|
||||
const betsData = await betsRes.json();
|
||||
setBets(betsData.bets || []);
|
||||
setTotal(betsData.total || 0);
|
||||
const data = await betsRes.json();
|
||||
setBets(data.bets || []);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch tracker data');
|
||||
} catch {
|
||||
/* network failure — keep last successful state */
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [statusFilter]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const handleSettle = async (betId: string) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/bets/${betId}/settle`, {
|
||||
method: 'PATCH',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ status: settleStatus }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSettlingId(null);
|
||||
fetchData();
|
||||
}
|
||||
} catch (e) { console.error('Settle failed'); }
|
||||
};
|
||||
useEffect(() => { if (user) fetchData(); }, [fetchData, user]);
|
||||
|
||||
const handleQuickSlip = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/bets/quickslip`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
legs: [{ player: slipPlayer, stat_type: slipStat, line: Number(slipLine), direction: slipDirection, odds: Number(slipOdds) }],
|
||||
amount: Number(slipAmount),
|
||||
book: slipBook,
|
||||
bet_type: 'straight',
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setShowQuickSlip(false);
|
||||
setSlipPlayer(''); setSlipLine(''); setSlipAmount('');
|
||||
fetchData();
|
||||
}
|
||||
} catch (e) { console.error('Quick slip failed'); }
|
||||
const res = await fetch(`${API_BASE}/api/bets/quickslip`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
legs: [{
|
||||
player: slip.player,
|
||||
stat_type: slip.stat,
|
||||
line: Number(slip.line),
|
||||
direction: slip.direction,
|
||||
odds: Number(slip.odds),
|
||||
}],
|
||||
amount: Number(slip.amount),
|
||||
book: slip.book,
|
||||
bet_type: 'straight',
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setShowQuickSlip(false);
|
||||
setSlip({ ...slip, player: '', line: '', amount: '' });
|
||||
fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
const settleBet = async (id: string, status: 'won' | 'lost' | 'push' | 'void') => {
|
||||
await fetch(`${API_BASE}/api/bets/${id}/settle`, {
|
||||
method: 'PATCH',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const currentPerf = performance[period];
|
||||
|
||||
if (authLoading || !user) {
|
||||
return (
|
||||
<section style={{ minHeight: '60vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p className="mono" style={{ color: 'var(--text-tertiary)' }}>Loading tracker…</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-8 px-4 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8">Bet Tracker</h1>
|
||||
<section style={{ maxWidth: 900, margin: '0 auto', padding: '32px 16px 120px' }}>
|
||||
<header style={{ marginBottom: 24 }}>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em', marginBottom: 4 }}>
|
||||
Bet tracker
|
||||
</h1>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: 14 }}>
|
||||
Log every bet. See what worked. The honest mirror.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Performance Cards */}
|
||||
<div className="mb-6">
|
||||
<div className="flex gap-2 mb-4">
|
||||
{(['weekly', 'monthly', 'all_time'] as Period[]).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPeriod(p)}
|
||||
className={`px-3 py-1 text-sm rounded-lg transition ${period === p ? 'bg-[var(--accent)] text-white' : 'bg-[var(--card)] text-[var(--text-muted)] hover:text-white'}`}
|
||||
>
|
||||
{p === 'all_time' ? 'All Time' : p.charAt(0).toUpperCase() + p.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-xl bg-[var(--card)] border border-[var(--border)] text-center">
|
||||
<div className={`text-2xl font-mono font-bold ${(currentPerf?.roi ?? 0) >= 0 ? 'text-[var(--grade-a)]' : 'text-[var(--grade-d)]'}`}>
|
||||
{currentPerf ? `${currentPerf.roi > 0 ? '+' : ''}${currentPerf.roi}%` : '--'}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)] mt-1">ROI</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-[var(--card)] border border-[var(--border)] text-center">
|
||||
<div className="text-2xl font-mono font-bold">
|
||||
{currentPerf ? `${currentPerf.win_rate}%` : '--'}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)] mt-1">Win Rate</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-[var(--card)] border border-[var(--border)] text-center">
|
||||
<div className="text-2xl font-mono font-bold">
|
||||
{currentPerf?.sample_size ?? '--'}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)] mt-1">Bets</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Submit */}
|
||||
<div className="flex gap-3 mb-8">
|
||||
<button
|
||||
onClick={() => setShowQuickSlip(!showQuickSlip)}
|
||||
className="px-4 py-2 text-sm border border-[var(--border)] rounded-lg hover:border-[var(--accent)] transition"
|
||||
>
|
||||
Quick Slip
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showQuickSlip && (
|
||||
<form onSubmit={handleQuickSlip} className="p-4 rounded-xl bg-[var(--card)] border border-[var(--border)] mb-8 space-y-3">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<input placeholder="Player" value={slipPlayer} onChange={(e) => setSlipPlayer(e.target.value)} required
|
||||
className="px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-white placeholder:text-[var(--text-muted)]" />
|
||||
<select value={slipStat} onChange={(e) => setSlipStat(e.target.value)}
|
||||
className="px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-white">
|
||||
{['points', 'rebounds', 'assists', 'threes', 'blocks', 'steals', 'pra', 'turnovers'].map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
<input type="number" step="0.5" placeholder="Line" value={slipLine} onChange={(e) => setSlipLine(e.target.value)} required
|
||||
className="px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-white placeholder:text-[var(--text-muted)]" />
|
||||
<select value={slipDirection} onChange={(e) => setSlipDirection(e.target.value)}
|
||||
className="px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-white">
|
||||
<option value="over">Over</option><option value="under">Under</option>
|
||||
</select>
|
||||
<input type="number" placeholder="Odds (e.g. -110)" value={slipOdds} onChange={(e) => setSlipOdds(e.target.value)} required
|
||||
className="px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-white placeholder:text-[var(--text-muted)]" />
|
||||
<input type="number" step="0.01" placeholder="Amount ($)" value={slipAmount} onChange={(e) => setSlipAmount(e.target.value)} required
|
||||
className="px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-white placeholder:text-[var(--text-muted)]" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<select value={slipBook} onChange={(e) => setSlipBook(e.target.value)}
|
||||
className="px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-white">
|
||||
{['draftkings', 'fanduel', 'betmgm'].map((b) => <option key={b} value={b}>{b}</option>)}
|
||||
</select>
|
||||
<button type="submit" className="px-6 py-2 bg-[var(--accent)] text-white rounded-lg text-sm font-medium hover:opacity-90 transition">
|
||||
Submit Bet
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Bet History */}
|
||||
<div className="mb-4 flex gap-2">
|
||||
{['', 'pending', 'won', 'lost', 'push'].map((s) => (
|
||||
{/* Period switch */}
|
||||
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
|
||||
{(['weekly', 'monthly', 'all_time'] as Period[]).map((p) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setStatusFilter(s)}
|
||||
className={`px-3 py-1 text-xs rounded-lg transition ${statusFilter === s ? 'bg-[var(--accent)] text-white' : 'bg-[var(--card)] text-[var(--text-muted)]'}`}
|
||||
key={p}
|
||||
onClick={() => setPeriod(p)}
|
||||
className={period === p ? 'btn-primary' : 'btn-ghost'}
|
||||
style={{ padding: '8px 14px', fontSize: 12, textTransform: 'capitalize' }}
|
||||
>
|
||||
{s || 'All'}
|
||||
{p === 'all_time' ? 'All time' : p}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{loading ? (
|
||||
<p className="text-[var(--text-muted)] text-sm">Loading...</p>
|
||||
) : bets.length === 0 ? (
|
||||
<p className="text-[var(--text-muted)] text-sm">No bets yet. Submit your first bet above.</p>
|
||||
) : (
|
||||
bets.map((bet) => (
|
||||
<div key={bet.id} className="p-4 rounded-xl bg-[var(--card)] border border-[var(--border)]">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{(bet.slip_data?.legs || []).map((l: any) =>
|
||||
`${l.player} ${l.direction?.[0]?.toUpperCase() || ''}${l.line} ${l.stat_type}`
|
||||
).join(' / ') || `${bet.bet_type} bet`}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)] mt-1">
|
||||
{bet.book} | ${bet.amount} | {new Date(bet.placed_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`text-xs font-mono px-2 py-0.5 rounded ${
|
||||
bet.status === 'won' ? 'bg-[var(--grade-a)]/10 text-[var(--grade-a)]' :
|
||||
bet.status === 'lost' ? 'bg-[var(--grade-d)]/10 text-[var(--grade-d)]' :
|
||||
'bg-[var(--border)] text-[var(--text-muted)]'
|
||||
}`}>
|
||||
{bet.status.toUpperCase()}
|
||||
</span>
|
||||
{bet.status === 'won' && (
|
||||
<div className="text-xs text-[var(--grade-a)] mt-1 font-mono">
|
||||
+${(bet.potential_payout - bet.amount).toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{bet.status === 'pending' && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
{settlingId === bet.id ? (
|
||||
<>
|
||||
<select value={settleStatus} onChange={(e) => setSettleStatus(e.target.value)}
|
||||
className="px-2 py-1 text-xs rounded bg-[var(--bg)] border border-[var(--border)] text-white">
|
||||
<option value="won">Won</option>
|
||||
<option value="lost">Lost</option>
|
||||
<option value="push">Push</option>
|
||||
<option value="void">Void</option>
|
||||
</select>
|
||||
<button onClick={() => handleSettle(bet.id)} className="px-3 py-1 text-xs bg-[var(--accent)] text-white rounded hover:opacity-90">
|
||||
Confirm
|
||||
</button>
|
||||
<button onClick={() => setSettlingId(null)} className="px-3 py-1 text-xs text-[var(--text-muted)]">
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={() => setSettlingId(bet.id)} className="text-xs text-[var(--accent)] hover:underline">
|
||||
Settle
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Performance tiles */}
|
||||
<section
|
||||
className="surface diagonal-cut animate-fade-up"
|
||||
style={{ padding: 24, marginBottom: 24, display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: 12 }}
|
||||
>
|
||||
<Tile
|
||||
label="ROI"
|
||||
value={currentPerf ? `${currentPerf.roi > 0 ? '+' : ''}${currentPerf.roi}%` : '—'}
|
||||
tone={(currentPerf?.roi ?? 0) >= 0 ? 'good' : 'bad'}
|
||||
/>
|
||||
<Tile label="Win rate" value={currentPerf ? `${currentPerf.win_rate}%` : '—'} />
|
||||
<Tile label="Bets" value={currentPerf?.sample_size?.toString() ?? '—'} />
|
||||
<Tile label="Wagered" value={currentPerf ? `$${currentPerf.total_wagered.toFixed(0)}` : '—'} />
|
||||
<Tile
|
||||
label="Profit"
|
||||
value={currentPerf ? `${currentPerf.total_profit > 0 ? '+' : ''}$${currentPerf.total_profit.toFixed(0)}` : '—'}
|
||||
tone={(currentPerf?.total_profit ?? 0) >= 0 ? 'good' : 'bad'}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Quick slip */}
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<button onClick={() => setShowQuickSlip((s) => !s)} className="btn-primary">
|
||||
{showQuickSlip ? 'Close quick slip' : '+ Log a bet'}
|
||||
</button>
|
||||
{showQuickSlip && (
|
||||
<form onSubmit={handleQuickSlip} className="surface" style={{ padding: 20, marginTop: 12, display: 'grid', gap: 12 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: 8 }}>
|
||||
<input required placeholder="Player" className="input-field" value={slip.player} onChange={(e) => setSlip({ ...slip, player: e.target.value })} />
|
||||
<select className="input-field" value={slip.stat} onChange={(e) => setSlip({ ...slip, stat: e.target.value })}>
|
||||
{['points', 'rebounds', 'assists', 'threes', 'blocks', 'steals', 'pra', 'turnovers'].map((s) => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
<input required type="number" step="0.5" placeholder="Line" className="input-field" value={slip.line} onChange={(e) => setSlip({ ...slip, line: e.target.value })} />
|
||||
</div>
|
||||
))
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||
<select className="input-field" value={slip.direction} onChange={(e) => setSlip({ ...slip, direction: e.target.value })}>
|
||||
<option value="over">Over</option><option value="under">Under</option>
|
||||
</select>
|
||||
<input required type="number" placeholder="Odds" className="input-field" value={slip.odds} onChange={(e) => setSlip({ ...slip, odds: e.target.value })} />
|
||||
<input required type="number" step="0.01" placeholder="Stake $" className="input-field" value={slip.amount} onChange={(e) => setSlip({ ...slip, amount: e.target.value })} />
|
||||
<select className="input-field" value={slip.book} onChange={(e) => setSlip({ ...slip, book: e.target.value })}>
|
||||
{['draftkings', 'fanduel', 'betmgm', 'caesars', 'fanatics', 'bet365', 'hardrockbet', 'pointsbet'].map((b) => <option key={b} value={b}>{b}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" className="btn-primary" style={{ padding: 12 }}>Submit bet</button>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Filter chips */}
|
||||
<div style={{ display: 'flex', gap: 4, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
{['', 'pending', 'won', 'lost', 'push'].map((s) => (
|
||||
<button key={s} onClick={() => setStatusFilter(s)} className={statusFilter === s ? 'btn-primary' : 'btn-ghost'} style={{ padding: '6px 12px', fontSize: 11 }}>
|
||||
{s ? s.charAt(0).toUpperCase() + s.slice(1) : 'All'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bet list */}
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{loading ? (
|
||||
<p className="mono" style={{ color: 'var(--text-tertiary)', padding: 16 }}>Loading…</p>
|
||||
) : bets.length === 0 ? (
|
||||
<p
|
||||
style={{
|
||||
color: 'var(--text-secondary)',
|
||||
padding: 32,
|
||||
textAlign: 'center',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 12,
|
||||
background: 'var(--bg-surface)',
|
||||
}}
|
||||
>
|
||||
No bets logged yet. Use the quick slip above to start the ledger.
|
||||
</p>
|
||||
) : (
|
||||
bets.map((b) => <BetRow key={b.id} bet={b} onSettle={settleBet} />)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{bets.length > 0 && bets.length < total && (
|
||||
<button className="w-full mt-4 py-2 text-sm text-[var(--text-muted)] hover:text-white transition">
|
||||
Load More
|
||||
</button>
|
||||
)}
|
||||
<p style={{ marginTop: 32, fontSize: 12, color: 'var(--text-tertiary)' }}>
|
||||
Future: connect DraftKings / FanDuel for auto-sync (coming soon).
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Tile({ label, value, tone }: { label: string; value: string; tone?: 'good' | 'bad' }) {
|
||||
const color = tone === 'good' ? 'var(--grade-a)' : tone === 'bad' ? 'var(--grade-d)' : 'var(--text-primary)';
|
||||
return (
|
||||
<div>
|
||||
<div className="mono" style={{ fontSize: 10, color: 'var(--text-tertiary)', letterSpacing: '0.08em' }}>
|
||||
{label.toUpperCase()}
|
||||
</div>
|
||||
<div className="mono" style={{ fontSize: 22, fontWeight: 800, color, marginTop: 4 }}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BetRow({ bet, onSettle }: { bet: Bet; onSettle: (id: string, status: 'won' | 'lost' | 'push' | 'void') => void }) {
|
||||
const [openSettle, setOpenSettle] = useState(false);
|
||||
const profit = bet.status === 'won' ? bet.potential_payout - bet.amount : 0;
|
||||
const statusColor = bet.status === 'won' ? 'var(--grade-a)' : bet.status === 'lost' ? 'var(--grade-d)' : 'var(--text-secondary)';
|
||||
return (
|
||||
<div className="surface" style={{ padding: 14 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 4 }}>
|
||||
{(bet.slip_data?.legs || []).map((l) =>
|
||||
`${l.player ?? '—'} ${l.direction?.charAt(0).toUpperCase()}${l.line ?? ''} ${l.stat_type ?? ''}`
|
||||
).join(' / ') || `${bet.bet_type} bet`}
|
||||
</div>
|
||||
<div className="mono" style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
||||
{bet.book} · ${bet.amount} · {new Date(bet.placed_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<span className="mono" style={{ fontSize: 11, fontWeight: 700, padding: '2px 8px', borderRadius: 999, background: 'var(--bg-elevated)', color: statusColor }}>
|
||||
{bet.status.toUpperCase()}
|
||||
</span>
|
||||
{bet.status === 'won' && (
|
||||
<div className="mono" style={{ fontSize: 12, color: 'var(--grade-a)', marginTop: 4 }}>
|
||||
+${profit.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{bet.status === 'pending' && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{openSettle ? (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['won', 'lost', 'push', 'void'] as const).map((s) => (
|
||||
<button key={s} onClick={() => { setOpenSettle(false); onSettle(bet.id, s); }}
|
||||
className="btn-ghost" style={{ padding: '6px 12px', fontSize: 11, textTransform: 'capitalize' }}>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
<button onClick={() => setOpenSettle(false)} className="btn-ghost" style={{ padding: '6px 12px', fontSize: 11 }}>Cancel</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => setOpenSettle(true)} className="btn-ghost" style={{ padding: '6px 12px', fontSize: 11 }}>
|
||||
Settle bet
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user