mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
dev day #69
This commit is contained in:
+109
-148
@@ -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 e‑shopu</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
|
||||
|
||||
Reference in New Issue
Block a user