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,302 @@
|
||||
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';
|
||||
|
||||
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 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; // use backend origin root
|
||||
return u.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;
|
||||
Reference in New Issue
Block a user