This commit is contained in:
Tomas Dvorak
2025-10-23 22:26:50 +02:00
parent 63700eedb2
commit 70ea0c3c91
75 changed files with 3337 additions and 1160 deletions
+109 -148
View File
@@ -2,6 +2,7 @@ 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';
import '../styles/sparta-styles.css';
import './styles/UnifiedHome.css';
import { getPublicSettings } from '../services/settings';
import { assetUrl, sanitizeClubName } from '../utils/url';
@@ -11,9 +12,11 @@ import BlogCardsScroller from '../components/home/BlogCardsScroller';
import BlogSwiper from '../components/home/BlogSwiper';
import VideosSection from '../components/home/VideosSection';
import MerchSection from '../components/home/MerchSection';
import PollsWidget from '../components/home/PollsWidget';
import GallerySection from '../components/home/GallerySection';
import { getArticles as apiGetArticles, Article as ApiArticle } from '../services/articles';
import { getCompetitionAliasesPublic, CompetitionAlias } from '../services/competitionAliases';
import { getUpcomingEvents } from '../services/eventService';
import NewsletterSubscribe from '../components/newsletter/NewsletterSubscribe';
import MyUIbrixStyleEditor from '../components/editor/MyUIbrixEditor';
import MyUIbrixErrorBoundary from '../components/editor/MyUIbrixErrorBoundary';
@@ -91,6 +94,7 @@ const HomePage: React.FC = () => {
type UiSponsor = { id:number|string; name:string; logo:string; url?: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[]>([]);
@@ -99,6 +103,7 @@ const HomePage: React.FC = () => {
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[]>([]);
// Aliases
const [aliases, setAliases] = useState<CompetitionAlias[]>([]);
const [aliasMap, setAliasMap] = useState<Record<string, { alias: string; original_name?: string }>>({});
@@ -540,76 +545,7 @@ const HomePage: React.FC = () => {
setTimeout(run, 0);
}
}, [facrCompetitions, matchesTab, closestIndexByComp]);
// Auto-theme from club logo dominant color
useEffect(() => {
if (!clubLogo) return;
let disposed = false;
const toHex = (v: number) => {
const h = Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');
return h;
};
const rgbToHex = (r: number, g: number, b: number) => `#${toHex(r)}${toHex(g)}${toHex(b)}`;
const lighten = (r: number, g: number, b: number, amt = 20) => [
Math.min(255, r + amt),
Math.min(255, g + amt),
Math.min(255, b + amt),
] as const;
const darkenIfLowContrast = (r: number, g: number, b: number) => {
// ensure contrast versus white text (used in .next-match)
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; // 0..1
if (luminance > 0.75) {
// too light, darken
return [r * 0.6, g * 0.6, b * 0.6] as const;
}
return [r, g, b] as const;
};
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = assetUrl(clubLogo) || clubLogo;
img.onload = () => {
if (disposed) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
const w = 100, h = 100;
canvas.width = w; canvas.height = h;
try {
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
let r = 0, g = 0, b = 0, n = 0;
for (let i = 0; i < data.length; i += 4) {
const a = data[i + 3];
if (a < 64) continue; // skip transparent
const rr = data[i], gg = data[i + 1], bb = data[i + 2];
// skip near-white background pixels to better catch logo color
if (rr > 240 && gg > 240 && bb > 240) continue;
r += rr; g += gg; b += bb; n++;
}
if (n > 0) {
r /= n; g /= n; b /= n;
// adjust for readability
[r, g, b] = darkenIfLowContrast(r, g, b);
const [lr, lg, lb] = lighten(r, g, b, 24);
const primary = rgbToHex(r, g, b);
const primaryLight = rgbToHex(lr, lg, lb);
// secondary: golden fallback if color is blueish, else a subtle accent
const isBlueish = b > r && b > g;
const secondary = isBlueish ? '#d69e2e' : '#2c5282';
const root = document.documentElement;
root.style.setProperty('--primary', primary);
root.style.setProperty('--primary-light', primaryLight);
root.style.setProperty('--secondary', secondary);
}
} catch {
// ignore CORS or canvas tainting
}
};
return () => { disposed = true; };
}, [clubLogo]);
// MyUIbrix events are handled by useAllPageElementConfigs hook
// It automatically updates getVariant() and isVisible() when changes occur in edit mode
@@ -656,6 +592,26 @@ const HomePage: React.FC = () => {
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 {}
})();
return () => { active = false; };
}, []);
// Removed: Edge auto-cycle
// Removed: Aurora layout
@@ -1381,8 +1337,8 @@ const HomePage: React.FC = () => {
// }
return (
<MainLayout>
<div className="container">
<MainLayout headerInsideContainer>
<div className="container" data-element="container" style={{ ...getStyles('container') }}>
{/* Header: logo + club name */}
<div className="home-header">
<img src={assetUrl(clubLogo) || '/images/club-logo.png'} alt="Klub" />
@@ -1553,7 +1509,7 @@ const HomePage: React.FC = () => {
</section>
) : null}
{/* Matches slider with scores by competition */}
{/* Matches slider with scores by competition (moved after news+tables) */}
{facrCompetitions.length > 0 && (
<section data-element="matches-slider" className="matches-slider" style={{ position: 'relative', ...getStyles('matches-slider') }}>
<div className="section-head" style={{ marginTop: 16, marginBottom: 16 }}>
@@ -1612,54 +1568,54 @@ const HomePage: React.FC = () => {
</div>
</section>
)}
{/* Competition tables moved into right column below */}
{/* Standings: tabs per competition (only FACR), clicking row opens ClubModal */}
{isVisible('table', true) && (() => {
// Match standings to current competition by name instead of assuming same index
{/* 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 && (
const hasStandingsForCurrentTab = !!matchingStanding && (
(matchingStanding.table && matchingStanding.table.length > 0) ||
(matchingStanding.rows && matchingStanding.rows.length > 0)
);
return (
<section
data-element="table"
className="standings"
data-variant={hasStandingsForCurrentTab ? undefined : 'standard'}
style={{ marginTop: 32, ...getStyles('table') }}
>
<div>
<div className="section-head" style={{ marginTop: 0 }}>
<h3>Další aktuality</h3>
</div>
<div className="blog-list">
{news.length > 0 ? news.slice(0, 4).map((n) => (
<a key={n.id} href={`/news/${n.slug || n.id}`} className="card" style={{ textDecoration: 'none', color: 'inherit' }}>
<div className="thumb" style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }} />
<div>
<h4>{n.title}</h4>
<div style={{ color: 'var(--dark-gray)', fontSize: '0.9rem' }}>{n.excerpt}</div>
<>
{isVisible('news', true) && (
<section data-element="news" className="news-list" style={{ marginTop: 32, ...getStyles('news') }}>
<div className="section-head" style={{ marginTop: 0 }}>
<h3>Další aktuality</h3>
</div>
<div className="blog-list">
{news.length > 0 ? news.slice(0, 4).map((n) => (
<a key={n.id} href={`/news/${n.slug || n.id}`} className="card" style={{ textDecoration: 'none', color: 'inherit' }}>
<div className="thumb" style={{ backgroundImage: `url(${assetUrl(n.image) || '/images/news/placeholder.jpg'})` }} />
<div>
<h4>{n.title}</h4>
<div style={{ color: 'var(--dark-gray)', fontSize: '0.9rem' }}>{n.excerpt}</div>
</div>
</a>
)) : (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--dark-gray)', background: 'var(--bg-soft)', borderRadius: '12px' }}>
<p>Zatím nejsou k dispozici žádné aktuality.</p>
</div>
</a>
)) : (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--dark-gray)', background: 'var(--bg-soft)', borderRadius: '12px' }}>
<p>Zatím nejsou k dispozici žádné aktuality.</p>
)}
</div>
{news.length > 0 && (
<div style={{ marginTop: 12 }}>
<a className="btn" href="/news">Zobrazit všechny aktuality</a>
</div>
)}
</div>
{news.length > 0 && (
<div style={{ marginTop: 12 }}>
<a className="btn" href="/news">Zobrazit všechny aktuality</a>
</div>
)}
</div>
{hasStandingsForCurrentTab && (
<div>
</section>
)}
{isVisible('table', true) && hasStandingsForCurrentTab && (
<section
data-element="table"
className="standings"
style={{ marginTop: 32, ...getStyles('table') }}
>
<div className="table-card">
<div className="section-head" style={{ marginTop: 0, marginBottom: 12 }}>
<h3>Tabulky</h3>
@@ -1709,12 +1665,10 @@ const HomePage: React.FC = () => {
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)';
}}
@@ -1745,13 +1699,39 @@ const HomePage: React.FC = () => {
</table>
</div>
</div>
</div>
</section>
)}
</section>
</>
);
})()}
{/* Players scroller (optional) */}
{/* Competition tables moved into right column below */}
{upcomingEvents.length > 0 && isVisible('activities', true) && (
<section data-element="activities" style={{ marginTop: 32, marginBottom: 16, position: 'relative', ...getStyles('activities') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<div className="section-head" style={{ marginTop: 0 }}>
<h3>Aktivity</h3>
<a href="/aktivity" className="see-all">Zobrazit vše <FiArrowRight /></a>
</div>
<div className="blog-list">
{upcomingEvents.slice(0,4).map((e) => (
<a key={e.id} href={`/aktivita/${e.id}`} className="card" style={{ textDecoration: 'none', color: 'inherit' }}>
<div className="thumb" style={{ backgroundImage: `url(${assetUrl(e.image_url) || '/images/news/placeholder.jpg'})` }} />
<div>
<h4>{e.title}</h4>
<div style={{ color: 'var(--dark-gray)', fontSize: '0.9rem' }}>
{new Date(e.start_time).toLocaleDateString()} {e.location ? `${e.location}` : ''}
</div>
</div>
</a>
))}
</div>
</div>
</section>
)}
{/* Players scroller */}
{players.length > 0 && isVisible('team', false) && (
<section data-element="team" className="players-scroller" style={{ marginTop: 32, position: 'relative', ...getStyles('team') }}>
<div className="section-head">
@@ -1770,22 +1750,6 @@ const HomePage: React.FC = () => {
</section>
)}
{/* Merchandise / clothing (optional; only if shop URL is set) */}
{shopUrl && (
<section className="merch-cta">
<div className="card">
<div>
<h3>Oficiální fanshop</h3>
<p>Pořiďte si dresy, šály a další. Podpořte tým!</p>
<a className="btn" href={shopUrl || undefined} target="_blank" rel="noopener noreferrer">Přejít do eshopu</a>
</div>
<div className="mockup" aria-hidden>
<div className="shirt" />
</div>
</div>
</section>
)}
{/* Gallery */}
{isVisible('gallery', false) && (
<section data-element="gallery" style={{ marginTop: 32, marginBottom: 32, position: 'relative', ...getStyles('gallery') }}>
@@ -1812,27 +1776,15 @@ const HomePage: React.FC = () => {
</section>
)}
{/* Newsletter subscription CTA */}
{isVisible('newsletter', false) && (
<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 />
{/* Polls / Voting */}
{isVisible('poll', false) && (
<section data-element="poll" style={{ marginTop: 32, marginBottom: 32, position: 'relative', ...getStyles('poll') }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 12px' }}>
<PollsWidget featuredOnly={true} maxPolls={1} title="Anketa" />
</div>
</section>
)}
{/* Banner: homepage_top */}
{(banners || []).some(b => b.placement === 'homepage_top') && (
<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 */}
<img 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>
)}
{/* Banner: homepage_footer */}
{(banners || []).some(b => b.placement === 'homepage_footer') && (
<section data-element="banner" className="banner banner-footer" style={{ margin: '24px 0', textAlign: 'center', ...getStyles('banner') }}>
@@ -1845,6 +1797,15 @@ const HomePage: React.FC = () => {
</section>
)}
{/* CTA (Newsletter) moved up */}
{isVisible('newsletter', false) && (
<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>
</section>
)}
{/* Sponsors: grid or slider (controlled by settings); dark theme supported; full-bleed */}
{isVisible('sponsors', true) && (
<section