mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
2109 lines
106 KiB
TypeScript
2109 lines
106 KiB
TypeScript
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;
|