mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 11:12:56 +00:00
dev day #89
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user