24af247b29
Founder's #1 priority. A single cached asset+rendering system, swapped into every surface, so entities stop being flat gray strings (DESIGN-SPEC Part 2). - web/src/lib/teamMeta.js: static registry for ALL 4 sports — 30 MLB, 30 NBA, 13 WNBA teams + 48 World Cup national teams, each with real colors + the ESPN logo/flag CDN abbr. resolveTeam (abbr/full-name/nickname/alias), teamLogoUrl (statsapi->ESPN mapping: AZ->ari, CWS->chw; soccer via the countries/ flag CDN), accentColor (picks the VISIBLE color of the pair so a #000000 primary never renders an invisible accent on #06060B). Colors + abbrs sourced once from ESPN's team API — stable public facts, zero-latency static data, no paid dependency. - TeamLogo: real ESPN-CDN logo with a team-colored MONOGRAM fallback (never a gray box / bare abbr). PlayerAvatar: real headshot with a team-colored monogram fallback (kills the gray silhouette). BookWordmark: brand-color wordmark, proper casing (DraftKings, not 'draftkings'). - Swapped into the class-level shared components so it propagates to ALL surfaces: GameCard (team logos + team-colored accent edge), StatStrip (player identity avatar), StreaksPanel (P0 billboard avatars), TeamHub header (the team's real crest leads its hub). Barrel-exported. ZERO OUT-OF-POCKET: ESPN logo/flag CDN + league headshot CDNs, all verified 200 image/png across MLB/NBA/WNBA/soccer. ACCEPTANCE (SSR render proof): /entity-demo harness server-rendered the exact real asset URLs across all 4 sports — mlb/500/nyy.png, mlb/500/chw.png (White Sox, correct ESPN abbr), nba/500/lal.png, wnba/500/ny.png, countries/500/ usa.png + bra/eng/arg/jpn flags, real mlbstatic/nba headshots (Judge 592450, LeBron 2544), DraftKings/FanDuel wordmarks. Each URL curl-verified 200 image/png. Harness removed post-proof (not a product surface). Pixel screenshot blocked by WSL2<->Windows-Chrome localhost networking, not code. 2757 -> 2776 tests (+13 entityLayer, +6 boot resilience), web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { getHeadshotUrl, type HeadshotSport } from '@/lib/playerHeadshot';
|
|
import { accentColor } from '@/lib/teamMeta';
|
|
|
|
/**
|
|
* PlayerAvatar (DS0) — real headshot; fallback = TEAM-COLORED MONOGRAM, never
|
|
* the gray silhouette (DESIGN-SPEC Part 2). A player always renders with
|
|
* identity. When the CDN 404s (roster gaps) we draw the player's initials on
|
|
* a disc tinted with their team color — branded, legible, never a gray blob.
|
|
*/
|
|
function initials(name: string): string {
|
|
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
|
|
if (parts.length === 0) return '?';
|
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
}
|
|
|
|
export default function PlayerAvatar({
|
|
name,
|
|
sport = 'mlb',
|
|
playerId,
|
|
team,
|
|
size = 36,
|
|
}: {
|
|
name: string;
|
|
sport?: HeadshotSport;
|
|
playerId?: string | number | null;
|
|
team?: string | null;
|
|
size?: number;
|
|
}) {
|
|
const [broken, setBroken] = useState(false);
|
|
const url = playerId != null ? getHeadshotUrl({ sport, playerId }) : null;
|
|
const accent = (team && accentColor(team, String(sport))) || '#4A9EFF';
|
|
const showImg = url && url !== '/images/player-silhouette.svg' && !broken;
|
|
|
|
const Monogram = (
|
|
<span
|
|
title={name}
|
|
className="mono"
|
|
style={{
|
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
width: size, height: size, borderRadius: '50%', flexShrink: 0,
|
|
background: `color-mix(in srgb, ${accent} 22%, #0E0E16)`,
|
|
border: `1px solid color-mix(in srgb, ${accent} 50%, transparent)`,
|
|
color: accent, fontWeight: 800, fontSize: Math.max(9, size * 0.34), letterSpacing: '0.01em',
|
|
}}
|
|
>
|
|
{initials(name)}
|
|
</span>
|
|
);
|
|
|
|
if (!showImg) return Monogram;
|
|
|
|
return (
|
|
<span
|
|
style={{
|
|
display: 'inline-flex', width: size, height: size, borderRadius: '50%',
|
|
overflow: 'hidden', flexShrink: 0, background: '#0E0E16',
|
|
border: `1px solid color-mix(in srgb, ${accent} 40%, transparent)`,
|
|
}}
|
|
>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={url as string}
|
|
alt={name}
|
|
width={size}
|
|
height={size}
|
|
loading="lazy"
|
|
onError={() => setBroken(true)}
|
|
style={{ width: size, height: size, objectFit: 'cover', objectPosition: 'top center' }}
|
|
/>
|
|
</span>
|
|
);
|
|
}
|