260 lines
9.8 KiB
TypeScript
260 lines
9.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { GradePill } from './GradeCard';
|
|
|
|
const STAT_TYPES = ['points', 'rebounds', 'assists', 'threes', 'blocks', 'steals', 'pra', 'turnovers'];
|
|
const BOOKS = ['draftkings', 'fanduel', 'betmgm', 'caesars', 'fanatics', 'bet365', 'hardrockbet', 'pointsbet', 'betrivers'];
|
|
|
|
const ACCURACY: Record<string, string> = {
|
|
A: '73%',
|
|
B: '61%',
|
|
C: '48%',
|
|
D: '34%',
|
|
};
|
|
|
|
interface KillCondition {
|
|
code: string;
|
|
reason: string;
|
|
}
|
|
|
|
interface DemoResult {
|
|
grade: string;
|
|
confidence: number;
|
|
edge_pct: number;
|
|
kill_conditions_triggered: KillCondition[];
|
|
reasoning: { summary: string };
|
|
implied_probability?: number;
|
|
}
|
|
|
|
function oddsToImplied(odds: number): number {
|
|
if (odds > 0) return Math.round((100 / (odds + 100)) * 1000) / 10;
|
|
return Math.round(((-odds) / (-odds + 100)) * 1000) / 10;
|
|
}
|
|
|
|
export default function DemoScan() {
|
|
const [player, setPlayer] = useState('');
|
|
const [statType, setStatType] = useState('points');
|
|
const [line, setLine] = useState('');
|
|
const [direction, setDirection] = useState('over');
|
|
const [book, setBook] = useState('draftkings');
|
|
const [scanning, setScanning] = useState(false);
|
|
const [result, setResult] = useState<DemoResult | null>(null);
|
|
const [error, setError] = useState('');
|
|
|
|
// Live stats
|
|
const [stats, setStats] = useState<{ parlays_graded: number; kill_conditions_caught: number } | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function fetchStats() {
|
|
try {
|
|
const res = await fetch('/api/stats/public');
|
|
const data = await res.json();
|
|
setStats(data);
|
|
} catch {
|
|
setStats(null);
|
|
}
|
|
}
|
|
fetchStats();
|
|
const interval = setInterval(fetchStats, 30000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const handleScan = async () => {
|
|
if (!player || !line) { setError('Enter a player name and line.'); return; }
|
|
|
|
setScanning(true);
|
|
setError('');
|
|
setResult(null);
|
|
|
|
try {
|
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/analyze/prop`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
player,
|
|
stat_type: statType,
|
|
line: Number(line),
|
|
direction,
|
|
book,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Analysis failed');
|
|
|
|
// Default implied probability for standard -110 line
|
|
const implied = oddsToImplied(-110);
|
|
setResult({ ...data, implied_probability: implied });
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
} finally {
|
|
setScanning(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section className="py-20 px-4 bg-[var(--card)]">
|
|
<div className="max-w-md mx-auto">
|
|
{/* Header */}
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-2xl md:text-3xl font-bold mb-2">See it work. Right now.</h2>
|
|
<p className="text-[var(--text-muted)] text-sm">No account. No card. One prop read.</p>
|
|
</div>
|
|
|
|
{!result ? (
|
|
<>
|
|
{/* Form — single column, mobile-first */}
|
|
<div className="space-y-3">
|
|
<input
|
|
placeholder="Player name"
|
|
value={player}
|
|
onChange={(e) => setPlayer(e.target.value)}
|
|
className="w-full px-4 py-3 rounded-xl bg-[var(--bg)] border border-[var(--border)] text-white text-sm placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--cyan)]"
|
|
/>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<select
|
|
value={statType}
|
|
onChange={(e) => setStatType(e.target.value)}
|
|
className="px-4 py-3 rounded-xl bg-[var(--bg)] border border-[var(--border)] text-white text-sm"
|
|
>
|
|
{STAT_TYPES.map((s) => <option key={s} value={s}>{s}</option>)}
|
|
</select>
|
|
<input
|
|
type="number"
|
|
step="0.5"
|
|
placeholder="Line (e.g. 24.5)"
|
|
value={line}
|
|
onChange={(e) => setLine(e.target.value)}
|
|
className="px-4 py-3 rounded-xl bg-[var(--bg)] border border-[var(--border)] text-white text-sm placeholder:text-[var(--text-muted)]"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<select
|
|
value={direction}
|
|
onChange={(e) => setDirection(e.target.value)}
|
|
className="px-4 py-3 rounded-xl bg-[var(--bg)] border border-[var(--border)] text-white text-sm"
|
|
>
|
|
<option value="over">Over</option>
|
|
<option value="under">Under</option>
|
|
</select>
|
|
<select
|
|
value={book}
|
|
onChange={(e) => setBook(e.target.value)}
|
|
className="px-4 py-3 rounded-xl bg-[var(--bg)] border border-[var(--border)] text-white text-sm"
|
|
>
|
|
{BOOKS.map((b) => <option key={b} value={b}>{b}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="mt-3 text-sm text-[var(--kill)]">{error}</p>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleScan}
|
|
disabled={scanning || !player || !line}
|
|
className="w-full mt-4 py-3.5 bg-[var(--cyan)] text-black font-semibold rounded-xl text-sm hover:bg-[var(--cyan-hover)] transition disabled:opacity-40"
|
|
>
|
|
{scanning ? 'Reading...' : 'Read This Prop'}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* Result */}
|
|
<div className="p-5 rounded-2xl bg-[var(--forest-dark)] border border-[var(--border)]">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div>
|
|
<h3 className="font-semibold">{result.grade === 'A' || result.grade === 'B' ? player : player}</h3>
|
|
<p className="text-sm text-[var(--text-muted)]">
|
|
{direction.charAt(0).toUpperCase() + direction.slice(1)} {line} {statType}
|
|
</p>
|
|
</div>
|
|
<GradePill grade={result.grade} confidence={result.confidence} />
|
|
</div>
|
|
|
|
<p className="text-sm text-[var(--text-muted)] leading-relaxed mb-4">
|
|
{result.reasoning.summary}
|
|
</p>
|
|
|
|
{/* Kill conditions */}
|
|
{result.kill_conditions_triggered.length > 0 && (
|
|
<div className="p-3 rounded-lg bg-[var(--kill)]/10 border border-[var(--kill)]/30 mb-4">
|
|
{result.kill_conditions_triggered.map((k) => (
|
|
<div key={k.code} className="flex items-start gap-2 text-sm mb-1 last:mb-0">
|
|
<span className="text-[var(--kill)] font-mono text-xs font-bold">{k.code}</span>
|
|
<span className="text-[var(--kill)]">{k.reason}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Accuracy context */}
|
|
<p className="text-xs text-[var(--text-muted)] mb-2">
|
|
{result.grade} grades like this hit {ACCURACY[result.grade] || '—'} of the time based on our model accuracy to date.
|
|
</p>
|
|
|
|
{/* Implied probability */}
|
|
{result.implied_probability != null && (
|
|
<>
|
|
<p className="text-sm font-mono text-[var(--cyan)]">
|
|
Implied probability: {result.implied_probability}%
|
|
</p>
|
|
<p className="text-xs text-[var(--text-dim)] mt-1">
|
|
Your book already knows this number.
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Post-scan CTA */}
|
|
<div className="mt-6 text-center space-y-3">
|
|
<p className="text-sm text-[var(--text-muted)]">
|
|
This used 1 of your 5 free reads.
|
|
</p>
|
|
<p className="text-sm text-[var(--text-muted)]">
|
|
Sign up free to read your full parlay.
|
|
</p>
|
|
<a
|
|
href="/signup"
|
|
className="inline-block w-full py-3.5 bg-[var(--cyan)] text-black font-semibold rounded-xl text-sm hover:bg-[var(--cyan-hover)] transition text-center"
|
|
>
|
|
Read Your Full Parlay Free
|
|
</a>
|
|
<button
|
|
onClick={() => setResult(null)}
|
|
className="w-full py-3 border border-[var(--border)] rounded-xl text-sm text-[var(--text-muted)] hover:border-[var(--cyan)] transition"
|
|
>
|
|
Try Another Prop
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Honest Stats */}
|
|
<div className="mt-12 grid grid-cols-3 gap-4">
|
|
<div className="text-center">
|
|
<div className="text-xl font-mono font-bold text-[var(--grade-a)]">73%</div>
|
|
<div className="text-xs text-[var(--text-muted)] mt-1">A Grade Accuracy</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-xl font-mono font-bold">
|
|
{stats?.kill_conditions_caught?.toLocaleString() ?? '—'}
|
|
</div>
|
|
<div className="text-xs text-[var(--text-muted)] mt-1">Kills Caught</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-xl font-mono font-bold">
|
|
{stats?.parlays_graded?.toLocaleString() ?? '—'}
|
|
</div>
|
|
<div className="text-xs text-[var(--text-muted)] mt-1">Parlays Graded</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-center text-xs text-[var(--text-dim)] mt-3">
|
|
Live model data. Updated in real time.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|