Files
MyClub/frontend/src/pages/TablesPage.tsx
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

248 lines
11 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import MainLayout from '../components/layout/MainLayout';
import { Box, Container, Heading, Text, Tabs, TabList, TabPanels, Tab, TabPanel, Table, Thead, Tbody, Tr, Th, Td, Flex, Spinner, Badge, Link, useColorModeValue } from '@chakra-ui/react';
import { getCompetitionAliasesPublic, CompetitionAlias } from '../services/competitionAliases';
import SponsorsSection from '../components/common/SponsorsSection';
import NewsletterCTA from '../components/common/NewsletterCTA';
import ClubModal from '../components/home/ClubModal';
import { usePublicSettings } from '../hooks/usePublicSettings';
import { sortCategoriesWithOrder } from '../utils/categorySort';
import { TeamLogo } from '../components/common/TeamLogo';
type TableRow = {
rank: string;
team: string;
team_id?: string;
team_logo_url?: string;
played: string;
wins: string;
draws: string;
losses: string;
score: string;
points: string;
};
type CompetitionTable = {
id: string;
name: string;
code?: string;
matches_link?: string;
rows: TableRow[];
};
const resolveBackendUrl = (path: string) => {
try {
if (/^https?:\/\//i.test(path)) return path;
if (path.startsWith('/cache') || path.startsWith('/uploads')) {
const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1';
const u = new URL(base);
u.pathname = path;
return u.toString();
}
return path;
} catch {
return path;
}
};
const TablesPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [competitions, setCompetitions] = useState<CompetitionTable[]>([]);
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 { data: settings } = usePublicSettings();
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
const textSecondary = useColorModeValue('gray.600', 'gray.400');
const tableBg = useColorModeValue('white', 'gray.800');
const tableHeaderBg = useColorModeValue('var(--primary, #C53030)', 'var(--primary, #9b2c2c)');
const tableTextColor = useColorModeValue('gray.800', 'gray.200');
const rowOddBg = useColorModeValue('white', 'gray.800');
const rowEvenBg = useColorModeValue('gray.50', 'gray.700');
const handleClubClick = (club: any) => {
setSelectedClub(club);
setIsModalOpen(true);
};
useEffect(() => {
let cancelled = false;
(async () => {
setLoading(true);
setError(null);
try {
// Load aliases first
let amap: Record<string, { alias: string; original_name?: string; display_order?: number }> = {};
try {
const aliases: CompetitionAlias[] = await getCompetitionAliasesPublic();
(aliases || []).forEach((a) => { if (a?.code && a?.alias) amap[a.code] = { alias: a.alias, original_name: a.original_name, display_order: a.display_order }; });
} catch {}
const res = await fetch(resolveBackendUrl('/cache/prefetch/facr_tables.json'), { cache: 'no-cache' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const comps: CompetitionTable[] = Array.isArray(json?.competitions)
? json.competitions.map((c: any, idx: number) => {
const overall = c?.table?.overall || [];
const rows: TableRow[] = Array.isArray(overall) ? overall.map((r: any) => ({
rank: String(r.rank ?? ''),
team: String(r.team ?? ''),
team_id: r.team_id,
team_logo_url: r.team_logo_url ? resolveBackendUrl(r.team_logo_url) : undefined,
played: String(r.played ?? ''),
wins: String(r.wins ?? ''),
draws: String(r.draws ?? ''),
losses: String(r.losses ?? ''),
score: String(r.score ?? ''),
points: String(r.points ?? ''),
})) : [];
return {
id: String(c.id || idx),
name: (amap?.[c?.code]?.alias) || (amap?.[String(c.id || idx)]?.alias) || c.name || c.code || `Soutěž ${idx+1}`,
alias: (amap?.[c?.code]?.alias) || (amap?.[String(c.id || idx)]?.alias),
display_order: (amap?.[c?.code]?.display_order) ?? (amap?.[String(c.id || idx)]?.display_order),
code: c.code,
matches_link: c.matches_link,
rows,
};
}) : [];
if (!cancelled) {
setAliasMap(amap);
// Only keep competitions that have at least one row in the table
const filtered = (comps || []).filter((c) => Array.isArray(c.rows) && c.rows.length > 0);
// Sort competitions by age (Muži first, then U19, U17, etc.) and respect custom order
const sorted = sortCategoriesWithOrder(filtered) as typeof filtered;
setCompetitions(sorted);
}
} catch (e: any) {
if (!cancelled) setError(e?.message || 'Nepodařilo se načíst tabulky.');
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => { cancelled = true; };
}, []);
return (
<MainLayout>
<Container maxW="7xl" py={{ base: 6, md: 10 }}>
<Heading as="h1" size="xl" mb={2}>Tabulky</Heading>
<Text color={textSecondary} mb={6}>Oficiální tabulky FACR podle soutěží.</Text>
{loading && (
<Flex align="center" gap={3} color={textSecondary} mb={6}>
<Spinner size="sm" />
<span>Načítám tabulky</span>
</Flex>
)}
{error && (
<Box color="red.600" mb={6}>{error}</Box>
)}
{!!competitions.length && (
<Tabs variant="enclosed">
<TabList>
{competitions.map((c) => (
<Tab
key={c.id}
_selected={{ bg: 'brand.primary', color: 'text.onPrimary', borderColor: 'brand.primary' }}
_hover={{ bg: 'rgba(0,0,0,0.04)' }}
>
{c.name}
</Tab>
))}
</TabList>
<TabPanels>
{competitions.map((c) => (
<TabPanel key={c.id} px={0}>
<Flex align="center" justify="space-between" mb={3}>
<Heading as="h2" size="md">{c.name}</Heading>
{c.matches_link && (
<Link href={c.matches_link} isExternal color="brand.primary" _hover={{ filter: 'brightness(0.9)' }}>Rozpis a detail soutěže</Link>
)}
</Flex>
<Box borderWidth="1px" borderColor={borderColor} borderRadius="lg" overflowX="auto" boxShadow="sm" bg={tableBg}>
<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>
</Tr>
</Thead>
<Tbody>
{c.rows.map((r, idx) => (
<Tr
key={`${c.id}-${r.rank}-${r.team}`}
transition="all 0.15s"
_hover={{ transform: 'translateY(-3px)', boxShadow: 'lg', zIndex: 3, cursor: 'pointer' }}
position="relative"
bg={idx % 2 === 0 ? rowOddBg : rowEvenBg}
onClick={() => handleClubClick(r)}
>
<Td>
<Badge variant="subtle" bg={useColorModeValue('gray.100', 'gray.700')} color={tableTextColor} borderWidth="1px" borderColor={borderColor}>{r.rank}</Badge>
</Td>
<Td>
<Flex align="center" gap={3}>
<TeamLogo
teamId={r.team_id}
teamName={r.team}
facrLogo={r.team_logo_url}
size="small"
alt={r.team}
borderRadius="full"
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
/>
<Text fontWeight="medium" color={tableTextColor}>{r.team}</Text>
</Flex>
</Td>
<Td isNumeric color={tableTextColor}>{r.played}</Td>
<Td isNumeric color={tableTextColor}>{r.wins}</Td>
<Td isNumeric color={tableTextColor}>{r.draws}</Td>
<Td isNumeric color={tableTextColor}>{r.losses}</Td>
<Td isNumeric color={tableTextColor}>{r.score}</Td>
<Td isNumeric>
<Badge variant="solid" bg="blue.600" color="white">{r.points}</Badge>
</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
</TabPanel>
))}
</TabPanels>
</Tabs>
)}
{!loading && !error && competitions.length === 0 && (
<Text color={textSecondary}>Pro tento klub nejsou dostupné tabulky.</Text>
)}
</Container>
{/* Newsletter CTA */}
<NewsletterCTA />
{/* Sponsors Section */}
<SponsorsSection />
<ClubModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
club={selectedClub}
clubType={(settings?.club_type as 'football' | 'futsal') || 'football'}
/>
</MainLayout>
);
};
export default TablesPage;