mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
248 lines
11 KiB
TypeScript
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;
|