mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
dev day #75
This commit is contained in:
+172
-75
@@ -10,6 +10,8 @@ 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 BannerDisplay from '../components/banners/BannerDisplay';
|
||||
import BlogCardsScroller from '../components/home/BlogCardsScroller';
|
||||
import BlogSwiper from '../components/home/BlogSwiper';
|
||||
import VideosSection from '../components/home/VideosSection';
|
||||
@@ -98,8 +100,8 @@ const HomePage: React.FC = () => {
|
||||
// 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 };
|
||||
type UiSponsor = { id:number|string; name:string; logo:string; url?:string };
|
||||
type UiPlayer = { id:number|string; name:string; number?:number; position?:string; image?:string; slug?:string; age?: number };
|
||||
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 };
|
||||
@@ -400,11 +402,21 @@ const HomePage: React.FC = () => {
|
||||
number: p.jersey_number,
|
||||
position: p.position,
|
||||
image: assetUrl(p.image_url) || undefined,
|
||||
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 (also used for banners with placement metadata)
|
||||
// Load sponsors via API (sponsors only)
|
||||
try {
|
||||
const apiSponsors: ApiSponsor[] = await apiGetSponsors();
|
||||
const mapped: UiSponsor[] = (apiSponsors || []).map((s: ApiSponsor) => ({
|
||||
@@ -412,21 +424,24 @@ const HomePage: React.FC = () => {
|
||||
name: s.name,
|
||||
logo: assetUrl(s.logo_url) || '/images/sponsors/placeholder.png',
|
||||
url: s.website_url || undefined,
|
||||
tier: (s as any).tier,
|
||||
}));
|
||||
setSponsors(mapped);
|
||||
// Extract banners by placement metadata if provided
|
||||
const mappedBanners: UiBanner[] = (apiSponsors || [])
|
||||
.filter((s: any) => s && (s as any).placement)
|
||||
.map((s: any) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
image: assetUrl(s.logo_url) || '/images/sponsors/placeholder.png',
|
||||
url: s.website_url || undefined,
|
||||
placement: s.placement,
|
||||
width: typeof s.width === 'number' ? s.width : undefined,
|
||||
height: typeof s.height === 'number' ? s.height : undefined,
|
||||
}));
|
||||
if (mappedBanners.length) setBanners(mappedBanners);
|
||||
} 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 API
|
||||
@@ -472,6 +487,7 @@ const HomePage: React.FC = () => {
|
||||
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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -1032,7 +1048,8 @@ const HomePage: React.FC = () => {
|
||||
<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>
|
||||
<div className="number">#{p.number || '—'}</div>
|
||||
{typeof p.number !== 'undefined' && <div className="number">#{p.number}</div>}
|
||||
{typeof p.age === 'number' && <div className="age">{p.age} let</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1321,14 +1338,14 @@ const HomePage: React.FC = () => {
|
||||
// }
|
||||
|
||||
return (
|
||||
<MainLayout headerInsideContainer>
|
||||
<MainLayout headerInsideContainer 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 data-element="hero-topbar" data-variant={getVariant('hero-topbar', 'brand')} style={{ ...getStyles('hero-topbar') }}>
|
||||
<section data-element="hero-topbar" data-variant={getVariant('hero-topbar', 'minimal')} style={{ ...getStyles('hero-topbar') }}>
|
||||
<ClubHeroTopbar
|
||||
variant={(getVariant('hero-topbar', 'brand') as any) as 'brand' | 'minimal' | 'badge'}
|
||||
variant={(getVariant('hero-topbar', 'minimal') as any) as 'brand' | 'minimal' | 'badge'}
|
||||
fullBleed={getVariant('header', 'unified') === 'fullwidth'}
|
||||
/>
|
||||
</section>
|
||||
@@ -1488,6 +1505,11 @@ const HomePage: React.FC = () => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Full-bleed top banner (homepage_top) */}
|
||||
{(banners || []).some(b => b.placement === 'homepage_top') && (
|
||||
<BannerDisplay banners={banners as any} placement="homepage_top" />
|
||||
)}
|
||||
|
||||
{/* Matches slider with scores by competition (moved after news+tables) */}
|
||||
{facrCompetitions.length > 0 && (
|
||||
<MatchesSlider
|
||||
@@ -1513,8 +1535,10 @@ const HomePage: React.FC = () => {
|
||||
(matchingStanding.rows && matchingStanding.rows.length > 0)
|
||||
);
|
||||
|
||||
const newsVariant = getVariant('news', 'grid_one');
|
||||
const showNews = isVisible('news', true);
|
||||
const showTable = isVisible('table', true) && hasStandingsForCurrentTab;
|
||||
let showTable = isVisible('table', true) && hasStandingsForCurrentTab;
|
||||
if (newsVariant === 'grid_one') { showTable = false; }
|
||||
const variant = showNews && showTable ? undefined : 'standard';
|
||||
if (!showNews && !showTable) return null;
|
||||
|
||||
@@ -1525,7 +1549,7 @@ const HomePage: React.FC = () => {
|
||||
style={{ marginTop: 32 }}
|
||||
>
|
||||
{showNews && (
|
||||
<section data-element="news" data-variant={getVariant('news', 'grid')} className="news-list" style={{ ...getStyles('news') }}>
|
||||
<section data-element="news" data-variant={newsVariant} className="news-list" style={{ ...getStyles('news') }}>
|
||||
<div className="section-head" style={{ marginTop: 0 }}>
|
||||
<h3>Další aktuality</h3>
|
||||
<a href="/news" className="see-all" style={{ fontSize: '0.85rem' }}>Zobrazit vše <FiArrowRight size={14} /></a>
|
||||
@@ -1541,6 +1565,7 @@ const HomePage: React.FC = () => {
|
||||
<a href="/tabulky" className="see-all" style={{ fontSize: '0.85rem' }}>Zobrazit vše <FiArrowRight size={14} /></a>
|
||||
</div>
|
||||
<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 = {
|
||||
@@ -1565,6 +1590,11 @@ const HomePage: React.FC = () => {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Banner under tables (homepage_under_table) */}
|
||||
{(banners || []).some(b => b.placement === 'homepage_under_table') && (
|
||||
<BannerDisplay banners={banners as any} placement="homepage_under_table" />
|
||||
)}
|
||||
|
||||
{/* Competition tables moved into right column below */}
|
||||
|
||||
{upcomingEvents.length > 0 && isVisible('activities', true) && (
|
||||
@@ -1590,8 +1620,9 @@ const HomePage: React.FC = () => {
|
||||
{players.map((p) => (
|
||||
<a key={p.id} href={p.slug ? `/players/${p.slug}` : `/players/${p.id}`} className="player-card">
|
||||
<div className="photo" style={{ backgroundImage: `url(${assetUrl(p.image) || p.image})` }} />
|
||||
<div className="meta"><span className="nr">#{p.number}</span> {p.name}</div>
|
||||
<div className="meta">{typeof p.number !== 'undefined' ? (<><span className="nr">#{p.number}</span> {p.name}</>) : p.name}</div>
|
||||
<div className="pos">{p.position}</div>
|
||||
{typeof p.age === 'number' && <div className="age">{p.age} let</div>}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -1654,62 +1685,128 @@ const HomePage: React.FC = () => {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Sponsors: grid or slider (controlled by settings); dark theme supported; full-bleed */}
|
||||
{/* Sponsors: MyUIbrix-controlled variant (grid | slider | scroller | pyramid); dark theme supported; full-bleed */}
|
||||
{isVisible('sponsors', true) && (
|
||||
<section
|
||||
data-element="sponsors"
|
||||
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',
|
||||
...getStyles('sponsors')
|
||||
}}
|
||||
>
|
||||
<div className="section-head">
|
||||
<h3>Sponzoři</h3>
|
||||
</div>
|
||||
{sponsorLayout==='grid' ? (
|
||||
(()=>{
|
||||
const title = sponsors.find((s:any)=>s.tier==='title') || sponsors[0];
|
||||
const others = sponsors.filter((s)=>s !== title);
|
||||
(() => {
|
||||
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 (
|
||||
<>
|
||||
{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 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>
|
||||
)}
|
||||
<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>
|
||||
</>
|
||||
))}
|
||||
{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>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</section>
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
data-element="sponsors"
|
||||
data-variant={variant}
|
||||
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',
|
||||
...getStyles('sponsors')
|
||||
}}
|
||||
>
|
||||
<div className="section-head">
|
||||
<h3>Sponzoři</h3>
|
||||
</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} />
|
||||
</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 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 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 src={assetUrl(s.logo) || '/images/sponsors/placeholder.png'} alt={s.name} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
<ClubModal
|
||||
|
||||
Reference in New Issue
Block a user