feat: add participation and network support pages, update about fields and home winners section

- Added support for '/teilnahme' and '/netzwerk' in findSupportPages function.
- Updated the description in aboutFields to clarify the relationship with the homepage Preisträger grid.
- Enhanced the About page by integrating HomeWinnersSection for better modularity.
- Created HomeWinnersSection component to encapsulate winners display logic and improve code organization.
- Removed unused WinnerCardData type and related logic from Home component.
This commit is contained in:
syntaxbullet
2026-07-13 19:18:12 +02:00
parent 234da4cdac
commit 308d75c44a
6 changed files with 217 additions and 275 deletions

View File

@@ -89,6 +89,9 @@ async function findSupportPages(payload: Awaited<ReturnType<typeof getPayload>>,
{ spaPath: { equals: '/netzwerk' } }, { spaPath: { equals: '/netzwerk' } },
{ slug: { equals: 'netzwerk' } }, { slug: { equals: 'netzwerk' } },
{ slug: { equals: 'network' } }, { slug: { equals: 'network' } },
{ spaPath: { equals: '/teilnahme' } },
{ slug: { equals: 'teilnahme' } },
{ slug: { equals: 'participation' } },
], ],
}, },
}) })

View File

@@ -47,7 +47,7 @@ export const aboutFields: Field[] = [
type: 'group', type: 'group',
admin: { admin: {
description: description:
'Edit the Der BMP page in the same order it appears on the frontend. The generic Hero and Content tabs are hidden for this SPA-managed route.', 'Edit the Der BMP page in the same order it appears on the frontend. The Preisträger grid reuses the homepage Preisträger card grid and is edited on the Startseite page. The generic Hero and Content tabs are hidden for this SPA-managed route.',
}, },
fields: [ fields: [
{ {
@@ -127,7 +127,10 @@ export const aboutFields: Field[] = [
name: 'highlights', name: 'highlights',
label: '04 · Preisträger highlights', label: '04 · Preisträger highlights',
type: 'group', type: 'group',
admin: sectionAdmin('Header copy and editor-selected winners for the photo grid.'), admin: {
...sectionAdmin('Legacy data retained for compatibility. The rendered section now reuses the homepage Preisträger card grid.'),
hidden: true,
},
fields: [ fields: [
uploadField('fallbackImage', 'Fallback winner image', `Current frontend fallback image: /images/${aboutContent.highlights.fallbackImageFilename}`), uploadField('fallbackImage', 'Fallback winner image', `Current frontend fallback image: /images/${aboutContent.highlights.fallbackImageFilename}`),
text('eyebrow', 'Eyebrow', aboutContent.highlights.eyebrow), text('eyebrow', 'Eyebrow', aboutContent.highlights.eyebrow),

View File

@@ -1118,7 +1118,7 @@ export interface Page {
}; };
}; };
/** /**
* Edit the Der BMP page in the same order it appears on the frontend. The generic Hero and Content tabs are hidden for this SPA-managed route. * Edit the Der BMP page in the same order it appears on the frontend. The Preisträger grid reuses the homepage Preisträger card grid and is edited on the Startseite page. The generic Hero and Content tabs are hidden for this SPA-managed route.
*/ */
about?: { about?: {
/** /**
@@ -1195,7 +1195,7 @@ export interface Page {
| null; | null;
}; };
/** /**
* Header copy and editor-selected winners for the photo grid. * Legacy data retained for compatibility. The rendered section now reuses the homepage Preisträger card grid.
*/ */
highlights?: { highlights?: {
/** /**

View File

@@ -0,0 +1,186 @@
import React, { useRef, useState } from 'react';
import { ChevronLeft, ChevronRight, Star } from 'lucide-react';
import Image from '@/spa/components/ui/UnoptimizedImage';
import { mediaUrl } from '@/spa/cmsMediaField';
import { useCmsCollection } from '@/spa/cmsRoute';
import { WINNERS } from '@/spa/data/winners';
import { homeContent } from '@/spa/homeContent';
import { resolveHomeWinners } from '@/spa/homeWinnerSelection';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder';
import { Link } from '@/spa/router';
type CmsRecord = Record<string, unknown>;
export type HomeWinnersContent = CmsRecord & {
cardFallbackTitle?: string | null;
cta?: { label?: string | null; url?: string | null } | null;
eyebrow?: string | null;
fallbackImage?: unknown;
featured?: unknown;
heading?: string | null;
hoverLabel?: string | null;
};
type WinnerCardData = CmsRecord & {
id?: string | number;
slug?: string;
title?: string;
name?: string;
img?: string;
image?: unknown;
spaPath?: string;
year?: string | number;
awardType?: string;
type?: string;
category?: string;
};
const fallbackText = (value: unknown, fallback: string) =>
typeof value === 'string' && value.length > 0 ? value : fallback;
function Lines({ text }: { text: string }) {
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
}
function HomeWinnerCard({
winner,
fallbackTitle,
fallbackImage,
hoverLabel,
horizontalScroll = false,
}: {
winner: WinnerCardData;
fallbackTitle: string;
fallbackImage: string;
hoverLabel: string;
horizontalScroll?: boolean;
}) {
const [hovered, setHovered] = useState(false);
const isMobile = useIsMobile();
const staticWinner = WINNERS.find(
(item) =>
String(item.id) === String(winner?.id) ||
item.slug === winner?.slug ||
item.name === winner?.title ||
item.name === winner?.name,
);
const title = fallbackText(winner?.title, fallbackText(winner?.name, staticWinner?.name || fallbackTitle));
const imageSrc = winner?.img || mediaUrl(winner?.image, fallbackImage);
const href = winner?.spaPath || (winner?.slug ? `/preistraeger/${winner.slug}` : staticWinner ? `/preistraeger/${staticWinner.slug}` : '/preistraeger');
return (
<Link
to={href}
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile || horizontalScroll ? 'start' : undefined }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Image unoptimized src={imageSrc} alt={title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '18px 20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 5 }}>
<Star size={10} style={{ color: '#EFBF04', flexShrink: 0 }} fill="#EFBF04" />
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, color: '#EFBF04', letterSpacing: '0.12em', textTransform: 'uppercase' }}>{winner?.year || staticWinner?.year} · {winner?.awardType || winner?.type || staticWinner?.type}</span>
</div>
<h3 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight: 700, color: '#fff', margin: '0 0 3px', lineHeight: 1.3 }}>{title}</h3>
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 14, color: 'rgba(255,255,255,0.75)', margin: 0 }}>{winner?.category || staticWinner?.category}</p>
<div style={{ marginTop: 10, display: 'inline-block', background: '#EFBF04', color: '#101828', padding: '5px 14px', borderRadius: 999, fontSize: 10, fontFamily: '"IBM Plex Sans", sans-serif', fontWeight: 800, letterSpacing: '0.1em', textTransform: 'uppercase', opacity: hovered ? 1 : 0, transform: hovered ? 'translateY(0)' : 'translateY(6px)', transition: 'all 0.25s' }}>
{hoverLabel}
</div>
</div>
</Link>
);
}
export default function HomeWinnersSection({ content }: { content?: HomeWinnersContent }) {
const winnersScrollRef = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile();
const placeholderImage = usePreistraegerPlaceholderImage();
const cmsPreistraeger = useCmsCollection('preistraeger');
const winnersSection = content || {};
const homeWinners = resolveHomeWinners(winnersSection.featured, cmsPreistraeger, WINNERS);
const scrollDesktopWinners = !isMobile && homeWinners.length > 8;
const scrollWinnerGrid = isMobile || scrollDesktopWinners;
const scrollWinners = (direction: -1 | 1) => {
const container = winnersScrollRef.current;
if (!container) return;
container.scrollBy({
left: direction * Math.max(container.clientWidth * 0.8, 320),
behavior: 'smooth',
});
};
return (
<section style={{ background: '#111D55' }}>
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', justifyContent: 'space-between', gap: isMobile ? 16 : 0 }}>
<div>
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>{fallbackText(winnersSection.eyebrow, homeContent.winners.eyebrow)}</span>
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: 0, lineHeight: 1 }}>
<Lines text={fallbackText(winnersSection.heading, homeContent.winners.heading)} />
</h2>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
{scrollDesktopWinners && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button
type="button"
aria-label="Vorherige Preisträger anzeigen"
onClick={() => scrollWinners(-1)}
style={{ width: 42, height: 42, border: '1px solid rgba(255,255,255,0.28)', background: 'transparent', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
>
<ChevronLeft size={18} />
</button>
<button
type="button"
aria-label="Weitere Preisträger anzeigen"
onClick={() => scrollWinners(1)}
style={{ width: 42, height: 42, border: '1px solid rgba(255,255,255,0.28)', background: 'transparent', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
>
<ChevronRight size={18} />
</button>
</div>
)}
<Link
to={fallbackText(winnersSection.cta?.url, homeContent.winners.cta.url)}
style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#EFBF04', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: '1px solid #EFBF04', whiteSpace: 'nowrap' }}
>
{fallbackText(winnersSection.cta?.label, homeContent.winners.cta.label)} <ChevronRight size={14} />
</Link>
</div>
</div>
<div ref={winnersScrollRef} style={{
display: 'grid',
gridTemplateColumns: scrollWinnerGrid ? undefined : 'repeat(4, 1fr)',
gridAutoFlow: scrollWinnerGrid ? 'column' : undefined,
gridTemplateRows: scrollWinnerGrid ? `repeat(2, ${isMobile ? '1fr' : '280px'})` : undefined,
gridAutoColumns: isMobile ? '66vw' : scrollDesktopWinners ? '25vw' : undefined,
overflowX: scrollWinnerGrid ? 'auto' : undefined,
scrollSnapType: scrollWinnerGrid ? 'x mandatory' : undefined,
overscrollBehaviorX: scrollWinnerGrid ? 'contain' : undefined,
gap: isMobile ? 8 : 0,
padding: isMobile ? '0 16px 8px' : scrollDesktopWinners ? '0 0 12px' : 0,
WebkitOverflowScrolling: 'touch',
scrollbarWidth: scrollDesktopWinners ? 'thin' : 'none',
scrollbarColor: scrollDesktopWinners ? '#EFBF04 rgba(255,255,255,0.14)' : undefined,
}}>
{homeWinners.map((winner) => (
<HomeWinnerCard
key={(winner as WinnerCardData).id || (winner as WinnerCardData).slug}
winner={winner as WinnerCardData}
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
fallbackImage={placeholderImage}
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
horizontalScroll={scrollDesktopWinners}
/>
))}
</div>
</section>
);
}

View File

@@ -1,40 +1,32 @@
import React, { useMemo, useState } from 'react' import React, { useState } from 'react'
import { ChevronRight, Star } from 'lucide-react' import { ChevronRight } from 'lucide-react'
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection' import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection'
import HomeWinnersSection, { type HomeWinnersContent } from '@/spa/components/HomeWinnersSection'
import TestimonialsSection from '@/spa/components/TestimonialsSection' import TestimonialsSection from '@/spa/components/TestimonialsSection'
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg' import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'
import Image from '@/spa/components/ui/UnoptimizedImage' import Image from '@/spa/components/ui/UnoptimizedImage'
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField' import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
import { resolveCmsRelationship, useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute' import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute'
import { WINNERS, type Winner } from '@/spa/data/winners'
import { useIsMobile } from '@/spa/hooks/useIsMobile' import { useIsMobile } from '@/spa/hooks/useIsMobile'
import { Link } from '@/spa/router' import { Link } from '@/spa/router'
import { aboutContent } from '@/spa/aboutContent' import { aboutContent } from '@/spa/aboutContent'
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
const FF = '"IBM Plex Sans", sans-serif' const FF = '"IBM Plex Sans", sans-serif'
const FB = '"Inter", sans-serif' const FB = '"Inter", sans-serif'
const NAVY = '#111D55' const NAVY = '#111D55'
const GOLD = '#EFBF04' const GOLD = '#EFBF04'
const CREAM = '#E4E2E3' const WHITE = '#fff'
const HIDE_MEMBERSHIP_ABOUT_CTA = true const HIDE_MEMBERSHIP_ABOUT_CTA = true
type AboutCms = Partial<typeof aboutContent> & { type AboutCms = Partial<typeof aboutContent> & {
hero?: Partial<typeof aboutContent.hero> & { backgroundImage?: unknown } hero?: Partial<typeof aboutContent.hero> & { backgroundImage?: unknown }
prize?: Partial<typeof aboutContent.prize> & { image?: unknown } prize?: Partial<typeof aboutContent.prize> & { image?: unknown }
mittelstand?: Partial<typeof aboutContent.mittelstand> & { image?: unknown } mittelstand?: Partial<typeof aboutContent.mittelstand> & { image?: unknown }
highlights?: Partial<typeof aboutContent.highlights> & {
fallbackImage?: unknown
fallbackWinner?: Partial<typeof aboutContent.highlights.fallbackWinner>
selectedWinners?: unknown[] | null
}
} }
type TextRow = { text?: string | null } type TextRow = { text?: string | null }
const FALLBACK_HIGHLIGHTS = WINNERS.filter((w) => w.year === aboutContent.highlights.year).slice(0, 8)
const fallbackText = (value: unknown, fallback: string) => (typeof value === 'string' && value.length > 0 ? value : fallback) const fallbackText = (value: unknown, fallback: string) => (typeof value === 'string' && value.length > 0 ? value : fallback)
const fallbackArray = <T,>(value: unknown, fallback: T[]) => (Array.isArray(value) && value.length ? (value as T[]) : fallback) const fallbackArray = <T,>(value: unknown, fallback: T[]) => (Array.isArray(value) && value.length ? (value as T[]) : fallback)
@@ -69,6 +61,12 @@ const isParticipationPage = (page: CmsRouteDoc) => {
return page.spaPath === '/teilnahme' || page.slug === 'teilnahme' || page.slug === 'participation' || title === 'teilnahme' || title === 'participation' return page.spaPath === '/teilnahme' || page.slug === 'teilnahme' || page.slug === 'participation' || title === 'teilnahme' || title === 'participation'
} }
const isHomePage = (page: CmsRouteDoc) => {
const title = String(page.title || '').toLowerCase()
return page.spaPath === '/' || page.slug === 'startseite' || page.slug === 'home' || title === 'startseite' || title === 'home'
}
function HighlightedText({ text, highlight }: { text: string; highlight: string }) { function HighlightedText({ text, highlight }: { text: string; highlight: string }) {
const index = text.indexOf(highlight) const index = text.indexOf(highlight)
if (!highlight || index < 0) return <>{text}</> if (!highlight || index < 0) return <>{text}</>
@@ -82,72 +80,23 @@ function HighlightedText({ text, highlight }: { text: string; highlight: string
) )
} }
const normalizeWinner = (
doc: CmsRouteDoc | undefined,
placeholderImage: string,
fallbackWinner: typeof aboutContent.highlights.fallbackWinner,
): Winner | undefined => {
if (!doc) return undefined
return {
id: String(doc.id),
slug: String(doc.slug || ''),
name: String(doc.title || fallbackWinner.name),
category: String(doc.category || fallbackWinner.category),
year: Number(doc.year || aboutContent.highlights.year),
type: String(doc.awardType || fallbackWinner.awardType),
img: mediaUrl(doc.image, placeholderImage),
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
quote: String(doc.quote || ''),
quotePerson: String(doc.quotePerson || doc.title || ''),
quoteRole: String(doc.quoteRole || ''),
location: String(doc.location || ''),
industry: String(doc.industry || ''),
website: String(doc.website || ''),
hasMedia: Boolean(doc.hasMedia),
}
}
const About: React.FC = () => { const About: React.FC = () => {
const isMobile = useIsMobile() const isMobile = useIsMobile()
const placeholderImage = usePreistraegerPlaceholderImage()
const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms
const cmsWinners = useCmsCollection('preistraeger')
const cmsPages = useCmsCollection('pages') const cmsPages = useCmsCollection('pages')
const hero = { ...aboutContent.hero, ...(cms.hero || {}) } const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
const prize = { ...aboutContent.prize, ...(cms.prize || {}) } const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) } const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
const highlights = useMemo(() => ({ ...aboutContent.highlights, ...(cms.highlights || {}) }), [cms.highlights])
const testimonials = { ...aboutContent.testimonials, ...(cms.testimonials || {}) } const testimonials = { ...aboutContent.testimonials, ...(cms.testimonials || {}) }
const history = { ...aboutContent.history, ...(cms.history || {}) } const history = { ...aboutContent.history, ...(cms.history || {}) }
const goals = { ...aboutContent.goals, ...(cms.goals || {}) } const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
const values = { ...aboutContent.values, ...(cms.values || {}) } const values = { ...aboutContent.values, ...(cms.values || {}) }
const cta = { ...aboutContent.cta, ...(cms.cta || {}) } const cta = { ...aboutContent.cta, ...(cms.cta || {}) }
const participationPage = cmsPages.find(isParticipationPage) const participationPage = cmsPages.find(isParticipationPage)
const homePage = cmsPages.find(isHomePage)
const participationAwardsGrid = (participationPage?.participation as { awardsGrid?: AwardsGridContent } | undefined)?.awardsGrid const participationAwardsGrid = (participationPage?.participation as { awardsGrid?: AwardsGridContent } | undefined)?.awardsGrid
const homeWinners = (homePage?.home as { winners?: HomeWinnersContent } | undefined)?.winners
const showSecondaryCta = !HIDE_MEMBERSHIP_ABOUT_CTA || !isMembershipCta(cta.secondaryCta) const showSecondaryCta = !HIDE_MEMBERSHIP_ABOUT_CTA || !isMembershipCta(cta.secondaryCta)
const highlightFallbackWinner = useMemo(
() => ({ ...aboutContent.highlights.fallbackWinner, ...(highlights.fallbackWinner || {}) }),
[highlights],
)
const highlightWinners = useMemo(() => {
const selected = fallbackArray<unknown>(highlights.selectedWinners, [])
.map((entry) => normalizeWinner(resolveCmsRelationship(entry, cmsWinners), placeholderImage, highlightFallbackWinner))
.filter((winner): winner is Winner => Boolean(winner))
if (selected.length) return selected.slice(0, 8)
const isWinnerFromYear = (winner: Winner | undefined): winner is Winner =>
Boolean(winner) && winner?.year === Number(highlights.year || aboutContent.highlights.year)
const cmsByYear = cmsWinners
.map((winner) => normalizeWinner(winner, placeholderImage, highlightFallbackWinner))
.filter(isWinnerFromYear)
.slice(0, 8)
return cmsByYear.length ? cmsByYear : FALLBACK_HIGHLIGHTS
}, [cmsWinners, highlightFallbackWinner, highlights, placeholderImage])
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
@@ -213,7 +162,7 @@ const About: React.FC = () => {
<section id="was-ist" style={{ position: 'relative', overflow: 'hidden', isolation: 'isolate' }}> <section id="was-ist" style={{ position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg /> <MunichSkylineBg />
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 580 }}> <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 580 }}>
<div style={{ background: CREAM, padding: isMobile ? '32px 24px 44px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}> <div style={{ background: WHITE, padding: isMobile ? '32px 24px 44px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}>
{!isMobile && ( {!isMobile && (
<> <>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span> <span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
@@ -247,7 +196,7 @@ const About: React.FC = () => {
/> />
<div style={{ position: 'absolute', inset: 0, background: isMobile <div style={{ position: 'absolute', inset: 0, background: isMobile
? 'linear-gradient(to top, rgba(3,9,58,0.94) 0%, rgba(3,9,58,0.55) 38%, rgba(3,9,58,0.12) 72%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)' ? 'linear-gradient(to top, rgba(3,9,58,0.94) 0%, rgba(3,9,58,0.55) 38%, rgba(3,9,58,0.12) 72%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)'
: `linear-gradient(to right, ${CREAM} 0%, transparent 25%), linear-gradient(to top, rgba(3,9,58,0.75) 0%, transparent 60%)` }} /> : `linear-gradient(to right, ${WHITE} 0%, transparent 25%), linear-gradient(to top, rgba(3,9,58,0.75) 0%, transparent 60%)` }} />
{isMobile && ( {isMobile && (
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}> <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span> <span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
@@ -312,26 +261,7 @@ const About: React.FC = () => {
</div> </div>
</section> </section>
<section style={{ background: '#111D55' }}> <HomeWinnersSection content={homeWinners} />
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', gap: isMobile ? 16 : 0, justifyContent: 'space-between', maxWidth: 1600, margin: '0 auto' }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>{fallbackText(highlights.eyebrow, aboutContent.highlights.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: 0, lineHeight: 1 }}>
<Lines text={fallbackText(highlights.heading, aboutContent.highlights.heading)} />
</h2>
</div>
<Link
to={fallbackText(highlights.cta?.url, aboutContent.highlights.cta.url)}
style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: GOLD, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: `1px solid ${GOLD}`, whiteSpace: 'nowrap' }}
>
{fallbackText(highlights.cta?.label, aboutContent.highlights.cta.label)} <ChevronRight size={14} />
</Link>
</div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)', gridAutoFlow: isMobile ? 'column' : undefined, gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined, gridAutoColumns: isMobile ? '66vw' : undefined, overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 8 : 0, padding: isMobile ? '0 16px 8px' : 0, WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
{highlightWinners.map((w) => <HighlightCard key={w.id} winner={w} hoverLabel={fallbackText(highlights.hoverLabel, aboutContent.highlights.hoverLabel)} />)}
</div>
</section>
<TestimonialsSection data={testimonials} /> <TestimonialsSection data={testimonials} />
@@ -385,7 +315,7 @@ const About: React.FC = () => {
</div> </div>
</section> </section>
<section id="vorteile" style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}> <section id="vorteile" style={{ background: WHITE, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg /> <MunichSkylineBg />
<div style={{ padding: isMobile ? '48px 24px 32px' : '88px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 16 : 40, alignItems: 'flex-end', borderBottom: '1px solid #D0D5DD' }}> <div style={{ padding: isMobile ? '48px 24px 32px' : '88px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 16 : 40, alignItems: 'flex-end', borderBottom: '1px solid #D0D5DD' }}>
<div> <div>
@@ -404,7 +334,7 @@ const About: React.FC = () => {
))} ))}
</section> </section>
<AwardsGridSection content={participationAwardsGrid} /> <AwardsGridSection content={participationAwardsGrid} featuredGoldeneBavaria />
<section style={{ background: NAVY, padding: isMobile ? '48px 24px' : '100px 80px', position: 'relative', overflow: 'hidden' }}> <section style={{ background: NAVY, padding: isMobile ? '48px 24px' : '100px 80px', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '60%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.3 }} /> <div style={{ position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '60%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.3 }} />
@@ -449,37 +379,6 @@ const About: React.FC = () => {
) )
} }
function HighlightCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string }) {
const [hovered, setHovered] = useState(false)
const isMobile = useIsMobile()
return (
<Link
to={`/preistraeger/${winner.slug}`}
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile ? 'start' : undefined }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Image unoptimized src={winner.img} alt={winner.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '18px 20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 5 }}>
<Star size={10} style={{ color: GOLD, flexShrink: 0 }} fill={GOLD} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.12em', textTransform: 'uppercase' }}>
{winner.year} · {winner.type}
</span>
</div>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', margin: '0 0 3px', lineHeight: 1.3, textShadow: '0 1px 4px rgba(0,0,0,0.5)' }}>{winner.name}</h3>
<p style={{ fontFamily: FF, fontSize: 14, color: 'rgba(255,255,255,0.75)', margin: 0, letterSpacing: '0.03em' }}>{winner.category}</p>
</div>
<div style={{ position: 'absolute', bottom: 16, right: 16, background: GOLD, color: '#101828', padding: '6px 14px', borderRadius: 999, fontSize: 10, fontFamily: FF, fontWeight: 800, letterSpacing: '0.1em', textTransform: 'uppercase', opacity: hovered ? 1 : 0, transform: hovered ? 'translateY(0)' : 'translateY(6px)', transition: 'all 0.25s' }}>
{hoverLabel}
</div>
</Link>
)
}
function ValueRow({ item, last }: { item: { num?: string | null; title?: string | null }; last: boolean }) { function ValueRow({ item, last }: { item: { num?: string | null; title?: string | null }; last: boolean }) {
const [hovered, setHovered] = useState(false) const [hovered, setHovered] = useState(false)
const isMobileRow = useIsMobile() const isMobileRow = useIsMobile()

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Button } from '@/spa/components/ui/button'; import { Button } from '@/spa/components/ui/button';
import { Play, ChevronLeft, ChevronRight, X, Star } from 'lucide-react'; import { Play, ChevronRight, X } from 'lucide-react';
import { Link } from '@/spa/router'; import { Link } from '@/spa/router';
import { PartnerTicker } from '@/spa/components/ui/partner-ticker'; import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
import { WINNERS } from '@/spa/data/winners';
import { ApplicationFormSection } from '@/spa/components/forms/ApplicationFormSection'; import { ApplicationFormSection } from '@/spa/components/forms/ApplicationFormSection';
import HomeWinnersSection from '@/spa/components/HomeWinnersSection';
import TestimonialsSection from '@/spa/components/TestimonialsSection'; import TestimonialsSection from '@/spa/components/TestimonialsSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
@@ -13,9 +13,7 @@ import type { SpaApplicationPhaseData, SpaApplicationPhaseItem } from '@/spa/app
import { useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRoute' import { useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRoute'
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField' import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
import { homeContent } from '@/spa/homeContent' import { homeContent } from '@/spa/homeContent'
import { resolveHomeWinners } from '@/spa/homeWinnerSelection'
import { netzwerkContent } from '@/spa/netzwerkContent' import { netzwerkContent } from '@/spa/netzwerkContent'
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo' import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
@@ -93,20 +91,6 @@ type RenderedStatusPhase = Omit<SpaApplicationPhaseItem, 'accent' | 'bg' | 'cta'
cta?: { label: string; to: string } | null; cta?: { label: string; to: string } | null;
membershipCta?: { label: string; to: string } | null; membershipCta?: { label: string; to: string } | null;
}; };
type WinnerCardData = CmsRecord & {
id?: string | number;
slug?: string;
title?: string;
name?: string;
img?: string;
image?: unknown;
spaPath?: string;
year?: string | number;
awardType?: string;
type?: string;
category?: string;
};
const fallbackText = (value: unknown, fallback: string) => const fallbackText = (value: unknown, fallback: string) =>
typeof value === 'string' && value.length > 0 ? value : fallback; typeof value === 'string' && value.length > 0 ? value : fallback;
@@ -168,63 +152,10 @@ function sortedPartners<T extends PartnerLogoData>(partners: T[]) {
.map(({ partner }) => partner); .map(({ partner }) => partner);
} }
function HomeWinnerCard({
winner,
fallbackTitle,
fallbackImage,
hoverLabel,
horizontalScroll = false,
}: {
winner: WinnerCardData;
fallbackTitle: string;
fallbackImage: string;
hoverLabel: string;
horizontalScroll?: boolean;
}) {
const [hovered, setHovered] = useState(false);
const isMobile = useIsMobile();
const staticWinner = WINNERS.find(
(item) =>
String(item.id) === String(winner?.id) ||
item.slug === winner?.slug ||
item.name === winner?.title ||
item.name === winner?.name,
);
const title = fallbackText(winner?.title, fallbackText(winner?.name, staticWinner?.name || fallbackTitle));
const imageSrc = winner?.img || mediaUrl(winner?.image, fallbackImage);
const href = winner?.spaPath || (winner?.slug ? `/preistraeger/${winner.slug}` : staticWinner ? `/preistraeger/${staticWinner.slug}` : '/preistraeger');
return (
<Link
to={href}
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile || horizontalScroll ? 'start' : undefined }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Image unoptimized src={imageSrc} alt={title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '18px 20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 5 }}>
<Star size={10} style={{ color: '#EFBF04', flexShrink: 0 }} fill="#EFBF04" />
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, color: '#EFBF04', letterSpacing: '0.12em', textTransform: 'uppercase' }}>{winner?.year || staticWinner?.year} · {winner?.awardType || winner?.type || staticWinner?.type}</span>
</div>
<h3 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight: 700, color: '#fff', margin: '0 0 3px', lineHeight: 1.3 }}>{title}</h3>
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 14, color: 'rgba(255,255,255,0.75)', margin: 0 }}>{winner?.category || staticWinner?.category}</p>
<div style={{ marginTop: 10, display: 'inline-block', background: '#EFBF04', color: '#101828', padding: '5px 14px', borderRadius: 999, fontSize: 10, fontFamily: '"IBM Plex Sans", sans-serif', fontWeight: 800, letterSpacing: '0.1em', textTransform: 'uppercase', opacity: hovered ? 1 : 0, transform: hovered ? 'translateY(0)' : 'translateY(6px)', transition: 'all 0.25s' }}>
{hoverLabel}
</div>
</div>
</Link>
);
}
const Home: React.FC = () => { const Home: React.FC = () => {
const [showVideo, setShowVideo] = useState(false); const [showVideo, setShowVideo] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const winnersScrollRef = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const placeholderImage = usePreistraegerPlaceholderImage();
const home = (useCmsRoute()?.doc?.home || {}) as HomeCms; const home = (useCmsRoute()?.doc?.home || {}) as HomeCms;
const cmsPartners = useCmsCollection('partners') as PartnerLogoData[]; const cmsPartners = useCmsCollection('partners') as PartnerLogoData[];
const networkPartners = sortedPartners(cmsPartners) const networkPartners = sortedPartners(cmsPartners)
@@ -240,19 +171,14 @@ const Home: React.FC = () => {
const benefits = home.benefits || {}; const benefits = home.benefits || {};
const videoModal = home.videoModal || {}; const videoModal = home.videoModal || {};
const applicationPhase = useApplicationPhase(); const applicationPhase = useApplicationPhase();
const cmsPreistraeger = useCmsCollection('preistraeger');
const heroSecondaryCta = { const heroSecondaryCta = {
label: fallbackText(hero.secondaryCta?.label, homeContent.hero.secondaryCta.label), label: fallbackText(hero.secondaryCta?.label, homeContent.hero.secondaryCta.label),
url: fallbackText(hero.secondaryCta?.url, homeContent.hero.secondaryCta.url), url: fallbackText(hero.secondaryCta?.url, homeContent.hero.secondaryCta.url),
}; };
const showHeroSecondaryCta = !HIDE_MEMBERSHIP_HOME_HERO_CTA || !isMembershipCta(heroSecondaryCta); const showHeroSecondaryCta = !HIDE_MEMBERSHIP_HOME_HERO_CTA || !isMembershipCta(heroSecondaryCta);
const homeWinners = resolveHomeWinners(winnersSection.featured, cmsPreistraeger, WINNERS);
const scrollDesktopWinners = !isMobile && homeWinners.length > 8;
const scrollWinnerGrid = isMobile || scrollDesktopWinners;
const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria); const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria);
const introStats = fallbackArray(intro.stats, homeContent.intro.stats); const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items); const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
const winnerFallbackImage = placeholderImage;
const videoTitle = fallbackText(videoModal.title, homeContent.videoModal.title); const videoTitle = fallbackText(videoModal.title, homeContent.videoModal.title);
const videoSrc = mediaUrl(videoModal.video, ''); const videoSrc = mediaUrl(videoModal.video, '');
@@ -264,16 +190,6 @@ const Home: React.FC = () => {
}); });
}, [showVideo, videoSrc]); }, [showVideo, videoSrc]);
const scrollWinners = (direction: -1 | 1) => {
const container = winnersScrollRef.current;
if (!container) return;
container.scrollBy({
left: direction * Math.max(container.clientWidth * 0.8, 320),
behavior: 'smooth',
});
};
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
{/* Hero Section */} {/* Hero Section */}
@@ -364,72 +280,7 @@ const Home: React.FC = () => {
{/* Status Phase Slider */} {/* Status Phase Slider */}
<StatusSlider data={applicationPhase} /> <StatusSlider data={applicationPhase} />
{/* Winners Grid neue Preisträger-Übersicht */} <HomeWinnersSection content={winnersSection} />
<section style={{ background: '#111D55' }}>
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', justifyContent: 'space-between', gap: isMobile ? 16 : 0 }}>
<div>
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>{fallbackText(winnersSection.eyebrow, homeContent.winners.eyebrow)}</span>
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: 0, lineHeight: 1 }}>
<Lines text={fallbackText(winnersSection.heading, homeContent.winners.heading)} />
</h2>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
{scrollDesktopWinners && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button
type="button"
aria-label="Vorherige Preisträger anzeigen"
onClick={() => scrollWinners(-1)}
style={{ width: 42, height: 42, border: '1px solid rgba(255,255,255,0.28)', background: 'transparent', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
>
<ChevronLeft size={18} />
</button>
<button
type="button"
aria-label="Weitere Preisträger anzeigen"
onClick={() => scrollWinners(1)}
style={{ width: 42, height: 42, border: '1px solid rgba(255,255,255,0.28)', background: 'transparent', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
>
<ChevronRight size={18} />
</button>
</div>
)}
<Link
to={fallbackText(winnersSection.cta?.url, homeContent.winners.cta.url)}
style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#EFBF04', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: '1px solid #EFBF04', whiteSpace: 'nowrap' }}
>
{fallbackText(winnersSection.cta?.label, homeContent.winners.cta.label)} <ChevronRight size={14} />
</Link>
</div>
</div>
<div ref={winnersScrollRef} style={{
display: 'grid',
gridTemplateColumns: scrollWinnerGrid ? undefined : 'repeat(4, 1fr)',
gridAutoFlow: scrollWinnerGrid ? 'column' : undefined,
gridTemplateRows: scrollWinnerGrid ? `repeat(2, ${isMobile ? '1fr' : '280px'})` : undefined,
gridAutoColumns: isMobile ? '66vw' : scrollDesktopWinners ? '25vw' : undefined,
overflowX: scrollWinnerGrid ? 'auto' : undefined,
scrollSnapType: scrollWinnerGrid ? 'x mandatory' : undefined,
overscrollBehaviorX: scrollWinnerGrid ? 'contain' : undefined,
gap: isMobile ? 8 : 0,
padding: isMobile ? '0 16px 8px' : scrollDesktopWinners ? '0 0 12px' : 0,
WebkitOverflowScrolling: 'touch',
scrollbarWidth: scrollDesktopWinners ? 'thin' : 'none',
scrollbarColor: scrollDesktopWinners ? '#EFBF04 rgba(255,255,255,0.14)' : undefined,
}}>
{homeWinners.map((w) => (
<HomeWinnerCard
key={(w as WinnerCardData).id || (w as WinnerCardData).slug}
winner={w as WinnerCardData}
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
fallbackImage={winnerFallbackImage}
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
horizontalScroll={scrollDesktopWinners}
/>
))}
</div>
</section>
{/* Schnell-Check Section */} {/* Schnell-Check Section */}
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}> <section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>