'use client'; import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; import { useParlay } from '@/contexts/ParlayContext'; import { GradePill } from '@/components/GradeCard'; type Sport = 'NBA' | 'MLB' | 'WNBA'; interface Game { id: string; away: string; home: string; start_time: string; sport: Sport; status: 'scheduled' | 'live' | 'final'; prop_count?: number; ab_grade_count?: number; injury_note?: string; } interface TopGrade { player: string; stat: string; line: number; direction: 'over' | 'under'; sport: Sport; grade: string; confidence?: number; } interface ParlayLegStat { player: string; stat: string; line: number; direction: 'over' | 'under'; sport: Sport; grade?: string; parlay_count: number; } interface RecentScan { id: string; player_name: string; stat: string; line: number; direction: 'over' | 'under'; grade: string; sport: Sport; created_at: string; } const SPORT_TABS: Sport[] = ['NBA', 'MLB', 'WNBA']; const SPORT_COLOR: Record = { NBA: '#E94B3C', MLB: '#1E90FF', WNBA: '#FFB347', }; export default function DashboardPage() { const router = useRouter(); const { user, tier, scansRemaining, loading: authLoading } = useAuth(); const { addLeg, open } = useParlay(); const [sport, setSport] = useState('NBA'); const [games, setGames] = useState(null); const [topGrades, setTopGrades] = useState(null); const [mostParlayed, setMostParlayed] = useState(null); const [recentScans, setRecentScans] = useState(null); // Gate useEffect(() => { if (!authLoading && !user) router.replace('/login?next=/dashboard'); }, [authLoading, user, router]); // Fetch slate when sport changes useEffect(() => { let cancelled = false; setGames(null); setTopGrades(null); Promise.all([ fetch(`/api/games/tonight?sport=${sport}`).then((r) => r.json()).catch(() => ({ games: [] })), fetch(`/api/props/top-graded?sport=${sport}`).then((r) => r.json()).catch(() => ({ props: [] })), ]).then(([gamesData, gradesData]) => { if (cancelled) return; setGames(Array.isArray(gamesData?.games) ? gamesData.games : []); setTopGrades(Array.isArray(gradesData?.props) ? gradesData.props.slice(0, 10) : []); }); return () => { cancelled = true; }; }, [sport]); // Most parlayed + recent scans don't depend on sport useEffect(() => { fetch('/api/props/most-parlayed') .then((r) => r.json()) .then((data) => setMostParlayed(Array.isArray(data?.props) ? data.props.slice(0, 5) : [])) .catch(() => setMostParlayed([])); if (user) { const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null; fetch('/api/user/recent-scans', { headers: token ? { Authorization: `Bearer ${token}` } : {}, }) .then((r) => r.json()) .then((data) => setRecentScans(Array.isArray(data?.scans) ? data.scans.slice(0, 5) : [])) .catch(() => setRecentScans([])); } }, [user]); const gameCountsBySport = useMemo(() => { // Updated whenever the sport's slate refreshes. We only know counts for // the current sport — others show a dash until clicked. return { [sport]: games?.length ?? 0 } as Partial>; }, [sport, games]); if (authLoading || !user) { return (

Loading the slate…

); } const isFirstTimer = recentScans?.length === 0; const slateEmpty = games?.length === 0; return (
{/* Top welcome strip */}

Tonight's slate

{new Date().toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' }).toUpperCase()}

{tier === 'free' && scansRemaining != null && (
{scansRemaining}/5 READS · MO
)}
{/* Sport tabs */}
{SPORT_TABS.map((s) => { const active = s === sport; const count = gameCountsBySport[s]; return ( ); })}
{/* Top grades horizontal scroll */}
{topGrades === null ? ( ) : topGrades.length === 0 ? (

No grades yet. The model is waiting on lines.

) : (
{topGrades.map((g, i) => ( ))}
)}
{/* Tonight's games */}
{games === null ? ( ) : games.length === 0 ? (

NO SLATE

No games on the slate tonight.

Check the Ledger to review past grades, or explore the Leaderboard.

) : ( )}
{/* Most parlayed tonight */}
🔥 TRENDING}> {mostParlayed === null ? ( ) : mostParlayed.length === 0 ? (

No parlays built yet tonight. Be the first.

) : (
    {mostParlayed.map((p, i) => (
  • 🔥 {p.player}
    {p.sport} · {p.direction} {p.line} {p.stat.replace(/_/g, ' ')}
    {p.grade && }
  • ))}
)}
{/* Recent scans OR first-timer onboarding */}
{recentScans === null ? ( ) : recentScans.length === 0 ? (

WELCOME TO THE LEDGER

Tonight's slate is loaded. {games?.length ?? 0} {games?.length === 1 ? 'game' : 'games'} across 3 sports.

Pick a game and read your first prop — it's on us.

Run a read →
) : (
    {recentScans.map((s) => (
  • {s.player_name}
    {s.sport} · {s.direction} {s.line} {s.stat.replace(/_/g, ' ')}
  • ))}
)}
); } function Section({ title, subtitle, right, children }: { title: string; subtitle: string | null; right?: React.ReactNode; children: React.ReactNode }) { return (

{title}

{subtitle && (

{subtitle}

)}
{right}
{children}
); } function SportPill({ sport }: { sport: Sport }) { return ( {sport} ); } function SkeletonRow({ stacked = false }: { stacked?: boolean }) { if (stacked) { return (
{[0, 1, 2].map((i) => (
))}
); } return (
{[0, 1, 2, 3].map((i) => (
))}
); } function formatTime(iso: string): string { try { return new Date(iso).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }); } catch { return iso; } } const emptyCopy: React.CSSProperties = { padding: '24px 16px', textAlign: 'center', color: 'var(--text-secondary)', fontSize: 14, background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 12, };