232 lines
7.5 KiB
TypeScript
232 lines
7.5 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useParlay, type ParlayLeg } from '@/contexts/ParlayContext';
|
|
import { GradePill } from './GradeCard';
|
|
import { trackParlayBuilt } from '@/lib/analytics';
|
|
|
|
interface ParlayGradeResponse {
|
|
parlay_grade: string;
|
|
parlay_confidence: number;
|
|
correlation_flags: { type: string; legs: number[]; detail: string; impact: string }[];
|
|
decimal_odds?: number;
|
|
}
|
|
|
|
export default function ParlayTray() {
|
|
const { legs, isOpen, close, removeLeg, clear } = useParlay();
|
|
const [grading, setGrading] = useState(false);
|
|
const [parlayResult, setParlayResult] = useState<ParlayGradeResponse | null>(null);
|
|
|
|
// Reset the parlay grade whenever the leg set changes
|
|
useEffect(() => {
|
|
setParlayResult(null);
|
|
}, [legs]);
|
|
|
|
const sports = useMemo(() => Array.from(new Set(legs.map((l) => l.sport))), [legs]);
|
|
|
|
const gradeParlay = async () => {
|
|
if (legs.length < 2) return;
|
|
setGrading(true);
|
|
try {
|
|
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
|
const res = await fetch('/api/parlay/grade', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
legs: legs.map((l) => ({
|
|
sport: l.sport,
|
|
player: l.player,
|
|
stat_type: l.stat,
|
|
line: l.line,
|
|
direction: l.direction,
|
|
})),
|
|
}),
|
|
});
|
|
const data = (await res.json()) as ParlayGradeResponse;
|
|
if (res.ok) {
|
|
setParlayResult(data);
|
|
trackParlayBuilt({ legs: legs.length, sports, grade: data.parlay_grade });
|
|
}
|
|
} finally {
|
|
setGrading(false);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Parlay tray"
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
zIndex: 60,
|
|
display: 'flex',
|
|
alignItems: 'flex-end',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<button
|
|
aria-label="Close parlay tray"
|
|
onClick={close}
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
background: 'rgba(0,0,0,0.55)',
|
|
backdropFilter: 'blur(4px)',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
}}
|
|
/>
|
|
<section
|
|
className="surface-elevated diagonal-cut animate-fade-up"
|
|
style={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
maxWidth: 560,
|
|
maxHeight: '85vh',
|
|
margin: '0 auto',
|
|
borderTopLeftRadius: 24,
|
|
borderTopRightRadius: 24,
|
|
borderBottomLeftRadius: 0,
|
|
borderBottomRightRadius: 0,
|
|
padding: 24,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 16,
|
|
overflowY: 'auto',
|
|
}}
|
|
>
|
|
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
|
<div>
|
|
<h2 style={{ fontSize: 18, fontWeight: 700 }}>Parlay tray</h2>
|
|
<p className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', letterSpacing: '0.05em' }}>
|
|
{legs.length} LEG{legs.length === 1 ? '' : 'S'} · {sports.join(' · ') || 'ADD A LEG'}
|
|
</p>
|
|
</div>
|
|
<button onClick={close} className="btn-ghost" style={{ padding: '6px 12px', fontSize: 12 }}>
|
|
Close
|
|
</button>
|
|
</header>
|
|
|
|
{legs.length === 0 ? (
|
|
<EmptyTrayCopy />
|
|
) : (
|
|
<ul style={{ display: 'grid', gap: 8 }}>
|
|
{legs.map((l) => (
|
|
<LegRow key={l.id} leg={l} onRemove={() => removeLeg(l.id)} />
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{parlayResult && (
|
|
<div
|
|
className="surface diagonal-cut"
|
|
style={{
|
|
padding: 16,
|
|
textAlign: 'center',
|
|
border: '1px solid var(--border-focus)',
|
|
}}
|
|
>
|
|
<p className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', letterSpacing: '0.08em' }}>
|
|
PARLAY GRADE
|
|
</p>
|
|
<div style={{ display: 'flex', justifyContent: 'center', marginTop: 8 }}>
|
|
<GradePill grade={parlayResult.parlay_grade} confidence={parlayResult.parlay_confidence} />
|
|
</div>
|
|
{parlayResult.correlation_flags.length > 0 && (
|
|
<div
|
|
style={{
|
|
marginTop: 12,
|
|
padding: 12,
|
|
textAlign: 'left',
|
|
borderRadius: 8,
|
|
background: 'rgba(255,179,71,0.10)',
|
|
border: '1px solid rgba(255,179,71,0.30)',
|
|
}}
|
|
>
|
|
<p className="mono" style={{ fontSize: 11, fontWeight: 700, color: 'var(--grade-c)', marginBottom: 4 }}>
|
|
CORRELATION WARNINGS
|
|
</p>
|
|
{parlayResult.correlation_flags.map((f, i) => (
|
|
<p key={i} style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 4 }}>
|
|
{f.detail}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{legs.length > 0 && (
|
|
<footer style={{ display: 'grid', gap: 8 }}>
|
|
<button
|
|
onClick={gradeParlay}
|
|
disabled={legs.length < 2 || grading}
|
|
className={grading ? 'shimmer-loading' : 'btn-primary'}
|
|
style={{ padding: 14, fontWeight: 600, fontSize: 14, border: 'none', borderRadius: 12, color: 'var(--text-primary)', cursor: legs.length < 2 ? 'not-allowed' : 'pointer', opacity: legs.length < 2 ? 0.4 : 1 }}
|
|
>
|
|
{grading ? 'Running correlation analysis…' : legs.length < 2 ? 'Add 2+ legs to grade' : 'Grade parlay'}
|
|
</button>
|
|
<button onClick={clear} className="btn-ghost" style={{ padding: 12, fontSize: 13 }}>
|
|
Clear tray
|
|
</button>
|
|
</footer>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EmptyTrayCopy() {
|
|
return (
|
|
<div style={{ padding: '32px 0', textAlign: 'center' }}>
|
|
<p className="mono" style={{ fontSize: 12, color: 'var(--text-tertiary)', letterSpacing: '0.08em' }}>
|
|
NO LEGS YET
|
|
</p>
|
|
<p style={{ marginTop: 12, color: 'var(--text-secondary)', fontSize: 14, lineHeight: 1.6 }}>
|
|
Read a prop, hit <strong>Add to Parlay</strong>, and we'll build the slip here.
|
|
We grade overall correlation and surface the legs that secretly fight each other.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function LegRow({ leg, onRemove }: { leg: ParlayLeg; onRemove: () => void }) {
|
|
return (
|
|
<li
|
|
className="surface"
|
|
style={{
|
|
padding: 12,
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
}}
|
|
>
|
|
<div>
|
|
<div style={{ fontSize: 14, fontWeight: 600 }}>{leg.player}</div>
|
|
<div className="mono" style={{ fontSize: 12, color: 'var(--text-secondary)', textTransform: 'capitalize' }}>
|
|
{leg.sport} · {leg.direction} {leg.line} {leg.stat.replace(/_/g, ' ')}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<GradePill grade={leg.grade} />
|
|
<button
|
|
onClick={onRemove}
|
|
aria-label={`Remove ${leg.player}`}
|
|
className="btn-ghost"
|
|
style={{ padding: '4px 10px', fontSize: 11 }}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|