dev day #90 🥳

This commit is contained in:
Tomas Dvorak
2025-11-12 20:31:37 +01:00
parent 8762bde4bf
commit f3db65d350
103 changed files with 4053 additions and 2189 deletions
+279 -125
View File
@@ -39,6 +39,7 @@ 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 = {
@@ -92,7 +93,7 @@ const HomePage: React.FC = () => {
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 }>>([]);
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);
@@ -118,10 +119,11 @@ const HomePage: React.FC = () => {
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 }>>({});
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);
@@ -164,6 +166,33 @@ const HomePage: React.FC = () => {
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;
@@ -262,8 +291,8 @@ const HomePage: React.FC = () => {
try {
aliasesList = await getCompetitionAliasesPublic();
} catch {}
const amap: Record<string, { alias: string; original_name?: string }> = {};
(aliasesList || []).forEach((a) => { if (a?.code && a?.alias) amap[a.code] = { alias: a.alias, original_name: a.original_name }; });
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 {
@@ -392,10 +421,12 @@ const HomePage: React.FC = () => {
return {
name: (amap?.[c?.code]?.alias) || c.name || c.code || 'Soutěž',
matches_link: c.matches_link,
matches: filtered
matches: filtered,
display_order: (amap?.[c?.code]?.display_order),
};
});
setFacrCompetitions(comps);
const sortedComps = sortCategoriesWithOrder(comps as any);
setFacrCompetitions(sortedComps as any);
// Next match FACR link
const first = filteredMatches?.[0];
@@ -414,7 +445,7 @@ const HomePage: React.FC = () => {
// Load players via API (include inactive to show as non-active instead of hiding)
try {
const apiPlayers: ApiPlayer[] = await apiGetPlayers({ active: false });
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(' '),
@@ -481,7 +512,7 @@ const HomePage: React.FC = () => {
const top3 = all.slice(0, 3);
setFeatured(top3);
setNews((prev) => {
const featuredKeys = new Set(top3.map((f) => (f.slug ? `s:${f.slug}` : `i:${f.id}`)));
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 {}
@@ -531,6 +562,8 @@ const HomePage: React.FC = () => {
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 || '-',
@@ -544,7 +577,8 @@ const HomePage: React.FC = () => {
score: r.score || '0:0',
})),
}));
setStandings(comps);
const sortedTables = sortCategoriesWithOrder(comps as any);
setStandings(sortedTables);
}
// Club name/logo from FACR if not provided by settings
@@ -630,6 +664,9 @@ const HomePage: React.FC = () => {
}));
if (active) setUpcomingEvents(mapped);
} catch {}
finally {
if (active) setActivitiesLoaded(true);
}
})();
return () => { active = false; };
}, []);
@@ -1402,13 +1439,17 @@ const HomePage: React.FC = () => {
</div>
</a>
) : (
<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>
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) => (
@@ -1421,13 +1462,17 @@ const HomePage: React.FC = () => {
</a>
))}
{Array.from({ length: Math.max(0, 2 - Math.min(2, Math.max(0, featured.length - 1))) }).map((_, idx) => (
<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>
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>
@@ -1438,7 +1483,7 @@ const HomePage: React.FC = () => {
{(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 src={b.image} alt={b.name} style={{ maxWidth: '100%', width: b.width ? `${b.width}px` : undefined, height: b.height ? `${b.height}px` : 'auto' }} />
<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>
@@ -1446,34 +1491,37 @@ const HomePage: React.FC = () => {
{/* Featured articles are now shown in the hero grid above, not here */}
{/* Sidebar banners (homepage_sidebar) - fixed edge rail, left/right via MyUIbrix variant */}
{/* 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={{
// Use configured styles but force fixed rail placement
...getStyles('sidebar'),
position: 'fixed',
top: 112,
left: getVariant('sidebar', 'right') === 'left' ? 12 : 'auto',
right: getVariant('sidebar', 'right') === 'left' ? 'auto' : 12,
width: 320,
maxWidth: '100%',
zIndex: 50,
pointerEvents: 'none',
}}
style={{ margin: '24px 0', ...getStyles('sidebar') }}
>
{(banners || []).filter(b => b.placement === 'homepage_sidebar').map((b) => (
<div key={b.id} className="card" style={{ display: 'block', marginBottom: 12, pointerEvents: 'auto', 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" 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 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) && (
@@ -1492,58 +1540,68 @@ const HomePage: React.FC = () => {
)}
{/* Next match: categories (competitions) with left/right navigation - synced with matchesTab */}
{facrCompetitions.length > 0 && isVisible('matches', true) ? (
(() => {
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 link = (show && (show.facr_link || show.report_url)) || comp?.matches_link || nextMatchLink;
const handleNextMatchClick = () => {
if (show) {
setSelectedMatch({
...show,
competition: comp?.name,
});
setIsMatchModalOpen(true);
} else if (link) {
window.open(link, '_blank', 'noopener,noreferrer');
}
};
{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 (
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
data={show}
competitionName={comp?.name}
countdown={countdown}
onPrev={() => setMatchesTab((i) => (i - 1 + facrCompetitions.length) % facrCompetitions.length)}
onNext={() => setMatchesTab((i) => (i + 1) % facrCompetitions.length)}
onOpen={handleNextMatchClick}
elementProps={{
'data-element': 'matches' as any,
'data-variant': getVariant('matches', 'compact') as any,
style: { ...getStyles('matches') },
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') } }}
/>
);
})()
) : isVisible('matches', true) ? (
<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'), style: { position: 'relative', ...getStyles('matches') } }}
/>
</div>
</div>
)
) : null}
{/* Sweepstakes / Lottery widget (visible around matches section) */}
@@ -1570,6 +1628,20 @@ const HomePage: React.FC = () => {
</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 */}
{(() => {
@@ -1597,23 +1669,31 @@ const HomePage: React.FC = () => {
style={{ marginTop: 32 }}
>
{showNews && (
<section key={`news-${refreshKey}-${newsVariant}`} data-element="news" data-variant={newsVariant} className="news-list" style={{ ...getStyles('news') }}>
<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>Další aktuality</h3>
<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 />
) : (
<NewsList items={news as any} />
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')} style={{ ...getStyles('table') }}>
<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>Tabulky</h3>
<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 ? (
@@ -1639,7 +1719,15 @@ const HomePage: React.FC = () => {
}}
/>
</Suspense>
) : null}
) : (
<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 ? (
@@ -1657,12 +1745,28 @@ const HomePage: React.FC = () => {
{/* (Moved) Banner under tables now renders inside the table column above */}
{/* Competition tables moved into right column below */}
{upcomingEvents.length > 0 && isVisible('activities', true) && (
<section key={`activities-${refreshKey}-${getVariant('activities', 'list')}`} data-element="activities" data-variant={getVariant('activities', 'list')} style={{ marginTop: 32, marginBottom: 16, position: 'relative', ...getStyles('activities') }}>
{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>Aktivity</h3>
<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} />
@@ -1671,10 +1775,25 @@ const HomePage: React.FC = () => {
)}
{/* Players scroller */}
{players.length > 0 && isVisible('team', false) && (
<section key={`team-${refreshKey}-${getVariant('team', 'grid')}`} data-element="team" data-variant={getVariant('team', 'grid')} className="players-scroller" style={{ marginTop: 32, position: 'relative', ...getStyles('team') }}>
{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>Hráči</h3>
<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">
@@ -1691,7 +1810,7 @@ const HomePage: React.FC = () => {
{/* Gallery */}
{isVisible('gallery', false) && (
<section key={`gallery-${refreshKey}-${getVariant('gallery', 'grid')}`} data-element="gallery" data-variant={getVariant('gallery', 'grid')} style={{ marginTop: 32, marginBottom: 32, position: 'relative', ...getStyles('gallery') }}>
<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}>
@@ -1704,7 +1823,7 @@ const HomePage: React.FC = () => {
{/* Videos */}
{isVisible('videos', false) && (
<section key={`videos-${refreshKey}-${getVariant('videos', 'carousel')}`} data-element="videos" data-variant={getVariant('videos', 'carousel')} style={{ marginTop: 32, marginBottom: 32, position: 'relative', ...getStyles('videos') }}>
<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}>
@@ -1713,26 +1832,50 @@ const HomePage: React.FC = () => {
variant={(getVariant('videos', 'carousel') as any) as 'grid' | 'carousel'}
/>
</Suspense>
) : null}
) : (
<>
<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')} style={{ marginTop: 24, marginBottom: 24, position: 'relative', ...getStyles('merch') }}>
<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>
) : null}
) : (
<>
<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')} style={{ marginTop: 32, marginBottom: 32, position: 'relative', ...getStyles('poll') }}>
<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}>
@@ -1740,7 +1883,9 @@ const HomePage: React.FC = () => {
<PollsWidget featuredOnly={true} maxPolls={1} title="Anketa" />
</div>
</Suspense>
) : null}
) : (
<div className="card skeleton" style={{ height: 320, borderRadius: 12 }} />
)}
</div>
</section>
)}
@@ -1751,7 +1896,7 @@ const HomePage: React.FC = () => {
{(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 src={b.image} alt={b.name} style={{ maxWidth: '100%', width: b.width ? `${b.width}px` : undefined, height: b.height ? `${b.height}px` : 'auto' }} />
<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>
@@ -1759,13 +1904,15 @@ const HomePage: React.FC = () => {
{/* 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" style={{ marginTop: 24, marginBottom: 24, position: 'relative', ...getStyles('newsletter') }}>
<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>
) : null}
) : (
<div className="skeleton" style={{ height: 280, borderRadius: 12 }} />
)}
</div>
</section>
)}
@@ -1830,6 +1977,7 @@ const HomePage: React.FC = () => {
data-element="sponsors"
data-variant={variant}
className={`sponsors ${sponsorsTheme === 'dark' ? 'dark' : ''}`}
aria-labelledby="home-sponsors-heading"
style={{
width: '100vw',
position: 'relative',
@@ -1839,19 +1987,28 @@ const HomePage: React.FC = () => {
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>Sponzoři</h3>
<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 src={assetUrl(g.logo) || '/images/sponsors/placeholder.png'} alt={g.name} />
<img loading="lazy" decoding="async" src={assetUrl(g.logo) || '/images/sponsors/placeholder.png'} alt={g.name} />
</a>
))}
</div>
@@ -1860,7 +2017,7 @@ const HomePage: React.FC = () => {
<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 src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
<img loading="lazy" decoding="async" src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
@@ -1872,7 +2029,7 @@ const HomePage: React.FC = () => {
<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 src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
<img loading="lazy" decoding="async" src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
@@ -1883,7 +2040,7 @@ const HomePage: React.FC = () => {
<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 src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
<img loading="lazy" decoding="async" src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
</a>
))}
</div>
@@ -1943,11 +2100,8 @@ const HomePage: React.FC = () => {
};
function czYears(n: number): string {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 14) return 'let';
const mod10 = n % 10;
if (mod10 === 1) return 'rok';
if (mod10 >= 2 && mod10 <= 4) return 'roky';
if (n === 1) return 'rok';
if (n >= 2 && n <= 4) return 'roky';
return 'let';
}