import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { sanitizeClubName } from '../../utils/url'; import { TeamLogo } from '../common/TeamLogo'; export interface StandingRow { position?: number; pos?: number; rank?: number; team?: any; team_id?: string | number; team_logo_url?: string; club?: string; points?: number | string; pts?: number | string; played?: number | string; matches?: number | string; wins?: number | string; win?: number | string; draws?: number | string; draw?: number | string; losses?: number | string; loss?: number | string; score?: string; } function deriveTeamIdFromLogoUrl(url?: string): string | undefined { try { const u = String(url || ''); if (!u) return undefined; const m = u.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/); return m ? m[0].toLowerCase() : undefined; } catch { return undefined; } } type TeamOverrides = { by_id?: Record; by_name?: Record }; const StandingsCard: React.FC<{ rows: StandingRow[]; onRowClick?: (row: StandingRow, index: number) => void; variant?: 'logos' | 'plain' }>= ({ rows, onRowClick, variant = 'logos' }) => { const { t } = useTranslation(); const safe = Array.isArray(rows) ? rows : []; const [sortKey, setSortKey] = useState<'rank' | 'team' | 'played' | 'wins' | 'draws' | 'losses' | 'score' | 'points' | null>(null); const [sortOrder, setSortOrder] = useState<'desc' | 'asc' | null>(null); const [overrides, setOverrides] = useState({}); const toNumber = (v: any): number => { if (typeof v === 'number') return v; const n = parseFloat(String(v ?? '').replace(/[^0-9\-\.]/g, '')); return isNaN(n) ? 0 : n; }; const scoreDiff = (s: any): number => { const str = String(s ?? '').trim(); const m = str.match(/(-?\d+)\s*[:\-]\s*(-?\d+)/); if (m) return Number(m[1]) - Number(m[2]); return toNumber(str); }; const normalize = (s: string) => String(s || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[\u2012\u2013\u2014\u2015\u2212]/g, '-') .replace(/[.,!;:()\[\]{}]/g, ' ') .replace(/[\s,]*(z\.?\s*s\.?|o\.?\s*s\.?)\s*$/g, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); const byIdMap = useMemo(() => (overrides?.by_id || {}) as Record, [overrides]); const overridesNameIndex = useMemo(() => { const idx: Record = {}; try { for (const [id, v] of Object.entries(byIdMap)) { const nm = String((v as any)?.name || '').trim(); if (!nm) continue; const key = normalize(nm); if (!key) continue; idx[key] = { id, name: nm }; } } catch {} return idx; }, [byIdMap]); const pickName = (teamId?: string | number, original?: string, logoUrl?: string): string => { const id = String(teamId || '') || deriveTeamIdFromLogoUrl(logoUrl) || ''; const v = id ? (byIdMap[id]?.name || '') : ''; if (v && v.trim().length > 0) return v; if (original) { const n = normalize(original); let hit = overridesNameIndex[n]; if (!hit) { for (const [k, val] of Object.entries(overridesNameIndex)) { if (!k) continue; if (n.endsWith(k) || k.endsWith(n)) { hit = val as any; break; } } } if (hit?.name) return hit.name; } return original || ''; }; useEffect(() => { let mounted = true; (async () => { const now = Date.now(); let data: any = null; try { const res = await fetch(`/api/v1/public/team-logo-overrides?t=${now}`, { cache: 'no-cache' }); if (res.ok) data = await res.json(); } catch {} if (!data) { try { const res2 = await fetch('/cache/prefetch/team_logo_overrides.json', { cache: 'no-cache' }); if (res2.ok) data = await res2.json(); } catch {} } if (mounted) setOverrides(data || { by_id: {}, by_name: {} }); })(); return () => { mounted = false; }; }, []); const getTeam = (r: any): string => sanitizeClubName(pickName(r?.team_id, r?.team?.name ?? r?.team ?? r?.club ?? '', r?.team_logo_url)); const getRank = (r: any): number => toNumber(r?.rank ?? r?.pos ?? r?.position); const toggleSort = (key: 'rank' | 'team' | 'played' | 'wins' | 'draws' | 'losses' | 'score' | 'points') => { if (sortKey !== key) { setSortKey(key); setSortOrder('desc'); return; } if (sortOrder === 'desc') { setSortOrder('asc'); return; } setSortKey(null); setSortOrder(null); }; const sorted = useMemo(() => { if (!sortKey || !sortOrder) return safe; const arr = [...safe]; arr.sort((a: any, b: any) => { let va: any; let vb: any; let isText = false; switch (sortKey) { case 'team': va = getTeam(a); vb = getTeam(b); isText = true; break; case 'rank': va = getRank(a); vb = getRank(b); break; case 'played': va = toNumber(a?.played ?? a?.matches); vb = toNumber(b?.played ?? b?.matches); break; case 'wins': va = toNumber(a?.wins ?? a?.win); vb = toNumber(b?.wins ?? b?.win); break; case 'draws': va = toNumber(a?.draws ?? a?.draw); vb = toNumber(b?.draws ?? b?.draw); break; case 'losses': va = toNumber(a?.losses ?? a?.loss); vb = toNumber(b?.losses ?? b?.loss); break; case 'score': va = scoreDiff(a?.score); vb = scoreDiff(b?.score); break; case 'points': va = toNumber(a?.points ?? a?.pts); vb = toNumber(b?.points ?? b?.pts); break; default: va = 0; vb = 0; } let res = isText ? String(va).localeCompare(String(vb)) : (va as number) - (vb as number); if (sortOrder === 'desc') res = -res; if (res === 0) { const ra = getRank(a); const rb = getRank(b); res = ra - rb; } return res; }); return arr; }, [safe, sortKey, sortOrder]); const arrow = (key: 'rank' | 'team' | 'played' | 'wins' | 'draws' | 'losses' | 'score' | 'points') => sortKey === key ? (sortOrder === 'desc' ? ' ▼' : ' ▲') : ''; return (
{sorted.slice(0, 8).map((row, idx) => { const teamNameRaw = (row as any).team?.name ?? (row as any).team ?? (row as any).club ?? '-'; const teamName = sanitizeClubName(pickName((row as any).team_id, teamNameRaw, (row as any).team_logo_url)); const tid = (row as any).team_id || deriveTeamIdFromLogoUrl((row as any).team_logo_url); return ( onRowClick?.(row, idx)} style={{ cursor: onRowClick ? 'pointer' : 'default', background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: '8px', transition: 'all 0.2s ease', }} onMouseEnter={(e) => { (e.currentTarget as HTMLTableRowElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.08)'; (e.currentTarget as HTMLTableRowElement).style.borderColor = 'var(--primary)'; }} onMouseLeave={(e) => { (e.currentTarget as HTMLTableRowElement).style.boxShadow = 'none'; (e.currentTarget as HTMLTableRowElement).style.borderColor = 'var(--card-border)'; }} > ); })}
toggleSort('rank')} style={{ padding: '6px 8px', textAlign: 'left', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }}># {arrow('rank')} toggleSort('team')} style={{ padding: '6px 8px', textAlign: 'left', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }}>{t('homepage.team')}{arrow('team')} toggleSort('played')} style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }}>{t('homepage.played')}{arrow('played')} toggleSort('wins')} style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }}>{t('homepage.won')}{arrow('wins')} toggleSort('draws')} style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }}>{t('homepage.drawn')}{arrow('draws')} toggleSort('losses')} style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }}>{t('homepage.lost')}{arrow('losses')} toggleSort('score')} style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600, display: 'none', cursor: 'pointer', userSelect: 'none' }} className="hide-mobile">{t('homepage.goals')}{arrow('score')} toggleSort('points')} style={{ padding: '6px 8px', textAlign: 'center', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }}>{t('homepage.points')}{arrow('points')}
{row.position ?? row.pos ?? row.rank ?? idx + 1} {variant === 'logos' ? ( {teamName} ) : ( teamName )} {(row as any).played ?? (row as any).matches ?? '-'} {(row as any).wins ?? (row as any).win ?? '-'} {(row as any).draws ?? (row as any).draw ?? '-'} {(row as any).losses ?? (row as any).loss ?? '-'} {(row as any).score ?? '-'} {(row as any).points ?? (row as any).pts ?? '-'}
); }; export default StandingsCard;