import React, { useEffect, useMemo, useState } from 'react'; import MainLayout from '../components/layout/MainLayout'; import { Box, Container, Heading, Text, Tabs, TabList, TabPanels, Tab, TabPanel, Flex, Badge, Stack, Spinner, Grid, IconButton, ButtonGroup, Button, Link, Image, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, useToast, Input, useColorModeValue } from '@chakra-ui/react'; import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; import { addMonths, format, isSameDay, isSameMonth, startOfMonth, startOfWeek } from 'date-fns'; import { cs } from 'date-fns/locale'; import { getCompetitionAliasesPublic, CompetitionAlias } from '../services/competitionAliases'; import { useSearchParams } from 'react-router-dom'; import { useCountdown, useMultipleCountdowns } from '../hooks/useCountdown'; import SponsorsSection from '../components/common/SponsorsSection'; import NewsletterCTA from '../components/common/NewsletterCTA'; import { sortCategoriesWithOrder } from '../utils/categorySort'; import ClubModal from '../components/home/ClubModal'; import { assetUrl } from '../utils/url'; import { API_URL } from '../services/api'; // Weekday headers (Czech, starting Monday) const WEEKDAYS_SHORT: string[] = ['Po', 'Út', 'St', 'Čt', 'Pá', 'So', 'Ne']; type MatchItem = { id: number | string; date: string; // yyyy-mm-dd time: string; // HH:MM home: string; away: string; home_id?: string; away_id?: string; venue?: string; home_logo_url?: string; away_logo_url?: string; report_url?: string; facr_link?: string; score?: string; // optional: e.g., "2:1" for finished matches // enriched when building the "Všechny soutěže" tab __compId?: string; __compName?: string; __compDisplayOrder?: number; }; type Competition = { id: string; name: string; code?: string; matches: MatchItem[]; }; const CalendarPage: React.FC = () => { const [loading, setLoading] = useState(true); const [competitions, setCompetitions] = useState([]); const [error, setError] = useState(null); const [compLinks, setCompLinks] = useState>({}); const [aliasMap, setAliasMap] = useState>({}); const [tabIndex, setTabIndex] = useState(0); const [searchParams] = useSearchParams(); const toast = useToast(); const [clubName, setClubName] = useState(''); const [clubId, setClubId] = useState(''); const [clubType, setClubType] = useState<'football' | 'futsal'>('football'); const [standings, setStandings] = useState([]); // Color mode values for dark/light theme const calendarDayBg = useColorModeValue('white', 'gray.800'); const calendarDayBorder = useColorModeValue('gray.200', 'gray.700'); const calendarMatchBg = useColorModeValue('gray.50', 'gray.700'); const calendarMatchHoverBg = useColorModeValue('blue.50', 'blue.900'); const listGroupBg = useColorModeValue('white', 'gray.800'); const listGroupBorder = useColorModeValue('gray.200', 'gray.700'); const listGroupHeaderBg = useColorModeValue('gray.50', 'gray.700'); const listGroupHeaderBgHighlight = useColorModeValue('blue.50', 'blue.900'); const listGroupHeaderBorderLeft = useColorModeValue('gray.300', 'gray.600'); const listGroupHeaderText = useColorModeValue('gray.800', 'gray.100'); const listMatchBg = useColorModeValue('gray.50', 'gray.700'); const listMatchBorder = useColorModeValue('gray.200', 'gray.600'); const listMatchHoverBg = useColorModeValue('blue.50', 'blue.900'); const listDateText = useColorModeValue('gray.800', 'gray.100'); const listVenueText = useColorModeValue('gray.600', 'gray.400'); const listTimeText = useColorModeValue('gray.700', 'gray.300'); // Modal state const { isOpen, onOpen, onClose } = useDisclosure(); const [selected, setSelected] = useState<{ match: MatchItem; comp?: Competition } | null>(null); // Removed admin notification form state (moved to admin page) // Fan subscribe state const [fanEmail, setFanEmail] = useState(""); const [fanSubscribing, setFanSubscribing] = useState(false); // ClubModal state const [isClubModalOpen, setIsClubModalOpen] = useState(false); const [selectedClub, setSelectedClub] = useState(null); 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; } }; // Helper functions shared across effects and render helpers const normalize = (s: string) => String(s || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); const stripPrefixes = (s: string) => { let x = normalize(s); x = x.replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g, ''); return x.replace(/\s+/g, ' ').trim(); }; const subscribeFan = async () => { if (!fanEmail) { toast({ title: 'Zadejte email', status: 'warning' }); return; } setFanSubscribing(true); try { const res = await fetch(resolveBackendUrl('/api/v1/newsletter/subscribe'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: fanEmail }) }); if (!res.ok) { const jt = await res.json().catch(() => ({} as any)); throw new Error(jt?.error || `HTTP ${res.status}`); } toast({ title: 'Přihlášeno k odběru', status: 'success' }); setFanEmail(''); } catch (e: any) { toast({ title: 'Chyba přihlášení', description: e?.message || String(e), status: 'error' }); } finally { setFanSubscribing(false); } }; // Handle team click to show team stats in ClubModal const handleTeamClick = (teamName: string, teamLogoUrl?: string) => { try { const normalizedTeamName = normalize(teamName); // Search through all standings to find the team let foundTeam: any = null; for (const standing of standings) { const table = standing.table || standing.rows || []; for (const row of table) { const rowTeamName = normalize(row.team_name || row.team || ''); if (rowTeamName === normalizedTeamName || rowTeamName.includes(normalizedTeamName) || normalizedTeamName.includes(rowTeamName)) { foundTeam = row; break; } } if (foundTeam) break; } // If team found in standings, show detailed modal if (foundTeam) { const clubData = { team: foundTeam.team_name || foundTeam.team || teamName, team_id: foundTeam.team_id || String(foundTeam.position || 0), team_logo_url: foundTeam.logo_url ? (assetUrl(foundTeam.logo_url) || foundTeam.logo_url) : teamLogoUrl, rank: foundTeam.position || foundTeam.rank || foundTeam.pos, played: foundTeam.played || foundTeam.matches, wins: foundTeam.wins || foundTeam.win, draws: foundTeam.draws || foundTeam.draw, losses: foundTeam.losses || foundTeam.loss, score: foundTeam.score || ((foundTeam.goals_for || foundTeam.gf) && (foundTeam.goals_against || foundTeam.ga) ? `${foundTeam.goals_for || foundTeam.gf}:${foundTeam.goals_against || foundTeam.ga}` : undefined), points: foundTeam.points || foundTeam.pts, }; setSelectedClub(clubData); setIsClubModalOpen(true); } else { // If not found in standings, show minimal modal with just the name and logo const minimalClubData = { team: teamName, team_id: teamName, team_logo_url: teamLogoUrl, rank: '-', played: '-', wins: '-', draws: '-', losses: '-', score: '-', points: '-', }; setSelectedClub(minimalClubData); setIsClubModalOpen(true); } } catch { // noop } }; useEffect(() => { let cancelled = false; (async () => { setLoading(true); setError(null); try { // Load public competition aliases for display let amap: Record = {}; 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 {} // Load team-logo overrides (API + fallback to cached file) const now = Date.now(); let overrides: any = null; try { const res = await fetch(resolveBackendUrl(`/api/v1/public/team-logo-overrides?t=${now}`), { cache: 'no-cache' }); if (res.ok) overrides = await res.json(); } catch {} if (!overrides) { try { const res2 = await fetch(resolveBackendUrl('/cache/prefetch/team_logo_overrides.json'), { cache: 'no-cache' }); if (res2.ok) overrides = await res2.json(); } catch {} } const byName: Record = (overrides?.by_name || {}) as any; const byNameNormalized: Record = Object.keys(byName || {}).reduce((acc: Record, k: string) => { acc[normalize(k)] = byName[k]; return acc; }, {}); const byNameStrippedPairs: Array<{ keyNorm: string; url: string }> = Object.keys(byName || {}).map((k: string) => ({ keyNorm: stripPrefixes(k), url: byName[k] })); const getOverrideLogo = (teamName?: string, original?: string) => { if (!teamName) return original; const exact = (byName || {})[teamName]; const normName = normalize(teamName); let candidate = exact || byNameNormalized[normName]; if (!candidate) { const stripped = stripPrefixes(teamName); for (const { keyNorm, url } of byNameStrippedPairs) { if (!keyNorm) continue; if (stripped.endsWith(keyNorm) || keyNorm.endsWith(stripped)) { candidate = url; break; } } } const chosen = candidate || original; if (typeof chosen === 'string' && chosen.startsWith('/')) return resolveBackendUrl(chosen); return chosen; }; const res = await fetch(resolveBackendUrl('/cache/prefetch/facr_club_info.json'), { cache: 'no-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); const compLinkMap: Record = {}; const baseComps: Competition[] = Array.isArray(json?.competitions) ? json.competitions.map((c: any, cIdx: number) => { const compId = String(c.id || cIdx); if (c.matches_link) compLinkMap[compId] = String(c.matches_link); const matches: MatchItem[] = (Array.isArray(c.matches) ? c.matches : []).map((m: any, idx: number) => { 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(); return { id: m.match_id || `${cIdx}-${idx}`, date: isoDate, time, home: m.home, away: m.away, home_id: m.home_id, away_id: m.away_id, venue: m.venue, home_logo_url: getOverrideLogo(m.home, m.home_logo_url), away_logo_url: getOverrideLogo(m.away, 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, }; }) .sort((a: MatchItem, b: MatchItem) => new Date(`${a.date}T${a.time}:00`).getTime() - new Date(`${b.date}T${b.time}:00`).getTime()); return { id: compId, code: c.code, name: (amap?.[c?.code]?.alias) || (amap?.[compId]?.alias) || c.name || c.code || `Soutěž ${cIdx+1}`, matches, }; }) : []; // Try to enrich competitions missing matches using facr detailed info file let enriched: Competition[] = baseComps; try { const clubId = json?.club_id; const clubType = json?.club_type || 'football'; if (clubId) { const infoPath = `/cache/facr/${clubType}_${clubId}_info.json`; const res2 = await fetch(resolveBackendUrl(infoPath), { cache: 'no-cache' }); if (res2.ok) { const j2 = await res2.json(); if (Array.isArray(j2?.competitions)) { const mapCompMatches = (c: any, cIdx: number) => { const list: MatchItem[] = (Array.isArray(c.matches) ? c.matches : []).map((m: any, idx: number) => { 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(); return { id: m.match_id || `${cIdx}-${idx}`, date: isoDate, time, home: m.home, away: m.away, home_id: m.home_id, away_id: m.away_id, venue: m.venue, home_logo_url: getOverrideLogo(m.home, m.home_logo_url), away_logo_url: getOverrideLogo(m.away, m.away_logo_url), report_url: m.report_url, score: score && /\d+\s*:\s*\d+/.test(score) ? score.replace(/\s+/g,'') : undefined, } as MatchItem; }).sort((a: MatchItem, b: MatchItem) => new Date(`${a.date}T${a.time}:00`).getTime() - new Date(`${b.date}T${b.time}:00`).getTime()); return list; }; enriched = baseComps.map((bc) => { if (bc.matches.length) return bc; const found = j2.competitions.find((c: any) => String(c.id) === String(bc.id) || c.name === bc.name || c.code === bc.name || c.code === bc.id); if (!found) return bc; const mm = mapCompMatches(found, 0); return { ...bc, matches: mm }; }); } } } } catch {} // Sort competitions by age (Muži first, then U19, U17, etc.) and respect custom order const sortedEnriched = sortCategoriesWithOrder( enriched.map(c => ({ ...c, alias: (amap?.[c?.code || '']?.alias) || (amap?.[c?.id]?.alias), display_order: (amap?.[c?.code || '']?.display_order) ?? (amap?.[c?.id]?.display_order), })) ); // Build unified "All competitions" entry as the default tab const allMatches: MatchItem[] = (sortedEnriched || []) .flatMap((c, idx) => (c.matches || []).map(m => ({ ...m, __compId: c.id, __compName: c.name, __compDisplayOrder: c.display_order ?? (1000 + idx) // Use display_order or fallback to sorted index } as MatchItem))) .sort((a, b) => new Date(`${a.date}T${a.time}:00`).getTime() - new Date(`${b.date}T${b.time}:00`).getTime()); const allComp: Competition = { id: 'all', name: 'Všechny soutěže', matches: allMatches }; // Load standings data let standingsData: any[] = []; try { const tablesRes = await fetch(resolveBackendUrl('/cache/prefetch/facr_tables.json'), { cache: 'no-cache' }); if (tablesRes.ok) { const facrTablesJSON = await tablesRes.json(); if (facrTablesJSON?.competitions?.length) { standingsData = (facrTablesJSON.competitions || []).map((c: any) => ({ name: (amap?.[c?.code]?.alias) || (amap?.[c?.id]?.alias) || c.name || c.code, table: (c.table?.overall || []).map((r: any, idx: number) => { const teamName = ((): string => { const t = r.team; if (typeof t === 'string') return t; if (t && typeof t === 'object') { return t.name || t.Name || t.team_name || t.TeamName || '-'; } return r.team_name || r.TeamName || '-'; })(); const teamId = ((): string | undefined => { const t = r.team; if (t && typeof t === 'object') { return t.id || t.team_id || t.teamId || r.team_id; } return r.team_id; })(); return { position: Number(r.rank || idx + 1), team_name: teamName, team_id: teamId, points: Number(r.points || r.pts || 0), played: Number(r.played || r.matches || 0), wins: Number(r.wins || r.win || 0), draws: Number(r.draws || r.draw || 0), losses: Number(r.losses || r.loss || 0), goals_for: Number(r.goals_for ?? r.gf ?? r.goalsFor ?? r.scored ?? r.goals ?? 0), goals_against: Number(r.goals_against ?? r.ga ?? r.goalsAgainst ?? r.conceded ?? 0), logo_url: r.team_logo_url || undefined, }; }), })); } } } catch {} if (!cancelled) { setAliasMap(amap); const comps = [allComp, ...sortedEnriched]; setCompetitions(comps); setCompLinks(compLinkMap); setStandings(standingsData); if (json?.name) setClubName(String(json.name)); if (json?.club_internal_id) setClubId(String(json.club_internal_id)); if (json?.club_type) setClubType(json.club_type); // Set active tab from query ?comp= const compQ = searchParams.get('comp'); if (compQ) { const idx = comps.findIndex(cmp => String(cmp.id) === String(compQ)); setTabIndex(idx >= 0 ? idx : 0); } else { setTabIndex(0); } } } catch (e: any) { if (!cancelled) setError(e?.message || 'Nepodařilo se načíst kalendář.'); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, []); // Optimized countdown hooks const modalCountdown = useCountdown( selected ? `${selected.match.date}T${selected.match.time || '00:00'}:00` : null, 1000 // Update every second for modal ); // Get upcoming matches for live countdowns (only future matches) const upcomingMatches = useMemo(() => { return competitions.flatMap(comp => comp.matches.filter(match => { const matchTime = new Date(`${match.date}T${match.time || '00:00'}:00`).getTime(); return matchTime > Date.now(); }) ); }, [competitions]); const liveCountdowns = useMultipleCountdowns(upcomingMatches, 30000); // Update every 30 seconds for better performance const openMatchModal = (match: MatchItem, comp?: Competition) => { setSelected({ match, comp }); onOpen(); }; // Handle middle click to open in new tab const handleMatchMouseDown: React.MouseEventHandler = (e) => { // Middle click or Ctrl/Cmd+Click should open link in new tab naturally when using . // This handler is a safety net when match is rendered as a div. if (e.button === 1) { const target = e.currentTarget as HTMLElement & { dataset?: any }; const href = (target.getAttribute && target.getAttribute('data-href')) || (target as any).dataset?.href; if (href) { window.open(href as string, '_blank', 'noopener'); } } }; const hasData = useMemo(() => competitions.some(c => c.matches.length), [competitions]); // Month-view state and helpers const [monthRef, setMonthRef] = useState(startOfMonth(new Date())); const [viewMode, setViewMode] = useState<'calendar'|'list'>('calendar'); const [expandedDates, setExpandedDates] = useState>({}); const [showPast, setShowPast] = useState(false); const weeks = useMemo(() => { const start = startOfWeek(startOfMonth(monthRef), { weekStartsOn: 1 }); // Build 6 weeks x 7 days const days: Date[] = []; for (let i = 0; i < 42; i++) { days.push(new Date(start.getTime() + i * 86400000)); } return days; }, [monthRef]); const groupByDate = (matches: MatchItem[]) => { const map = new Map(); matches.forEach((m) => { const key = m.date; const arr = map.get(key) || []; arr.push(m); map.set(key, arr); }); // Sort matches within each day by competition order, then by time map.forEach((matchList, dateKey) => { matchList.sort((a, b) => { // First sort by competition display order (if available) const aOrder = a.__compDisplayOrder ?? 9999; const bOrder = b.__compDisplayOrder ?? 9999; if (aOrder !== bOrder) { return aOrder - bOrder; } // Then sort by time const aTime = a.time || '00:00'; const bTime = b.time || '00:00'; return aTime.localeCompare(bTime); }); }); return map; }; // Sentiment helpers const isClubTeam = (team: string) => { try { const a = stripPrefixes(team); const b = stripPrefixes(clubName || ''); if (!a || !b) return false; // Allow equality or suffix match (handles prefixes like TJ, SK, etc.) return a === b || a.endsWith(b) || b.endsWith(a); } catch { return false; } }; const parseScore = (score?: string): { h: number; a: number } | null => { if (!score) return null; const m = score.match(/^(\d+)\s*[:\-]\s*(\d+)$/); if (!m) return null; return { h: parseInt(m[1], 10), a: parseInt(m[2], 10) }; }; const getSentiment = (m: MatchItem): { label: 'Výhra'|'Remíza'|'Prohra'; color: string } | null => { // Don't show sentiment for future matches const dt = new Date(`${m.date}T${(m.time || '00:00')}:00`); const isPast = Date.now() >= dt.getTime(); if (!isPast) return null; const s = parseScore(m.score); if (!s) return null; // First try ID-based matching (most reliable) let ourIsHome = false; let ourIsAway = false; if (clubId) { // Check each team ID individually - even if one is missing, we can still match the other if (m.home_id) { ourIsHome = m.home_id === clubId; } if (m.away_id) { ourIsAway = m.away_id === clubId; } } // Fallback to name matching if IDs not available or no match if (!ourIsHome && !ourIsAway) { ourIsHome = isClubTeam(m.home); ourIsAway = isClubTeam(m.away); } if (!ourIsHome && !ourIsAway) return null; // unknown perspective if (s.h === s.a) return { label: 'Remíza', color: 'blue' }; const ourGoals = ourIsHome ? s.h : s.a; const oppGoals = ourIsHome ? s.a : s.h; if (ourGoals > oppGoals) return { label: 'Výhra', color: 'green' }; return { label: 'Prohra', color: 'red' }; }; return ( Kalendář Přehled zápasů podle soutěží (FACR). {loading && ( Načítám rozpis… )} {error && ( {error} )} {!loading && !hasData && !error && ( Zatím nemáme žádné zápasy k zobrazení. )} {!!competitions.length && ( setTabIndex(i)}> {/* Compact, wrapped TabList with better spacing (no overlap) */} button': { minW: 'auto', px: { base: 3, md: 4 }, py: { base: 1.5, md: 2 }, fontSize: { base: 'xs', md: 'sm' }, fontWeight: 600, borderRadius: 'md', transition: 'all 0.2s', _selected: { bg: 'brand.primary', color: 'white', transform: 'translateY(-2px)', boxShadow: 'md' }, _hover: { transform: 'translateY(-1px)', boxShadow: 'sm' } } }} > {competitions.map((c) => ( {c.name} ))} {competitions.map((c) => { const byDate = groupByDate(c.matches); const mkHref = (m: MatchItem) => (m.facr_link || m.report_url || undefined) ?? (`/zapas/${m.id}`); // Build latest results (only matches with score) const nowTs = Date.now(); const compareByDateDesc = (a: MatchItem, b: MatchItem) => new Date(`${b.date}T${(b.time||'00:00')}:00`).getTime() - new Date(`${a.date}T${(a.time||'00:00')}:00`).getTime(); let latestResults: MatchItem[] = []; if (c.id === 'all') { // For 'all', pick most recent scored match per competition const grouped: Record = {}; (c.matches || []).forEach((m) => { if (!m.score) return; const ts = new Date(`${m.date}T${(m.time||'00:00')}:00`).getTime(); if (isNaN(ts) || ts > nowTs) return; // future results not allowed const key = m.__compId || 'na'; grouped[key] = grouped[key] || []; grouped[key].push(m); }); latestResults = Object.values(grouped) .map(list => list.sort(compareByDateDesc)[0]) .filter(Boolean) .sort(compareByDateDesc); } else { // Single competition: pick the most recent scored match latestResults = (c.matches || []) .filter(m => !!m.score && new Date(`${m.date}T${(m.time||'00:00')}:00`).getTime() <= nowTs) .sort(compareByDateDesc) .slice(0, 1); } return ( {/* Latest results header list rendered above both calendar and list modes */} {latestResults.length > 0 && ( Nejnovější výsledky {latestResults.map((m) => { const href = mkHref(m); return ( openMatchModal(m, c)} borderWidth="1px" borderRadius="md" p={2} _hover={{ textDecoration: 'none', bg: 'rgba(0,0,0,0.03)', borderColor: 'brand.primary', cursor: 'pointer' }}> {m.date} {m.time || ''} {m.__compName || c.name} {m.home_logo_url && {m.home}} {m.home} {m.score || 'vs'} {m.away_logo_url && {m.away}} {m.away} {href && e.stopPropagation()} display="none"/>} ); })} )} {viewMode === 'calendar' ? ( <> setMonthRef(addMonths(monthRef, -1))} icon={} variant="outline" _hover={{ bg: 'rgba(0,0,0,0.04)', borderColor: 'brand.primary' }} /> setMonthRef(addMonths(monthRef, 1))} icon={} variant="outline" _hover={{ bg: 'rgba(0,0,0,0.04)', borderColor: 'brand.primary' }} /> {WEEKDAYS_SHORT.map((w) => ( {w} ))} {weeks.map((day, idx) => { const key = format(day, 'yyyy-MM-dd'); const list = byDate.get(key) || []; const faded = !isSameMonth(day, monthRef); const today = isSameDay(day, new Date()); return ( {format(day, 'd')} {!!list.length && ( {list.length} )} {(expandedDates[key] ? list : list.slice(0,3)).map((m: MatchItem) => { const href = mkHref(m); const isPast = new Date(`${m.date}T${(m.time||'00:00')}:00`).getTime() < Date.now(); const countdown = liveCountdowns[String(m.id)]; return ( openMatchModal(m, c)}> {!isPast && countdown ? ( <> {m.home_logo_url && {m.home}} za {countdown} {m.away_logo_url && {m.away}} {m.time || '—'} ) : ( <> {m.home_logo_url && {m.home}} {isPast && m.score ? m.score : 'vs'} {m.away_logo_url && {m.away}} {m.time || '—'} )} {href && ( e.stopPropagation()} display="none"/> )} ); })} {list.length > 3 && !expandedDates[key] && ( )} {expandedDates[key] && list.length > 3 && ( )} ); })} ) : ( {(() => { const keys = Array.from(byDate.keys()); const todayStr = format(new Date(), 'yyyy-MM-dd'); const pastKeys = keys.filter(k => k < todayStr).sort().reverse(); const futureKeys = keys.filter(k => k >= todayStr).sort(); const renderGroup = (dKey: string, highlight: boolean) => { const dayMatches = byDate.get(dKey) || []; return ( {format(new Date(dKey), 'EEEE d. M. yyyy', { locale: cs })} {highlight && ( Dnes )} {dayMatches.map((m: MatchItem) => { const href = mkHref(m); const isPast = new Date(`${m.date}T${(m.time||'00:00')}:00`).getTime() < Date.now(); const sentiment = isPast ? getSentiment(m) : null; const countdown = liveCountdowns[String(m.id)]; return ( openMatchModal(m, c)}> {m.date} {m.time || '—'} {m.venue && {m.venue}} {/* Home Team */} {m.home} {m.home_logo_url && ( {m.home} )} {/* Score or Countdown */} {!isPast && countdown ? ( za {countdown} ) : ( {isPast && m.score ? m.score : 'vs'} )} {sentiment && ( {sentiment.label} )} {/* Away Team */} {m.away_logo_url && ( {m.away} )} {m.away} {href && ( e.stopPropagation()} display="none"/> )} ); })} ); }; return ( <> {!!pastKeys.length && ( {!showPast ? ( ) : ( )} {showPast && ( {pastKeys.map((k) => renderGroup(k, false))} )} )} {futureKeys.map((k) => renderGroup(k, k === todayStr))} ); })()} )} ); })} )} {/* Match details modal */} {selected ? (selected.match.__compName || selected.comp?.name || 'Detail zápasu') : 'Detail zápasu'} {selected && ( {/* Show match status (Výhra/Remíza/Prohra) if determinable; otherwise fall back to competition name */} {(() => { const s = getSentiment(selected.match); if (s) { return ( {s.label} ); } const compName = selected.comp?.name || selected.match.__compName; // Don't show "Všechny soutěže" badge - only show specific competition names if (compName && compName !== 'Všechny soutěže') { return ( {compName} ); } return null; })()} {selected.match.home_logo_url && ( {selected.match.home} handleTeamClick(selected.match.home, selected.match.home_logo_url)} _hover={{ opacity: 0.8, transform: 'scale(1.1)' }} transition="all 0.2s" title={`Klikněte pro zobrazení statistik: ${selected.match.home}`} /> )} {(() => { const dt = new Date(`${selected.match.date}T${(selected.match.time || '00:00')}:00`); const isPast = Date.now() >= dt.getTime(); const hasScore = Boolean(selected.match.score); // For future matches, always show countdown or "vs" - never the score if (!isPast) { if (modalCountdown.countdownString) { return ( za {modalCountdown.countdownString} ); } return ( vs ); } // For past matches, show score or "vs" return ( {hasScore ? selected.match.score : 'vs'} ); })()} {selected.match.away_logo_url && ( {selected.match.away} handleTeamClick(selected.match.away, selected.match.away_logo_url)} _hover={{ opacity: 0.8, transform: 'scale(1.1)' }} transition="all 0.2s" title={`Klikněte pro zobrazení statistik: ${selected.match.away}`} /> )} {/* Date and Time Display with Countdown */} {(() => { try { return format(new Date(selected.match.date), 'EEEE d. MMMM yyyy', { locale: cs }); } catch { return selected.match.date; } })()} {selected.match.time || '—'} {/* Enhanced Countdown Display for Upcoming Matches */} {(() => { const dt = new Date(`${selected.match.date}T${(selected.match.time || '00:00')}:00`); const isPast = Date.now() >= dt.getTime(); const hasScore = Boolean(selected.match.score); if (!hasScore && !isPast && modalCountdown.isActive && modalCountdown.timeRemaining > 0) { const days = Math.floor(modalCountdown.timeRemaining / (24 * 60 * 60 * 1000)); const hours = Math.floor((modalCountdown.timeRemaining % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); const minutes = Math.floor((modalCountdown.timeRemaining % (60 * 60 * 1000)) / (60 * 1000)); const seconds = Math.floor((modalCountdown.timeRemaining % (60 * 1000)) / 1000); return ( Zápas začíná za 0 ? "repeat(4, 1fr)" : "repeat(3, 1fr)"} gap={3} > {days > 0 && ( {days} {days === 1 ? 'den' : days < 5 ? 'dny' : 'dní'} )} {String(hours).padStart(2, '0')} {hours === 1 ? 'hodina' : hours < 5 ? 'hodiny' : 'hodin'} {String(minutes).padStart(2, '0')} {minutes === 1 ? 'minuta' : minutes < 5 ? 'minuty' : 'minut'} {String(seconds).padStart(2, '0')} {seconds === 1 ? 'sekunda' : seconds < 5 ? 'sekundy' : 'sekund'} ); } return null; })()} Odběr notifikací pro fanoušky Zadejte svůj email a budeme vás informovat o novinkách a zápasech. setFanEmail(e.target.value)} /> )} {selected && (selected.match.facr_link || selected.match.report_url) && ( )} {/* Newsletter CTA */} {/* Sponsors Section */} {/* Club Modal for team statistics */} setIsClubModalOpen(false)} club={selectedClub} clubType={clubType} /> ); }; export default CalendarPage;