import React, { useEffect, useRef, useState } from 'react'; import MainLayout from '../components/layout/MainLayout'; import { FiArrowRight, FiCalendar, FiUsers, FiAward, FiChevronLeft, FiChevronRight } from 'react-icons/fi'; import '../styles/theme.css'; import './styles/UnifiedHome.css'; import { getPublicSettings } from '../services/settings'; import { assetUrl, sanitizeClubName } from '../utils/url'; import { getPlayers as apiGetPlayers, Player as ApiPlayer } from '../services/players'; import { getSponsors as apiGetSponsors, Sponsor as ApiSponsor } from '../services/sponsors'; import BlogCardsScroller from '../components/home/BlogCardsScroller'; import BlogSwiper from '../components/home/BlogSwiper'; import VideosSection from '../components/home/VideosSection'; import MerchSection from '../components/home/MerchSection'; import GallerySection from '../components/home/GallerySection'; import { getArticles as apiGetArticles, Article as ApiArticle } from '../services/articles'; import { getCompetitionAliasesPublic, CompetitionAlias } from '../services/competitionAliases'; import NewsletterSubscribe from '../components/newsletter/NewsletterSubscribe'; import MyUIbrixStyleEditor from '../components/editor/MyUIbrixEditor'; import ClubModal from '../components/home/ClubModal'; import MatchModal from '../components/home/MatchModal'; import { useAllPageElementConfigs } from '../hooks/usePageElementConfig'; // Types for real API-driven data type NewsItem = { id: number | string; title: string; excerpt?: string; image?: string; date?: string; category?: string; slug?: string; }; type MatchItem = { id: number | string; homeTeam: string; awayTeam: string; competition?: string; date: string; // yyyy-mm-dd time: string; // HH:MM venue?: string; isHome?: boolean; homeLogoURL?: string; awayLogoURL?: string; }; const HomePage: React.FC = () => { // Local state now starts empty; filled by FACR/cache/live APIs const [news, setNews] = useState([]); const [matches, setMatches] = useState([]); const [countdown, setCountdown] = useState(''); const [clubName, setClubName] = useState(''); const [clubLogo, setClubLogo] = useState(''); const [standings, setStandings] = useState([]); const [activeComp, setActiveComp] = useState(0); const [sponsorLayout, setSponsorLayout] = useState<'grid'|'slider'|'scroller'|'pyramid'>('grid'); const [shopUrl, setShopUrl] = useState(null); const [facebookUrl, setFacebookUrl] = useState(null); const [instagramUrl, setInstagramUrl] = useState(null); const [youtubeUrl, setYoutubeUrl] = useState(null); const [galleryUrl, setGalleryUrl] = useState(null); const [galleryLabel, setGalleryLabel] = useState('Fotogalerie'); const [sponsorsTheme, setSponsorsTheme] = useState<'dark'|'light'>('light'); const [unifiedCategory, setUnifiedCategory] = useState('Vše'); const [heroStyle, setHeroStyle] = useState<'grid' | 'scroller' | 'swiper' | 'swiper_full'>('grid'); // Removed: custom styles - using unified only // PRO style: hero index const [proHeroIndex, setProHeroIndex] = useState(0); // EDGE style: hero index and back-to-top button const [edgeHeroIndex, setEdgeHeroIndex] = useState(0); const [showEdgeTop, setShowEdgeTop] = useState(false); const [edgeRoleIdx, setEdgeRoleIdx] = useState(0); const blogAutoRef = useRef(null); // FACR competitions with matches (for slider) const [facrCompetitions, setFacrCompetitions] = useState; matches_link?:string }>>([]); const [matchesTab, setMatchesTab] = useState(0); const [selectedClub, setSelectedClub] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedMatch, setSelectedMatch] = useState(null); const [isMatchModalOpen, setIsMatchModalOpen] = useState(false); // Index for the NEXT MATCH competition carousel const [nextCompIdx, setNextCompIdx] = useState(0); const [nextMatchLink, setNextMatchLink] = useState(undefined); // Ref to the draggable matches track and per-competition closest index const trackRef = useRef(null); const [closestIndexByComp, setClosestIndexByComp] = useState([]); // API-driven players and sponsors type UiPlayer = { id:number|string; name:string; number?:number; position?:string; image?:string; slug?:string }; type UiSponsor = { id:number|string; name:string; logo:string; url?:string }; type UiBanner = { id:number|string; name:string; image:string; url?:string; placement?:string; width?:number; height?:number }; type UiMerch = { id?: number|string; title?: string; image_url: string; url?: string }; const [players, setPlayers] = useState([]); const [sponsors, setSponsors] = useState([]); const [banners, setBanners] = useState([]); const [featured, setFeatured] = useState([]); const [videos, setVideos] = useState([]); const [videosRich, setVideosRich] = useState>([]); const [merchItems, setMerchItems] = useState([]); const [merchEnabled, setMerchEnabled] = useState(false); // Aliases const [aliases, setAliases] = useState([]); const [aliasMap, setAliasMap] = useState>({}); const [settings, setSettings] = useState(null); const [isLoading, setIsLoading] = useState(true); // MyUIbrix element configuration hook for live preview const { getVariant, isVisible, loading: configLoading } = useAllPageElementConfigs('homepage'); useEffect(() => { let cancelled = false; 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); // We want backend origin root, not /api/v1 u.pathname = path; return u.toString(); } return path; } catch { return path; } }; const fetchJSON = async (url: string) => { try { const res = await fetch(resolveBackendUrl(url), { cache: 'no-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch { return null; } }; const mapArticles = (data: any): NewsItem[] => { if (!data) return [] as any; // Try common shapes: {items: []}, {data: []}, [] const items = Array.isArray(data) ? data : Array.isArray(data?.items) ? data.items : Array.isArray(data?.data) ? data.data : []; const mapped: NewsItem[] = items.slice(0, 6).map((a: any, idx: number) => ({ id: a.id ?? idx + 1, title: a.title ?? a.name ?? 'Article', excerpt: a.excerpt ?? a.summary ?? a.content?.slice?.(0, 140) ?? '', image: a.imageUrl ?? a.image_url ?? a.cover ?? '/images/news/placeholder.jpg', date: a.createdAt ?? a.created_at ?? a.publishedAt ?? a.published_at ?? new Date().toISOString(), category: a.category?.name ?? a.category ?? 'News', slug: a.slug ?? a.urlSlug ?? a.seoSlug ?? undefined, })); return mapped; }; const mapMatches = (data: any): MatchItem[] => { if (!data) return [] as any; const items = Array.isArray(data) ? data : Array.isArray(data?.items) ? data.items : Array.isArray(data?.data) ? data.data : []; const mapped: MatchItem[] = items.slice(0, 2).map((m: any, idx: number) => ({ id: m.id ?? idx + 1, homeTeam: m.homeTeam ?? m.home_team ?? m.home ?? 'Home', awayTeam: m.awayTeam ?? m.away_team ?? m.away ?? 'Away', competition: m.competition ?? m.league ?? 'Match', date: m.date ?? m.kickoffDate ?? m.kickoff_date ?? new Date().toISOString().slice(0, 10), time: m.time ?? m.kickoffTime ?? m.kickoff_time ?? '18:00', venue: m.venue ?? 'Stadium', isHome: Boolean(m.isHome ?? m.is_home ?? true), homeLogoURL: m.homeLogoURL ?? m.HomeLogoURL ?? m.home_logo_url ?? m.home_logo ?? m.homeLogo ?? undefined, awayLogoURL: m.awayLogoURL ?? m.AwayLogoURL ?? m.away_logo_url ?? m.away_logo ?? m.awayLogo ?? undefined, })); return mapped; }; (async () => { // Prefer FACR prefetch cache if available const [ articlesJSON, matchesJSON, cachedSettingsJSON, facrClubJSON, facrTablesJSON, teamLogoOverridesAPI, teamLogoOverridesFile, ] = await Promise.all([ fetchJSON('/cache/prefetch/articles.json'), fetchJSON('/cache/prefetch/matches.json'), fetchJSON('/cache/prefetch/settings.json'), fetchJSON('/cache/prefetch/facr_club_info.json'), fetchJSON('/cache/prefetch/facr_tables.json'), // Prefer public endpoint (cache-busted) for overrides fetchJSON(`/api/v1/public/team-logo-overrides?t=${Date.now()}`), // Fallback to cached JSON snapshot written by backend after saves fetchJSON('/cache/prefetch/team_logo_overrides.json'), ]); // load aliases (public) let aliasesList: CompetitionAlias[] = []; try { aliasesList = await getCompetitionAliasesPublic(); } catch {} const amap: Record = {}; (aliasesList || []).forEach((a) => { if (a?.code && a?.alias) amap[a.code] = { alias: a.alias, original_name: a.original_name }; }); // Try live settings API first let liveSettings: any = null; try { liveSettings = await getPublicSettings(); } catch {} if (!cancelled) { setNews(mapArticles(articlesJSON)); setAliases(aliasesList || []); setAliasMap(amap); // Build override helpers const teamLogoOverridesJSON = (teamLogoOverridesAPI && teamLogoOverridesAPI.by_name) ? teamLogoOverridesAPI : (teamLogoOverridesFile || {}); const byName: Record = (teamLogoOverridesJSON?.by_name || {}) as any; const normalize = (s: string) => String(s) .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); const stripPrefixes = (s: string) => { // Remove common Czech prefixes/words in club names for more robust matching // e.g., "Městský fotbalový klub Kravaře" -> "Kravaře" 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, '').replace(/\s+/g, ' ').trim(); return x; }; 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) { // Try suffix/containment match after stripping prefixes 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; }; // Matches: map from FACR club info if available, otherwise fallback to matches.json if (facrClubJSON?.competitions?.length) { const allMatches = (facrClubJSON.competitions || []) .flatMap((c: any) => (Array.isArray(c.matches) ? c.matches : []).map((m: any, idx: number) => { // m.date_time e.g. "12.08.2023 18:00" -> split 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 || '18:00').slice(0,5); return { id: m.match_id || idx + 1, homeTeam: m.home, awayTeam: m.away, competition: m.competition || m.competition_name || c?.name || c?.code || '', date: isoDate, time, venue: m.venue || '', isHome: facrClubJSON?.name ? (m.home || '').toLowerCase().includes(String(facrClubJSON.name).toLowerCase()) : true, homeLogoURL: getOverrideLogo(m.home, m.home_logo_url), awayLogoURL: getOverrideLogo(m.away, m.away_logo_url), score: m.score, facr_link: m.facr_link, report_url: m.report_url, }; }) ); // Sort by datetime and pick upcoming first const parseDT = (d: string, t: string) => new Date(`${d}T${(t || '00:00')}:00`).getTime(); const now = Date.now(); const upcoming = allMatches .map((m: any & { __ts?: number }) => ({ ...m, __ts: parseDT(m.date, m.time) })) .filter((m: { __ts?: number }) => typeof m.__ts === 'number' && !isNaN(m.__ts!) && (m.__ts as number) >= now) .sort((a: { __ts?: number }, b: { __ts?: number }) => (a.__ts as number) - (b.__ts as number)) .map(({ __ts, ...rest }: any & { __ts?: number }) => rest); const chosen = upcoming.length ? upcoming : allMatches; setMatches(chosen); // Build competitions with their matches for slider const comps = (facrClubJSON.competitions || []).map((c: any) => ({ name: (amap?.[c?.code]?.alias) || c.name || c.code || 'Soutěž', matches_link: c.matches_link, matches: (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 || '18:00').slice(0,5); return { id: m.match_id || idx + 1, date: isoDate, time, home: m.home, away: m.away, home_logo_url: getOverrideLogo(m.home, m.home_logo_url), away_logo_url: getOverrideLogo(m.away, m.away_logo_url), score: m.score, facr_link: m.facr_link, report_url: m.report_url, venue: m.venue || '', }; }) })); setFacrCompetitions(comps); // Compute closest match index per competition to current time const nowTs = Date.now(); const closestIdx: number[] = comps.map((c: { matches: any[] }) => { let bestIdx = -1; let bestDiff = Number.POSITIVE_INFINITY; (c.matches || []).forEach((m: any, idx: number) => { const ts = new Date(`${m.date}T${(m.time || '00:00')}:00`).getTime(); if (!isNaN(ts)) { const diff = Math.abs(ts - nowTs); if (diff < bestDiff) { bestDiff = diff; bestIdx = idx; } } }); return bestIdx; }); setClosestIndexByComp(closestIdx); // Next match FACR link const first = chosen?.[0]; setNextMatchLink((first && (first.facr_link || first.report_url)) || comps?.[0]?.matches_link || facrClubJSON?.url); } else { setMatches(mapMatches(matchesJSON)); } const settingsJSON = liveSettings || cachedSettingsJSON; setSettings(settingsJSON); if (settingsJSON) { const name = settingsJSON?.club_name || settingsJSON?.clubName || settingsJSON?.name || settingsJSON?.siteName; const logo = settingsJSON?.club_logo_url || settingsJSON?.clubLogo || settingsJSON?.logo || settingsJSON?.logoUrl || settingsJSON?.logoURL; if (name) setClubName(name); if (logo) setClubLogo(logo); // Load players via API try { const apiPlayers: ApiPlayer[] = await apiGetPlayers(); const mappedPlayers: UiPlayer[] = (apiPlayers || []).map((p: ApiPlayer) => ({ id: p.id, name: [p.first_name, p.last_name].filter(Boolean).join(' '), number: p.jersey_number, position: p.position, image: assetUrl(p.image_url) || undefined, })); setPlayers(mappedPlayers); } catch {} // Load sponsors via API (also used for banners with placement metadata) try { const apiSponsors: ApiSponsor[] = await apiGetSponsors(); const mapped: UiSponsor[] = (apiSponsors || []).map((s: ApiSponsor) => ({ id: s.id, name: s.name, logo: assetUrl(s.logo_url) || '/images/sponsors/placeholder.png', url: s.website_url || undefined, })); setSponsors(mapped); // Extract banners by placement metadata if provided const mappedBanners: UiBanner[] = (apiSponsors || []) .filter((s: any) => s && (s as any).placement) .map((s: any) => ({ id: s.id, name: s.name, image: assetUrl(s.logo_url) || '/images/sponsors/placeholder.png', url: s.website_url || undefined, placement: s.placement, width: typeof s.width === 'number' ? s.width : undefined, height: typeof s.height === 'number' ? s.height : undefined, })); if (mappedBanners.length) setBanners(mappedBanners); } catch {} // Load featured articles (homepage primary) via API try { const resp = await apiGetArticles({ featured: true, page_size: 3 }); const items = (resp?.data || []).map((a: ApiArticle, idx: number) => ({ id: a.id ?? idx + 1, title: a.title, excerpt: (a as any).excerpt || (a.content || '').slice(0, 140), image: (a as any).image || a.image_url || '/images/news/placeholder.jpg', date: a.created_at || new Date().toISOString(), category: 'Aktuality', slug: a.slug, })); setFeatured(items); // Ensure non-featured 'news' excludes featured items setNews((prev) => { const featuredKeys = new Set(items.map((f) => (f.slug ? `s:${f.slug}` : `i:${f.id}`))); return (prev || []).filter((n) => !featuredKeys.has(n.slug ? `s:${n.slug}` : `i:${n.id}`)); }); } catch {} // Shop URL for merchandise CTA const shop = settingsJSON?.shop_url || settingsJSON?.shopUrl || settingsJSON?.eshop_url || settingsJSON?.e_shop_url || null; if (shop) setShopUrl(String(shop)); // Hero style variant const hs = settingsJSON?.hero_style || settingsJSON?.homepage?.hero_style || settingsJSON?.frontpage_hero_style; if (hs === 'grid' || hs === 'scroller' || hs === 'swiper' || hs === 'swiper_full') setHeroStyle(hs); // Sponsor layout preference: 'grid' or 'slider' const sponsorPref = settingsJSON?.homepage?.sponsors_layout || settingsJSON?.sponsors_layout || settingsJSON?.sponsorsLayout; if (sponsorPref === 'slider' || sponsorPref === 'grid' || sponsorPref === 'scroller' || sponsorPref === 'pyramid') { setSponsorLayout(sponsorPref); } // Sponsors theme: dark or light const sTheme = settingsJSON?.sponsors_theme || settingsJSON?.homepage?.sponsors_theme || settingsJSON?.sponsorsTheme; if (sTheme === 'dark' || sTheme === 'light') setSponsorsTheme(sTheme); // Sponsors data, if present in settings/cache const sponsorsData = settingsJSON?.sponsors || settingsJSON?.partners || null; if (Array.isArray(sponsorsData) && sponsorsData.length) { setSponsors( sponsorsData.map((s: any, i: number) => ({ id: s.id ?? i + 1, name: s.name || 'Sponsor', logo: s.logo_url || s.logoUrl || s.logo || '/images/sponsors/placeholder.png', url: s.url || s.website || s.link || '#', })) ); } // Socials & gallery setFacebookUrl(settingsJSON?.facebook_url || null); setInstagramUrl(settingsJSON?.instagram_url || null); setYoutubeUrl(settingsJSON?.youtube_url || null); setGalleryUrl(settingsJSON?.gallery_url || settingsJSON?.zonerama_url || null); setGalleryLabel(settingsJSON?.gallery_label || 'Fotogalerie'); // Removed: custom style selection - using unified only // Videos (optional) if (Array.isArray(settingsJSON?.videos)) setVideos(settingsJSON.videos as string[]); if (Array.isArray(settingsJSON?.videos_items)) setVideosRich(settingsJSON.videos_items as any); // Merch (optional) if (typeof settingsJSON?.merch_module_enabled === 'boolean') setMerchEnabled(!!settingsJSON.merch_module_enabled); if (Array.isArray(settingsJSON?.merch_items)) setMerchItems((settingsJSON.merch_items as any[]).map((m:any, i:number)=> ({ id: m.id ?? i, title: m.title, image_url: m.image_url, url: m.url }))); } // Standings: prefer FACR tables JSON if (facrTablesJSON?.competitions?.length) { const comps = (facrTablesJSON.competitions || []).map((c: any) => ({ name: (amap?.[c?.code]?.alias) || c.name || c.code, table: (c.table?.overall || []).map((r: any, idx: number) => ({ position: Number(r.rank || idx + 1), team: r.team || r.team_name || '-', team_logo_url: r.team_logo_url, points: Number(r.points || r.pts || 0), played: Number(r.played || 0), wins: Number(r.wins || 0), draws: Number(r.draws || 0), losses: Number(r.losses || 0), score: r.score || '0:0', })), })); setStandings(comps); } // Club name/logo from FACR if not provided by settings if (facrClubJSON) { if (facrClubJSON.name && !clubName) setClubName(facrClubJSON.name); if (facrClubJSON.logo_url && !clubLogo) setClubLogo(facrClubJSON.logo_url); // If settings did not override, still prefer FACR values if (facrClubJSON.name) setClubName(facrClubJSON.name); if (facrClubJSON.logo_url) setClubLogo(facrClubJSON.logo_url); } // Mark loading complete setIsLoading(false); } })(); return () => { cancelled = true; }; }, []); // Auto-scroll matches track to the closest match for current tab useEffect(() => { const el = trackRef.current; if (!el) return; const idx = (closestIndexByComp[matchesTab] ?? 0); const child = el.children?.[idx] as HTMLElement | undefined; if (!child) return; const run = () => { const targetLeft = child.offsetLeft - (el.clientWidth - child.clientWidth) / 2; el.scrollTo({ left: Math.max(0, targetLeft), behavior: 'smooth' }); }; // Wait for layout if (typeof requestAnimationFrame !== 'undefined') { requestAnimationFrame(run); } else { setTimeout(run, 0); } }, [facrCompetitions, matchesTab, closestIndexByComp]); // Auto-theme from club logo dominant color useEffect(() => { if (!clubLogo) return; let disposed = false; const toHex = (v: number) => { const h = Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0'); return h; }; const rgbToHex = (r: number, g: number, b: number) => `#${toHex(r)}${toHex(g)}${toHex(b)}`; const lighten = (r: number, g: number, b: number, amt = 20) => [ Math.min(255, r + amt), Math.min(255, g + amt), Math.min(255, b + amt), ] as const; const darkenIfLowContrast = (r: number, g: number, b: number) => { // ensure contrast versus white text (used in .next-match) const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; // 0..1 if (luminance > 0.75) { // too light, darken return [r * 0.6, g * 0.6, b * 0.6] as const; } return [r, g, b] as const; }; const img = new Image(); img.crossOrigin = 'anonymous'; img.src = assetUrl(clubLogo) || clubLogo; img.onload = () => { if (disposed) return; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return; const w = 100, h = 100; canvas.width = w; canvas.height = h; try { ctx.drawImage(img, 0, 0, w, h); const data = ctx.getImageData(0, 0, w, h).data; let r = 0, g = 0, b = 0, n = 0; for (let i = 0; i < data.length; i += 4) { const a = data[i + 3]; if (a < 64) continue; // skip transparent const rr = data[i], gg = data[i + 1], bb = data[i + 2]; // skip near-white background pixels to better catch logo color if (rr > 240 && gg > 240 && bb > 240) continue; r += rr; g += gg; b += bb; n++; } if (n > 0) { r /= n; g /= n; b /= n; // adjust for readability [r, g, b] = darkenIfLowContrast(r, g, b); const [lr, lg, lb] = lighten(r, g, b, 24); const primary = rgbToHex(r, g, b); const primaryLight = rgbToHex(lr, lg, lb); // secondary: golden fallback if color is blueish, else a subtle accent const isBlueish = b > r && b > g; const secondary = isBlueish ? '#d69e2e' : '#2c5282'; const root = document.documentElement; root.style.setProperty('--primary', primary); root.style.setProperty('--primary-light', primaryLight); root.style.setProperty('--secondary', secondary); } } catch { // ignore CORS or canvas tainting } }; return () => { disposed = true; }; }, [clubLogo]); // MyUIbrix events are handled by useAllPageElementConfigs hook // It automatically updates getVariant() and isVisible() when changes occur in edit mode // Countdown to next match (uses selected competition upcoming if available) useEffect(() => { const getUpcomingForComp = (c: any) => { const items = Array.isArray(c?.matches) ? c.matches : []; const future = items .map((m: any) => ({ m, t: new Date(`${m.date}T${(m.time || '00:00')}:00`).getTime() })) .filter((x: any) => !isNaN(x.t) && x.t > Date.now()) .sort((a: any, b: any) => a.t - b.t); return future[0]?.m || null; }; const getNextKickoff = () => { if (facrCompetitions.length) { const sel = facrCompetitions[Math.max(0, Math.min(matchesTab, facrCompetitions.length - 1))]; const up = getUpcomingForComp(sel); if (up) { const iso = `${up.date}T${(up.time || '00:00')}:00`; const d = new Date(iso); return isNaN(d.getTime()) ? null : d; } } if (!matches.length) return null; const m = matches[0]; const iso = `${m.date}T${(m.time || '00:00')}:00`; const d = new Date(iso); return isNaN(d.getTime()) ? null : d; }; const update = () => { const next = getNextKickoff(); if (!next) { setCountdown(''); return; } const diff = next.getTime() - Date.now(); if (diff <= 0) { setCountdown('Začátek'); return; } const s = Math.floor(diff / 1000); const days = Math.floor(s / 86400); const hrs = Math.floor((s % 86400) / 3600); const mins = Math.floor((s % 3600) / 60); const secs = s % 60; setCountdown(`${days} d ${hrs} h ${mins} m ${secs} s`); }; update(); const id = setInterval(update, 1000); return () => clearInterval(id); }, [matches, facrCompetitions, matchesTab]); // Removed: Edge auto-cycle // Removed: Aurora layout if (false) { const heroItems = [...featured, ...news].slice(0, 3); const top = heroItems[0] || news[0]; return (
Klub

{clubName || 'Fotbal Club'}

{top && ({top.title})}
{galleryUrl && ({galleryLabel})} {facebookUrl && (Facebook)} {instagramUrl && (Instagram)} {youtubeUrl && (YouTube)}

Nejbližší zápasy

{facrCompetitions.map((c, i) => ( ))}

Aktuality

{/* Merch section (Aurora) */}

Tabulky

{(standings.length ? standings.map((s:any)=> s.name || 'Soutěž') : ['Liga']).map((t: string, i: number) => ( ))}
{(standings[activeComp]?.table || standings[activeComp]?.rows || []).slice(0,8).map((row:any, idx:number) => (
#{row.position ?? row.pos ?? idx+1}{row.team?.name ?? row.team ?? row.club ?? '-'}{row.points ?? row.pts ?? '-'}
))}
{sponsors.length > 0 && (
{[...sponsors, ...sponsors].slice(0, Math.max(8, sponsors.length)).map((s, idx) => ( {s.name} ))}
)}
); } // Removed: Magazine layout if (false) { return (
{/* Top color bars */}
{/* Club header */}
Klub

{clubName || 'Fotbal Club'}

{/* Category nav in club color (optional) */}
{/* 3-feature grid like in picture */}
{[0,1,2].map((i) => { const n = featured[i] || news[i]; if (!n) return ( ); return (
{/* Upcoming matches slider (2 visible), with competition switcher */} {facrCompetitions.length > 0 && (

Nejbližší zápasy

{facrCompetitions.map((c, i) => ( ))}
)}
{/* Merch section (Magazine) - optional via settings */}
{/* Sponsors: grid or slider (controlled by settings) */}

Sponzoři

{(sponsorLayout==='grid' || sponsorLayout==='pyramid') ? ( (()=>{ const title = sponsors.find((s:any)=>s.tier==='title') || sponsors[0]; const others = sponsors.filter((s)=>s !== title); return ( <> {title && ( )}
{others.map((s) => ( {s.name} ))}
); })() ) : sponsorLayout==='slider' ? (
{[...sponsors, ...sponsors].map((s, idx) => ( {s.name} ))}
) : (
{[...sponsors, ...sponsors, ...sponsors].map((s, idx) => ( {s.name} ))}
)}
); } // Removed: Edge layout if (false) { const heroItems = [...featured, ...news].slice(0, 5); const heroItem = heroItems[edgeHeroIndex]; const splitTitle = (t?: string) => { if (!t) return { em: '', strong: '' } as const; const parts = t.split(' '); const n = Math.max(1, Math.floor(parts.length / 3)); return { em: parts.slice(0, n).join(' '), strong: parts.slice(n).join(' ') } as const; }; const { em, strong } = splitTitle(heroItem?.title); const heroKeys = new Set(heroItems.map((f) => (f?.slug ? `s:${f.slug}` : `i:${f?.id}`))); const restBlogs = (news || []).filter((n) => !heroKeys.has(n.slug ? `s:${n.slug}` : `i:${n.id}`)); const videoUrlToEmbed = (u: string) => { try { if (/youtu\.be\//i.test(u)) { const id = u.split('/').pop(); return `https://www.youtube.com/embed/${id}`; } const m = u.match(/v=([^&]+)/); if (m) return `https://www.youtube.com/embed/${m[1]}`; } catch {} return u; }; return (
{heroItems.map((n, i) => ( ))}
{heroItems.map((_, i) => ( ))}
{facrCompetitions.length > 0 && (()=>{ const comp = facrCompetitions[Math.max(0, Math.min(matchesTab, facrCompetitions.length - 1))]; const items = Array.isArray(comp?.matches) ? comp.matches : []; const upcoming = items .map((m: any) => ({ m, t: new Date(`${m.date}T${(m.time || '00:00')}:00`).getTime() })) .filter((x: any) => !isNaN(x.t) && x.t > Date.now()) .sort((a: any, b: any) => a.t - b.t)[0]?.m; const show = upcoming || items[0] || null; const isFuture = show ? (new Date(`${show.date}T${(show.time||'00:00')}:00`).getTime() > Date.now()) : false; return (

Nejbližší zápasy

{facrCompetitions.map((c, i) => ( ))}
Domácí
{show?.home || clubName}
{isFuture ? <>
{countdown || '—'}
Začátek
:
{show?.score || '—'}
}
Hosté
{show?.away || 'Soupeř'}
{show && (show.facr_link || show.report_url) && Detail na FACR} Tabulky Všechny zápasy
); })()}
{restBlogs.slice(0,4).map((n) => (
{(videosRich.length > 0 || (Array.isArray(videos) && videos.length > 0)) && (
{videosRich.length > 0 ? (