Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+248
View File
@@ -0,0 +1,248 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { GradePill } from '@/components/GradeCard';
type Sport = 'ALL' | 'NBA' | 'MLB' | 'WNBA';
type TierFilter = 'ALL' | 'A' | 'B' | 'C';
interface LedgerEntry {
id: string;
player: string;
stat: string;
line: number;
direction: 'over' | 'under';
sport: Exclude<Sport, 'ALL'>;
grade: string;
projection?: number;
actual?: number;
hit: boolean | null;
miss_reason?: string;
graded_at: string;
}
interface AccuracyBucket {
tier: string;
hits: number;
losses: number;
pct: number;
}
const SPORT_COLOR: Record<Exclude<Sport, 'ALL'>, string> = {
NBA: '#E94B3C',
MLB: '#1E90FF',
WNBA: '#FFB347',
};
export default function LedgerPage() {
const [sport, setSport] = useState<Sport>('ALL');
const [tier, setTier] = useState<TierFilter>('ALL');
const [entries, setEntries] = useState<LedgerEntry[] | null>(null);
const [accuracy, setAccuracy] = useState<AccuracyBucket[] | null>(null);
useEffect(() => {
const params = new URLSearchParams();
if (sport !== 'ALL') params.set('sport', sport);
if (tier !== 'ALL') params.set('tier', tier);
Promise.all([
fetch(`/api/ledger?${params}`).then((r) => r.json()).catch(() => ({ entries: [] })),
fetch('/api/ledger/accuracy').then((r) => r.json()).catch(() => ({ buckets: [] })),
]).then(([entriesData, accuracyData]) => {
setEntries(Array.isArray(entriesData?.entries) ? entriesData.entries : []);
setAccuracy(Array.isArray(accuracyData?.buckets) ? accuracyData.buckets : []);
});
}, [sport, tier]);
const overall = useMemo(() => {
if (!accuracy?.length) return null;
const totals = accuracy.reduce(
(acc, b) => ({ h: acc.h + b.hits, l: acc.l + b.losses }),
{ h: 0, l: 0 },
);
const total = totals.h + totals.l;
if (!total) return null;
return { hits: totals.h, losses: totals.l, pct: Math.round((totals.h / total) * 100) };
}, [accuracy]);
return (
<section style={{ maxWidth: 1100, margin: '0 auto', padding: '32px 16px 120px' }}>
<header style={{ marginBottom: 32 }}>
<h1 style={{ fontSize: 32, fontWeight: 700, letterSpacing: '-0.03em', marginBottom: 6 }}>
The Ledger.
</h1>
<p style={{ color: 'var(--text-secondary)', fontSize: 16 }}>
Every grade. Every result. No hiding. No deleting.
</p>
</header>
{/* Accuracy header strip */}
{accuracy && accuracy.length > 0 && (
<section
className="surface diagonal-cut animate-fade-up"
style={{
padding: 24,
marginBottom: 24,
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
gap: 12,
}}
>
{accuracy.map((b) => (
<AccuracyTile key={b.tier} bucket={b} />
))}
{overall && (
<AccuracyTile
bucket={{ tier: 'Overall', hits: overall.hits, losses: overall.losses, pct: overall.pct }}
highlight
/>
)}
</section>
)}
{/* Filters */}
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<FilterRow label="Sport" value={sport} onChange={(v) => setSport(v as Sport)} options={['ALL', 'NBA', 'MLB', 'WNBA']} />
<FilterRow label="Grade" value={tier} onChange={(v) => setTier(v as TierFilter)} options={['ALL', 'A', 'B', 'C']} />
</div>
{/* Grid */}
{entries === null ? (
<p className="mono" style={{ color: 'var(--text-tertiary)', padding: 32, textAlign: 'center' }}>Loading</p>
) : entries.length === 0 ? (
<div
className="surface diagonal-cut tex-scan"
style={{ padding: 48, textAlign: 'center', display: 'grid', gap: 10, justifyItems: 'center' }}
>
<p className="lbl" style={{ color: 'var(--grade-c)' }}>LEDGER EMPTY</p>
<h3 style={{ fontSize: 18, fontWeight: 700 }}>
No grades yet.
</h3>
<p style={{ color: 'var(--text-1)', fontSize: 14, maxWidth: 440 }}>
Read your first prop to start building your Ledger. Every grade you run shows up here, with the result.
</p>
<a href="/scan" className="btn-primary" style={{ marginTop: 8, padding: '10px 18px' }}>
Read a Prop
</a>
</div>
) : (
<div
style={{
display: 'grid',
gap: 12,
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
}}
>
{entries.map((entry, i) => (
<article
key={entry.id}
className={`surface diagonal-cut animate-fade-up stagger-${(i % 6) + 1}`}
style={{ padding: 16 }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
<span
className="mono"
style={{
fontSize: 10,
fontWeight: 700,
padding: '2px 8px',
borderRadius: 999,
background: `${SPORT_COLOR[entry.sport]}1F`,
color: SPORT_COLOR[entry.sport],
}}
>
{entry.sport}
</span>
<GradePill grade={entry.grade} />
</div>
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 2 }}>{entry.player}</h3>
<p className="mono" style={{ fontSize: 12, color: 'var(--text-secondary)', textTransform: 'capitalize', marginBottom: 12 }}>
{entry.direction} {entry.line} {entry.stat.replace(/_/g, ' ')}
</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<span className="mono" style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
{entry.actual != null ? `Actual ${entry.actual}` : 'Pending'}
</span>
{entry.hit !== null && (
<span
className="mono"
style={{
fontSize: 12,
fontWeight: 700,
color: entry.hit ? 'var(--grade-a)' : 'var(--grade-d)',
}}
>
{entry.hit ? 'HIT' : 'MISS'}
</span>
)}
</div>
{entry.hit === false && entry.miss_reason && (
<p
style={{
marginTop: 12,
padding: 10,
fontSize: 12,
color: 'var(--grade-d)',
background: 'rgba(255,107,107,0.10)',
borderRadius: 8,
border: '1px solid rgba(255,107,107,0.30)',
}}
>
<strong style={{ fontWeight: 700 }}>Why we missed:</strong> {entry.miss_reason}
</p>
)}
</article>
))}
</div>
)}
</section>
);
}
function AccuracyTile({ bucket, highlight }: { bucket: AccuracyBucket; highlight?: boolean }) {
return (
<div
style={{
padding: 16,
borderRadius: 12,
background: highlight ? 'var(--bg-elevated)' : 'var(--bg-surface)',
border: highlight ? '1px solid var(--grade-a)' : '1px solid var(--border)',
}}
>
<div className="mono" style={{ fontSize: 10, color: 'var(--text-tertiary)', letterSpacing: '0.08em' }}>
{bucket.tier.toUpperCase()}
</div>
<div className="mono" style={{ fontSize: 24, fontWeight: 800, color: highlight ? 'var(--grade-a)' : 'var(--text-primary)', marginTop: 4 }}>
{bucket.pct}%
</div>
<div className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }}>
{bucket.hits}-{bucket.losses}
</div>
</div>
);
}
function FilterRow({ label, value, onChange, options }: { label: string; value: string; onChange: (v: string) => void; options: string[] }) {
return (
<div>
<span className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', letterSpacing: '0.08em', marginRight: 12 }}>
{label.toUpperCase()}
</span>
<span style={{ display: 'inline-flex', gap: 4 }}>
{options.map((o) => {
const active = o === value;
return (
<button
key={o}
onClick={() => onChange(o)}
className={active ? 'btn-primary' : 'btn-ghost'}
style={{ padding: '6px 12px', fontSize: 11 }}
>
{o}
</button>
);
})}
</span>
</div>
);
}