import React, { useEffect, useRef, useState, useMemo, Suspense } from 'react'; import { IconButton, Tooltip } from '@chakra-ui/react'; import MainLayout from '../components/layout/MainLayout'; import { FiArrowRight, FiCalendar, FiUsers, FiAward, FiChevronLeft, FiChevronRight, FiEdit } from 'react-icons/fi'; import '../styles/theme.css'; import '../styles/sparta-styles.css'; import '../styles/club-styles.css'; import '../styles/home-style-pack.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 { getBanners as apiGetBanners, Banner as ApiBanner } from '../services/banners'; import { translateNationality, getCountryFlag } from '../utils/nationality'; const BannerDisplay = React.lazy(() => import('../components/banners/BannerDisplay')); const BlogCardsScroller = React.lazy(() => import('../components/home/BlogCardsScroller')); const BlogSwiper = React.lazy(() => import('../components/home/BlogSwiper')); const VideosSection = React.lazy(() => import('../components/home/VideosSection')); const MerchSection = React.lazy(() => import('../components/home/MerchSection')); const PollsWidget = React.lazy(() => import('../components/home/PollsWidget')); const GallerySection = React.lazy(() => import('../components/home/GallerySection')); import { getArticles as apiGetArticles, getFeaturedArticles, Article as ApiArticle } from '../services/articles'; import { getCompetitionAliasesPublic, CompetitionAlias } from '../services/competitionAliases'; import { getUpcomingEvents } from '../services/eventService'; const NewsletterSubscribe = React.lazy(() => import('../components/newsletter/NewsletterSubscribe')); const MyUIbrixStyleEditor = React.lazy(() => import('../components/editor/MyUIbrixEditor')); const MyUIbrixErrorBoundary = React.lazy(() => import('../components/editor/MyUIbrixErrorBoundary')); import ClubModal from '../components/home/ClubModal'; import MatchModal from '../components/home/MatchModal'; import { useAllPageElementConfigs } from '../hooks/usePageElementConfig'; import { API_URL } from '../services/api'; import { TeamLogo } from '../components/common/TeamLogo'; import ClubHeroTopbar from '../components/home/ClubHeroTopbar'; import NewsList from '../components/pack/NewsList'; const StandingsCard = React.lazy(() => import('../components/pack/StandingsCard')); import NextMatch from '../components/pack/NextMatch'; const MatchesSlider = React.lazy(() => import('../components/pack/MatchesSlider')); import ActivitiesList from '../components/pack/ActivitiesList'; import { useAuth } from '../contexts/AuthContext'; import SweepstakeWidget from '../components/sweepstakes/SweepstakeWidget'; import { sortCategoriesWithOrder } from '../utils/categorySort'; // 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; display_order?: number }>>([]); 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); // Matches slider auto-centering handled internally by MatchesSlider component // API-driven players and sponsors type UiPlayer = { id:number|string; name:string; number?:number; position?:string; image?:string; slug?:string; age?: number; nationality?: string; active?: boolean }; type UiSponsor = { id:number|string; name:string; logo:string; url?:string; tier?: 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 }; type UiEvent = { id:number|string; title:string; start_time:string; end_time?:string|null; location?:string|null; type?:string; image_url?:string|null }; 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); const [upcomingEvents, setUpcomingEvents] = useState([]); const [activitiesLoaded, setActivitiesLoaded] = useState(false); const [defer, setDefer] = useState(false); // Aliases const [aliases, setAliases] = useState([]); const [aliasMap, setAliasMap] = useState>({}); const [settings, setSettings] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isEditingMode, setIsEditingMode] = useState(false); const { user } = useAuth(); // MyUIbrix element configuration hook for live preview const { getVariant, isVisible, getStyles, loading: configLoading, refreshKey } = useAllPageElementConfigs('homepage'); const stylePack = getVariant('style-pack', 'default'); useEffect(() => { try { const cls = `style-pack-${stylePack}`; const all = ['style-pack-default','style-pack-modern','style-pack-minimal','style-pack-sparta']; all.forEach(c => document.body.classList.remove(c)); document.body.classList.add(cls); } catch {} }, [stylePack]); useEffect(() => { const ric: any = (window as any).requestIdleCallback || ((cb: any) => setTimeout(cb, 1)); ric(() => setDefer(true)); }, []); useEffect(() => { try { const has = typeof document !== 'undefined' && document.body.classList.contains('myuibrix-edit-mode'); setIsEditingMode(!!has); } catch {} }, []); const heroFallbackArticles = useMemo(() => featured.map((item, index) => ({ id: typeof item.id === 'number' ? item.id : index, title: item.title, excerpt: item.excerpt, image: item.image, date: item.date, category: item.category ? { id: index, name: item.category } : undefined, slug: item.slug, })), [featured]); const upcomingCompIndices = useMemo(() => { const now = Date.now(); try { return (facrCompetitions || []) .map((c, i) => { const items = Array.isArray(c?.matches) ? c.matches : []; const hasUpcoming = items.some((m: any) => { const t = new Date(`${m.date || ''}T${(m.time || '00:00')}:00`).getTime(); return !isNaN(t) && t > now; }); return hasUpcoming ? i : -1; }) .filter((i) => i !== -1); } catch { return [] as number[]; } }, [facrCompetitions]); useEffect(() => { try { if (!Array.isArray(upcomingCompIndices) || upcomingCompIndices.length === 0) return; if (!upcomingCompIndices.includes(nextCompIdx)) { setNextCompIdx(upcomingCompIndices[0]); } } catch {} }, [upcomingCompIndices, nextCompIdx]); 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 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 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, display_order: a.display_order }; }); // 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 filter to 14-day range (14 days past + 14 days future) const parseDT = (d: string, t: string) => new Date(`${d}T${(t || '00:00')}:00`).getTime(); const now = Date.now(); const fourteenDaysInMs = 14 * 24 * 60 * 60 * 1000; const minDate = now - fourteenDaysInMs; const maxDate = now + fourteenDaysInMs; const filteredMatches = allMatches .map((m: any & { __ts?: number }) => ({ ...m, __ts: parseDT(m.date, m.time) })) .filter((m: { __ts?: number }) => { const ts = m.__ts; return typeof ts === 'number' && !isNaN(ts) && ts >= minDate && ts <= maxDate; }) .sort((a: { __ts?: number }, b: { __ts?: number }) => (a.__ts as number) - (b.__ts as number)) .map(({ __ts, ...rest }: any & { __ts?: number }) => rest); setMatches(filteredMatches); // Build competitions with their matches for slider (also filter to 14-day range) const comps = (facrClubJSON.competitions || []).map((c: any) => { const compMatches = (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_id: m.home_id, away_id: m.away_id, 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 || '', }; }); // Filter to 14-day range const filtered = compMatches.filter((m: any) => { const ts = new Date(`${m.date}T${(m.time || '00:00')}:00`).getTime(); return !isNaN(ts) && ts >= minDate && ts <= maxDate; }); return { name: (amap?.[c?.code]?.alias) || c.name || c.code || 'Soutěž', matches_link: c.matches_link, matches: filtered, display_order: (amap?.[c?.code]?.display_order), }; }); const sortedComps = sortCategoriesWithOrder(comps as any); setFacrCompetitions(sortedComps as any); // Next match FACR link const first = filteredMatches?.[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 (include inactive to show as non-active instead of hiding) 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, nationality: (p as any).nationality, active: Boolean((p as any).is_active), age: (function(iso?: string){ if (!iso) return undefined; const d = new Date(iso); if (isNaN(d.getTime())) return undefined; const today = new Date(); let age = today.getFullYear() - d.getFullYear(); const m = today.getMonth() - d.getMonth(); if (m < 0 || (m === 0 && today.getDate() < d.getDate())) age--; return age; })( (p as any).date_of_birth ), })); setPlayers(mappedPlayers); } catch {} // Load sponsors via API (sponsors only) 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, tier: (s as any).tier, })); setSponsors(mapped); } catch {} // Load banners via dedicated API (separate from sponsors) try { const apiBanners: ApiBanner[] = await apiGetBanners({ active: true }); const mappedBanners: UiBanner[] = (apiBanners || []).map((b: any) => ({ id: b.id, name: b.name, image: assetUrl(b.image_url) || '/images/sponsors/placeholder.png', url: b.click_url || undefined, placement: b.placement, width: typeof b.width === 'number' ? b.width : undefined, height: typeof b.height === 'number' ? b.height : undefined, })); setBanners(mappedBanners); } catch {} // Load featured articles (homepage primary) via dedicated endpoint try { const resp = await getFeaturedArticles({ page_size: 100 }); const all = (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, })); // Show only first 3 in hero; exclude only those 3 from the other news list const top3 = all.slice(0, 3); setFeatured(top3); setNews((prev) => { const featuredKeys = new Set(all.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 || '#', tier: s.tier, })) ); } // 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, display_order: (amap?.[c?.code]?.display_order), code: 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: getOverrideLogo(r.team || r.team_name, r.team_logo_url), team_id: r.team_id, 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', })), })); const sortedTables = sortCategoriesWithOrder(comps as any); setStandings(sortedTables); } // 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; }; }, []); // Removed: legacy auto-scroll. Handled by MatchesSlider. // 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]); useEffect(() => { let active = true; (async () => { try { const evs = await getUpcomingEvents(); const mapped: UiEvent[] = (evs || []).map((e: any) => ({ id: e.id, title: e.title, start_time: e.start_time, end_time: e.end_time, location: e.location, type: e.type, image_url: e.image_url, })); if (active) setUpcomingEvents(mapped); } catch {} finally { if (active) setActivitiesLoaded(true); } })(); return () => { active = false; }; }, []); // 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 ? (