'use client'; import { useEffect, useState } from 'react'; import LineMovementChart, { Snapshot } from '@/components/LineMovementChart'; import { getVisibleCount, getHiddenCount, type Tier } from '@/lib/tierGate'; /** * MoversPanel (Session 28). * * Biggest line moves for a sport โ€” the market confirming (or contradicting) * a grade. Reads /api/lines/:sport/movers (Redis snapshot history, zero * credits). Self-hides when there's nothing moving. Free users see the top * 3; paid see the full board. */ interface Mover { player: string; stat: string; opening: number; current: number; delta: number; movement: 'stable' | 'rising' | 'dropping'; sharpSignal: boolean; snapshots: Snapshot[]; } export interface MoversPanelProps { sport: string; tier?: Tier; limit?: number; } export default function MoversPanel({ sport, tier = 'free', limit }: MoversPanelProps) { const [movers, setMovers] = useState(null); useEffect(() => { let cancelled = false; async function load() { try { const res = await fetch(`/api/lines/${sport}/movers`); if (!res.ok) { if (!cancelled) setMovers([]); return; } const data = await res.json(); if (!cancelled) setMovers(Array.isArray(data?.movers) ? data.movers : []); } catch { if (!cancelled) setMovers([]); } } load(); return () => { cancelled = true; }; }, [sport]); if (!movers || movers.length === 0) return null; const tierCount = getVisibleCount(tier, movers.length); const cap = limit && limit > 0 ? Math.min(limit, tierCount) : tierCount; const visible = movers.slice(0, cap); const hidden = limit ? movers.length - visible.length : getHiddenCount(tier, movers.length); return (

๐Ÿ“ˆ BIGGEST MOVERS

{visible.map((m) => (
{m.player}
{m.stat.replace(/_/g, ' ')} ยท open {m.opening} โ†’ {m.current}
0 ? '#00D4A0' : '#FF4D4D' }}> {m.delta > 0 ? '+' : ''}{m.delta} {m.sharpSignal ? SHARP : null}
))}
{hidden > 0 && ( {hidden} more โ€” upgrade for the full board โ†’ )}
); } const heading: React.CSSProperties = { fontSize: 12, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--text-tertiary, #6B6B7B)', margin: '0 0 10px', }; const row: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 12, padding: '8px 10px', borderRadius: 10, background: 'var(--bg-2, #12121A)', border: '1px solid var(--border, #1A1A24)', }; const playerName: React.CSSProperties = { fontSize: 14, fontWeight: 700, color: 'var(--text-0, #F0F0F5)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }; const propLine: React.CSSProperties = { fontSize: 12, color: 'var(--text-secondary, #8A8A9A)', textTransform: 'capitalize' }; const delta: React.CSSProperties = { flex: '0 0 auto', fontSize: 13, fontWeight: 800, whiteSpace: 'nowrap' }; const sharp: React.CSSProperties = { fontSize: 9, color: '#FFB347', letterSpacing: '0.06em' }; const upsell: React.CSSProperties = { display: 'inline-block', marginTop: 10, fontSize: 12, fontWeight: 600, color: 'var(--grade-a, #00D4A0)', textDecoration: 'none', };