191 lines
5.8 KiB
TypeScript
191 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
/**
|
|
* SportSelector — pill tabs for the four launch verticals.
|
|
*
|
|
* Soccer reveals a secondary league pill row (WC default for the
|
|
* tournament launch; EPL/La Liga/etc available year-round). The
|
|
* selected `{ sport, league }` is emitted via `onChange` so the
|
|
* parent owns the actual scan/odds state and can refetch on switch.
|
|
*
|
|
* The component is intentionally pure-UI — no fetches, no auth, no
|
|
* persistence. A parent that wants the selection to stick should
|
|
* pass `initialSport` / `initialLeague` from URL params or
|
|
* localStorage.
|
|
*/
|
|
|
|
export type Sport = 'NBA' | 'WNBA' | 'MLB' | 'Soccer';
|
|
|
|
// Soccer league codes match the GET /api/odds/soccer/:league path
|
|
// segment AND the `SOCCER_LEAGUES` env on the backend. Source of truth
|
|
// is `src/services/oddsService.js SOCCER_SPORT_KEYS`.
|
|
export type SoccerLeague =
|
|
| 'wc'
|
|
| 'epl'
|
|
| 'laliga'
|
|
| 'bundesliga'
|
|
| 'seriea'
|
|
| 'ligue1'
|
|
| 'ucl'
|
|
| 'mls'
|
|
| 'ligamx';
|
|
|
|
export interface SportSelection {
|
|
sport: Sport;
|
|
league?: SoccerLeague;
|
|
}
|
|
|
|
const SPORTS: Array<{ id: Sport; label: string; status?: 'live' | 'beta' }> = [
|
|
{ id: 'NBA', label: 'NBA', status: 'live' },
|
|
{ id: 'WNBA', label: 'WNBA', status: 'live' },
|
|
{ id: 'MLB', label: 'MLB', status: 'live' },
|
|
{ id: 'Soccer', label: 'Soccer', status: 'beta' },
|
|
];
|
|
|
|
const SOCCER_LEAGUES: Array<{ id: SoccerLeague; label: string; sub?: string }> = [
|
|
{ id: 'wc', label: 'World Cup', sub: '2026' },
|
|
{ id: 'epl', label: 'EPL' },
|
|
{ id: 'laliga', label: 'La Liga' },
|
|
{ id: 'bundesliga', label: 'Bundesliga' },
|
|
{ id: 'seriea', label: 'Serie A' },
|
|
{ id: 'ligue1', label: 'Ligue 1' },
|
|
{ id: 'ucl', label: 'UCL' },
|
|
{ id: 'mls', label: 'MLS' },
|
|
{ id: 'ligamx', label: 'Liga MX' },
|
|
];
|
|
|
|
interface Props {
|
|
initialSport?: Sport;
|
|
initialLeague?: SoccerLeague;
|
|
onChange?: (selection: SportSelection) => void;
|
|
}
|
|
|
|
export default function SportSelector({
|
|
initialSport = 'NBA',
|
|
initialLeague = 'wc',
|
|
onChange,
|
|
}: Props) {
|
|
const [sport, setSport] = useState<Sport>(initialSport);
|
|
const [league, setLeague] = useState<SoccerLeague>(initialLeague);
|
|
|
|
// Emit on every change so parents stay in sync. Effect (not inline
|
|
// in setSport) so React batches both pieces of state correctly.
|
|
useEffect(() => {
|
|
if (onChange) {
|
|
onChange(sport === 'Soccer' ? { sport, league } : { sport });
|
|
}
|
|
}, [sport, league, onChange]);
|
|
|
|
function selectSport(next: Sport) {
|
|
setSport(next);
|
|
}
|
|
|
|
return (
|
|
<div data-testid="sport-selector" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<div
|
|
role="tablist"
|
|
aria-label="Sport"
|
|
style={{
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: 8,
|
|
}}
|
|
>
|
|
{SPORTS.map((s) => {
|
|
const active = sport === s.id;
|
|
return (
|
|
<button
|
|
key={s.id}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={active}
|
|
onClick={() => selectSport(s.id)}
|
|
className="mono"
|
|
style={{
|
|
padding: '8px 16px',
|
|
fontSize: 13,
|
|
fontWeight: 700,
|
|
letterSpacing: '0.04em',
|
|
textTransform: 'uppercase',
|
|
border: active ? '1px solid var(--grade-a)' : '1px solid var(--border)',
|
|
background: active ? 'var(--grade-a)' : 'transparent',
|
|
color: active ? 'var(--bg-primary)' : 'var(--text-primary)',
|
|
cursor: 'pointer',
|
|
borderRadius: 6,
|
|
position: 'relative',
|
|
transition: 'all 0.15s ease',
|
|
}}
|
|
>
|
|
{s.label}
|
|
{s.status === 'beta' && (
|
|
<span
|
|
style={{
|
|
marginLeft: 6,
|
|
fontSize: 9,
|
|
padding: '1px 4px',
|
|
background: active ? 'var(--bg-primary)' : 'var(--grade-a)',
|
|
color: active ? 'var(--grade-a)' : 'var(--bg-primary)',
|
|
borderRadius: 3,
|
|
verticalAlign: 'middle',
|
|
}}
|
|
>
|
|
BETA
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{sport === 'Soccer' && (
|
|
<div
|
|
role="tablist"
|
|
aria-label="Soccer league"
|
|
style={{
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: 6,
|
|
padding: '8px 0 4px',
|
|
borderTop: '1px solid var(--border)',
|
|
}}
|
|
>
|
|
{SOCCER_LEAGUES.map((l) => {
|
|
const active = league === l.id;
|
|
return (
|
|
<button
|
|
key={l.id}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={active}
|
|
onClick={() => setLeague(l.id)}
|
|
style={{
|
|
padding: '6px 10px',
|
|
fontSize: 12,
|
|
fontWeight: 600,
|
|
border: active ? '1px solid var(--grade-a)' : '1px solid var(--border)',
|
|
background: active ? 'var(--bg-elevated)' : 'transparent',
|
|
color: active ? 'var(--grade-a)' : 'var(--text-secondary)',
|
|
cursor: 'pointer',
|
|
borderRadius: 4,
|
|
display: 'inline-flex',
|
|
alignItems: 'baseline',
|
|
gap: 4,
|
|
}}
|
|
>
|
|
<span>{l.label}</span>
|
|
{l.sub && (
|
|
<span className="mono" style={{ fontSize: 10, opacity: 0.6 }}>
|
|
{l.sub}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|