This commit is contained in:
Tomas Dvorak
2025-11-11 10:29:30 +01:00
parent d5b4faea61
commit 8762bde4bf
139 changed files with 7240 additions and 2870 deletions
+176 -16
View File
@@ -22,6 +22,17 @@ type TableRow = {
points: 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 CompetitionTable = {
id: string;
name: string;
@@ -51,6 +62,7 @@ const TablesPage: React.FC = () => {
const [aliasMap, setAliasMap] = useState<Record<string, { alias: string; original_name?: string; display_order?: number }>>({});
const [selectedClub, setSelectedClub] = useState<any>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [overrides, setOverrides] = useState<{ by_id?: Record<string, { name?: string; logo_url?: string }>; by_name?: Record<string, string> } | null>(null);
const { data: settings } = usePublicSettings();
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
@@ -61,6 +73,32 @@ const TablesPage: React.FC = () => {
const rowOddBg = useColorModeValue('white', 'gray.800');
const rowEvenBg = useColorModeValue('gray.50', 'gray.700');
type SortKey = 'rank' | 'team' | 'played' | 'wins' | 'draws' | 'losses' | 'score' | 'points';
type SortOrder = 'desc' | 'asc';
const [sortState, setSortState] = useState<Record<string, { key: SortKey; order: SortOrder } | null>>({});
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 toggleSort = (compId: string, key: SortKey) => {
const cur = sortState[compId];
if (!cur || cur.key !== key) { setSortState({ ...sortState, [compId]: { key, order: 'desc' } }); return; }
if (cur.order === 'desc') { setSortState({ ...sortState, [compId]: { key, order: 'asc' } }); return; }
const next = { ...sortState }; next[compId] = null; setSortState(next);
};
const arrow = (compId: string, key: SortKey) => {
const cur = sortState[compId];
if (!cur || cur.key !== key) return '';
return cur.order === 'desc' ? '▼' : '▲';
};
const handleClubClick = (club: any) => {
setSelectedClub(club);
setIsModalOpen(true);
@@ -72,6 +110,22 @@ const TablesPage: React.FC = () => {
setLoading(true);
setError(null);
try {
// Load overrides (API + cached file)
try {
const now = Date.now();
let ovr: any = null;
try {
const res = await fetch(`/api/v1/public/team-logo-overrides?t=${now}`, { cache: 'no-cache' });
if (res.ok) ovr = await res.json();
} catch {}
if (!ovr) {
try {
const res2 = await fetch('/cache/prefetch/team_logo_overrides.json', { cache: 'no-cache' });
if (res2.ok) ovr = await res2.json();
} catch {}
}
if (!cancelled) setOverrides(ovr || { by_id: {}, by_name: {} });
} catch {}
// Load aliases first
let amap: Record<string, { alias: string; original_name?: string; display_order?: number }> = {};
try {
@@ -123,6 +177,83 @@ const TablesPage: React.FC = () => {
return () => { cancelled = true; };
}, []);
// Normalization helpers (same as CalendarPage/TableSection)
const normalize = (s: string) => String(s || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
const stripPrefixes = (s: string) => {
let x = normalize(s);
x = x.replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g, '');
return x.replace(/\s+/g, ' ').trim();
};
const byNameMap = useMemo(() => {
const m: Record<string, string> = {};
const src = overrides?.by_name || {};
for (const k of Object.keys(src)) m[normalize(k)] = src[k];
return m;
}, [overrides]);
const byIdMap = useMemo(() => (overrides?.by_id || {}) as Record<string, { name?: string; logo_url?: string }>, [overrides]);
const overridesNameIndex = useMemo(() => {
const idx: Record<string, { id: string; name: string }> = {};
try {
for (const [id, v] of Object.entries(byIdMap)) {
const name = String((v as any)?.name || '').trim();
if (!name) continue;
const key = normalize(name);
if (!key) continue;
idx[key] = { id, name };
}
} catch {}
return idx;
}, [byIdMap]);
const pickName = (teamId?: string, original?: string, logoUrl?: string) => {
const id = String(teamId || '') || deriveTeamIdFromLogoUrl(logoUrl) || '';
const v = id ? byIdMap?.[id]?.name : undefined;
if (v && String(v).trim().length > 0) return String(v);
const orig = String(original || '');
if (orig) {
const n = normalize(orig);
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) {
const t1 = n.split(' ')[0];
if (t1 && t1.length >= 5) {
for (const [k, val] of Object.entries(overridesNameIndex)) {
const k1 = String(k).split(' ')[0];
if (k1 === t1) { hit = val as any; break; }
}
}
}
if (hit?.name) return hit.name;
}
return orig;
};
const pickLogo = (teamId?: string, teamName?: string, original?: string): string | undefined => {
if (teamId && byIdMap?.[teamId]?.logo_url) return byIdMap[teamId]!.logo_url as string;
if (teamName) {
const exact = (overrides?.by_name || {})[teamName];
if (exact) return exact;
const n = normalize(teamName);
const cand = byNameMap[n];
if (cand) return cand;
const stripped = stripPrefixes(teamName);
for (const k of Object.keys(overrides?.by_name || {})) {
const kn = stripPrefixes(k);
if (!kn) continue;
if (stripped.endsWith(kn) || kn.endsWith(stripped)) return (overrides!.by_name as any)[k];
}
}
return original;
};
return (
<MainLayout>
<Container maxW="7xl" py={{ base: 6, md: 10 }}>
@@ -172,18 +303,47 @@ const TablesPage: React.FC = () => {
<Table size="sm" variant="unstyled" color={tableTextColor}>
<Thead position="sticky" top={0} zIndex={2}>
<Tr bg={tableHeaderBg} color="white">
<Th w="56px" color="white">#</Th>
<Th color="white">Tým</Th>
<Th isNumeric color="white">Z</Th>
<Th isNumeric color="white">V</Th>
<Th isNumeric color="white">R</Th>
<Th isNumeric color="white">P</Th>
<Th isNumeric color="white">Skóre</Th>
<Th isNumeric color="white">Body</Th>
<Th w="56px" color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'rank')}># {arrow(c.id, 'rank')}</Th>
<Th color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'team')}>Tým {arrow(c.id, 'team')}</Th>
<Th isNumeric color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'played')}>Z {arrow(c.id, 'played')}</Th>
<Th isNumeric color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'wins')}>V {arrow(c.id, 'wins')}</Th>
<Th isNumeric color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'draws')}>R {arrow(c.id, 'draws')}</Th>
<Th isNumeric color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'losses')}>P {arrow(c.id, 'losses')}</Th>
<Th isNumeric color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'score')}>Skóre {arrow(c.id, 'score')}</Th>
<Th isNumeric color="white" cursor="pointer" onClick={() => toggleSort(c.id, 'points')}>Body {arrow(c.id, 'points')}</Th>
</Tr>
</Thead>
<Tbody>
{c.rows.map((r, idx) => (
{(() => {
const cur = sortState[c.id];
const arr = [...(c.rows || [])];
if (cur) {
arr.sort((a: any, b: any) => {
let va: any; let vb: any; let isText = false;
switch (cur.key) {
case 'team': va = pickName(a.team_id, a.team, a.team_logo_url); vb = pickName(b.team_id, b.team, b.team_logo_url); isText = true; break;
case 'rank': va = toNumber(a.rank); vb = toNumber(b.rank); break;
case 'played': va = toNumber(a.played); vb = toNumber(b.played); break;
case 'wins': va = toNumber(a.wins); vb = toNumber(b.wins); break;
case 'draws': va = toNumber(a.draws); vb = toNumber(b.draws); break;
case 'losses': va = toNumber(a.losses); vb = toNumber(b.losses); break;
case 'score': va = scoreDiff(a.score); vb = scoreDiff(b.score); break;
case 'points': va = toNumber(a.points); vb = toNumber(b.points); break;
default: va = 0; vb = 0;
}
let res = isText ? String(va).localeCompare(String(vb)) : (va as number) - (vb as number);
if (cur.order === 'desc') res = -res;
if (res === 0) {
const ra = toNumber(a.rank); const rb = toNumber(b.rank);
res = ra - rb;
}
return res;
});
}
return arr.map((r, idx) => {
const displayTeam = pickName(r.team_id, r.team, r.team_logo_url);
const displayLogo = pickLogo(r.team_id, displayTeam, r.team_logo_url);
return (
<Tr
key={`${c.id}-${r.rank}-${r.team}`}
transition="all 0.15s"
@@ -198,17 +358,17 @@ const TablesPage: React.FC = () => {
<Td>
<Flex align="center" gap={3}>
<TeamLogo
teamId={r.team_id}
teamName={r.team}
facrLogo={r.team_logo_url}
teamId={r.team_id || deriveTeamIdFromLogoUrl(r.team_logo_url)}
teamName={displayTeam}
facrLogo={displayLogo}
size="small"
alt={r.team}
borderRadius="full"
alt={displayTeam}
objectFit="contain"
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
/>
<Text fontWeight="medium" color={tableTextColor}>{r.team}</Text>
<Text fontWeight="medium" color={tableTextColor}>{displayTeam}</Text>
</Flex>
</Td>
<Td isNumeric color={tableTextColor}>{r.played}</Td>
@@ -220,7 +380,7 @@ const TablesPage: React.FC = () => {
<Badge variant="solid" bg="blue.600" color="white">{r.points}</Badge>
</Td>
</Tr>
))}
);});})()}
</Tbody>
</Table>
</Box>