Files
MyClub/frontend/src/pages/HomePage.tsx
T
Tomas Dvorak f3db65d350 dev day #90 🥳
2025-11-12 20:31:37 +01:00

2109 lines
106 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<NewsItem[]>([]);
const [matches, setMatches] = useState<MatchItem[]>([]);
const [countdown, setCountdown] = useState<string>('');
const [clubName, setClubName] = useState<string>('');
const [clubLogo, setClubLogo] = useState<string>('');
const [standings, setStandings] = useState<any[]>([]);
const [activeComp, setActiveComp] = useState<number>(0);
const [sponsorLayout, setSponsorLayout] = useState<'grid'|'slider'|'scroller'|'pyramid'>('grid');
const [shopUrl, setShopUrl] = useState<string | null>(null);
const [facebookUrl, setFacebookUrl] = useState<string | null>(null);
const [instagramUrl, setInstagramUrl] = useState<string | null>(null);
const [youtubeUrl, setYoutubeUrl] = useState<string | null>(null);
const [galleryUrl, setGalleryUrl] = useState<string | null>(null);
const [galleryLabel, setGalleryLabel] = useState<string>('Fotogalerie');
const [sponsorsTheme, setSponsorsTheme] = useState<'dark'|'light'>('light');
const [unifiedCategory, setUnifiedCategory] = useState<string>('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<number>(0);
// EDGE style: hero index and back-to-top button
const [edgeHeroIndex, setEdgeHeroIndex] = useState<number>(0);
const [showEdgeTop, setShowEdgeTop] = useState<boolean>(false);
const [edgeRoleIdx, setEdgeRoleIdx] = useState<number>(0);
const blogAutoRef = useRef<HTMLDivElement | null>(null);
// FACR competitions with matches (for slider)
const [facrCompetitions, setFacrCompetitions] = useState<Array<{ name:string; matches:Array<any>; matches_link?:string; display_order?: number }>>([]);
const [matchesTab, setMatchesTab] = useState<number>(0);
const [selectedClub, setSelectedClub] = useState<any>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedMatch, setSelectedMatch] = useState<any>(null);
const [isMatchModalOpen, setIsMatchModalOpen] = useState(false);
// Index for the NEXT MATCH competition carousel
const [nextCompIdx, setNextCompIdx] = useState<number>(0);
const [nextMatchLink, setNextMatchLink] = useState<string | undefined>(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<UiPlayer[]>([]);
const [sponsors, setSponsors] = useState<UiSponsor[]>([]);
const [banners, setBanners] = useState<UiBanner[]>([]);
const [featured, setFeatured] = useState<NewsItem[]>([]);
const [videos, setVideos] = useState<string[]>([]);
const [videosRich, setVideosRich] = useState<Array<{ url:string; title?:string; length?:string; uploaded_at?:string; thumbnail_url?:string }>>([]);
const [merchItems, setMerchItems] = useState<UiMerch[]>([]);
const [merchEnabled, setMerchEnabled] = useState<boolean>(false);
const [upcomingEvents, setUpcomingEvents] = useState<UiEvent[]>([]);
const [activitiesLoaded, setActivitiesLoaded] = useState<boolean>(false);
const [defer, setDefer] = useState<boolean>(false);
// Aliases
const [aliases, setAliases] = useState<CompetitionAlias[]>([]);
const [aliasMap, setAliasMap] = useState<Record<string, { alias: string; original_name?: string; display_order?: number }>>({});
const [settings, setSettings] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isEditingMode, setIsEditingMode] = useState<boolean>(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<string, { alias: string; original_name?: string; display_order?: number }> = {};
(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<string, string> = (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<string, string> = Object.keys(byName || {}).reduce((acc: Record<string, string>, 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 (
<MainLayout>
<div className="aurora">
<section className="aurora-hero">
<div className="bg" style={{ backgroundImage: `url(${assetUrl(top?.image) || '/images/news/placeholder.jpg'})` }} />
<div className="gradient" aria-hidden />
<div className="glow" aria-hidden />
<div className="content">
<img className="logo" src={assetUrl(clubLogo) || '/images/club-logo.png'} alt="Klub" />
<h1 className="title">{clubName || 'Fotbal Club'}</h1>
{top && (<a className="pill" href={`/news/${top.slug || top.id}`}>{top.title}</a>)}
<div className="quick">
{galleryUrl && (<a href={galleryUrl || undefined} target="_blank" rel="noreferrer noopener">{galleryLabel}</a>)}
{facebookUrl && (<a href={facebookUrl || undefined} target="_blank" rel="noreferrer noopener">Facebook</a>)}
{instagramUrl && (<a href={instagramUrl || undefined} target="_blank" rel="noreferrer noopener">Instagram</a>)}
{youtubeUrl && (<a href={youtubeUrl || undefined} target="_blank" rel="noreferrer noopener">YouTube</a>)}
</div>
</div>
</section>
<section className="aurora-main">
<div className="card matches">
<div className="head">
<h3>Nejbližší zápasy</h3>
<div className="tabs">
{facrCompetitions.map((c, i) => (
<button key={`${c.name}-${i}`} className={i===matchesTab ? 'active' : ''} onClick={()=>setMatchesTab(i)}>{c.name}</button>
))}
</div>
</div>
<div className="list">
{(facrCompetitions[matchesTab]?.matches || []).slice(0,4).map((m:any, idx:number) => (
<a key={m.id || idx} className="row" href={m.facr_link || m.report_url || '#'} target="_blank" rel="noopener noreferrer">
<div className="team"><img src={assetUrl(m.home_logo_url)} alt={m.home} /><span>{sanitizeClubName(m.home)}</span></div>
<div className="meta"><span>{new Date(`${m.date}T${(m.time||'00:00')}:00`).toLocaleDateString()}</span><span></span><span>{m.time || ''}</span></div>
<div className="team"><img src={assetUrl(m.away_logo_url)} alt={m.away} /><span>{sanitizeClubName(m.away)}</span></div>
</a>
))}
</div>
<div className="action"><a className="btn" href="/kalendar">Všechny zápasy</a></div>
</div>
<div className="card news">
<div className="head"><h3>Aktuality</h3></div>
<div className="grid">
{news.slice(0,6).map((n)=> (
<a key={n.id} href={`/news/${n.slug || n.id}`} className="n">
<div className="thumb" style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }} />
<div className="meta"><div className="tag">{n.category || 'Aktuality'}</div><div className="title">{n.title}</div></div>
</a>
))}
</div>
<div className="action"><a className="btn" href="/news">Další články</a></div>
</div>
{/* Merch section (Aurora) */}
<div className="card" style={{ marginTop: 24 }}>
<MerchSection />
</div>
<div className="card tables">
<div className="head"><h3>Tabulky</h3></div>
<div className="tabs small">
{(standings.length ? standings.map((s:any)=> s.name || 'Soutěž') : ['Liga']).map((t: string, i: number) => (
<button key={`${t}-${i}`} className={i===activeComp ? 'active' : ''} onClick={()=>setActiveComp(i)}>{t}</button>
))}
</div>
<div className="table">
{(standings[activeComp]?.table || standings[activeComp]?.rows || []).slice(0,8).map((row:any, idx:number) => (
<div key={idx} className="tr"><span>#{row.position ?? row.pos ?? idx+1}</span><span className="team">{row.team?.name ?? row.team ?? row.club ?? '-'}</span><span>{row.points ?? row.pts ?? '-'}</span></div>
))}
</div>
<div className="action"><a className="btn" href="/tabulky">Zobrazit vše</a></div>
</div>
</section>
{sponsors.length > 0 && (
<section className="aurora-sponsors">
<div className="belt">
{[...sponsors, ...sponsors].slice(0, Math.max(8, sponsors.length)).map((s, idx) => (
<a key={`${s.id}-${idx}`} className="tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener"><img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} /></a>
))}
</div>
</section>
)}
</div>
</MainLayout>
);
}
// Removed: Magazine layout
if (false) {
return (
<MainLayout>
<div className="magazine">
{/* Top color bars */}
<div className="mag-bars" aria-hidden>
<div className="c1" />
<div className="c2" />
</div>
<div className="mag-container">
{/* Club header */}
<header className="mag-header">
<div className="club-colors">
<span className="swatch" style={{ background: 'var(--primary)' }} />
<span className="swatch" style={{ background: 'var(--secondary)' }} />
</div>
<div className="brand">
<img className="logo" src={assetUrl(clubLogo) || '/images/club-logo.png'} alt="Klub" />
<h1 className="name">{clubName || 'Fotbal Club'}</h1>
</div>
{/* Category nav in club color (optional) */}
<nav className="mag-nav" aria-label="Sekce webu">
<a href="/" className="link">Domů</a>
<a href="/blog" className="link">Aktuality</a>
<a href="/kalendar" className="link">Kalendář</a>
<a href="/tabulky" className="link">Tabulky</a>
<a href="/kontakt" className="link">Kontakt</a>
</nav>
</header>
{/* 3-feature grid like in picture */}
<section className="mag-hero">
{[0,1,2].map((i) => {
const n = featured[i] || news[i];
if (!n) return (
<a key={`ph-${i}`} className="mag-card" href="#" aria-hidden />
);
return (
<a key={n.id} href={`/news/${n.slug || n.id}`} className={`mag-card ${i===0 ? 'large' : ''}`}>
<div className="bg" style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }} />
<div className="overlay">
<div className="cat">{n.category || 'Aktuality'}</div>
<h2 className="title">{n.title}</h2>
</div>
</a>
);
})}
</section>
{/* Upcoming matches slider (2 visible), with competition switcher */}
{facrCompetitions.length > 0 && (
<section className="mag-upcoming">
<div className="head">
<h3>Nejbližší zápasy</h3>
<div className="tabs">
{facrCompetitions.map((c, i) => (
<button key={`${c.name}-${i}`} className={i===matchesTab ? 'active' : ''} onClick={() => setMatchesTab(i)}>{c.name}</button>
))}
</div>
</div>
<div className="match-slider">
{(facrCompetitions[matchesTab]?.matches || []).map((m:any, idx:number) => (
<a key={m.id || idx} className="match-tile" href={m.facr_link || m.report_url || '#'} target="_blank" rel="noopener noreferrer">
<div className="row top">
<span className="meta">{new Date(`${m.date}T${(m.time||'00:00')}:00`).toLocaleDateString()} {(m.time||'')}</span>
</div>
<div className="row teams">
<div className="team">
<img src={assetUrl(m.home_logo_url)} alt={m.home} />
<span>{sanitizeClubName(m.home)}</span>
</div>
<span className="vs">vs</span>
<div className="team">
<img src={assetUrl(m.away_logo_url)} alt={m.away} />
<span>{sanitizeClubName(m.away)}</span>
</div>
</div>
</a>
))}
</div>
</section>
)}
<aside className="col right">
<div className="widget">
<h3>Tabulky</h3>
{standings.length ? (
<div>
{(standings[activeComp]?.table || standings[activeComp]?.rows || []).slice(0,8).map((row: any, idx: number) => (
<div key={idx} className="row-table">
<div>#{row.position ?? row.pos ?? idx+1}</div>
<div className="team">{row.team?.name ?? row.team ?? row.club ?? '-'}</div>
<div>{row.points ?? row.pts ?? '-'}</div>
</div>
))}
</div>
) : (
<div className="muted">Zde se zobrazí tabulky podle soutěží.</div>
)}
</div>
<div className="widget">
<h3>O klubu</h3>
<p className="muted">{clubName} oficiální web klubu. Sledujte novinky, zápasy a tabulky.</p>
</div>
</aside>
</div>
{/* Merch section (Magazine) - optional via settings */}
<section className="mag-merch" style={{ marginTop: 16, marginBottom: 16 }}>
<div className="mag-container">
<MerchSection />
</div>
</section>
{/* Sponsors: grid or slider (controlled by settings) */}
<section
className={`sponsors ${sponsorsTheme === 'dark' ? 'dark' : ''}`}
style={{
width: '100vw',
position: 'relative',
left: '50%',
right: '50%',
transform: 'translateX(-50%)',
paddingLeft: 'max(16px, calc((100vw - 1200px) / 2))',
paddingRight: 'max(16px, calc((100vw - 1200px) / 2))',
boxSizing: 'border-box',
}}
>
<div className="section-head">
<h3>Sponzoři</h3>
</div>
{(sponsorLayout==='grid' || sponsorLayout==='pyramid') ? (
(()=>{
const title = sponsors.find((s:any)=>s.tier==='title') || sponsors[0];
const others = sponsors.filter((s)=>s !== title);
return (
<>
{title && (
<div className="title-sponsor">
<a className="sponsor-tile" href={title.url || '#'} target="_blank" rel="noreferrer noopener">
<img src={assetUrl(title.logo) || '/images/sponsors/placeholder.png'} alt={title.name} />
</a>
</div>
)}
<div className="divider" aria-hidden />
<div className="sponsors-grid">
{others.map((s) => (
<a key={s.id} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
</>
);
})()
) : sponsorLayout==='slider' ? (
<div className="sponsors-slider">
<div className="track">
{[...sponsors, ...sponsors].map((s, idx) => (
<a key={`${s.id}-${idx}`} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
</div>
) : (
<div className="sponsors-scroller">
<div className="belt">
{[...sponsors, ...sponsors, ...sponsors].map((s, idx) => (
<a key={`${s.id}-${idx}`} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
</div>
)}
</section>
</div>
</MainLayout>
);
}
// 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 (
<MainLayout>
<div className="edge">
<section className="edge-hero bleed">
<div className="slides">
{heroItems.map((n, i) => (
<a key={n?.id || i} href={n ? `/news/${n.slug || n.id}` : '#'} className={`slide ${i===edgeHeroIndex ? 'active' : ''}`} style={{ backgroundImage: `url(${assetUrl(n?.image) || '/images/news/placeholder.jpg'})` }}>
<div className="overlay">
<h2 className="title"><span className="em">{em}</span>{em ? ' ' : ''}<span className="strong">{strong}</span></h2>
{n && <a className="link" href={`/news/${n.slug || n.id}`}>Přejít na článek</a>}
</div>
</a>
))}
<div className="pagers">
{heroItems.map((_, i) => (
<button key={i} className={`pager ${i===edgeHeroIndex ? 'active' : ''}`} onClick={()=>setEdgeHeroIndex(i)}>{i+1}</button>
))}
</div>
</div>
</section>
{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 (
<section className="edge-upcoming">
<div className="head">
<h3>Nejbližší zápasy</h3>
<div className="tabs">
{facrCompetitions.map((c, i) => (
<button key={`${c.name}-${i}`} className={i===matchesTab ? 'active' : ''} onClick={()=>setMatchesTab(i)}>{c.name}</button>
))}
</div>
</div>
<div className="card">
<div className="team">
<img src={assetUrl(show?.home_logo_url) || assetUrl(clubLogo) || '/images/club-logo.png'} alt="Domácí" />
<div>{show?.home || clubName}</div>
</div>
<div className="meta">
{isFuture ? <><div style={{ fontWeight: 800 }}>{countdown || '—'}</div><div>Začátek</div></> : <div style={{ fontWeight: 800 }}>{show?.score || '—'}</div>}
</div>
<div className="team">
<img src={assetUrl(show?.away_logo_url) || '/images/club-opponent.png'} alt="Hosté" />
<div>{show?.away || 'Soupeř'}</div>
</div>
</div>
<div className="links">
{show && (show.facr_link || show.report_url) && <a href={show.facr_link || show.report_url} target="_blank" rel="noreferrer noopener">Detail na FACR</a>}
<a href="#edge-tables">Tabulky</a>
<a href="/kalendar">Všechny zápasy</a>
</div>
</section>
);
})()}
<section className="edge-grid">
<div className="left">
<div className="blogs4">
{restBlogs.slice(0,4).map((n) => (
<a key={n.id} href={`/news/${n.slug || n.id}`} className="bcard">
<div className="thumb" style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }} />
<div className="meta">{n.title}</div>
</a>
))}
</div>
<div style={{ marginTop: 10 }}>
<a className="btn" href="/news">Všechny aktuality</a>
</div>
</div>
<aside className="right" id="edge-tables">
<div className="tables">
<div className="tabs">
{(standings.length ? standings.map((s:any)=> s.name || 'Soutěž') : ['Liga']).map((t: string, i: number) => (
<button key={`${t}-${i}`} className={i===activeComp ? 'active' : ''} onClick={()=>setActiveComp(i)}>{t}</button>
))}
</div>
{standings.length ? (
<div>
{(standings[activeComp]?.table || standings[activeComp]?.rows || []).slice(0,10).map((row: any, idx: number) => (
<div className="row" key={idx}>
<div>#{row.position ?? row.pos ?? idx+1}</div>
<div>{row.team?.name ?? row.team ?? row.club ?? '-'}</div>
<div>{row.points ?? row.pts ?? '-'}</div>
</div>
))}
</div>
) : (
<div style={{ color: 'var(--dark-gray)' }}>Tabulky ještě nejsou k dispozici.</div>
)}
</div>
</aside>
</section>
{(videosRich.length > 0 || (Array.isArray(videos) && videos.length > 0)) && (
<section className="edge-videos">
<div className="wrap">
<div className="main">
{videosRich.length > 0 ? (
<iframe title="video-main" src={videoUrlToEmbed(videosRich[0].url)} allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen />
) : (
<iframe title="video-main" src={videoUrlToEmbed(videos[0])} allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen />
)}
</div>
<div className="grid4">
{(videosRich.length > 0 ? videosRich.slice(1,5).map(v=>v.url) : videos.slice(1,5)).map((v, i) => (
<div key={i} className="small"><a href={v} target="_blank" rel="noreferrer noopener" /></div>
))}
</div>
</div>
</section>
)}
{players.length > 0 && (
<section className="edge-team">
{(() => {
const roles = Array.from(new Set(players.map((p:any)=> p.position || 'Tým')));
const role = roles[Math.max(0, Math.min(edgeRoleIdx, roles.length - 1))];
const items = players.filter((p:any)=> (p.position || 'Tým') === role);
return (
<>
<div className="tabs">
{roles.map((r, i)=> (
<button key={r + i} className={i===edgeRoleIdx? 'active' : ''} onClick={()=>setEdgeRoleIdx(i)}>{r}</button>
))}
</div>
<div className="track">
{items.map((p)=> (
<div key={p.id} className="card" style={{ opacity: p.active === false ? 0.6 : 1 }}>
<div className="photo" style={{ backgroundImage: `url(${assetUrl((p as any).image) || '/images/player-placeholder.jpg'})` }} />
<div className="name">{p.name}</div>
<div className="role">{p.position || 'Hráč'}</div>
{typeof p.number !== 'undefined' && <div className="number">#{p.number}</div>}
</div>
))}
</div>
</>
);
})()}
</section>
)}
{merchEnabled && (shopUrl || merchItems.length > 0) && (
<section className="edge-merch">
<div className="head" style={{ marginBottom: 8 }}><h3>Oblečení týmu</h3></div>
<div className="grid">
{(merchItems.length > 0 ? merchItems.map((m)=> ({ id: m.id, name: m.title, image: m.image_url, url: m.url })) : (banners || []).filter((b:any)=> (b.placement === 'merch' || b.placement === 'homepage_merch'))).slice(0,8).map((it:any) => (
<a key={it.id} className="item" href={it.url || shopUrl || '#'} target="_blank" rel="noreferrer noopener">
<div className="thumb" style={{ backgroundImage: `url(${it.image})` }} />
<div className="meta">{it.name || 'Fanshop'}</div>
</a>
))}
</div>
</section>
)}
{sponsors.length > 0 && (
<section className="edge-sponsors">
<div className="grid">
{sponsors.map((s) => (
<a key={s.id} className="tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
</section>
)}
<div className={`edge-top ${showEdgeTop ? 'show' : ''}`}>
<button className="btn" onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>Nahoru</button>
</div>
</div>
</MainLayout>
);
}
// Removed: Pro layout
// if (false) {
// const heroItems = [...featured, ...news].slice(0, 5);
// return (
// <MainLayout>
// <div className="pro">
// {/* Fullscreen hero with numeric pagers and glow */}
// <section className="pro-hero">
// <div className="slides">
// {heroItems.map((n, i) => (
// <a
// key={n.id}
// href={`/news/${n.slug || n.id}`}
// className={`slide ${i===proHeroIndex ? 'active' : ''}`}
// aria-hidden={i!==proHeroIndex}
// style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }}
// >
// <div className="overlay">
// <h2 className="title">{n.title}</h2>
// </div>
// </a>
// ))}
// <div className="glow" aria-hidden />
// <div className="pagers">
// {heroItems.map((_, i) => (
// <button key={i} className={`pager ${i===proHeroIndex ? 'active' : ''}`} aria-label={`Slide ${i+1}`} onClick={()=>setProHeroIndex(i)}>{i+1}</button>
// ))}
// </div>
// </div>
// </section>
// {/* Main two-column grid */}
// <div className="pro-container">
// <section className="pro-grid">
// {/* Left column */}
// <div className="col left">
// {/* Matches by competitions with switcher and horizontal scroller */}
// {facrCompetitions.length > 0 && (
// <div className="widget">
// <div className="head">
// <h3>Nejbližší zápasy</h3>
// <div className="tabs">
// {facrCompetitions.map((c, i) => (
// <button key={`${c.name}-${i}`} className={i===matchesTab ? 'active' : ''} onClick={() => setMatchesTab(i)}>{c.name}</button>
// ))}
// </div>
// </div>
// <div className="match-scroller">
// {(facrCompetitions[matchesTab]?.matches || []).map((m:any, idx:number) => (
// <a key={m.id || idx} className="mcard" href={m.facr_link || m.report_url || '#'} target="_blank" rel="noopener noreferrer">
// <div className="row meta">
// <span>{new Date(`${m.date}T${(m.time||'00:00')}:00`).toLocaleDateString()}</span>
// <span>•</span>
// <span>{m.time || ''}</span>
// </div>
// <div className="row teams">
// <div className="team">
// <img src={assetUrl(m.home_logo_url)} alt={m.home} />
// <span>{m.home}</span>
// </div>
// <span className="vs">vs</span>
// <div className="team">
// <img src={assetUrl(m.away_logo_url)} alt={m.away} />
// <span>{m.away}</span>
// </div>
// </div>
// </a>
// ))}
// </div>
// <div className="more"><a href="/kalendar" className="btn">Všechny zápasy</a></div>
// </div>
// )}
// {/* Blogs auto scroller with arrows */}
// <div className="widget">
// <div className="head">
// <h3>Aktuality</h3>
// <div className="arrows" aria-hidden>
// <button className="prev" aria-label="Zpět" onClick={()=>{ const el = blogAutoRef.current; if (el) el.scrollBy({ left: -280, behavior: 'smooth' }); }}></button>
// <button className="next" aria-label="Vpřed" onClick={()=>{ const el = blogAutoRef.current; if (el) el.scrollBy({ left: 280, behavior: 'smooth' }); }}></button>
// </div>
// </div>
// <div className="blog-auto" ref={blogAutoRef}>
// {news.slice(0, 10).map((n) => (
// <a key={n.id} href={`/news/${n.slug || n.id}`} className="bcard">
// <div className="thumb" style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }} />
// <div className="meta">
// <div className="tag">{n.category || 'Aktuality'}</div>
// <div className="title">{n.title}</div>
// <div className="date">{new Date(n.date || Date.now()).toLocaleDateString()}</div>
// </div>
// </a>
// ))}
// </div>
// <div className="more"><a href="/news" className="btn">Všechny aktuality</a></div>
// </div>
// {/* Standings Z V R P B with switcher */}
// {standings.length > 0 && (
// <div className="widget">
// <div className="head">
// <h3>Tabulky</h3>
// <div className="tabs">
// {standings.map((s:any, i:number) => (
// <button key={`${s.name}-${i}`} className={i===activeComp ? 'active' : ''} onClick={() => setActiveComp(i)}>{s.name || 'Soutěž'}</button>
// ))}
// </div>
// </div>
// <div className="table">
// <div className="thead">
// <span>#</span><span>Tým</span><span>Z</span><span>V</span><span>R</span><span>P</span><span>B</span>
// </div>
// <div className="tbody">
// {(standings[activeComp]?.table || []).map((row:any, idx:number) => (
// <div key={idx} className="tr">
// <span>{row.position ?? idx+1}</span>
// <span className="team">{row.team?.name ?? row.team ?? '-'}</span>
// <span>{row.played ?? row.matches ?? '-'}</span>
// <span>{row.wins ?? row.win ?? '-'}</span>
// <span>{row.draws ?? row.draw ?? '-'}</span>
// <span>{row.losses ?? row.loss ?? '-'}</span>
// <span>{row.points ?? row.pts ?? '-'}</span>
// </div>
// ))}
// </div>
// </div>
// </div>
// )}
// {/* Sponsors belt with faded edges */}
// <div className="widget">
// <div className="head"><h3>Sponzoři</h3></div>
// <div className="sponsor-belt">
// <div className="mask left" aria-hidden />
// <div className="track">
// {[...sponsors, ...sponsors].map((s, idx) => (
// <a key={`${s.id}-${idx}`} className="tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
// <img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
// </a>
// ))}
// </div>
// <div className="mask right" aria-hidden />
// </div>
// </div>
// </div>
// {/* Right column: ticket next match */}
// <aside className="col right">
// {(() => {
// const comp = facrCompetitions[Math.max(0, Math.min(nextCompIdx, 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 link = (show && (show.facr_link || show.report_url)) || comp?.matches_link || nextMatchLink;
// return (
// <div className="ticket">
// <div className="ticket-head">
// <h3>Další zápas</h3>
// <div className="tabs">
// {facrCompetitions.map((c, i) => (
// <button key={`${c.name}-${i}`} className={i===nextCompIdx ? 'active' : ''} onClick={() => setNextCompIdx(i)}>{c.name}</button>
// ))}
// </div>
// </div>
// <div className="ticket-body">
// <div className="team">
// <img src={assetUrl(show?.home_logo_url) || assetUrl(clubLogo) || '/images/club-logo.png'} alt="Domácí" />
// <div className="name">{show?.home || clubName}</div>
// </div>
// <div className="vs">VS</div>
// <div className="team">
// <img src={assetUrl(show?.away_logo_url) || '/images/club-opponent.png'} alt="Hosté" />
// <div className="name">{show?.away || 'Soupeř'}</div>
// </div>
// <div className="venue">{show?.venue || ''}</div>
// </div>
// <div className="ticket-countdown">{countdown || '—'}</div>
// <div className="ticket-action">
// {link && <a className="btn" href={link} target="_blank" rel="noopener noreferrer">Detail na FACR</a>}
// </div>
// </div>
// );
// })()}
// </aside>
// </section>
// </div>
// {/* Merch section (Pro) - optional via settings */}
// <section className="pro-merch" style={{ marginTop: 16, marginBottom: 16 }}>
// <div className="pro-container">
// <MerchSection />
// </div>
// </section>
// {/* Socials & Embeds */}
// {(facebookUrl || instagramUrl || youtubeUrl || galleryUrl) && (
// <section className="pro-socials">
// <div className="pro-container">
// <div className="links">
// {galleryUrl && (<a className="sbtn" href={galleryUrl || undefined} target="_blank" rel="noreferrer noopener">{galleryLabel}</a>)}
// {facebookUrl && (<a className="sbtn" href={facebookUrl || undefined} target="_blank" rel="noreferrer noopener">Facebook</a>)}
// {instagramUrl && (<a className="sbtn" href={instagramUrl || undefined} target="_blank" rel="noreferrer noopener">Instagram</a>)}
// {youtubeUrl && (<a className="sbtn" href={youtubeUrl || undefined} target="_blank" rel="noreferrer noopener">YouTube</a>)}
// </div>
// {facebookUrl && (
// <div className="embeds" style={{ marginTop: 12 }}>
// <iframe
// title="Facebook Page"
// src={`https://www.facebook.com/plugins/page.php?href=${encodeURIComponent(facebookUrl ?? '')}&tabs=timeline&width=500&height=280&small_header=false&adapt_container_width=true&hide_cover=false&show_facepile=true`}
// width="100%"
// height="280"
// style={{ border: 'none', overflow: 'hidden', maxWidth: 700 }}
// scrolling="no"
// frameBorder={0}
// allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
// />
// </div>
// )}
// </div>
// </section>
// )}
// {/* Optional Ads row from banners with placement 'homepage_ads' */}
// {(banners || []).some(b => b.placement === 'homepage_ads') && (
// <section className="pro-ads">
// <div className="pro-container">
// <div className="ads-row">
// {(banners || []).filter(b => b.placement === 'homepage_ads').map((b) => (
// <a key={b.id} className="ad" href={b.url || '#'} target={b.url ? '_blank' : undefined} rel={b.url ? 'noopener noreferrer' : undefined}>
// {/* eslint-disable-next-line jsx-a11y/alt-text */}
// <img src={b.image} alt={b.name} />
// </a>
// ))}
// </div>
// </div>
// </section>
// )}
// </div>
// </MainLayout>
// );
// }
return (
<MainLayout showSponsorsSection={false}>
<div className="container" data-element="container" style={{ ...getStyles('container') }}>
<div data-element="style-pack" data-variant={stylePack} style={{ display: 'none' }} />
{/* Above-hero club bar (MyUIbrix managed) */}
{isVisible('hero-topbar', true) && (
<section key={`hero-topbar-${refreshKey}-${getVariant('hero-topbar', 'minimal')}`} data-element="hero-topbar" data-variant={getVariant('hero-topbar', 'minimal')} style={{ ...getStyles('hero-topbar') }}>
<ClubHeroTopbar
variant={(getVariant('hero-topbar', 'minimal') as any) as 'brand' | 'minimal' | 'badge'}
fullBleed={getVariant('header', 'unified') === 'fullwidth'}
/>
</section>
)}
{/* Header: logo + club name (legacy). Hidden when hero-topbar is visible */}
{!isVisible('hero-topbar', true) && (
<div className="home-header">
<TeamLogo
teamId={settings?.club_id}
teamName={clubName}
facrLogo={assetUrl(clubLogo) || undefined}
size="custom"
alt="Klub"
borderRadius="full"
style={{ width: 56, height: 56 }}
/>
<div>
<h1 style={{ margin: 0 }}>{clubName}</h1>
<div className="subtitle" style={{ fontSize: '0.95rem' }}>Oficiální web klubu</div>
</div>
</div>
)}
{/* Hero section: variant controlled by MyUIbrix (getVariant) or fallback to settings.hero_style */}
{getVariant('hero', heroStyle) === 'grid' && isVisible('hero', true) && (
<section key={`hero-grid-${refreshKey}`} data-element="hero" data-variant={getVariant('hero', heroStyle)} className="hero-grid" style={{ position: 'relative', ...getStyles('hero') }}>
{featured[0] ? (
<a href={`/news/${featured[0].slug || featured[0].id}`} className="hero-card big" style={{ textDecoration: 'none' }}>
<div className="bg" style={{ backgroundImage: `url(${assetUrl(featured[0].image) || '/images/news/placeholder.jpg'})` }} />
<div className="overlay">
<div style={{ opacity: 0.9, fontSize: '0.8rem', color: '#ffffff' }}>{featured[0].category || 'Aktuality'}</div>
<h2 style={{ margin: '4px 0 0 0', color: '#ffffff' }}>{featured[0].title}</h2>
</div>
</a>
) : (
isLoading ? (
<div className="hero-card big skeleton" style={{ borderRadius: 16 }} />
) : (
<a href="/news" className="hero-card big" style={{ textDecoration: 'none' }}>
<div className="bg" style={{ backgroundImage: `url('/images/news/placeholder.jpg')` }} />
<div className="overlay">
<div style={{ opacity: 0.9, fontSize: '0.8rem', color: '#ffffff' }}>Aktuality</div>
<h2 style={{ margin: '4px 0 0 0', color: '#ffffff' }}>Nejnovější titulek</h2>
</div>
</a>
)
)}
<div className="small-col">
{featured.slice(1, 3).map((n, idx) => (
<a key={n.id} href={`/news/${n.slug || n.id}`} className="hero-card small" style={{ textDecoration: 'none' }}>
<div className="bg" style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }} />
<div className="overlay">
<div style={{ opacity: 0.9, fontSize: '0.8rem', color: '#ffffff' }}>{n.category || 'Aktuality'}</div>
<h3 style={{ margin: '4px 0 0 0', color: '#ffffff' }}>{n.title}</h3>
</div>
</a>
))}
{Array.from({ length: Math.max(0, 2 - Math.min(2, Math.max(0, featured.length - 1))) }).map((_, idx) => (
isLoading ? (
<div key={`placeholder-${idx}`} className="hero-card small skeleton" style={{ borderRadius: 16 }} />
) : (
<a key={`placeholder-${idx}`} href="/news" className="hero-card small" style={{ textDecoration: 'none' }}>
<div className="bg" style={{ backgroundImage: `url('/images/news/placeholder.jpg')`, filter: 'grayscale(50%) brightness(0.7)' }} />
<div className="overlay">
<div style={{ opacity: 0.8, fontSize: '0.8rem', color: '#fff' }}>Aktuality</div>
<h3 style={{ margin: '4px 0 0 0', color: '#fff' }}>Připravujeme...</h3>
</div>
</a>
)
))}
</div>
</section>
)}
{/* Banner: homepage_middle */}
{(banners || []).some(b => b.placement === 'homepage_middle') && isVisible('banner', true) && (
<section data-element="banner" data-variant={getVariant('banner', 'top')} className="banner banner-middle" style={{ margin: '24px 0', textAlign: 'center', ...getStyles('banner') }}>
{(banners || []).filter(b => b.placement === 'homepage_middle').map((b) => (
<a key={b.id} href={b.url || '#'} target={b.url ? '_blank' : undefined} rel={b.url ? 'noopener noreferrer' : undefined} style={{ display: 'inline-block', margin: 8 }}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<img loading="lazy" decoding="async" src={b.image} alt={b.name} style={{ maxWidth: '100%', width: b.width ? `${b.width}px` : undefined, height: b.height ? `${b.height}px` : 'auto' }} />
</a>
))}
</section>
)}
{/* Featured articles are now shown in the hero grid above, not here */}
{/* Sidebar banners (homepage_sidebar) - sticky within page container */}
{(banners || []).some(b => b.placement === 'homepage_sidebar') && (
<section
key={`sidebar-${refreshKey}-${getVariant('sidebar', 'right')}`}
data-element="sidebar"
data-variant={getVariant('sidebar', 'right')}
className={`banner banner-sidebar sidebar-${getVariant('sidebar', 'right')}`}
style={{ margin: '24px 0', ...getStyles('sidebar') }}
>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<div
style={{
position: 'sticky',
top: 112,
width: 320,
maxWidth: '100%',
marginLeft: getVariant('sidebar', 'right') === 'left' ? 0 : 'auto',
marginRight: getVariant('sidebar', 'right') === 'left' ? 'auto' : 0,
zIndex: 1,
}}
>
{(banners || []).filter(b => b.placement === 'homepage_sidebar').map((b) => (
<div key={b.id} className="card" style={{ display: 'block', marginBottom: 12, padding: 4 }}>
<a href={b.url || '#'} target={b.url ? '_blank' : undefined} rel={b.url ? 'noopener noreferrer' : undefined} style={{ display: 'block' }}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<img loading="lazy" decoding="async" src={b.image} alt={b.name} style={{ width: b.width ? `${b.width}px` : '100%', height: b.height ? `${b.height}px` : 'auto', maxWidth: '100%' }} />
</a>
</div>
))}
</div>
</div>
</section>
)}
{getVariant('hero', heroStyle) === 'scroller' && isVisible('hero', true) && (
<section key={`hero-scroller-${refreshKey}`} data-element="hero" data-variant={getVariant('hero', heroStyle)} style={{ position: 'relative', ...getStyles('hero') }}>
<Suspense fallback={<div style={{ minHeight: 240 }} />}>
<BlogCardsScroller />
</Suspense>
</section>
)}
{(getVariant('hero', heroStyle) === 'swiper' || getVariant('hero', heroStyle) === 'swiper_full') && isVisible('hero', true) && (
<section key={`hero-swiper-${refreshKey}`} data-element="hero" data-variant={getVariant('hero', heroStyle)} style={getVariant('hero', heroStyle) === 'swiper_full' ? { position: 'relative', marginLeft: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)', ...getStyles('hero') } : { position: 'relative', ...getStyles('hero') }}>
<Suspense fallback={<div style={{ minHeight: 280 }} />}>
<BlogSwiper fallbackArticles={heroFallbackArticles} />
</Suspense>
</section>
)}
{/* Next match: categories (competitions) with left/right navigation - synced with matchesTab */}
{isVisible('matches', true) ? (
facrCompetitions.length > 0 ? (
upcomingCompIndices.length > 0 ? (
(() => {
const safeIndex = Math.max(0, Math.min(nextCompIdx, facrCompetitions.length - 1));
const pos = upcomingCompIndices.indexOf(safeIndex);
const effectiveIndex = pos >= 0 ? upcomingCompIndices[pos] : upcomingCompIndices[0];
const comp = facrCompetitions[effectiveIndex];
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 || null;
const link = (show && (show.facr_link || show.report_url)) || comp?.matches_link || nextMatchLink;
const prevIdx = upcomingCompIndices[(Math.max(0, pos) - 1 + upcomingCompIndices.length) % upcomingCompIndices.length];
const nextIdx = upcomingCompIndices[(Math.max(0, pos) + 1) % upcomingCompIndices.length];
const handleNextMatchClick = () => {
if (show) {
setSelectedMatch({
...show,
competition: comp?.name,
});
setIsMatchModalOpen(true);
} else if (link) {
window.open(link, '_blank', 'noopener,noreferrer');
}
};
return (
<NextMatch
data={show}
competitionName={comp?.name}
countdown={countdown}
onPrev={() => setNextCompIdx(prevIdx)}
onNext={() => setNextCompIdx(nextIdx)}
onOpen={handleNextMatchClick}
elementProps={{
'data-element': 'matches' as any,
'data-variant': getVariant('matches', 'compact') as any,
'aria-live': 'polite' as any,
style: { ...getStyles('matches') },
}}
/>
);
})()
) : null
) : (
<div className="card">
<NextMatch
key={`matches-${refreshKey}-${getVariant('matches', 'compact')}`}
data={{
home: matches[0]?.homeTeam || clubName,
home_logo_url: matches[0]?.homeLogoURL || clubLogo,
away: matches[0]?.awayTeam || 'Soupeř',
away_logo_url: matches[0]?.awayLogoURL,
}}
countdown={countdown}
elementProps={{ 'data-element': 'matches', 'data-variant': getVariant('matches', 'compact'), 'aria-live': 'polite', style: { position: 'relative', ...getStyles('matches') } }}
/>
</div>
)
) : null}
{/* Sweepstakes / Lottery widget (visible around matches section) */}
<SweepstakeWidget />
{/* (Removed) Full-bleed top banner (homepage_top) */}
{/* Matches slider with scores by competition (moved after news+tables) */}
{facrCompetitions.length > 0 && (
defer ? (
<Suspense fallback={null}>
<MatchesSlider
key={`matches-slider-${refreshKey}-${getVariant('matches-slider', 'carousel')}`}
comps={facrCompetitions as any}
activeIndex={matchesTab}
onActiveChange={setMatchesTab}
onMatchClick={(m: any, compName?: string) => {
setSelectedMatch({ ...m, competition: compName, competitionName: compName });
setIsMatchModalOpen(true);
}}
variant={getVariant('matches-slider', 'carousel') as any}
elementProps={{ 'data-element': 'matches-slider', 'data-variant': getVariant('matches-slider', 'carousel'), style: { position: 'relative', ...getStyles('matches-slider') } }}
/>
</Suspense>
) : null
)}
{facrCompetitions.length === 0 && isLoading && (
<section data-element="matches-slider" data-variant={getVariant('matches-slider', 'carousel')} aria-label="Zápasy" style={{ position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '280px', ...getStyles('matches-slider') }}>
<div className="section-head" style={{ marginTop: 16, marginBottom: 16 }}>
<h3>Zápasy</h3>
<a href="/kalendar" className="see-all">Všechny zápasy</a>
</div>
<div style={{ display: 'flex', gap: 18, overflow: 'hidden', padding: '8px 2px 16px 2px' }}>
{[1,2,3].map((i) => (
<div key={i} className="card skeleton" style={{ minWidth: 340, height: 160, borderRadius: 12 }} />
))}
</div>
</section>
)}
{/* News + Tables: split into two independent sections */}
{(() => {
// Compute matching standings for the selected competition
const currentCompetition = facrCompetitions[matchesTab];
const currentCompetitionName = currentCompetition?.name || '';
const matchingStanding = standings.find((s: any) => s.name === currentCompetitionName);
const hasStandingsForCurrentTab = !!matchingStanding && (
(matchingStanding.table && matchingStanding.table.length > 0) ||
(matchingStanding.rows && matchingStanding.rows.length > 0)
);
const newsVariant = getVariant('news', 'grid_one');
const showNews = isVisible('news', true);
let showTable = isVisible('table', true) && hasStandingsForCurrentTab;
if (newsVariant === 'grid_one') { showTable = false; }
const variant = showNews && showTable ? undefined : 'standard';
if (!showNews && !showTable) return null;
return (
<section
key={`news-table-${refreshKey}-${newsVariant}-${getVariant('table', 'split_news')}`}
className="standings"
data-variant={variant}
style={{ marginTop: 32 }}
>
{showNews && (
<section key={`news-${refreshKey}-${newsVariant}`} data-element="news" data-variant={newsVariant} className="news-list" aria-labelledby="home-news-heading" style={{ ...getStyles('news'), contentVisibility: 'auto' as any, containIntrinsicSize: '600px' }}>
<div className="section-head" style={{ marginTop: 0 }}>
<h3 id="home-news-heading">Další aktuality</h3>
<a href="/news" className="see-all" style={{ fontSize: '0.85rem' }}>Zobrazit vše <FiArrowRight size={14} /></a>
</div>
{newsVariant === 'scroller' ? (
<BlogCardsScroller />
) : (
isLoading && (!news || (news as any).length === 0) ? (
<div className="blog-list">
{[1,2,3,4].map(i => (
<div key={i} className="card skeleton" style={{ height: 96, borderRadius: 12 }} />
))}
</div>
) : (
<NewsList items={news as any} />
)
)}
</section>
)}
{showTable && (
<div key={`table-${refreshKey}-${getVariant('table', 'split_news')}`} data-element="table" data-variant={getVariant('table', 'split_news')} role="region" aria-labelledby="home-table-heading" style={{ ...getStyles('table'), contentVisibility: 'auto' as any, containIntrinsicSize: '520px' }}>
<div className="section-head" style={{ marginTop: 0, marginBottom: 12 }}>
<h3 id="home-table-heading">Tabulky</h3>
<a href="/tabulky" className="see-all" style={{ fontSize: '0.85rem' }}>Zobrazit vše <FiArrowRight size={14} /></a>
</div>
{defer ? (
<Suspense fallback={null}>
<StandingsCard
variant={((): 'logos'|'plain' => { const v = getVariant('table_rows', 'logos'); return v === 'plain' ? 'plain' : 'logos'; })()}
rows={(matchingStanding?.table || matchingStanding?.rows || []) as any}
onRowClick={(row) => {
const clubData = {
team: (row as any).team?.name ?? (row as any).team ?? (row as any).club ?? '-',
team_id: (row as any).team_id || '',
team_logo_url: (row as any).team_logo_url,
rank: (row as any).position ?? (row as any).pos ?? (row as any).rank ?? 0,
played: (row as any).played ?? (row as any).matches ?? '-',
wins: (row as any).wins ?? (row as any).win ?? '-',
draws: (row as any).draws ?? (row as any).draw ?? '-',
losses: (row as any).losses ?? (row as any).loss ?? '-',
score: (row as any).score ?? '-',
points: (row as any).points ?? (row as any).pts ?? '-',
};
setSelectedClub(clubData);
setIsModalOpen(true);
}}
/>
</Suspense>
) : (
<div className="table-card">
<div className="standings">
{[1,2,3,4,5,6,7,8].map(i => (
<div key={i} className="standing-row skeleton" style={{ borderRadius: 12 }} />
))}
</div>
</div>
)}
{/* Banners under the table, inside the table column */}
{(banners || []).some(b => b.placement === 'homepage_under_table') && (
defer ? (
<Suspense fallback={null}>
<BannerDisplay banners={banners as any} placement="homepage_under_table" />
</Suspense>
) : null
)}
</div>
)}
</section>
);
})()}
{/* (Moved) Banner under tables now renders inside the table column above */}
{/* Competition tables moved into right column below */}
{isVisible('activities', true) && !activitiesLoaded && (
<section data-element="activities" data-variant={getVariant('activities', 'list')} aria-labelledby="home-activities-heading" style={{ marginTop: 32, marginBottom: 16, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '600px', ...getStyles('activities') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<div className="section-head" style={{ marginTop: 0 }}>
<h3 id="home-activities-heading">Aktivity</h3>
<a href="/aktivity" className="see-all">Zobrazit vše <FiArrowRight /></a>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 12 }}>
{[1,2,3].map(i => (
<div key={i} className="card skeleton" style={{ height: 120, borderRadius: 12 }} />
))}
</div>
</div>
</section>
)}
{upcomingEvents.length > 0 && isVisible('activities', true) && (
<section key={`activities-${refreshKey}-${getVariant('activities', 'list')}`} data-element="activities" data-variant={getVariant('activities', 'list')} aria-labelledby="home-activities-heading" style={{ marginTop: 32, marginBottom: 16, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '600px', ...getStyles('activities') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<div className="section-head" style={{ marginTop: 0 }}>
<h3 id="home-activities-heading">Aktivity</h3>
<a href="/aktivity" className="see-all">Zobrazit vše <FiArrowRight /></a>
</div>
<ActivitiesList items={upcomingEvents as any} />
</div>
</section>
)}
{/* Players scroller */}
{isVisible('team', false) && players.length === 0 && isLoading && (
<section data-element="team" data-variant={getVariant('team', 'grid')} className="players-scroller" aria-labelledby="home-players-heading" style={{ marginTop: 32, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '600px', ...getStyles('team') }}>
<div className="section-head">
<h3 id="home-players-heading">Hráči</h3>
<a href="/players" className="see-all">Zobrazit vše <FiArrowRight /></a>
</div>
<div className="scroll-x">
{[1,2,3,4,5,6].map(i => (
<div key={i} className="player-card skeleton" style={{ width: 170, height: 210, borderRadius: 14 }} />
))}
</div>
</section>
)}
{players.length > 0 && isVisible('team', false) && (
<section key={`team-${refreshKey}-${getVariant('team', 'grid')}`} data-element="team" data-variant={getVariant('team', 'grid')} className="players-scroller" aria-labelledby="home-players-heading" style={{ marginTop: 32, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '600px', ...getStyles('team') }}>
<div className="section-head">
<h3 id="home-players-heading">Hráči</h3>
<a href="/players" className="see-all">Zobrazit vše <FiArrowRight /></a>
</div>
<div className="scroll-x">
{players.map((p) => (
<a key={p.id} href={p.slug ? `/players/${p.slug}` : `/players/${p.id}`} className="player-card card" style={{ opacity: p.active === false ? 0.6 : 1 }}>
<div className="photo" style={{ backgroundImage: `url(${assetUrl(p.image) || p.image})` }} />
<div className="meta">{typeof p.number !== 'undefined' ? (<><span className="nr">#{p.number}</span> {p.name}</>) : p.name}</div>
<div className="pos">{p.position}</div>
</a>
))}
</div>
</section>
)}
{/* Gallery */}
{isVisible('gallery', false) && (
<section key={`gallery-${refreshKey}-${getVariant('gallery', 'grid')}`} data-element="gallery" data-variant={getVariant('gallery', 'grid')} aria-labelledby="home-gallery-heading" style={{ marginTop: 32, marginBottom: 32, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '700px', ...getStyles('gallery') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
{defer ? (
<Suspense fallback={null}>
<GallerySection zoneramaUrl={galleryUrl} />
</Suspense>
) : null}
</div>
</section>
)}
{/* Videos */}
{isVisible('videos', false) && (
<section key={`videos-${refreshKey}-${getVariant('videos', 'carousel')}`} data-element="videos" data-variant={getVariant('videos', 'carousel')} aria-labelledby="home-videos-heading" style={{ marginTop: 32, marginBottom: 32, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '700px', ...getStyles('videos') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
{defer ? (
<Suspense fallback={null}>
<VideosSection
key={`videos-comp-${refreshKey}-${getVariant('videos', 'carousel')}`}
variant={(getVariant('videos', 'carousel') as any) as 'grid' | 'carousel'}
/>
</Suspense>
) : (
<>
<div className="section-head">
<h3 id="home-videos-heading">Videa</h3>
<a href="/videa" className="see-all">Více videí</a>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 12 }}>
{[1,2,3].map((i) => (
<div key={i} className="card skeleton" style={{ height: 240, borderRadius: 12 }} />
))}
</div>
</>
)}
</div>
</section>
)}
{isVisible('merch', true) && (
<section key={`merch-${refreshKey}-${getVariant('merch', 'grid')}`} data-element="merch" data-variant={getVariant('merch', 'grid')} aria-labelledby="home-merch-heading" style={{ marginTop: 24, marginBottom: 24, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '600px', ...getStyles('merch') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
{defer ? (
<Suspense fallback={null}>
<MerchSection variant={(getVariant('merch', 'grid') as any) as 'grid' | 'carousel' | 'featured' | 'list'} />
</Suspense>
) : (
<>
<div className="section-head">
<h3 id="home-merch-heading">Oblečení týmu</h3>
<a href="/obleceni" className="see-all">Zobrazit vše</a>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
{[1,2,3,4,5].map((i) => (
<div key={i} className="card skeleton" style={{ height: 180, borderRadius: 12 }} />
))}
</div>
</>
)}
</div>
</section>
)}
{/* Polls / Voting */}
{isVisible('poll', false) && (
<section key={`poll-${refreshKey}-${getVariant('poll', 'vertical')}`} data-element="poll" data-variant={getVariant('poll', 'vertical')} aria-label="Anketa" style={{ marginTop: 32, marginBottom: 32, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '500px', ...getStyles('poll') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
{defer ? (
<Suspense fallback={null}>
<div className="card">
<PollsWidget featuredOnly={true} maxPolls={1} title="Anketa" />
</div>
</Suspense>
) : (
<div className="card skeleton" style={{ height: 320, borderRadius: 12 }} />
)}
</div>
</section>
)}
{/* Banner: homepage_footer */}
{(banners || []).some(b => b.placement === 'homepage_footer') && (
<section data-element="banner" data-variant={getVariant('banner', 'bottom')} className="banner banner-footer" style={{ margin: '24px 0', textAlign: 'center', ...getStyles('banner') }}>
{(banners || []).filter(b => b.placement === 'homepage_footer').map((b) => (
<a key={b.id} href={b.url || '#'} target={b.url ? '_blank' : undefined} rel={b.url ? 'noopener noreferrer' : undefined} style={{ display: 'inline-block', margin: 8 }}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<img loading="lazy" decoding="async" src={b.image} alt={b.name} style={{ maxWidth: '100%', width: b.width ? `${b.width}px` : undefined, height: b.height ? `${b.height}px` : 'auto' }} />
</a>
))}
</section>
)}
{/* CTA (Newsletter) moved up */}
{isVisible('newsletter', false) && (
<section key={`newsletter-${refreshKey}-${getVariant('newsletter', 'default')}`} data-element="newsletter" data-variant={getVariant('newsletter', 'default')} className="newsletter-cta" aria-label="Přihlášení k newsletteru" style={{ marginTop: 24, marginBottom: 24, position: 'relative', contentVisibility: 'auto' as any, containIntrinsicSize: '420px', ...getStyles('newsletter') }}>
<div className="card" style={{ maxWidth: 960, margin: '0 auto' }}>
{defer ? (
<Suspense fallback={null}>
<NewsletterSubscribe />
</Suspense>
) : (
<div className="skeleton" style={{ height: 280, borderRadius: 12 }} />
)}
</div>
</section>
)}
{/* Sponsors: MyUIbrix-controlled variant (grid | slider | scroller | pyramid); dark theme supported; full-bleed */}
{isVisible('sponsors', true) && (
(() => {
const variant = (getVariant('sponsors', sponsorLayout) as any) as 'grid' | 'slider' | 'scroller' | 'pyramid';
const all = sponsors || [];
const general = all.filter((s: any) => String(s.tier || '').toLowerCase() === 'general' || String(s.tier || '').toLowerCase() === 'title' || String(s.tier || '').toLowerCase() === 'main');
const standard = all.filter((s: any) => !(String(s.tier || '').toLowerCase() === 'general' || String(s.tier || '').toLowerCase() === 'title' || String(s.tier || '').toLowerCase() === 'main'));
const ordered = [...general, ...standard];
const renderPyramid = () => {
const capacities = [1, 4, 8, 12, 16];
const takeRows = (items: typeof ordered) => {
const rows: Array<typeof ordered> = [];
let idx = 0;
for (let capIndex = 0; idx < items.length && capIndex < capacities.length; capIndex++) {
const cap = capacities[capIndex];
rows.push(items.slice(idx, idx + cap));
idx += cap;
}
// If still remaining, continue with last capacity repeated
const lastCap = capacities[capacities.length - 1];
while (idx < items.length) {
rows.push(items.slice(idx, idx + lastCap));
idx += lastCap;
}
return rows;
};
const generalRows = takeRows(general);
const standardRows = takeRows(standard);
return (
<div className="pyramid">
{generalRows.map((row, i) => (
<div key={`gen-${i}`} className="pyramid-row" style={{ display: 'grid', gridTemplateColumns: `repeat(${Math.max(1, row.length)}, 1fr)`, gap: 16, marginBottom: 12 }}>
{row.map((s) => (
<a key={`g-${s.id}`} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
))}
{generalRows.length > 0 && standardRows.length > 0 && <div className="divider" aria-hidden />}
{standardRows.map((row, i) => (
<div key={`std-${i}`} className="pyramid-row" style={{ display: 'grid', gridTemplateColumns: `repeat(${Math.max(1, row.length)}, 1fr)`, gap: 16, marginBottom: 12 }}>
{row.map((s) => (
<a key={`s-${s.id}`} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
))}
</div>
);
};
return (
<section
data-element="sponsors"
data-variant={variant}
className={`sponsors ${sponsorsTheme === 'dark' ? 'dark' : ''}`}
aria-labelledby="home-sponsors-heading"
style={{
width: '100vw',
position: 'relative',
left: '50%',
right: '50%',
transform: 'translateX(-50%)',
paddingLeft: 'max(16px, calc((100vw - 1200px) / 2))',
paddingRight: 'max(16px, calc((100vw - 1200px) / 2))',
boxSizing: 'border-box',
contentVisibility: 'auto' as any,
containIntrinsicSize: '520px',
...getStyles('sponsors')
}}
>
<div className="section-head">
<h3 id="home-sponsors-heading">Sponzoři</h3>
</div>
{isLoading && ordered.length === 0 && (
<div className="sponsors-grid">
{[1,2,3,4,5,6,7,8].map(i => (
<div key={i} className="sponsor-tile skeleton" style={{ minHeight: 90, borderRadius: 12 }} />
))}
</div>
)}
{variant === 'grid' && (
<>
{general.length > 0 && (
<div className="title-sponsor">
{general.map((g) => (
<a key={`g-${g.id}`} className="sponsor-tile" href={g.url || '#'} target="_blank" rel="noreferrer noopener">
<img loading="lazy" decoding="async" src={assetUrl(g.logo) || '/images/sponsors/placeholder.png'} alt={g.name} />
</a>
))}
</div>
)}
{(general.length > 0 && standard.length > 0) && <div className="divider" aria-hidden />}
<div className="sponsors-grid">
{(standard.length > 0 ? standard : (general.length === 0 ? ordered : [])).map((s) => (
<a key={s.id} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img loading="lazy" decoding="async" src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
</>
)}
{variant === 'pyramid' && renderPyramid()}
{variant === 'slider' && (
<div className="sponsors-slider">
<div className="track">
{[...ordered, ...ordered].map((s, idx) => (
<a key={`${s.id}-${idx}`} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img loading="lazy" decoding="async" src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
</div>
)}
{variant === 'scroller' && (
<div className="sponsors-scroller">
<div className="belt">
{[...ordered, ...ordered, ...ordered].map((s, idx) => (
<a key={`${s.id}-${idx}`} className="sponsor-tile" href={s.url || '#'} target="_blank" rel="noreferrer noopener">
<img loading="lazy" decoding="async" src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
</div>
)}
</section>
);
})()
)}
</div>
<ClubModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
club={selectedClub}
clubType={(settings?.club_type as 'football' | 'futsal') || 'football'}
/>
<MatchModal
isOpen={isMatchModalOpen}
onClose={() => setIsMatchModalOpen(false)}
match={selectedMatch}
onTeamClick={(teamName, teamLogoUrl) => {
// Optional: could open club modal when clicking team in match modal
console.log('Team clicked:', teamName);
}}
/>
{isEditingMode ? (
<Suspense fallback={null}>
<MyUIbrixErrorBoundary>
<MyUIbrixStyleEditor pageType="homepage" />
</MyUIbrixErrorBoundary>
</Suspense>
) : null}
{user?.role === 'admin' && !isEditingMode ? (
<div style={{ position: 'fixed', left: 16, bottom: 16, zIndex: 10000 }}>
<Tooltip label="Aktivovat MyUIbrix Editor" placement="right">
<IconButton
aria-label="Upravit stránku"
icon={<FiEdit />}
colorScheme="blue"
size="lg"
borderRadius="full"
onClick={() => {
try {
const url = new URL(window.location.href);
url.searchParams.set('myuibrix', 'edit');
window.history.replaceState({}, '', url.toString());
} catch {}
try { document.body.classList.add('myuibrix-edit-mode'); } catch {}
setIsEditingMode(true);
}}
/>
</Tooltip>
</div>
) : null}
</MainLayout>
);
};
function czYears(n: number): string {
if (n === 1) return 'rok';
if (n >= 2 && n <= 4) return 'roky';
return 'let';
}
export default HomePage;