This commit is contained in:
Tomas Dvorak
2025-10-21 15:02:05 +02:00
parent 68e69e00cc
commit 63700eedb2
103 changed files with 12442 additions and 446 deletions
+143 -65
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useMemo } from 'react';
import MainLayout from '../components/layout/MainLayout';
import { FiArrowRight, FiCalendar, FiUsers, FiAward, FiChevronLeft, FiChevronRight } from 'react-icons/fi';
import '../styles/theme.css';
@@ -16,6 +16,7 @@ import { getArticles as apiGetArticles, Article as ApiArticle } from '../service
import { getCompetitionAliasesPublic, CompetitionAlias } from '../services/competitionAliases';
import NewsletterSubscribe from '../components/newsletter/NewsletterSubscribe';
import MyUIbrixStyleEditor from '../components/editor/MyUIbrixEditor';
import MyUIbrixErrorBoundary from '../components/editor/MyUIbrixErrorBoundary';
import ClubModal from '../components/home/ClubModal';
import MatchModal from '../components/home/MatchModal';
import { useAllPageElementConfigs } from '../hooks/usePageElementConfig';
@@ -105,7 +106,19 @@ const HomePage: React.FC = () => {
const [isLoading, setIsLoading] = useState<boolean>(true);
// MyUIbrix element configuration hook for live preview
const { getVariant, isVisible, loading: configLoading } = useAllPageElementConfigs('homepage');
const { getVariant, isVisible, getStyles, loading: configLoading, refreshKey } = useAllPageElementConfigs('homepage');
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]);
useEffect(() => {
let cancelled = false;
@@ -287,25 +300,28 @@ const HomePage: React.FC = () => {
};
})
);
// Sort by datetime and pick upcoming first
// 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 upcoming = allMatches
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 }) => typeof m.__ts === 'number' && !isNaN(m.__ts!) && (m.__ts as number) >= now)
.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);
const chosen = upcoming.length ? upcoming : allMatches;
setMatches(chosen);
setMatches(filteredMatches);
// Build competitions with their matches for slider
const comps = (facrClubJSON.competitions || []).map((c: any) => ({
name: (amap?.[c?.code]?.alias) || c.name || c.code || 'Soutěž',
matches_link: c.matches_link,
matches: (Array.isArray(c.matches) ? c.matches : []).map((m: any, idx: number) => {
// 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('.');
@@ -324,8 +340,18 @@ const HomePage: React.FC = () => {
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
};
});
setFacrCompetitions(comps);
// Compute closest match index per competition to current time
@@ -345,7 +371,7 @@ const HomePage: React.FC = () => {
setClosestIndexByComp(closestIdx);
// Next match FACR link
const first = chosen?.[0];
const first = filteredMatches?.[0];
setNextMatchLink((first && (first.facr_link || first.report_url)) || comps?.[0]?.matches_link || facrClubJSON?.url);
} else {
setMatches(mapMatches(matchesJSON));
@@ -464,7 +490,8 @@ const HomePage: React.FC = () => {
table: (c.table?.overall || []).map((r: any, idx: number) => ({
position: Number(r.rank || idx + 1),
team: r.team || r.team_name || '-',
team_logo_url: r.team_logo_url,
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),
@@ -1367,7 +1394,7 @@ const HomePage: React.FC = () => {
{/* Hero section: variant controlled by MyUIbrix (getVariant) or fallback to settings.hero_style */}
{getVariant('hero', heroStyle) === 'grid' && isVisible('hero', true) && (
<section data-element="hero" className="hero-grid">
<section key={`hero-grid-${refreshKey}`} data-element="hero" 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'})` }} />
@@ -1409,7 +1436,7 @@ const HomePage: React.FC = () => {
)}
{/* Banner: homepage_middle */}
{(banners || []).some(b => b.placement === 'homepage_middle') && isVisible('banner', true) && (
<section data-element="banner" className="banner banner-middle" style={{ margin: '24px 0', textAlign: 'center' }}>
<section data-element="banner" 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 */}
@@ -1423,7 +1450,7 @@ const HomePage: React.FC = () => {
{/* Sidebar banners (homepage_sidebar) */}
{(banners || []).some(b => b.placement === 'homepage_sidebar') && (
<section data-element="sidebar" className="banner banner-sidebar" style={{ margin: '24px 0' }}>
<section data-element="sidebar" className="banner banner-sidebar" style={{ margin: '24px 0', ...getStyles('sidebar') }}>
{/* Simple responsive behavior: stack on mobile, sticky right rail on desktop */}
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ width: 320, maxWidth: '100%', position: 'sticky' as const, top: 96 }}>
@@ -1438,13 +1465,14 @@ const HomePage: React.FC = () => {
</section>
)}
{getVariant('hero', heroStyle) === 'scroller' && isVisible('hero', true) && (
<section data-element="hero">
<section key={`hero-scroller-${refreshKey}`} data-element="hero" style={{ position: 'relative', ...getStyles('hero') }}>
<BlogCardsScroller />
</section>
)}
{(getVariant('hero', heroStyle) === 'swiper' || getVariant('hero', heroStyle) === 'swiper_full') && isVisible('hero', true) && (
<section data-element="hero" style={getVariant('hero', heroStyle) === 'swiper_full' ? { marginLeft: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)' } : undefined}>
<BlogSwiper />
<section key={`hero-swiper-${refreshKey}`} data-element="hero" style={getVariant('hero', heroStyle) === 'swiper_full' ? { position: 'relative', marginLeft: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)', ...getStyles('hero') } : { position: 'relative', ...getStyles('hero') }}>
<BlogSwiper fallbackArticles={heroFallbackArticles}
/>
</section>
)}
@@ -1470,7 +1498,7 @@ const HomePage: React.FC = () => {
}
};
return (
<section data-element="matches" className="next-match" onClick={handleNextMatchClick} style={{ cursor: 'pointer' }}>
<section data-element="matches" className="next-match" onClick={handleNextMatchClick} style={{ cursor: 'pointer', position: 'relative', ...getStyles('matches') }}>
<button
aria-label="Předchozí soutěž"
onClick={(e) => { e.stopPropagation(); setMatchesTab((i) => (i - 1 + facrCompetitions.length) % facrCompetitions.length); }}
@@ -1504,7 +1532,7 @@ const HomePage: React.FC = () => {
);
})()
) : isVisible('matches', true) ? (
<section data-element="matches" className="next-match">
<section data-element="matches" className="next-match" style={{ position: 'relative', ...getStyles('matches') }}>
<div className="team">
<img className="logo" src={assetUrl(matches[0]?.homeLogoURL) || assetUrl(clubLogo) || '/images/club-logo.png'} alt="Domácí" />
<div>{sanitizeClubName(matches[0]?.homeTeam || clubName)}</div>
@@ -1527,7 +1555,7 @@ const HomePage: React.FC = () => {
{/* Matches slider with scores by competition */}
{facrCompetitions.length > 0 && (
<section className="matches-slider">
<section data-element="matches-slider" className="matches-slider" style={{ position: 'relative', ...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 <FiArrowRight /></a>
@@ -1603,7 +1631,7 @@ const HomePage: React.FC = () => {
data-element="table"
className="standings"
data-variant={hasStandingsForCurrentTab ? undefined : 'standard'}
style={{ marginTop: 32 }}
style={{ marginTop: 32, ...getStyles('table') }}
>
<div>
<div className="section-head" style={{ marginTop: 0 }}>
@@ -1637,37 +1665,84 @@ const HomePage: React.FC = () => {
<h3>Tabulky</h3>
<a href="/tabulky" className="see-all" style={{ fontSize: '0.85rem' }}>Zobrazit vše <FiArrowRight size={14} /></a>
</div>
<div className="standings">
{(matchingStanding?.table || matchingStanding?.rows || []).slice(0,8).map((row: any, idx: number) => {
const handleClick = () => {
const clubData = {
team: row.team?.name ?? row.team ?? row.club ?? '-',
team_id: row.team_id || '',
team_logo_url: row.team_logo_url,
rank: row.position ?? row.pos ?? row.rank ?? idx+1,
played: row.played ?? row.matches ?? '-',
wins: row.wins ?? row.win ?? '-',
draws: row.draws ?? row.draw ?? '-',
losses: row.losses ?? row.loss ?? '-',
score: row.score ?? '-',
points: row.points ?? row.pts ?? '-',
};
setSelectedClub(clubData);
setIsModalOpen(true);
};
return (
<div key={idx} className="standing-row" onClick={handleClick}>
<div className="pos">#{row.position ?? row.pos ?? row.rank ?? idx+1}</div>
<div className="team">
{row.team_logo_url && (
<img src={assetUrl(row.team_logo_url)} alt={row.team?.name ?? row.team ?? row.club ?? '-'} />
)}
<span className="name">{row.team?.name ?? row.team ?? row.club ?? '-'}</span>
</div>
<div className="pts">{row.points ?? row.pts ?? '-'}</div>
</div>
);
})}
<div className="standings-table-wrapper" style={{ overflowX: 'auto' }}>
<table className="standings-table-compact" style={{ width: '100%', borderCollapse: 'separate', borderSpacing: '0 4px' }}>
<thead>
<tr style={{ fontSize: '0.75rem', color: 'var(--dark-gray)', textTransform: 'uppercase' }}>
<th style={{ padding: '6px 8px', textAlign: 'left', fontWeight: 600 }}>#</th>
<th style={{ padding: '6px 8px', textAlign: 'left', fontWeight: 600 }}>Tým</th>
<th style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600 }}>Z</th>
<th style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600 }}>V</th>
<th style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600 }}>R</th>
<th style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600 }}>P</th>
<th style={{ padding: '6px 4px', textAlign: 'center', fontWeight: 600, display: 'none' }} className="hide-mobile">Skóre</th>
<th style={{ padding: '6px 8px', textAlign: 'center', fontWeight: 600 }}>Body</th>
</tr>
</thead>
<tbody>
{(matchingStanding?.table || matchingStanding?.rows || []).slice(0,8).map((row: any, idx: number) => {
const handleClick = () => {
const clubData = {
team: row.team?.name ?? row.team ?? row.club ?? '-',
team_id: row.team_id || '',
team_logo_url: row.team_logo_url,
rank: row.position ?? row.pos ?? row.rank ?? idx+1,
played: row.played ?? row.matches ?? '-',
wins: row.wins ?? row.win ?? '-',
draws: row.draws ?? row.draw ?? '-',
losses: row.losses ?? row.loss ?? '-',
score: row.score ?? '-',
points: row.points ?? row.pts ?? '-',
};
setSelectedClub(clubData);
setIsModalOpen(true);
};
return (
<tr
key={idx}
onClick={handleClick}
style={{
cursor: 'pointer',
background: 'var(--card-bg)',
border: '1px solid var(--card-border)',
borderRadius: '8px',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateX(2px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.08)';
e.currentTarget.style.borderColor = 'var(--primary)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateX(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'var(--card-border)';
}}
>
<td style={{ padding: '10px 8px', fontWeight: 700, color: 'var(--primary)', fontSize: '0.9rem' }}>#{row.position ?? row.pos ?? row.rank ?? idx+1}</td>
<td style={{ padding: '10px 8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
{row.team_logo_url && (
<img
src={assetUrl(row.team_logo_url)}
alt={row.team?.name ?? row.team ?? row.club ?? '-'}
style={{ width: '24px', height: '24px', borderRadius: '50%', objectFit: 'cover', background: 'var(--bg-soft)', border: '1px solid var(--card-border)', flexShrink: 0 }}
/>
)}
<span style={{ fontWeight: 600, color: 'var(--text)', fontSize: '0.9rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{row.team?.name ?? row.team ?? row.club ?? '-'}</span>
</div>
</td>
<td style={{ padding: '10px 4px', textAlign: 'center', fontSize: '0.85rem', color: 'var(--text)' }}>{row.played ?? row.matches ?? '-'}</td>
<td style={{ padding: '10px 4px', textAlign: 'center', fontSize: '0.85rem', color: 'var(--text)' }}>{row.wins ?? row.win ?? '-'}</td>
<td style={{ padding: '10px 4px', textAlign: 'center', fontSize: '0.85rem', color: 'var(--text)' }}>{row.draws ?? row.draw ?? '-'}</td>
<td style={{ padding: '10px 4px', textAlign: 'center', fontSize: '0.85rem', color: 'var(--text)' }}>{row.losses ?? row.loss ?? '-'}</td>
<td style={{ padding: '10px 4px', textAlign: 'center', fontSize: '0.85rem', color: 'var(--text)', display: 'none' }} className="hide-mobile">{row.score ?? '-'}</td>
<td style={{ padding: '10px 8px', textAlign: 'center', fontWeight: 700, color: 'var(--secondary)', fontSize: '1rem' }}>{row.points ?? row.pts ?? '-'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
@@ -1678,7 +1753,7 @@ const HomePage: React.FC = () => {
{/* Players scroller (optional) */}
{players.length > 0 && isVisible('team', false) && (
<section data-element="team" className="players-scroller" style={{ marginTop: 32 }}>
<section data-element="team" className="players-scroller" style={{ marginTop: 32, position: 'relative', ...getStyles('team') }}>
<div className="section-head">
<h3>Hráči</h3>
<a href="/players" className="see-all">Zobrazit vše <FiArrowRight /></a>
@@ -1713,7 +1788,7 @@ const HomePage: React.FC = () => {
{/* Gallery */}
{isVisible('gallery', false) && (
<section data-element="gallery" style={{ marginTop: 32, marginBottom: 32 }}>
<section data-element="gallery" style={{ marginTop: 32, marginBottom: 32, position: 'relative', ...getStyles('gallery') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<GallerySection />
</div>
@@ -1722,7 +1797,7 @@ const HomePage: React.FC = () => {
{/* Videos */}
{isVisible('videos', false) && (
<section data-element="videos" style={{ marginTop: 32, marginBottom: 32 }}>
<section data-element="videos" style={{ marginTop: 32, marginBottom: 32, position: 'relative', ...getStyles('videos') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<VideosSection />
</div>
@@ -1730,7 +1805,7 @@ const HomePage: React.FC = () => {
)}
{isVisible('merch', true) && (
<section data-element="merch" style={{ marginTop: 24, marginBottom: 24 }}>
<section data-element="merch" style={{ marginTop: 24, marginBottom: 24, position: 'relative', ...getStyles('merch') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<MerchSection />
</div>
@@ -1739,7 +1814,7 @@ const HomePage: React.FC = () => {
{/* Newsletter subscription CTA */}
{isVisible('newsletter', false) && (
<section data-element="newsletter" className="newsletter-cta" style={{ marginTop: 24, marginBottom: 24 }}>
<section data-element="newsletter" className="newsletter-cta" style={{ marginTop: 24, marginBottom: 24, position: 'relative', ...getStyles('newsletter') }}>
<div className="card" style={{ maxWidth: 960, margin: '0 auto' }}>
<NewsletterSubscribe />
</div>
@@ -1748,7 +1823,7 @@ const HomePage: React.FC = () => {
{/* Banner: homepage_top */}
{(banners || []).some(b => b.placement === 'homepage_top') && (
<section data-element="banner" className="banner banner-top" style={{ margin: '24px 0', textAlign: 'center' }}>
<section data-element="banner" className="banner banner-top" style={{ margin: '24px 0', textAlign: 'center', ...getStyles('banner') }}>
{(banners || []).filter(b => b.placement === 'homepage_top').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 */}
@@ -1760,7 +1835,7 @@ const HomePage: React.FC = () => {
{/* Banner: homepage_footer */}
{(banners || []).some(b => b.placement === 'homepage_footer') && (
<section data-element="banner" className="banner banner-footer" style={{ margin: '24px 0', textAlign: 'center' }}>
<section data-element="banner" 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 */}
@@ -1784,6 +1859,7 @@ const HomePage: React.FC = () => {
paddingLeft: 'max(16px, calc((100vw - 1200px) / 2))',
paddingRight: 'max(16px, calc((100vw - 1200px) / 2))',
boxSizing: 'border-box',
...getStyles('sponsors')
}}
>
<div className="section-head">
@@ -1842,7 +1918,9 @@ const HomePage: React.FC = () => {
console.log('Team clicked:', teamName);
}}
/>
<MyUIbrixStyleEditor pageType="homepage" />
<MyUIbrixErrorBoundary>
<MyUIbrixStyleEditor pageType="homepage" />
</MyUIbrixErrorBoundary>
</MainLayout>
);
};