Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export interface ParlayLeg {
|
||||
id: string;
|
||||
sport: 'NBA' | 'MLB' | 'WNBA';
|
||||
player: string;
|
||||
stat: string;
|
||||
line: number;
|
||||
direction: 'over' | 'under';
|
||||
grade: string;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
interface ParlayContextValue {
|
||||
legs: ParlayLeg[];
|
||||
legCount: number;
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
toggle: () => void;
|
||||
addLeg: (leg: Omit<ParlayLeg, 'id'>) => void;
|
||||
removeLeg: (id: string) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'bbk:parlay';
|
||||
const MAX_LEGS = 12;
|
||||
|
||||
const ParlayContext = createContext<ParlayContextValue | null>(null);
|
||||
|
||||
export default function ParlayProvider({ children }: { children: React.ReactNode }) {
|
||||
const [legs, setLegs] = useState<ParlayLeg[]>([]);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
// Restore from localStorage so a refresh doesn't drop the tray
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) setLegs(parsed);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(legs));
|
||||
} catch {
|
||||
/* ignore quota */
|
||||
}
|
||||
}, [legs]);
|
||||
|
||||
const addLeg = useCallback((leg: Omit<ParlayLeg, 'id'>) => {
|
||||
setLegs((prev) => {
|
||||
if (prev.length >= MAX_LEGS) return prev;
|
||||
// De-dupe by player+stat+line+direction
|
||||
const key = `${leg.player}|${leg.stat}|${leg.line}|${leg.direction}`;
|
||||
if (prev.some((p) => `${p.player}|${p.stat}|${p.line}|${p.direction}` === key)) return prev;
|
||||
const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
|
||||
const next = [...prev, { ...leg, id }];
|
||||
// Fire-and-forget: tell the backend so most-parlayed counts get bumped
|
||||
void fetch('/api/parlay/add-leg', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sport: leg.sport,
|
||||
player: leg.player,
|
||||
stat: leg.stat,
|
||||
line: leg.line,
|
||||
direction: leg.direction,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeLeg = useCallback((id: string) => {
|
||||
setLegs((prev) => prev.filter((l) => l.id !== id));
|
||||
}, []);
|
||||
|
||||
const clear = useCallback(() => setLegs([]), []);
|
||||
|
||||
const value = useMemo<ParlayContextValue>(() => ({
|
||||
legs,
|
||||
legCount: legs.length,
|
||||
isOpen,
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
toggle: () => setOpen((o) => !o),
|
||||
addLeg,
|
||||
removeLeg,
|
||||
clear,
|
||||
}), [legs, isOpen, addLeg, removeLeg, clear]);
|
||||
|
||||
return <ParlayContext.Provider value={value}>{children}</ParlayContext.Provider>;
|
||||
}
|
||||
|
||||
export function useParlay(): ParlayContextValue {
|
||||
const ctx = useContext(ParlayContext);
|
||||
if (!ctx) {
|
||||
// Provide a noop fallback so components can render outside the provider
|
||||
// (e.g. during prerender of marketing pages).
|
||||
return {
|
||||
legs: [],
|
||||
legCount: 0,
|
||||
isOpen: false,
|
||||
open: () => {},
|
||||
close: () => {},
|
||||
toggle: () => {},
|
||||
addLeg: () => {},
|
||||
removeLeg: () => {},
|
||||
clear: () => {},
|
||||
};
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user