'use client'; import { useEffect, useState } from 'react'; import PlayerAvatar from '@/components/vyndr/PlayerAvatar'; import GradeBadge from '@/components/vyndr/GradeBadge'; import ArchetypeBadge from '@/components/vyndr/ArchetypeBadge'; import { type Tier } from '@/lib/tierGate'; /** * StreaksPanel (Session 23 · rebuilt DS4 — the STREAKS BILLBOARD). * * DESIGN-SPEC Part 7 (#2): the STREAKS row is a P0 billboard — the timeline * is customer #1. Not a log line: a Bloomberg alert. ONE bold figure (the * streak LENGTH, mono + tabular, the largest thing in the row) among muted * context (real player identity + "built vs" lens), with a SINGLE severity * accent — tonight's matchup difficulty (step-up amber / step-down green). * The grade badge is tier-gated (the READ is the paid layer). * * Self-hides when there are no streaks so the landing page never shows an * empty box — the other layers (schedule, game lines, props) carry the * slate when no logs are warm yet. */ interface Streak { player: string; playerId: string | number | null; team: string | null; type: string; category: string; currentStreak: number; description: string; // Session 60 (night2/C) — THE LENS + optional snapshot grade letter. lens?: { builtVs?: string[] | null; matchup?: string | null; difficulty?: string | null; read?: string | null }; grade?: string | null; // DS5 (Part 5) — the player's locked archetype from the snapshot join, when // present. Optional + self-hiding: absent beats fabricated. archetype?: string | null; } export interface StreaksPanelProps { sport: string; tier?: Tier; stat?: string; /** Optional hard cap (teaser usage on the landing page). */ limit?: number; } export default function StreaksPanel({ sport, tier = 'free', stat = 'all', limit }: StreaksPanelProps) { const [streaks, setStreaks] = useState(null); useEffect(() => { let cancelled = false; async function load() { try { const res = await fetch(`/api/streaks/${sport}?stat=${encodeURIComponent(stat)}`); if (!res.ok) { if (!cancelled) setStreaks([]); return; } const data = await res.json(); if (!cancelled) setStreaks(Array.isArray(data?.streaks) ? data.streaks : []); } catch { if (!cancelled) setStreaks([]); } } load(); return () => { cancelled = true; }; }, [sport, stat]); if (!streaks || streaks.length === 0) return null; // Session 60 (product def) — the PICTURE is free: every streak shows for // every tier. Only an explicit `limit` (landing teaser) caps the list. const visible = limit && limit > 0 ? streaks.slice(0, limit) : streaks; const hidden = streaks.length - visible.length; // DS4 — the GRADE (the READ) is the paid layer; only analyst/desk see it. const canSeeGrade = tier === 'analyst' || tier === 'desk'; return (

🔥 STREAKS

{visible.map((s) => { const diff = s.lens?.difficulty; // ONE severity accent — tonight's matchup difficulty. const sev = diff === 'step up' ? { color: 'var(--amber, #FFB347)', label: '▲ STEP-UP SPOT TONIGHT' } : diff === 'step down' ? { color: 'var(--g-a, #00D4A0)', label: '▼ SOFTER SPOT TONIGHT' } : null; // The LENS — muted "built vs" context; never the raw streak alone. const builtVs = Array.isArray(s.lens?.builtVs) && s.lens!.builtVs!.length > 0 ? `built vs ${s.lens!.builtVs!.slice(0, 3).join(', ')}` : (s.lens?.read || s.description); return (
{/* Real identity — headshot / team-colored monogram (DS0). */} {/* THE HERO — streak length, mono + tabular, the largest figure in the row. One bold number; everything else demoted. */}
{s.currentStreak} GAME
STREAK
{/* Identity + the LENS — demoted, muted context. */}
{s.player}{s.team ? · {s.team} : null}
{/* Part 5 — the archetype glyph+chip (the Rosetta stone), propagated here; self-hides when the join has no archetype. */} {s.archetype && (
)}
{s.description}
{builtVs}
{/* The single severity accent + tier-gated grade badge. */}
{sev && {sev.label}} {s.grade && ( canSeeGrade ? : GRADE ◆ )}
); })}
{hidden > 0 && ( {hidden} more streak{hidden === 1 ? '' : 's'} on the board → )}
); } const panelHeading: React.CSSProperties = { fontSize: 12, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--text-tertiary, #6A6A78)', margin: '0 0 10px', }; const rowStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 14, padding: '12px 14px', borderRadius: 12, background: 'var(--surface, #12121A)', border: '1px solid var(--border, #2A2A36)', }; // THE HERO NUMBER — deliberately the largest font in the row (mono, tabular). const heroBlock: React.CSSProperties = { flex: '0 0 auto', display: 'flex', alignItems: 'baseline', gap: 6, minWidth: 62, }; const heroNum: React.CSSProperties = { fontSize: 38, fontWeight: 800, lineHeight: 1, color: 'var(--text-primary, #F0F0F4)', fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.03em', }; const heroUnit: React.CSSProperties = { fontSize: 8.5, fontWeight: 700, lineHeight: 1.05, letterSpacing: '0.1em', color: 'var(--text-tertiary, #6A6A78)', }; const playerName: React.CSSProperties = { fontSize: 14, fontWeight: 700, color: 'var(--text-primary, #F0F0F4)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }; const teamTag: React.CSSProperties = { fontSize: 12, fontWeight: 500, color: 'var(--text-tertiary, #6A6A78)', }; const categoryLine: React.CSSProperties = { fontSize: 11, color: 'var(--text-secondary, #9A9AA8)', marginTop: 1, }; const lensLine: React.CSSProperties = { fontSize: 11, color: 'var(--text-tertiary, #6A6A78)', marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }; const rightRail: React.CSSProperties = { flex: '0 0 auto', display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6, }; const sevChip: React.CSSProperties = { fontSize: 9.5, fontWeight: 700, letterSpacing: '0.06em', padding: '2px 7px', borderRadius: 5, border: '1px solid transparent', background: 'transparent', whiteSpace: 'nowrap', }; const gradeLock: React.CSSProperties = { fontSize: 10.5, fontWeight: 800, letterSpacing: '0.06em', color: 'var(--text-tertiary, #6A6A78)', textDecoration: 'none', border: '1px solid var(--border, #2A2A36)', borderRadius: 4, padding: '2px 6px', }; const upsellStyle: React.CSSProperties = { display: 'inline-block', marginTop: 10, fontSize: 12, fontWeight: 600, color: 'var(--accent, #E94B3C)', textDecoration: 'none', };