feat: update Preistraeger page to include a simple winner grid for 2023 and remove unused components

fix: change featured status for participation content and remove deprecated award card

feat: add migration for new participation phase form fields and backfill existing data

feat: create AwardsGridSection component for displaying awards with improved layout

feat: implement NewsletterInterestForm component for newsletter sign-up with validation
This commit is contained in:
syntaxbullet
2026-07-01 18:02:12 +02:00
parent 2fda7e8122
commit 2842caa936
15 changed files with 1274 additions and 255 deletions

View File

@@ -1,6 +1,7 @@
import React, { useMemo, useState } from 'react'
import { ChevronRight, Star } from 'lucide-react'
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection'
import TestimonialsSection from '@/spa/components/TestimonialsSection'
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'
import Image from '@/spa/components/ui/UnoptimizedImage'
@@ -61,6 +62,12 @@ const paragraphs = (value: unknown, fallback: string[]) =>
? (value as Array<string | TextRow>).map((item) => (typeof item === 'string' ? item : fallbackText(item.text, ''))).filter(Boolean)
: fallback
const isParticipationPage = (page: CmsRouteDoc) => {
const title = String(page.title || '').toLowerCase()
return page.spaPath === '/teilnahme' || page.slug === 'teilnahme' || page.slug === 'participation' || title === 'teilnahme' || title === 'participation'
}
function HighlightedText({ text, highlight }: { text: string; highlight: string }) {
const index = text.indexOf(highlight)
if (!highlight || index < 0) return <>{text}</>
@@ -115,6 +122,7 @@ const About: React.FC = () => {
const isMobile = useIsMobile()
const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms
const cmsWinners = useCmsCollection('preistraeger')
const cmsPages = useCmsCollection('pages')
const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
@@ -124,6 +132,8 @@ const About: React.FC = () => {
const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
const values = { ...aboutContent.values, ...(cms.values || {}) }
const cta = { ...aboutContent.cta, ...(cms.cta || {}) }
const participationPage = cmsPages.find(isParticipationPage)
const participationAwardsGrid = (participationPage?.participation as { awardsGrid?: AwardsGridContent } | undefined)?.awardsGrid
const showSecondaryCta = !HIDE_MEMBERSHIP_ABOUT_CTA || !isMembershipCta(cta.secondaryCta)
const highlightFallbackImage = mediaUrl(
highlights.fallbackImage,
@@ -407,6 +417,8 @@ const About: React.FC = () => {
))}
</section>
<AwardsGridSection content={participationAwardsGrid} />
<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', bottom: 0, left: '50%', transform: 'translateX(-50%)', width: '40%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.15 }} />

View File

@@ -5,6 +5,7 @@ import { Link } from '@/spa/router';
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
import { WINNERS } from '@/spa/data/winners';
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
import TestimonialsSection from '@/spa/components/TestimonialsSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
@@ -55,6 +56,8 @@ type HomeSectionCms = CmsRecord & {
imageAlt?: string | null;
imageLabel?: string | null;
items?: unknown;
newsletterPhaseCompleted?: NewsletterInterestCms | null;
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
note?: string | null;
quote?: string | null;
stats?: unknown;
@@ -116,6 +119,41 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
const HIDE_MEMBERSHIP_HOME_HERO_CTA = true;
const APPLICATION_PHASE_OPEN = '0';
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
'1': homeContent.application.newsletterPhaseEvaluation,
'2': homeContent.application.newsletterPhaseCompleted,
};
const mergeNewsletterInterestCopy = (
value: NewsletterInterestCms | null | undefined,
fallback: NewsletterInterestCopy,
): NewsletterInterestCopy => ({
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
heading: fallbackText(value?.heading, fallback.heading),
body: fallbackText(value?.body, fallback.body),
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
formBody: fallbackText(value?.formBody, fallback.formBody),
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
privacy: fallbackText(value?.privacy, fallback.privacy),
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
successBody: fallbackText(value?.successBody, fallback.successBody),
footnote: fallbackText(value?.footnote, fallback.footnote),
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
});
const getNewsletterInterestCopy = (phase: string | undefined, application: HomeSectionCms) => {
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
const cmsCopy = phase === '1' ? application.newsletterPhaseEvaluation : application.newsletterPhaseCompleted;
return mergeNewsletterInterestCopy(cmsCopy, fallback);
};
const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | null) =>
cta?.url === '/mitglied-werden' ||
@@ -238,6 +276,9 @@ const Home: React.FC = () => {
const form = home.form || {};
const videoModal = home.videoModal || {};
const applicationPhase = useApplicationPhase();
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, application);
const cmsPreistraeger = useCmsCollection('preistraeger');
const selectedWinners = fallbackArray(winnersSection.featured, []);
const heroSecondaryCta = {
@@ -260,6 +301,7 @@ const Home: React.FC = () => {
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
const applicationBenefits = fallbackArray(application.benefits, homeContent.application.benefits);
const finalSectionBenefits = showApplicationForm ? applicationBenefits : newsletterCopy.benefits;
const winnerFallbackImage = mediaUrl(winnersSection.fallbackImage, `/images/${homeContent.winners.fallbackImageFilename}`);
const videoTitle = fallbackText(videoModal.title, homeContent.videoModal.title);
const videoSrc = mediaUrl(videoModal.video, '');
@@ -354,6 +396,9 @@ const Home: React.FC = () => {
</div>
</section>
{/* Status Phase Slider */}
<StatusSlider data={applicationPhase} />
{/* Schnell-Check Section */}
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
<MunichSkylineBg />
@@ -503,9 +548,6 @@ const Home: React.FC = () => {
</div>
</section>
{/* Status Phase Slider */}
<StatusSlider data={applicationPhase} />
{/* Winners Grid – neue Preisträger-Übersicht */}
<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 }}>
@@ -617,8 +659,8 @@ const Home: React.FC = () => {
</div>
</section>
{/* CTA Section – with embedded form */}
<section style={{ background: '#24366A', position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : 'calc(100vh - 60px)', display: 'flex', flexDirection: 'column' }} id="bewerben">
{/* CTA Section – with phase-aware embedded form */}
<section style={{ background: '#24366A', position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : showApplicationForm ? 'calc(100vh - 60px)' : 'auto', minHeight: isMobile ? undefined : showApplicationForm ? undefined : 560, display: 'flex', flexDirection: 'column' }} id="bewerben">
{/* Subtle radial glow behind left copy */}
<div style={{ position: 'absolute', left: -120, top: '50%', transform: 'translateY(-50%)', width: 600, height: 600, borderRadius: '50%', background: 'radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%)', pointerEvents: 'none' }} />
@@ -626,18 +668,18 @@ const Home: React.FC = () => {
{/* Left – pitch copy */}
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: 'hidden' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.36em', fontWeight: 700, display: 'block', marginBottom: 16, opacity: 0.8 }}>{fallbackText(application.eyebrow, homeContent.application.eyebrow)}</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.36em', fontWeight: 700, display: 'block', marginBottom: 16, opacity: 0.8 }}>{showApplicationForm ? fallbackText(application.eyebrow, homeContent.application.eyebrow) : newsletterCopy.eyebrow}</span>
<h2 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.6rem)' : 'clamp(1.8rem, 2.8vw, 2.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 1.0, marginBottom: 16 }}>
<Lines text={fallbackText(application.heading, homeContent.application.heading)} />
<Lines text={showApplicationForm ? fallbackText(application.heading, homeContent.application.heading) : newsletterCopy.heading} />
</h2>
<div style={{ width: 32, height: 2, background: '#EFBF04', marginBottom: 20 }} />
<p style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.6)', lineHeight: 1.7, marginBottom: 32 }}>
{fallbackText(application.body, homeContent.application.body)}
{showApplicationForm ? fallbackText(application.body, homeContent.application.body) : newsletterCopy.body}
</p>
{/* Benefit rows */}
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
{applicationBenefits.map(item => (
{finalSectionBenefits.map(item => (
<div key={item.num} style={{ display: 'grid', gridTemplateColumns: '24px 1fr', gap: '0 14px', padding: '13px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', alignItems: 'center' }}>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, color: 'rgba(239,191,4,0.55)', letterSpacing: '0.1em' }}>{item.num}</span>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
@@ -687,10 +729,10 @@ const Home: React.FC = () => {
)}
{/* Form area */}
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{fallbackText(application.formEyebrow, homeContent.application.formEyebrow)}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{fallbackText(application.formTitle, homeContent.application.formTitle)}</h3>
<BewerbungsForm theme="gold" content={form} />
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile && showApplicationForm ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{showApplicationForm ? fallbackText(application.formEyebrow, homeContent.application.formEyebrow) : newsletterCopy.formEyebrow}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{showApplicationForm ? fallbackText(application.formTitle, homeContent.application.formTitle) : newsletterCopy.formTitle}</h3>
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} />}
</div>
</div>
@@ -698,8 +740,8 @@ const Home: React.FC = () => {
{/* Footnote row */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '16px 24px 20px' : '14px 56px 18px', display: 'flex', flexWrap: 'wrap', justifyContent: 'center', textAlign: 'center', borderTop: '1px solid rgba(255,255,255,0.06)', flexShrink: 0 }}>
<span style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)', marginRight: isMobile ? 6 : 10 }}>{fallbackText(application.footnote, homeContent.application.footnote)}</span>
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: 'rgba(239,191,4,0.8)' }}>{fallbackText(application.footnoteStrong, homeContent.application.footnoteStrong)}</span>
<span style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)', marginRight: isMobile ? 6 : 10 }}>{showApplicationForm ? fallbackText(application.footnote, homeContent.application.footnote) : newsletterCopy.footnote}</span>
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: 'rgba(239,191,4,0.8)' }}>{showApplicationForm ? fallbackText(application.footnoteStrong, homeContent.application.footnoteStrong) : newsletterCopy.footnoteStrong}</span>
</div>
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
</section>

View File

@@ -1,12 +1,14 @@
import React, { useRef } from 'react';
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star, Mail } from 'lucide-react';
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react';
import { Link } from '@/spa/router';
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useCmsRoute } from '@/spa/cmsRoute';
import { useApplicationPhase, useCmsRoute } from '@/spa/cmsRoute';
import { participationContent } from '@/spa/participationContent';
import Image from '@/spa/components/ui/UnoptimizedImage'
@@ -18,12 +20,15 @@ const CREAM = '#E4E2E3';
type ParticipationCms = Partial<typeof participationContent> & {
hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown };
awardsGrid?: Partial<typeof participationContent.awardsGrid> & { cards?: AwardCardCms[] };
applicationForm?: Partial<typeof participationContent.applicationForm> & { image?: unknown };
awardsGrid?: AwardsGridContent;
applicationForm?: ParticipationApplicationFormCms;
};
type AwardCard = (typeof participationContent.awardsGrid.cards)[number];
type AwardCardCms = Partial<AwardCard> & { image?: unknown };
type ParticipationApplicationFormCms = Partial<typeof participationContent.applicationForm> & {
image?: unknown;
newsletterPhaseCompleted?: NewsletterInterestCms | null;
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
};
const ICONS = {
building2: Building2,
@@ -38,12 +43,50 @@ const ICONS = {
const fallbackText = (value: unknown, fallback: string) =>
typeof value === 'string' && value.length > 0 ? value : fallback;
const fallbackArray = <T,>(value: unknown, fallback: T[]) =>
const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
const mergeAwardCards = (value: unknown, fallback: AwardCard[]): (AwardCard & { image?: unknown })[] => {
const cards = Array.isArray(value) && value.length > 0 ? (value as AwardCardCms[]) : fallback;
return cards.map((card, index) => ({ ...(fallback[index] || fallback[0]), ...card }));
const APPLICATION_PHASE_OPEN = '0';
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
'1': participationContent.applicationForm.newsletterPhaseEvaluation,
'2': participationContent.applicationForm.newsletterPhaseCompleted,
};
const mergeNewsletterInterestCopy = (
value: NewsletterInterestCms | null | undefined,
fallback: NewsletterInterestCopy,
): NewsletterInterestCopy => ({
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
heading: fallbackText(value?.heading, fallback.heading),
body: fallbackText(value?.body, fallback.body),
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
formBody: fallbackText(value?.formBody, fallback.formBody),
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
privacy: fallbackText(value?.privacy, fallback.privacy),
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
successBody: fallbackText(value?.successBody, fallback.successBody),
footnote: fallbackText(value?.footnote, fallback.footnote),
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
});
const getNewsletterInterestCopy = (phase: string | undefined, applicationForm: ParticipationApplicationFormCms) => {
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
const cmsCopy = phase === '1' ? applicationForm.newsletterPhaseEvaluation : applicationForm.newsletterPhaseCompleted;
return mergeNewsletterInterestCopy(cmsCopy, fallback);
};
const isFutureAwardApplicationWay = (value: { label?: unknown; title?: unknown }) => {
const label = typeof value.label === 'string' ? value.label.toLowerCase() : '';
const title = typeof value.title === 'string' ? value.title.toLowerCase() : '';
return label.includes('sonderpreis') || title.includes('bavarian') || title.includes('future award');
};
function Lines({ text }: { text: string }) {
@@ -54,6 +97,7 @@ const Participation: React.FC = () => {
const isMobile = useIsMobile();
const qualSectionRef = useRef<HTMLElement>(null);
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
const applicationPhase = useApplicationPhase();
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
const process = { ...participationContent.process, ...(cms.process || {}) };
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
@@ -62,11 +106,15 @@ const Participation: React.FC = () => {
const awardsGrid = { ...participationContent.awardsGrid, ...(cms.awardsGrid || {}) };
const applicationForm = { ...participationContent.applicationForm, ...(cms.applicationForm || {}) };
const form = { ...participationContent.form, ...(cms.form || {}) };
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, applicationForm);
const eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
const desktopDates = fallbackArray(datesBanner.desktopItems, participationContent.datesBanner.desktopItems);
const applicationFacts = fallbackArray(applicationForm.facts, participationContent.applicationForm.facts);
const finalSectionFacts = showApplicationForm ? applicationFacts : newsletterCopy.benefits;
return (
<div className="animate-fade-in">
@@ -224,21 +272,21 @@ const Participation: React.FC = () => {
<AwardsGridSection content={awardsGrid} />
{/* ── FORM SECTION ─────────────────────────────────────────────────────── */}
<section style={{ background: NAVY, position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : 'calc(100vh - 60px)', display: 'flex', flexDirection: 'column' }} id="bewerben">
<section style={{ background: NAVY, position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : showApplicationForm ? 'calc(100vh - 60px)' : 'auto', minHeight: isMobile ? undefined : showApplicationForm ? undefined : 560, display: 'flex', flexDirection: 'column' }} id="bewerben">
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr', flex: 1, minHeight: 0, overflow: isMobile ? 'visible' : 'hidden' }}>
{/* Left – copy */}
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: isMobile ? 'visible' : 'hidden' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(applicationForm.eyebrow, participationContent.applicationForm.eyebrow)}</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{showApplicationForm ? fallbackText(applicationForm.eyebrow, participationContent.applicationForm.eyebrow) : newsletterCopy.eyebrow}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.2vw, 2.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.04, margin: '0 0 20px' }}>
<Lines text={fallbackText(applicationForm.heading, participationContent.applicationForm.heading)} />
<Lines text={showApplicationForm ? fallbackText(applicationForm.heading, participationContent.applicationForm.heading) : newsletterCopy.heading} />
</h2>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.68)', lineHeight: 1.8, maxWidth: 400, marginBottom: 48 }}>
{fallbackText(applicationForm.description, participationContent.applicationForm.description)}
{showApplicationForm ? fallbackText(applicationForm.description, participationContent.applicationForm.description) : newsletterCopy.body}
</p>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
{applicationFacts.map((item, i) => (
<div key={i} style={{ display: 'grid', gridTemplateColumns: '36px 1fr', gap: '0 16px', padding: '16px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', alignItems: 'center' }}>
{finalSectionFacts.map((item, i) => (
<div key={`${fallbackText(item.num, String(i))}-${i}`} style={{ display: 'grid', gridTemplateColumns: '36px 1fr', gap: '0 16px', padding: '16px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', alignItems: 'center' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(239,191,4,0.5)', letterSpacing: '0.1em' }}>{fallbackText(item.num, '')}</span>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{fallbackText(item.label, '')}</span>
@@ -279,31 +327,51 @@ const Participation: React.FC = () => {
</div>
</div>
)}
<div style={{ padding: isMobile ? '28px 20px' : '24px 48px 32px 40px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: isMobile ? 560 : 0, position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.28em', fontWeight: 700 }}>{fallbackText(applicationForm.formEyebrow, participationContent.applicationForm.formEyebrow)}</span>
<Link
to={fallbackText(applicationForm.uploadCta?.url, participationContent.applicationForm.uploadCta.url)}
style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(17,29,85,0.55)', border: '1px solid rgba(17,29,85,0.2)', padding: '7px 14px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, transition: 'color 0.15s, border-color 0.15s', whiteSpace: 'nowrap', flexShrink: 0 }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = '#111D55'; (e.currentTarget as HTMLElement).style.borderColor = '#111D55'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'rgba(17,29,85,0.55)'; (e.currentTarget as HTMLElement).style.borderColor = 'rgba(17,29,85,0.2)'; }}
>
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M6 1v7M3 4l3-3 3 3M1 10h10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
{fallbackText(applicationForm.uploadCta?.label, participationContent.applicationForm.uploadCta.label)}
</Link>
</div>
<BewerbungsForm theme="gold" content={form} />
<div style={{ padding: isMobile ? '28px 20px' : '24px 48px 32px 40px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: isMobile && showApplicationForm ? 560 : 0, position: 'relative', zIndex: 1 }}>
{showApplicationForm ? (
<>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 10 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.28em', fontWeight: 700 }}>{fallbackText(applicationForm.formEyebrow, participationContent.applicationForm.formEyebrow)}</span>
<Link
to={fallbackText(applicationForm.uploadCta?.url, participationContent.applicationForm.uploadCta.url)}
style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(17,29,85,0.55)', border: '1px solid rgba(17,29,85,0.2)', padding: '7px 14px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, transition: 'color 0.15s, border-color 0.15s', whiteSpace: 'nowrap', flexShrink: 0 }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = '#111D55'; (e.currentTarget as HTMLElement).style.borderColor = '#111D55'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'rgba(17,29,85,0.55)'; (e.currentTarget as HTMLElement).style.borderColor = 'rgba(17,29,85,0.2)'; }}
>
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M6 1v7M3 4l3-3 3 3M1 10h10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
{fallbackText(applicationForm.uploadCta?.label, participationContent.applicationForm.uploadCta.label)}
</Link>
</div>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{fallbackText(applicationForm.formTitle, participationContent.applicationForm.formTitle)}</h3>
<BewerbungsForm theme="gold" content={form} />
</>
) : (
<>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{newsletterCopy.formEyebrow}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{newsletterCopy.formTitle}</h3>
<NewsletterInterestForm copy={newsletterCopy} />
</>
)}
</div>
</div>
</div>
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '16px 20px 20px' : '14px 56px 18px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, justifyContent: 'center', textAlign: 'center', borderTop: '1px solid rgba(255,255,255,0.06)', flexShrink: 0 }}>
<span style={{ fontFamily: FB, fontSize: 14, color: 'rgba(255,255,255,0.55)', lineHeight: 1.6, maxWidth: 520 }}>
{fallbackText(applicationForm.uploadPrompt, participationContent.applicationForm.uploadPrompt)}
</span>
<Link to={fallbackText(applicationForm.uploadFooterCta?.url, participationContent.applicationForm.uploadFooterCta.url)} style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(239,191,4,0.7)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, borderBottom: '1px solid rgba(239,191,4,0.3)', paddingBottom: 1 }}>
{fallbackText(applicationForm.uploadFooterCta?.label, participationContent.applicationForm.uploadFooterCta.label)}
</Link>
{showApplicationForm ? (
<>
<span style={{ fontFamily: FB, fontSize: 14, color: 'rgba(255,255,255,0.55)', lineHeight: 1.6, maxWidth: 520 }}>
{fallbackText(applicationForm.uploadPrompt, participationContent.applicationForm.uploadPrompt)}
</span>
<Link to={fallbackText(applicationForm.uploadFooterCta?.url, participationContent.applicationForm.uploadFooterCta.url)} style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(239,191,4,0.7)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, borderBottom: '1px solid rgba(239,191,4,0.3)', paddingBottom: 1 }}>
{fallbackText(applicationForm.uploadFooterCta?.label, participationContent.applicationForm.uploadFooterCta.label)}
</Link>
</>
) : (
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: isMobile ? '0 6px' : '0 10px' }}>
<span style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)' }}>{newsletterCopy.footnote}</span>
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: 'rgba(239,191,4,0.8)' }}>{newsletterCopy.footnoteStrong}</span>
</div>
)}
</div>
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
</section>
@@ -349,7 +417,9 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
const isMobile = useIsMobile();
const section = { ...participationContent.applicationWays, ...(content || {}) };
const institutions = fallbackArray(section.institutions, participationContent.applicationWays.institutions);
const ways = fallbackArray(section.ways, participationContent.applicationWays.ways);
const ways = fallbackArray(section.ways, participationContent.applicationWays.ways).filter(
(way) => !isFutureAwardApplicationWay(way),
);
const fallbackCta = { ...participationContent.applicationWays.fallbackCta, ...(section.fallbackCta || {}) };
return (
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
@@ -368,24 +438,21 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
</div>
</div>
{/* 3 Wege – premium swipe cards on mobile, 3-col grid on desktop */}
<div style={{ position: 'relative', zIndex: 1, display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(3, 1fr)', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', gap: isMobile ? 14 : 0, padding: isMobile ? '28px 24px 36px' : 0 }}>
{/* Wege – premium swipe cards on mobile, equal grid columns on desktop */}
<div style={{ position: 'relative', zIndex: 1, display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : `repeat(${Math.max(ways.length, 1)}, minmax(0, 1fr))`, overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', gap: isMobile ? 14 : 0, padding: isMobile ? '28px 24px 36px' : 0 }}>
{/* ── MOBILE: unified premium cards ── */}
{isMobile && ways.map((w) => {
const Icon = ICONS[w.icon as keyof typeof ICONS] || Send;
const facts = fallbackArray<{ label?: string | null; desc?: string | null }>(w.facts, []);
return (
<div key={fallbackText(w.label, '')} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: w.featured ? `1.5px solid ${GOLD}` : '1px solid rgba(17,29,85,0.10)', boxShadow: w.featured ? '0 14px 34px rgba(239,191,4,0.20), 0 6px 16px rgba(3,9,58,0.10)' : '0 10px 28px rgba(3,9,58,0.08)' }}>
<div key={fallbackText(w.label, '')} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: '1px solid rgba(17,29,85,0.10)', boxShadow: '0 10px 28px rgba(3,9,58,0.08)' }}>
{/* Header */}
<div style={{ padding: '22px 22px 0', position: 'relative' }}>
{w.featured && (
<span style={{ position: 'absolute', top: 22, right: 22, fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '5px 10px', borderRadius: 100 }}>{fallbackText(section.recommendedLabel, participationContent.applicationWays.recommendedLabel)}</span>
)}
<div style={{ width: 46, height: 46, borderRadius: 12, background: w.featured ? GOLD : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 18 }}>
<Icon size={20} style={{ color: w.featured ? '#101828' : GOLD }} strokeWidth={1.6} />
<div style={{ width: 46, height: 46, borderRadius: 12, background: NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 18 }}>
<Icon size={20} style={{ color: GOLD }} strokeWidth={1.6} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: w.featured ? '#A87800' : 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 8 }}>{fallbackText(w.label, '')}</span>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 8 }}>{fallbackText(w.label, '')}</span>
<h3 style={{ fontFamily: FF, fontSize: '1.45rem', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 10px' }}><Lines text={fallbackText(w.title, '')} /></h3>
<p style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.55)', lineHeight: 1.6, margin: '0 0 20px' }}>{fallbackText(w.desc, '')}</p>
<div style={{ width: 36, height: 2, background: GOLD }} />
@@ -420,23 +487,22 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
{ways.map((w, index) => {
const Icon = ICONS[w.icon as keyof typeof ICONS] || Send;
const isFeatured = Boolean(w.featured);
const facts = fallbackArray<{ label?: string | null; desc?: string | null }>(w.facts, []);
const isLast = index === ways.length - 1;
return (
<div key={`${w.label}-${index}`} style={{ padding: isMobile ? '32px 24px' : '56px 48px 56px', borderRight: !isLast ? '1px solid #D0D5DD' : 'none', display: 'flex', flexDirection: 'column', background: isFeatured ? NAVY : undefined, ...(isMobile ? { minWidth: '83vw', maxWidth: '83vw', flexShrink: 0, scrollSnapAlign: 'start' } : {}) }}>
<div style={{ width: 44, height: 44, background: isFeatured ? GOLD : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
<Icon size={18} style={{ color: isFeatured ? '#101828' : GOLD }} strokeWidth={1.5} />
<div key={`${w.label}-${index}`} style={{ padding: isMobile ? '32px 24px' : '56px 48px 56px', borderRight: !isLast ? '1px solid #D0D5DD' : 'none', display: 'flex', flexDirection: 'column', background: '#fff', ...(isMobile ? { minWidth: '83vw', maxWidth: '83vw', flexShrink: 0, scrollSnapAlign: 'start' } : {}) }}>
<div style={{ width: 44, height: 44, background: NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
<Icon size={18} style={{ color: GOLD }} strokeWidth={1.5} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: isFeatured ? 'rgba(255,255,255,0.35)' : 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 10 }}>{fallbackText(w.label, '')}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.7rem)', fontWeight: 900, color: isFeatured ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: '0 0 8px' }}>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 10 }}>{fallbackText(w.label, '')}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.7rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: '0 0 8px' }}>
<Lines text={fallbackText(w.title, '')} />
</h3>
<p style={{ fontFamily: FB, fontSize: 18, color: isFeatured ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.5)', lineHeight: 1.7, margin: '0 0 32px' }}>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, margin: '0 0 32px' }}>
{fallbackText(w.desc, '')}
</p>
<div style={{ borderTop: isFeatured ? '1px solid rgba(255,255,255,0.1)' : '1px solid #D0D5DD', flex: 1 }}>
<div style={{ borderTop: '1px solid #D0D5DD', flex: 1 }}>
{w.listType === 'institutions' ? institutions.map((inst, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 0', borderBottom: '1px solid #D0D5DD' }}>
<div style={{ width: 16, height: 16, background: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, marginTop: 2 }}>
@@ -445,9 +511,9 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
<span style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.65)', lineHeight: 1.55 }}>{fallbackText(inst.text, '')}</span>
</div>
)) : facts.map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '14px 0', borderBottom: isFeatured ? '1px solid rgba(255,255,255,0.07)' : '1px solid #D0D5DD' }}>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: isFeatured ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '0.07em' }}>{fallbackText(item.label, '')}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: isFeatured ? 'rgba(255,255,255,0.50)' : 'rgba(16,24,40,0.45)' }}>{fallbackText(item.desc, '')}</span>
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '14px 0', borderBottom: '1px solid #D0D5DD' }}>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.07em' }}>{fallbackText(item.label, '')}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.45)' }}>{fallbackText(item.desc, '')}</span>
</div>
))}
</div>
@@ -471,131 +537,4 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
);
}
type AwardsGridContent = Partial<typeof participationContent.awardsGrid>;
function AwardsGridSection({ content }: { content?: AwardsGridContent }) {
const isMobile = useIsMobile();
const section = { ...participationContent.awardsGrid, ...(content || {}) };
const cards = mergeAwardCards(section.cards, participationContent.awardsGrid.cards);
return (
<section id="auszeichnungen" style={{ background: '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>
{fallbackText(section.eyebrow, participationContent.awardsGrid.eyebrow)}
</span>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.9fr) minmax(0, 1fr)', gap: isMobile ? 16 : 56, alignItems: 'end' }}>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.5vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0, overflowWrap: 'anywhere' }}>
<Lines text={fallbackText(section.heading, participationContent.awardsGrid.heading)} />
</h2>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}>
{fallbackText(section.description, participationContent.awardsGrid.description)}
</p>
</div>
</div>
<div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))', borderBottom: '1px solid rgba(3,9,58,0.08)' }}>
{cards.map((card, index) => {
const imageFilename = fallbackText(card.imageFilename, participationContent.awardsGrid.cards[index]?.imageFilename || participationContent.awardsGrid.cards[0].imageFilename);
const isContainedImage = imageFilename.endsWith('.png') || imageFilename.includes('roland-berger');
const isLast = index === cards.length - 1;
return (
<article
key={`${fallbackText(card.title, 'award')}-${index}`}
style={{
display: 'flex',
flexDirection: 'column',
minHeight: isMobile ? 'auto' : 620,
borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
background: index === 1 ? NAVY : 'rgba(255,255,255,0.72)',
}}
>
<div style={{ height: isMobile ? 220 : 260, background: imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
<Image unoptimized
src={mediaUrl(card.image, `/images/${imageFilename}`)}
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
style={{
width: '100%',
height: '100%',
objectFit: isContainedImage ? 'contain' : 'cover',
objectPosition: index === 1 ? 'center top' : 'center',
padding: imageFilename.endsWith('.png') ? 22 : 0,
boxSizing: 'border-box',
display: 'block',
}}
/>
</div>
<div style={{ padding: isMobile ? '28px 24px 32px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.26em', textTransform: 'uppercase', color: index === 1 ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}>
{fallbackText(card.label, '')}
</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.25rem, 2vw, 1.7rem)', fontWeight: 900, color: index === 1 ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 16px', overflowWrap: 'anywhere' }}>
<Lines text={fallbackText(card.title, '')} />
</h3>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 22 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: index === 1 ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.55)', lineHeight: 1.7, margin: 0, flex: 1 }}>
{fallbackText(card.description, '')}
</p>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, marginTop: 32 }}>
<Link
to={fallbackText(card.articleCta?.url, '#bewerben')}
style={{
minHeight: 46,
padding: '0 18px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
background: GOLD,
color: '#101828',
fontFamily: FF,
fontSize: 13,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.08em',
textDecoration: 'none',
whiteSpace: 'normal',
textAlign: 'center',
}}
>
{fallbackText(card.articleCta?.label, 'Artikel lesen')} <ArrowRight size={13} />
</Link>
<a
href={fallbackText(card.mailtoCta?.url, `mailto:${participationContent.form.eligibility.ineligibleContactEmail}`)}
style={{
minHeight: 46,
padding: '0 18px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
color: index === 1 ? '#fff' : NAVY,
border: `1px solid ${index === 1 ? 'rgba(255,255,255,0.22)' : 'rgba(3,9,58,0.18)'}`,
fontFamily: FF,
fontSize: 13,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.08em',
textDecoration: 'none',
whiteSpace: 'normal',
textAlign: 'center',
}}
>
<Mail size={13} /> {fallbackText(card.mailtoCta?.label, 'Kontakt aufnehmen')}
</a>
</div>
</div>
</article>
);
})}
</div>
</section>
);
}
export default Participation;

View File

@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import { Link } from '@/spa/router';
import { Trophy, ChevronRight, ArrowRight } from 'lucide-react';
import { Trophy, ArrowRight } from 'lucide-react';
import { WINNERS, type Winner } from '@/spa/data/winners';
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
@@ -14,6 +14,7 @@ const GOLD = '#EFBF04';
const BORDER = '#D0D5DD';
const GRAY = '#666666';
const BG_ALT = '#E4E2E3';
const SIMPLE_LIST_YEAR = 2023;
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
@@ -69,6 +70,7 @@ export default function Preistraeger() {
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]);
const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR;
return (
<div style={{ background: '#fff', minHeight: '100vh' }}>
@@ -129,6 +131,8 @@ export default function Preistraeger() {
<div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}>
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
</div>
) : usesSimpleWinnerGrid ? (
<SimpleWinnerGrid winners={filtered} />
) : (
<div style={{
display: 'grid',
@@ -165,38 +169,6 @@ export default function Preistraeger() {
>
{fallbackText(cta.primaryCta?.label, preistraegerIndexContent.cta.primaryCta.label)} <ArrowRight size={14} />
</Link>
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : undefined, flexWrap: isMobile ? 'wrap' : 'nowrap', gap: 8 }}>
<span style={{ width: 20, height: 1, background: 'rgba(239,191,4,0.3)', display: 'inline-block' }} />
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 11, color: 'rgba(16,24,40,0.4)' }}>{fallbackText(cta.secondaryPrefix, preistraegerIndexContent.cta.secondaryPrefix)}</span>
<Link
to={fallbackText(cta.secondaryCta?.url, preistraegerIndexContent.cta.secondaryCta.url)}
style={{
fontFamily: '"IBM Plex Sans", sans-serif',
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: 'rgba(239,191,4,0.7)',
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
borderBottom: '1px solid rgba(239,191,4,0.3)',
paddingBottom: 1,
transition: 'color 0.15s, borderColor 0.15s',
}}
onMouseEnter={e => {
(e.currentTarget as HTMLElement).style.color = '#EFBF04';
(e.currentTarget as HTMLElement).style.borderBottomColor = '#EFBF04';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.color = 'rgba(239,191,4,0.7)';
(e.currentTarget as HTMLElement).style.borderBottomColor = 'rgba(239,191,4,0.3)';
}}
>
{fallbackText(cta.secondaryCta?.label, preistraegerIndexContent.cta.secondaryCta.label)} <ChevronRight size={10} />
</Link>
</div>
</div>
</div>
);
@@ -257,3 +229,91 @@ function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string
</Link>
);
}
function SimpleWinnerGrid({ winners }: { winners: Winner[] }) {
const isMobile = useIsMobile();
return (
<div style={{
background: '#F7F7F8',
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))',
gap: isMobile ? 12 : 16,
padding: isMobile ? '24px' : '40px 80px 56px',
}}>
{winners.map(winner => (
<SimpleWinnerCard key={winner.id} winner={winner} />
))}
</div>
);
}
function SimpleWinnerCard({ winner }: { winner: Winner }) {
const isMobile = useIsMobile();
const hasDescription = winner.shortDesc.trim().length > 0;
return (
<article
style={{
background: '#fff',
border: `1px solid ${BORDER}`,
color: '#101828',
display: 'flex',
flexDirection: 'column',
minHeight: isMobile ? 0 : 220,
padding: isMobile ? '20px' : '24px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 18 }}>
<span style={{
color: GOLD,
fontFamily: FF,
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}>
{winner.year} · {winner.type}
</span>
{winner.category ? (
<span style={{
border: `1px solid ${BORDER}`,
color: GRAY,
fontFamily: FF,
fontSize: 12,
fontWeight: 600,
lineHeight: 1.2,
padding: '5px 8px',
}}>
{winner.category}
</span>
) : null}
</div>
<h3 style={{
color: NAVY,
fontFamily: FF,
fontSize: isMobile ? 20 : 22,
fontWeight: 700,
lineHeight: 1.18,
margin: 0,
overflowWrap: 'anywhere',
}}>
{winner.name}
</h3>
{hasDescription ? (
<p style={{
color: GRAY,
fontFamily: FF,
fontSize: 15,
lineHeight: 1.55,
margin: '16px 0 0',
overflowWrap: 'anywhere',
}}>
{winner.shortDesc}
</p>
) : null}
</article>
);
}