Files
MyClub/frontend/src/pages/MatchDetailPage.tsx
T
2025-10-24 18:15:36 +02:00

302 lines
13 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import MainLayout from '../components/layout/MainLayout';
import { Box, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Button, Container, Flex, Heading, Image, Link, Spinner, Stack, Text, useToast } from '@chakra-ui/react';
import { Link as RouterLink, useParams } from 'react-router-dom';
import { usePublicSettings } from '../hooks/usePublicSettings';
import { useQuery } from '@tanstack/react-query';
import { getCompetitionAliasesPublic } from '../services/competitionAliases';
import SponsorsSection from '../components/common/SponsorsSection';
import NewsletterCTA from '../components/common/NewsletterCTA';
import { API_URL } from '../services/api';
interface MatchItem {
id: string | number;
date: string;
time: string;
home: string;
away: string;
venue?: string;
home_logo_url?: string;
away_logo_url?: string;
report_url?: string;
facr_link?: string;
score?: string;
}
const resolveBackendUrl = (path: string) => {
try {
if (/^https?:\/\//i.test(path)) return path;
if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) {
const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin;
return new URL(path, origin).toString();
}
return path;
} catch {
return path;
}
};
const MatchDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const toast = useToast();
const { data: settings } = usePublicSettings();
const [loading, setLoading] = useState(true);
const [match, setMatch] = useState<MatchItem | null>(null);
const [competitionName, setCompetitionName] = useState<string>("");
const [competitionId, setCompetitionId] = useState<string>("");
const [competitionCode, setCompetitionCode] = useState<string>("");
const [prevMatch, setPrevMatch] = useState<MatchItem | null>(null);
const [nextMatch, setNextMatch] = useState<MatchItem | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
if (!id) return;
setLoading(true);
try {
// Try main prefetch file first
const res = await fetch(resolveBackendUrl('/cache/prefetch/facr_club_info.json'), { cache: 'no-cache' });
let found: MatchItem | null = null;
let neighborSource: MatchItem[] = [];
if (res.ok) {
const json = await res.json();
for (const c of (json?.competitions || [])) {
const listForNeighbors: MatchItem[] = [];
for (const m of (c?.matches || [])) {
const mid = String(m.match_id || '');
if (mid && mid === String(id)) {
const dt: string = String(m.date_time || '');
const [d, t] = dt.includes(' ') ? dt.split(' ') : [dt, ''];
const [day, month, year] = d.split('.');
const isoDate = (day && month && year) ? `${year}-${month.padStart(2,'0')}-${day.padStart(2,'0')}` : new Date().toISOString().slice(0,10);
const time = (t || '00:00').slice(0,5);
const score = (m.score || m.result || (typeof m.goals_home === 'number' && typeof m.goals_away === 'number' ? `${m.goals_home}:${m.goals_away}` : '') || '').toString();
found = {
id: mid,
date: isoDate,
time,
home: m.home,
away: m.away,
venue: m.venue,
home_logo_url: m.home_logo_url,
away_logo_url: m.away_logo_url,
report_url: m.report_url,
facr_link: m.facr_link,
score: score && /\d+\s*:\s*\d+/.test(score) ? score.replace(/\s+/g,'') : undefined,
};
setCompetitionName(String(c?.name || c?.code || ''));
if (c?.id != null) setCompetitionId(String(c.id));
if (c?.code) setCompetitionCode(String(c.code));
break;
}
// also push into neighbor list when iterating
const dt2: string = String(m.date_time || '');
const [d2, t2] = dt2.includes(' ') ? dt2.split(' ') : [dt2, ''];
const [day2, month2, year2] = d2.split('.');
const iso2 = (day2 && month2 && year2) ? `${year2}-${month2.padStart(2,'0')}-${day2.padStart(2,'0')}` : new Date().toISOString().slice(0,10);
const tshort = (t2 || '00:00').slice(0,5);
listForNeighbors.push({ id: m.match_id || '', date: iso2, time: tshort, home: m.home, away: m.away } as MatchItem);
}
if (found) break;
neighborSource = listForNeighbors;
}
}
// Fallback to club-specific info file
if (!found && settings?.club_id) {
const clubType = settings.club_type || 'football';
const infoPath = `/cache/facr/${clubType}_${settings.club_id}_info.json`;
const res2 = await fetch(resolveBackendUrl(infoPath), { cache: 'no-cache' });
if (res2.ok) {
const j2 = await res2.json();
for (const c of (j2?.competitions || [])) {
const listForNeighbors: MatchItem[] = [];
for (const m of (c?.matches || [])) {
const mid = String(m.match_id || '');
if (mid && mid === String(id)) {
const dt: string = String(m.date_time || '');
const [d, t] = dt.includes(' ') ? dt.split(' ') : [dt, ''];
const [day, month, year] = d.split('.');
const isoDate = (day && month && year) ? `${year}-${month.padStart(2,'0')}-${day.padStart(2,'0')}` : new Date().toISOString().slice(0,10);
const time = (t || '00:00').slice(0,5);
const score = (m.score || m.result || (typeof m.goals_home === 'number' && typeof m.goals_away === 'number' ? `${m.goals_home}:${m.goals_away}` : '') || '').toString();
found = {
id: mid,
date: isoDate,
time,
home: m.home,
away: m.away,
venue: m.venue,
home_logo_url: m.home_logo_url,
away_logo_url: m.away_logo_url,
report_url: m.report_url,
facr_link: m.facr_link,
score: score && /\d+\s*:\s*\d+/.test(score) ? score.replace(/\s+/g,'') : undefined,
};
setCompetitionName(String(c?.name || c?.code || ''));
if (c?.id != null) setCompetitionId(String(c.id));
if (c?.code) setCompetitionCode(String(c.code));
break;
}
// collect neighbor list
const dt2: string = String(m.date_time || '');
const [d2, t2] = dt2.includes(' ') ? dt2.split(' ') : [dt2, ''];
const [day2, month2, year2] = d2.split('.');
const iso2 = (day2 && month2 && year2) ? `${year2}-${month2.padStart(2,'0')}-${day2.padStart(2,'0')}` : new Date().toISOString().slice(0,10);
const tshort = (t2 || '00:00').slice(0,5);
listForNeighbors.push({ id: m.match_id || '', date: iso2, time: tshort, home: m.home, away: m.away } as MatchItem);
}
if (found) break;
neighborSource = listForNeighbors;
}
}
}
// compute neighbors
if (found && neighborSource.length) {
const sorted = neighborSource.sort((a, b) => new Date(`${a.date}T${a.time}:00`).getTime() - new Date(`${b.date}T${b.time}:00`).getTime());
const idx = sorted.findIndex(x => String(x.id) === String(found!.id));
setPrevMatch(idx > 0 ? sorted[idx - 1] : null);
setNextMatch(idx >= 0 && idx < sorted.length - 1 ? sorted[idx + 1] : null);
} else {
setPrevMatch(null); setNextMatch(null);
}
if (!cancelled) setMatch(found);
} catch (e: any) {
if (!cancelled) toast({ title: 'Chyba', description: e?.message || 'Nelze načíst zápas', status: 'error' });
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => { cancelled = true; };
}, [id, settings, toast]);
const isPast = useMemo(() => {
if (!match) return false;
return new Date(`${match.date}T${(match.time||'00:00')}:00`).getTime() < Date.now();
}, [match]);
// Load aliases and resolve display name for competition
const { data: aliases } = useQuery({
queryKey: ['public-competition-aliases'],
queryFn: getCompetitionAliasesPublic,
});
const compDisplay = useMemo(() => {
if (!competitionCode) return competitionName;
const hit = (aliases || []).find(a => a.code === competitionCode);
return hit?.alias || competitionName;
}, [aliases, competitionCode, competitionName]);
return (
<MainLayout>
<Container maxW="3xl" py={{ base: 6, md: 10 }}>
<Breadcrumb fontSize="sm" mb={4} separator="/">
<BreadcrumbItem>
<BreadcrumbLink as={RouterLink} to="/">Domů</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink as={RouterLink} to={competitionId ? `/kalendar?comp=${competitionId}` : "/kalendar"}>Kalendář</BreadcrumbLink>
</BreadcrumbItem>
{compDisplay && (
<BreadcrumbItem>
<BreadcrumbLink as={RouterLink} to={competitionId ? `/kalendar?comp=${competitionId}` : "/kalendar"}>{compDisplay}</BreadcrumbLink>
</BreadcrumbItem>
)}
{match && (
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink>{match.home} vs {match.away}</BreadcrumbLink>
</BreadcrumbItem>
)}
</Breadcrumb>
<Box mb={4}>
<Button as={RouterLink} to={competitionId ? `/kalendar?comp=${competitionId}` : "/kalendar"} size="sm" variant="outline">Zpět na kalendář</Button>
</Box>
{loading && (
<Flex align="center" gap={3} color="gray.600"><Spinner size="sm" /> Načítám</Flex>
)}
{!loading && !match && (
<Box color="gray.600">Zápas nebyl nalezen.</Box>
)}
{match && (
<Stack spacing={5}>
<Heading size="lg">{match.home} vs {match.away}</Heading>
<Flex align="center" justify="center" gap={4}>
{match.home_logo_url && (
<Image src={match.home_logo_url} alt={match.home} boxSize="56px" borderRadius="full" />
)}
<Text fontSize="2xl" fontWeight="bold">{isPast && match.score ? match.score : 'vs'}</Text>
{match.away_logo_url && (
<Image src={match.away_logo_url} alt={match.away} boxSize="56px" borderRadius="full" />
)}
</Flex>
<Text textAlign="center" color="gray.700">{match.date} {match.time} {match.venue ? `${match.venue}` : ''}</Text>
{(prevMatch || nextMatch) && (
<Flex justify="space-between" mt={2}>
<Box>
{prevMatch && (
<Button as={RouterLink} to={`/zapas/${prevMatch.id}`} size="sm" variant="ghost"> {prevMatch.home} vs {prevMatch.away}</Button>
)}
</Box>
<Box>
{nextMatch && (
<Button as={RouterLink} to={`/zapas/${nextMatch.id}`} size="sm" variant="ghost">{nextMatch.home} vs {nextMatch.away} </Button>
)}
</Box>
</Flex>
)}
{(match.facr_link || match.report_url) && (
<Flex justify="center" gap={3}>
{match.facr_link && (
<Button
colorScheme="red"
variant="outline"
onClick={(e) => {
e.preventDefault();
// Open in background tab without switching focus
const link = document.createElement('a');
link.href = match.facr_link || '';
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
>
FAČR detail
</Button>
)}
{match.report_url && (
<Button
variant="outline"
onClick={(e) => {
e.preventDefault();
// Open in background tab without switching focus
const link = document.createElement('a');
link.href = match.report_url || '';
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
>
Zápis
</Button>
)}
</Flex>
)}
</Stack>
)}
</Container>
{/* Newsletter CTA */}
<NewsletterCTA />
{/* Sponsors Section */}
<SponsorsSection />
</MainLayout>
);
};
export default MatchDetailPage;