Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface LiveProp {
|
||||
player: string;
|
||||
stat: string;
|
||||
line: number;
|
||||
direction: string;
|
||||
grade: string;
|
||||
sport: string;
|
||||
}
|
||||
|
||||
const SPORT_COLOR: Record<string, string> = {
|
||||
NBA: '#E94B3C',
|
||||
MLB: '#1E90FF',
|
||||
WNBA: '#FFB347',
|
||||
};
|
||||
|
||||
function gradeColor(grade: string): string {
|
||||
const g = (grade || '').trim().toUpperCase().charAt(0);
|
||||
if (g === 'A') return 'var(--grade-a)';
|
||||
if (g === 'B') return 'var(--grade-b)';
|
||||
if (g === 'C') return 'var(--grade-c)';
|
||||
return 'var(--grade-d)';
|
||||
}
|
||||
|
||||
export default function LivePropsStrip() {
|
||||
const [props, setProps] = useState<LiveProp[] | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch('/api/props/live');
|
||||
if (!res.ok) {
|
||||
if (!cancelled) setError(true);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (cancelled) return;
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setProps(data.slice(0, 12));
|
||||
setError(false);
|
||||
} else {
|
||||
setProps([]);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setError(true);
|
||||
}
|
||||
}
|
||||
load();
|
||||
const id = setInterval(load, 60_000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Loading / fallback state
|
||||
if (props === null) return null;
|
||||
|
||||
if (error || props.length === 0) {
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
padding: '24px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
background: 'var(--bg-surface)',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="mono"
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
color: 'var(--text-tertiary)',
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
TONIGHT'S GRADES LOAD AT 5 PM ET
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Duplicate for seamless ticker scroll
|
||||
const ticker = [...props, ...props];
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
padding: '20px 0',
|
||||
borderTop: '1px solid var(--border)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
background: 'var(--bg-surface)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div className="animate-ticker" style={{ display: 'flex', gap: 16, whiteSpace: 'nowrap', width: 'max-content' }}>
|
||||
{ticker.map((p, i) => (
|
||||
<div
|
||||
key={`${p.player}-${i}`}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
padding: '8px 16px',
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="mono"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
padding: '2px 6px',
|
||||
borderRadius: 999,
|
||||
color: SPORT_COLOR[p.sport] || 'var(--text-secondary)',
|
||||
background: `${SPORT_COLOR[p.sport] || 'var(--text-secondary)'}1F`,
|
||||
}}
|
||||
>
|
||||
{p.sport}
|
||||
</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600 }}>{p.player}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
||||
{p.direction} {p.line} {p.stat}
|
||||
</span>
|
||||
<span
|
||||
className="mono"
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 800,
|
||||
color: gradeColor(p.grade),
|
||||
}}
|
||||
>
|
||||
{p.grade}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user