'use client'; import { useState } from 'react'; import PropRow, { PropRowProp, PropRowResult, propRowKey, Tier } from '@/components/PropRow'; import { getHeadshotUrl, PLAYER_SILHOUETTE, HeadshotSport } from '@/lib/playerHeadshot'; /** * PlayerCard — one player and all their props (Session 19). * * Slate redesign: previously each prop was an independent row, so a * player with 4 props (Pts/Reb/Ast/3PT) appeared as 4 stripes that * repeated the player's name. That reads like a spreadsheet, not a * sports product. PlayerCard groups: a header with headshot + name + * team sits above N PropRow children. * * Headshot resolution lives in `lib/playerHeadshot.ts`. The `` * fall-through swaps to the bundled silhouette if the CDN 404s on a * player whose league hasn't published a headshot yet. */ export interface PlayerCardProps { player: string; sport: HeadshotSport; team?: string; position?: string; /** League ID — NBA stats.com ID, WNBA player ID, or MLB people ID. */ playerId?: string | number | null; /** ESPN ID fallback (used when no league ID is known). */ espnId?: string | number | null; /** Pre-cached headshot URL (soccer / API-Football). */ photoUrl?: string | null; props: PropRowProp[]; gradedProps: Map; loadingKey?: string | null; errorByKey?: Record; tier?: Tier; onGrade: (prop: PropRowProp) => void; onUpgrade?: () => void; } export default function PlayerCard(props: PlayerCardProps) { const { player, sport, team, position, playerId, espnId, photoUrl, props: propList, gradedProps, loadingKey, errorByKey, tier = 'free', onGrade, onUpgrade, } = props; // Compute the initial headshot URL — onError swaps to silhouette. const initialUrl = getHeadshotUrl({ sport, playerId, espnId, cachedPhotoUrl: photoUrl, }); const [headshotSrc, setHeadshotSrc] = useState(initialUrl); const subtitle = [team, position].filter(Boolean).join(' · '); return (
{/* eslint-disable-next-line @next/next/no-img-element */} {`${player} { if (headshotSrc !== PLAYER_SILHOUETTE) setHeadshotSrc(PLAYER_SILHOUETTE); }} style={{ width: 40, height: 40, borderRadius: '50%', objectFit: 'cover', background: 'var(--bg-elevated, #15151F)', border: '1px solid var(--border, #1A1A24)', flexShrink: 0, }} />
{player}
{subtitle && (
{subtitle}
)}
{propList.length} prop{propList.length === 1 ? '' : 's'}
    {propList.map((p) => { const key = propRowKey(p); return ( ); })}
); } /** * groupPropsByPlayer — preserves the original prop order (first * appearance of a player wins their slot), so the Slate's sort * (alphabetical by player) is the actual visual sort. */ export function groupPropsByPlayer(propList: PropRowProp[]): Array<{ player: string; props: PropRowProp[] }> { const byPlayer = new Map(); for (const p of propList) { const existing = byPlayer.get(p.player); if (existing) existing.push(p); else byPlayer.set(p.player, [p]); } return Array.from(byPlayer.entries()).map(([player, props]) => ({ player, props })); }