mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user