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
969 lines
59 KiB
TypeScript
969 lines
59 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
||
import { Button } from '@/spa/components/ui/button';
|
||
import { Play, ChevronRight, X, Star } from 'lucide-react';
|
||
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';
|
||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||
import type { SpaApplicationPhaseData, SpaApplicationPhaseItem } from '@/spa/applicationPhase'
|
||
import { useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRoute'
|
||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
|
||
import { homeContent } from '@/spa/homeContent'
|
||
import { netzwerkContent } from '@/spa/netzwerkContent'
|
||
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
|
||
|
||
const FF = '"IBM Plex Sans", sans-serif';
|
||
|
||
type CmsRecord = Record<string, unknown>;
|
||
type CmsLink = { label?: string | null; to?: string | null; url?: string | null };
|
||
type HomeHeroCms = {
|
||
backgroundImage?: unknown;
|
||
badge?: string | null;
|
||
description?: string | null;
|
||
headingAccent?: string | null;
|
||
headingLine1?: string | null;
|
||
imageAlt?: string | null;
|
||
partners?: unknown;
|
||
partnersLabel?: string | null;
|
||
primaryCta?: CmsLink | null;
|
||
secondaryCta?: CmsLink | null;
|
||
videoLabel?: string | null;
|
||
};
|
||
type HomeSectionCms = CmsRecord & {
|
||
awardCaption?: string | null;
|
||
awardImage?: unknown;
|
||
awardImageAlt?: string | null;
|
||
benefits?: unknown;
|
||
body?: string | null;
|
||
cardFallbackTitle?: string | null;
|
||
criteria?: unknown;
|
||
cta?: CmsLink | null;
|
||
eyebrow?: string | null;
|
||
fallbackImage?: unknown;
|
||
featured?: unknown;
|
||
footnote?: string | null;
|
||
footnoteStrong?: string | null;
|
||
formEyebrow?: string | null;
|
||
formTitle?: string | null;
|
||
heading?: string | null;
|
||
hoverLabel?: string | null;
|
||
image?: unknown;
|
||
imageAlt?: string | null;
|
||
imageLabel?: string | null;
|
||
items?: unknown;
|
||
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
||
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
||
note?: string | null;
|
||
quote?: string | null;
|
||
stats?: unknown;
|
||
};
|
||
type HomeVideoModalCms = {
|
||
closeLabel?: string | null;
|
||
description?: string | null;
|
||
title?: string | null;
|
||
video?: unknown;
|
||
};
|
||
type HomeTestimonialsCms = {
|
||
desktopHeading?: string | null;
|
||
eyebrow?: string | null;
|
||
heading?: string | null;
|
||
items?: Array<Record<string, unknown>> | null;
|
||
nextLabel?: string | null;
|
||
previousLabel?: string | null;
|
||
slideLabelPrefix?: string | null;
|
||
yearPrefix?: string | null;
|
||
};
|
||
type HomeCms = {
|
||
application?: HomeSectionCms;
|
||
benefits?: HomeSectionCms;
|
||
form?: Partial<typeof homeContent.form>;
|
||
hero?: HomeHeroCms;
|
||
intro?: HomeSectionCms;
|
||
quickCheck?: HomeSectionCms;
|
||
testimonials?: HomeTestimonialsCms;
|
||
videoModal?: HomeVideoModalCms;
|
||
winners?: HomeSectionCms;
|
||
};
|
||
type NetzwerkPartnersCms = {
|
||
defaultLogoColor?: string | null;
|
||
};
|
||
type RenderedStatusPhase = Omit<SpaApplicationPhaseItem, 'accent' | 'bg' | 'cta' | 'membershipCta'> & {
|
||
accent: string;
|
||
bg: string;
|
||
cta?: { 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) =>
|
||
typeof value === 'string' && value.length > 0 ? value : fallback;
|
||
|
||
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' ||
|
||
cta?.to === '/mitglied-werden' ||
|
||
cta?.label?.trim().toLowerCase() === 'mitglied werden';
|
||
|
||
function Lines({ text }: { text: string }) {
|
||
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
|
||
}
|
||
|
||
const isNetzwerkPage = (page: CmsRecord) =>
|
||
page.spaPath === '/netzwerk' || page.slug === 'netzwerk' || page.slug === 'network'
|
||
|
||
function partnerLogos(partners: PartnerLogoData[], defaultLogoColor: string) {
|
||
return partners.map((partner, index) => ({
|
||
id: String(partner.stableId || partner.id || partner.name || index),
|
||
label: (
|
||
<span
|
||
aria-label={fallbackText(partner.name, 'Partner')}
|
||
style={{
|
||
width: 224,
|
||
height: 54,
|
||
padding: '9px 18px',
|
||
background: 'rgba(255,255,255,0.92)',
|
||
border: '1px solid rgba(255,255,255,0.18)',
|
||
borderRadius: 6,
|
||
boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<PartnerLogoMark partner={partner} defaultLogoColor={defaultLogoColor} />
|
||
</span>
|
||
),
|
||
}));
|
||
}
|
||
|
||
function sortedPartners<T extends PartnerLogoData>(partners: T[]) {
|
||
return partners
|
||
.map((partner, index) => ({ partner, index }))
|
||
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
|
||
.map(({ partner }) => partner);
|
||
}
|
||
|
||
function HomeWinnerCard({
|
||
winner,
|
||
fallbackTitle,
|
||
fallbackImage,
|
||
hoverLabel,
|
||
}: {
|
||
winner: WinnerCardData;
|
||
fallbackTitle: string;
|
||
fallbackImage: string;
|
||
hoverLabel: string;
|
||
}) {
|
||
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 = mediaUrl(winner?.image, winner?.img || staticWinner?.img || 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 ? '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 [showVideo, setShowVideo] = useState(false);
|
||
const videoRef = useRef<HTMLVideoElement>(null);
|
||
const isMobile = useIsMobile();
|
||
const home = (useCmsRoute()?.doc?.home || {}) as HomeCms;
|
||
const netzwerkPage = useCmsCollection('pages').find((page) => isNetzwerkPage(page as CmsRecord)) as
|
||
| ({ netzwerk?: { partners?: NetzwerkPartnersCms } } & CmsRecord)
|
||
| undefined;
|
||
const partnerSection = netzwerkPage?.netzwerk?.partners;
|
||
const defaultLogoColor = fallbackText(partnerSection?.defaultLogoColor, netzwerkContent.partners.defaultLogoColor);
|
||
const cmsPartners = useCmsCollection('partners') as PartnerLogoData[];
|
||
const partnerSource = cmsPartners.length ? cmsPartners : (netzwerkContent.partners.items as PartnerLogoData[]);
|
||
const networkPartners = sortedPartners(partnerSource)
|
||
.filter((partner) => partner.active !== false && partner.showOnHomepage !== false)
|
||
.map((partner) => ({
|
||
...partner,
|
||
id: partner.stableId || partner.id,
|
||
logoColor: fallbackText(partner.logoColor, defaultLogoColor),
|
||
}));
|
||
const hero = home.hero || {};
|
||
const quickCheck = home.quickCheck || {};
|
||
const intro = home.intro || {};
|
||
const winnersSection = home.winners || {};
|
||
const benefits = home.benefits || {};
|
||
const application = home.application || {};
|
||
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 = {
|
||
label: fallbackText(hero.secondaryCta?.label, homeContent.hero.secondaryCta.label),
|
||
url: fallbackText(hero.secondaryCta?.url, homeContent.hero.secondaryCta.url),
|
||
};
|
||
const showHeroSecondaryCta = !HIDE_MEMBERSHIP_HOME_HERO_CTA || !isMembershipCta(heroSecondaryCta);
|
||
const selectedWinnerDocs = selectedWinners
|
||
.map((selected) => {
|
||
const selectedRecord = selected as { id?: string | number }
|
||
const selectedID = typeof selected === 'object' && selected !== null ? selectedRecord.id : selected
|
||
const listDoc = cmsPreistraeger.find((doc) => String((doc as { id?: string | number }).id) === String(selectedID))
|
||
return listDoc || (typeof selected === 'object' ? selected : undefined)
|
||
})
|
||
.filter(Boolean)
|
||
const homeWinners = selectedWinnerDocs.length
|
||
? selectedWinnerDocs
|
||
: (cmsPreistraeger.length ? cmsPreistraeger : WINNERS).filter((w) => Number((w as { year?: string | number }).year) === 2025).slice(0, 8);
|
||
const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria);
|
||
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, '');
|
||
|
||
useEffect(() => {
|
||
if (!showVideo || !videoSrc) return;
|
||
|
||
videoRef.current?.play().catch(() => {
|
||
// Browser autoplay rules can still require the native control click.
|
||
});
|
||
}, [showVideo, videoSrc]);
|
||
|
||
return (
|
||
<div className="animate-fade-in">
|
||
{/* Hero Section */}
|
||
<section className="relative flex items-center overflow-hidden" style={{ background: '#111D55', height: isMobile ? '88svh' : '100vh' }}>
|
||
<div className="absolute inset-0 z-0">
|
||
<Image unoptimized
|
||
src={mediaUrl(hero.backgroundImage, `/images/${homeContent.hero.backgroundImageFilename}`)}
|
||
alt={mediaAlt(hero.backgroundImage, fallbackText(hero.imageAlt, homeContent.hero.imageAlt))}
|
||
className="w-full h-full object-cover scale-105"
|
||
style={{ objectPosition: 'center top' }}
|
||
/>
|
||
<div style={{ position:'absolute', inset:0, background: isMobile ? 'linear-gradient(to top, rgba(17,29,85,0.96) 0%, rgba(17,29,85,0.82) 42%, rgba(17,29,85,0.48) 72%, rgba(17,29,85,0.22) 100%)' : 'linear-gradient(to right, #111D55 0%, rgba(17,29,85,0.90) 38%, rgba(17,29,85,0.18) 65%, transparent 100%)' }}></div>
|
||
</div>
|
||
|
||
<div className="container mx-auto relative z-10" style={{ padding: isMobile ? '0 24px' : '0 24px' }}>
|
||
<div style={{ maxWidth: isMobile ? '100%' : '48rem' }}>
|
||
<div className="inline-flex items-center gap-3 bg-accent/20 border border-accent/30 rounded-full px-4 py-1.5 mb-8 backdrop-blur-sm animate-fade-in" style={{ marginBottom: isMobile ? 20 : undefined }}>
|
||
<span className="flex h-2 w-2 rounded-full bg-accent"></span>
|
||
<span className="text-accent text-[10px] uppercase tracking-[0.2em] font-bold">{fallbackText(hero.badge, homeContent.hero.badge)}</span>
|
||
</div>
|
||
|
||
<h1 className="font-display font-bold text-white mb-6 leading-[0.9] tracking-tight" style={{ fontSize: isMobile ? 'clamp(1.8rem, 10vw, 3rem)' : 'clamp(3.5rem, 8vw, 6rem)' }}>
|
||
{fallbackText(hero.headingLine1, homeContent.hero.headingLine1)} <br />
|
||
<span className="text-accent">{fallbackText(hero.headingAccent, homeContent.hero.headingAccent)}</span>
|
||
</h1>
|
||
|
||
<p className="text-white/70 mb-12 font-light leading-relaxed" style={{ fontSize: isMobile ? '1.0625rem' : '1.375rem', maxWidth: isMobile ? '100%' : '36rem', marginBottom: isMobile ? 28 : undefined }}>
|
||
{fallbackText(hero.description, homeContent.hero.description)}
|
||
</p>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
<Link
|
||
to={fallbackText(hero.primaryCta?.url, homeContent.hero.primaryCta.url)}
|
||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'#101828', background:'#EFBF04', padding:'16px 36px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'background 0.15s, box-shadow 0.2s', width: isMobile ? '100%' : 'auto' }}
|
||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
|
||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
||
>
|
||
{fallbackText(hero.primaryCta?.label, homeContent.hero.primaryCta.label)} <ChevronRight size={16} />
|
||
</Link>
|
||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 16, alignItems: isMobile ? 'stretch' : 'center' }}>
|
||
{showHeroSecondaryCta && (
|
||
<Link
|
||
to={heroSecondaryCta.url}
|
||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'rgba(255,255,255,0.85)', border:'1.5px solid rgba(255,255,255,0.35)', padding:'14px 32px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'border-color 0.15s, color 0.15s', background:'transparent', width: isMobile ? '100%' : 'auto' }}
|
||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#EFBF04'; }}
|
||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.35)'; (e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.85)'; }}
|
||
>
|
||
{heroSecondaryCta.label}
|
||
</Link>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowVideo(true)}
|
||
className="flex items-center gap-4 text-white hover:text-accent transition-colors group"
|
||
style={{ justifyContent: isMobile ? 'center' : 'flex-start' }}
|
||
>
|
||
<div className="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-all">
|
||
<Play size={20} fill="currentColor" />
|
||
</div>
|
||
<span className="uppercase text-xs tracking-widest font-bold">{fallbackText(hero.videoLabel, homeContent.hero.videoLabel)}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Partner Bar */}
|
||
<div className="absolute bottom-0 left-0 w-full bg-white/5 backdrop-blur-xl border-t border-white/10 hidden lg:block">
|
||
<div className="px-8 py-5 flex items-center justify-between opacity-60 hover:opacity-100 transition-opacity duration-500">
|
||
{/* Left: label – stays put */}
|
||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.5)', whiteSpace: 'nowrap', flexShrink: 0 }}>{fallbackText(hero.partnersLabel, homeContent.hero.partnersLabel)}</span>
|
||
{/* Right: ticker window */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
|
||
<div style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.2)' }} />
|
||
<div style={{ width: 'min(820px, calc(100vw - 260px))', overflow: 'hidden', flexShrink: 0 }}>
|
||
<PartnerTicker logos={partnerLogos(networkPartners, defaultLogoColor)} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Status Phase Slider */}
|
||
<StatusSlider data={applicationPhase} />
|
||
|
||
{/* Schnell-Check Section */}
|
||
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||
<MunichSkylineBg />
|
||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', position: 'relative' }}>
|
||
{/* Left (desktop) / Bottom (mobile) – text block (light bg, dark text) */}
|
||
<div style={{ background: '#fff', padding: isMobile ? '32px 24px 44px' : '80px 72px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}>
|
||
{/* Eyebrow + heading – desktop only (on mobile these are overlaid on the image) */}
|
||
{!isMobile && (
|
||
<>
|
||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.3em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(quickCheck.eyebrow, homeContent.quickCheck.eyebrow)}</span>
|
||
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(1.8rem, 2.8vw, 2.5rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.05, margin: '0 0 20px' }}>
|
||
<Lines text={fallbackText(quickCheck.heading, homeContent.quickCheck.heading)} />
|
||
</h2>
|
||
</>
|
||
)}
|
||
<div style={{ width: 40, height: 2, background: '#EFBF04', marginBottom: 28 }} />
|
||
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, marginBottom: 32 }}>
|
||
{fallbackText(quickCheck.body, homeContent.quickCheck.body)}
|
||
</p>
|
||
{/* Criteria list */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, marginBottom: 36, borderTop: '1px solid #D0D5DD' }}>
|
||
{quickCriteria.map((item, i) => (
|
||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '13px 0', borderBottom: '1px solid #D0D5DD' }}>
|
||
<div style={{ width: 24, height: 24, background: '#111D55', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||
<svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1,5 3.5,7.5 9,2" stroke="#EFBF04" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||
</div>
|
||
<div>
|
||
<div style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight: 700, color: '#101828' }}>{item.label}</div>
|
||
<div style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 14, color: 'rgba(16,24,40,0.5)' }}>{item.desc}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 14, color: 'rgba(16,24,40,0.55)', lineHeight: 1.6, marginTop: 16, marginBottom: 28 }}>
|
||
{fallbackText(quickCheck.note, homeContent.quickCheck.note)}
|
||
</p>
|
||
<Link
|
||
to={fallbackText(quickCheck.cta?.url, homeContent.quickCheck.cta.url)}
|
||
style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: '#101828', background: '#EFBF04', padding: '15px 32px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8, alignSelf: isMobile ? 'stretch' : 'flex-start', transition: 'background 0.15s, box-shadow 0.2s' }}
|
||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
|
||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
||
>
|
||
{fallbackText(quickCheck.cta?.label, homeContent.quickCheck.cta.label)} <ChevronRight size={14} />
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Right (desktop) / Top (mobile) – image; on mobile the heading is overlaid here */}
|
||
<div style={{ position: 'relative', minHeight: isMobile ? 280 : 560, overflow: 'hidden', order: isMobile ? 1 : 0 }}>
|
||
<Image unoptimized
|
||
src={mediaUrl(quickCheck.image, `/images/${homeContent.quickCheck.imageFilename}`)}
|
||
alt={mediaAlt(quickCheck.image, fallbackText(quickCheck.imageAlt, homeContent.quickCheck.imageAlt))}
|
||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: isMobile ? 'center right' : 'center center' }}
|
||
/>
|
||
{/* Overlay: mobile = navy shadow bottom-left (for the heading); desktop = leftward fade into text bg */}
|
||
<div style={{ position: 'absolute', inset: 0, background: isMobile
|
||
? 'linear-gradient(to top, rgba(3,9,58,0.92) 0%, rgba(3,9,58,0.55) 34%, rgba(3,9,58,0.12) 68%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.55) 0%, transparent 55%)'
|
||
: 'linear-gradient(to left, transparent 40%, #fff 100%)' }} />
|
||
{/* Heading overlay – mobile only */}
|
||
{isMobile && (
|
||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 26px', zIndex: 2 }}>
|
||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.3em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(quickCheck.eyebrow, homeContent.quickCheck.eyebrow)}</span>
|
||
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(1.9rem, 8vw, 2.5rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.05, margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.5)' }}>
|
||
<Lines text={fallbackText(quickCheck.heading, homeContent.quickCheck.heading)} />
|
||
</h2>
|
||
</div>
|
||
)}
|
||
{/* Gold accent line – bottom edge */}
|
||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(to right, rgba(239,191,4,0.2), #EFBF04, rgba(239,191,4,0.2))', zIndex: 3 }} />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Intro Section – editorial split, no radius, no floating badge */}
|
||
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||
<MunichSkylineBg />
|
||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '45% 55%' }}>
|
||
{/* Left – full-bleed image with stats overlay (+ heading on mobile) */}
|
||
<div style={{ position: 'relative', minHeight: isMobile ? 300 : 600, overflow: 'hidden' }}>
|
||
<Image unoptimized
|
||
src={mediaUrl(intro.image, `/images/${homeContent.intro.imageFilename}`)}
|
||
alt={mediaAlt(intro.image, fallbackText(intro.imageAlt, homeContent.intro.imageAlt))}
|
||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top' }}
|
||
/>
|
||
{/* Dark overlay */}
|
||
<div style={{ position: 'absolute', inset: 0, background: isMobile
|
||
? 'linear-gradient(to top, rgba(3,9,58,0.92) 0%, rgba(3,9,58,0.5) 36%, rgba(3,9,58,0.1) 70%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)'
|
||
: 'linear-gradient(to right, transparent 50%, #fff 100%), linear-gradient(to top, rgba(3,9,58,0.82) 0%, rgba(3,9,58,0.50) 60%, transparent 100%)' }} />
|
||
{/* Heading overlay – mobile only (bottom-left) */}
|
||
{isMobile && (
|
||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 26px', zIndex: 2 }}>
|
||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.3em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(intro.eyebrow, homeContent.intro.eyebrow)}</span>
|
||
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(1.9rem, 8vw, 2.5rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.05, margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.5)' }}>
|
||
<Lines text={fallbackText(intro.heading, homeContent.intro.heading)} />
|
||
</h2>
|
||
</div>
|
||
)}
|
||
{/* Stats bottom-left – desktop only (mobile shows them below the image) */}
|
||
{!isMobile && (
|
||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '32px 40px', background: 'linear-gradient(to top, rgba(3,9,58,0.88) 0%, transparent 100%)', display: 'flex', gap: 36 }}>
|
||
{introStats.map(({ value: v, label: l }) => (
|
||
<div key={l}>
|
||
<div style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 28, fontWeight: 900, color: '#EFBF04', lineHeight: 1 }}>{v}</div>
|
||
<div style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 9, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.6)', marginTop: 4 }}>{l}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Right – editorial text */}
|
||
<div style={{ background: '#fff', padding: isMobile ? '32px 24px 40px' : '80px 72px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||
{!isMobile && (
|
||
<>
|
||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.3em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(intro.eyebrow, homeContent.intro.eyebrow)}</span>
|
||
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(2rem, 3.2vw, 2.8rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.05, margin: '0 0 20px' }}>
|
||
<Lines text={fallbackText(intro.heading, homeContent.intro.heading)} />
|
||
</h2>
|
||
</>
|
||
)}
|
||
{/* Stats row – mobile only, below the image */}
|
||
{isMobile && (
|
||
<div style={{ display: 'flex', gap: 16, marginBottom: 28, paddingBottom: 24, borderBottom: '1px solid #D0D5DD' }}>
|
||
{introStats.map(({ value: v, label: l }) => (
|
||
<div key={l} style={{ flex: 1 }}>
|
||
<div style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 26, fontWeight: 900, color: '#101828', lineHeight: 1 }}>{v}</div>
|
||
<div style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 9, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#4A8FC9', marginTop: 6 }}>{l}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<div style={{ width: 40, height: 2, background: '#EFBF04', marginBottom: 28, display: isMobile ? 'none' : 'block' }} />
|
||
<blockquote style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 18, fontStyle: 'italic', color: '#101828', lineHeight: 1.7, borderLeft: '3px solid #EFBF04', paddingLeft: 20, margin: '0 0 24px' }}>
|
||
{fallbackText(intro.quote, homeContent.intro.quote)}
|
||
</blockquote>
|
||
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, margin: '0 0 36px' }}>
|
||
{fallbackText(intro.body, homeContent.intro.body)}
|
||
</p>
|
||
<Link
|
||
to={fallbackText(intro.cta?.url, homeContent.intro.cta.url)}
|
||
style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', color: '#101828', background: '#EFBF04', padding: '14px 28px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8, alignSelf: isMobile ? 'stretch' : 'flex-start', transition: 'background 0.15s, box-shadow 0.2s' }}
|
||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
|
||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
||
>
|
||
{fallbackText(intro.cta?.label, homeContent.intro.cta.label)} <ChevronRight size={14} />
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 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 }}>
|
||
<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>
|
||
<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 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',
|
||
}}>
|
||
{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)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Testimonials Section */}
|
||
<TestimonialsSection data={home.testimonials} />
|
||
|
||
|
||
{/* Benefits Section – editorial split */}
|
||
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||
<MunichSkylineBg />
|
||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 560 }}>
|
||
|
||
{/* Left – text on white */}
|
||
<div style={{ background: '#fff', padding: isMobile ? '32px 24px 44px' : '80px 72px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}>
|
||
{!isMobile && (
|
||
<>
|
||
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.3em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(benefits.eyebrow, homeContent.benefits.eyebrow)}</span>
|
||
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3vw, 2.6rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.05, margin: '0 0 48px' }}>
|
||
<Lines text={fallbackText(benefits.heading, homeContent.benefits.heading)} />
|
||
</h2>
|
||
</>
|
||
)}
|
||
|
||
{/* Numbered rows – no cards, no shadows */}
|
||
<div style={{ borderTop: '1px solid #D0D5DD' }}>
|
||
{benefitItems.map((item, i) => (
|
||
<div
|
||
key={i}
|
||
style={{ display: 'grid', gridTemplateColumns: '48px 1fr', gap: '0 20px', padding: '24px 0', borderBottom: '1px solid #D0D5DD', cursor: 'default', transition: 'background 0.15s' }}
|
||
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(17,29,85,0.04)')}
|
||
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
||
>
|
||
<span style={{ fontFamily: FF, fontSize: 11, fontWeight: 700, color: '#EFBF04', letterSpacing: '0.1em', paddingTop: 2 }}>{item.num}</span>
|
||
<div>
|
||
<div style={{ fontFamily: FF, fontSize: 17, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 5 }}>{item.title}</div>
|
||
<div style={{ fontFamily: FF, fontSize: 16, color: 'rgba(16,24,40,0.55)', lineHeight: 1.6 }}>{item.desc}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right – full-bleed photo (+ heading on mobile) */}
|
||
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : 'auto', order: isMobile ? 1 : 0 }}>
|
||
<Image unoptimized
|
||
src={mediaUrl(benefits.image, `/images/${homeContent.benefits.imageFilename}`)}
|
||
alt={mediaAlt(benefits.image, fallbackText(benefits.imageAlt, homeContent.benefits.imageAlt))}
|
||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center center', display: 'block' }}
|
||
/>
|
||
<div style={{ position: 'absolute', inset: 0, background: isMobile
|
||
? 'linear-gradient(to top, rgba(3,9,58,0.92) 0%, rgba(3,9,58,0.5) 36%, rgba(3,9,58,0.1) 70%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)'
|
||
: 'linear-gradient(to right, #fff 0%, transparent 30%), linear-gradient(to top, rgba(3,9,58,0.82) 0%, transparent 50%)' }} />
|
||
{/* Heading overlay – mobile only (bottom-left) */}
|
||
{isMobile && (
|
||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 26px', zIndex: 2 }}>
|
||
<span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.3em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(benefits.eyebrow, homeContent.benefits.eyebrow)}</span>
|
||
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.9rem, 8vw, 2.4rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.05, margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.5)' }}>
|
||
<Lines text={fallbackText(benefits.heading, homeContent.benefits.heading)} />
|
||
</h2>
|
||
</div>
|
||
)}
|
||
{/* Bottom-left label – desktop only */}
|
||
{!isMobile && (
|
||
<div style={{ position: 'absolute', bottom: 40, left: 40 }}>
|
||
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#EFBF04', marginBottom: 6 }}>{fallbackText(benefits.imageLabel, homeContent.benefits.imageLabel)}</div>
|
||
<div style={{ width: 32, height: 2, background: '#EFBF04' }} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 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' }} />
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr', position: 'relative', flex: isMobile ? 'none' : 1, minHeight: 0, overflow: 'hidden' }}>
|
||
|
||
{/* 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 }}>{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={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 }}>
|
||
{showApplicationForm ? fallbackText(application.body, homeContent.application.body) : newsletterCopy.body}
|
||
</p>
|
||
|
||
{/* Benefit rows */}
|
||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||
{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 }}>
|
||
<span style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{item.label}</span>
|
||
<span style={{ fontFamily: FF, fontSize: 14, color: 'rgba(255,255,255,0.45)', textAlign: 'right' }}>{item.desc}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Divider */}
|
||
{!isMobile && <div style={{ background: 'rgba(255,255,255,0.07)' }} />}
|
||
|
||
{/* Right – gold premium panel */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0, position: 'relative', background: 'linear-gradient(160deg,#DDB84A 0%,#C9A227 52%,#A87800 100%)' }}>
|
||
|
||
{/* Premium award crosshatch pattern */}
|
||
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 0 }} aria-hidden="true">
|
||
<defs>
|
||
<pattern id="bmpCross" width="18" height="18" patternUnits="userSpaceOnUse">
|
||
<path d="M0,0 L18,18 M18,0 L0,18" stroke="rgba(17,29,85,0.055)" strokeWidth="0.65" />
|
||
</pattern>
|
||
</defs>
|
||
<rect width="100%" height="100%" fill="url(#bmpCross)" />
|
||
</svg>
|
||
|
||
{/* Inner vignette – edges slightly deeper gold */}
|
||
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at center, transparent 55%, rgba(100,70,0,0.25) 100%)', pointerEvents: 'none', zIndex: 0 }} />
|
||
|
||
{/* Award ceremony photo */}
|
||
{!isMobile && (
|
||
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
||
<Image unoptimized
|
||
src={mediaUrl(application.awardImage, `/images/${homeContent.application.awardImageFilename}`)}
|
||
alt={mediaAlt(application.awardImage, fallbackText(application.awardImageAlt, homeContent.application.awardImageAlt))}
|
||
style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 25%', filter: 'sepia(0.18) brightness(0.92)' }}
|
||
/>
|
||
{/* Blend photo into gold panel */}
|
||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
|
||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
|
||
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<div style={{ width: 5, height: 5, borderRadius: '50%', background: 'rgba(17,29,85,0.7)' }} />
|
||
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(17,29,85,0.75)', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{fallbackText(application.awardCaption, homeContent.application.awardCaption)}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Form area */}
|
||
<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>
|
||
|
||
</div>
|
||
|
||
{/* 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 }}>{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>
|
||
|
||
{/* Video Modal */}
|
||
{showVideo && (
|
||
<div className="fixed inset-0 z-[200] bg-black flex items-center justify-center p-6 bg-opacity-95">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowVideo(false)}
|
||
className="absolute top-8 right-8 text-white/60 hover:text-white transition-colors"
|
||
aria-label={fallbackText(videoModal.closeLabel, homeContent.videoModal.closeLabel)}
|
||
>
|
||
<X size={40} />
|
||
</button>
|
||
<div className="w-full max-w-6xl aspect-video bg-white/5 rounded-3xl overflow-hidden flex items-center justify-center relative">
|
||
{videoSrc ? (
|
||
<video
|
||
ref={videoRef}
|
||
src={videoSrc}
|
||
className="h-full w-full bg-black object-contain"
|
||
controls
|
||
autoPlay
|
||
playsInline
|
||
aria-label={videoTitle}
|
||
/>
|
||
) : (
|
||
<div className="text-center">
|
||
<Play size={80} className="text-accent mx-auto mb-6 opacity-40" />
|
||
<p className="text-white/40 uppercase tracking-widest text-sm">{videoTitle}</p>
|
||
<p className="text-white/60 mt-4 text-xs italic font-body">{fallbackText(videoModal.description, homeContent.videoModal.description)}</p>
|
||
<Button type="button" onClick={() => setShowVideo(false)} className="mt-8" variant="outline">{fallbackText(videoModal.closeLabel, homeContent.videoModal.closeLabel)}</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ─── StatusSlider ─────────────────────────────────────────────────────────────
|
||
|
||
function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
|
||
const isMobile = useIsMobile();
|
||
if (!data?.phases?.length) return null;
|
||
|
||
const phases = data.phases.map((phase): RenderedStatusPhase => {
|
||
const cta = phase.cta?.label
|
||
? { label: phase.cta.label, to: phase.cta.to || phase.cta.url || '/' }
|
||
: null
|
||
const membershipCta = phase.membershipCta?.label
|
||
? { label: phase.membershipCta.label, to: phase.membershipCta.to || phase.membershipCta.url || '/mitglied-werden' }
|
||
: undefined
|
||
|
||
return {
|
||
...phase,
|
||
accent: phase.accent || '#EFBF04',
|
||
bg: phase.bg || '#020A1E',
|
||
cta,
|
||
membershipCta,
|
||
}
|
||
});
|
||
const configuredActive = Number(data.activePhase);
|
||
const active = Math.max(0, Math.min(phases.length - 1, Number.isFinite(configuredActive) ? configuredActive : 0));
|
||
const phase = phases[active];
|
||
|
||
return (
|
||
<section
|
||
style={{
|
||
position: 'relative',
|
||
overflow: 'hidden',
|
||
height: isMobile ? 'auto' : 480,
|
||
minHeight: isMobile ? 360 : 'auto',
|
||
}}
|
||
>
|
||
<StatusSlideCard
|
||
phase={phase}
|
||
isMobile={isMobile}
|
||
noActionLabel={data.noActionLabel}
|
||
successApplicationsLabel={data.successApplicationsLabel}
|
||
successWinnersLabel={data.successWinnersLabel}
|
||
/>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
// ─── StatusSlideCard ──────────────────────────────────────────────────────────
|
||
|
||
function StatusSlideCard({
|
||
phase,
|
||
isMobile,
|
||
noActionLabel,
|
||
successApplicationsLabel,
|
||
successWinnersLabel,
|
||
}: {
|
||
phase: RenderedStatusPhase;
|
||
isMobile: boolean;
|
||
noActionLabel: string;
|
||
successApplicationsLabel: string;
|
||
successWinnersLabel: string;
|
||
}) {
|
||
return (
|
||
<div
|
||
style={{
|
||
width: '100vw',
|
||
height: '100%',
|
||
flexShrink: 0,
|
||
position: 'relative',
|
||
background: phase.bg,
|
||
overflow: 'hidden',
|
||
display: 'grid',
|
||
gridTemplateColumns: isMobile ? '1fr' : '1fr 360px',
|
||
}}
|
||
>
|
||
|
||
{/* Left content */}
|
||
<div style={{ padding: isMobile ? '48px 24px' : '52px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', position: 'relative', zIndex: 1, gap: isMobile ? 24 : 0 }}>
|
||
|
||
{/* Status badge + phase label */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: `${phase.accent}18`, border: `1px solid ${phase.accent}35`, borderRadius: 999, padding: '5px 14px' }}>
|
||
{phase.pulse && <span className="animate-pulse" style={{ width: 6, height: 6, borderRadius: '50%', background: phase.accent, display: 'inline-block' }} />}
|
||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.32em', textTransform: 'uppercase', color: phase.accent }}>{phase.tag}</span>
|
||
</div>
|
||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(255,255,255,0.50)', letterSpacing: '0.15em', textTransform: 'uppercase' }}>{phase.phase}</span>
|
||
</div>
|
||
|
||
{/* Headline + body + CTA */}
|
||
<div>
|
||
<h2 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.6rem)' : 'clamp(2.4rem, 3.8vw, 3.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 0.94, margin: '0 0 24px' }}>
|
||
{phase.headline}
|
||
</h2>
|
||
<p style={{ fontFamily: '"Inter", sans-serif', fontSize: 17, color: 'rgba(255,255,255,0.68)', lineHeight: 1.8, marginBottom: 32, maxWidth: 520 }}>
|
||
{phase.body}
|
||
</p>
|
||
{phase.cta ? (
|
||
<Link
|
||
to={phase.cta.to}
|
||
onMouseDown={e => e.stopPropagation()}
|
||
style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: '#101828', background: phase.accent, padding: '14px 32px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, transition: 'opacity 0.2s' }}
|
||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '0.85')}
|
||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}
|
||
>
|
||
{phase.cta.label} <ChevronRight size={14} />
|
||
</Link>
|
||
) : (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<div style={{ width: 20, height: 1, background: 'rgba(255,255,255,0.2)' }} />
|
||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(255,255,255,0.50)', letterSpacing: '0.2em', textTransform: 'uppercase' }}>{noActionLabel}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Meta */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<div style={{ width: 20, height: 1, background: phase.accent, opacity: 0.35 }} />
|
||
<span style={{ fontFamily: FF, fontSize: 10, color: `${phase.accent}70`, letterSpacing: '0.18em', textTransform: 'uppercase' }}>{phase.meta}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right visual panel */}
|
||
{!isMobile && <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', gap: 36, padding: '52px 48px', position: 'relative', zIndex: 1, borderLeft: '1px solid rgba(255,255,255,0.05)' }}>
|
||
{phase.num !== '03' ? (
|
||
/* Concentric rings for active phases */
|
||
<div style={{ position: 'relative', width: 180, height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
{([0, 20, 44] as const).map((inset, i) => (
|
||
<div key={inset} style={{ position: 'absolute', inset, borderRadius: '50%', border: `1px solid ${phase.accent}${['12', '25', '45'][i]}` }} />
|
||
))}
|
||
<div style={{ width: 76, height: 76, borderRadius: '50%', background: `${phase.accent}15`, border: `1px solid ${phase.accent}55`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 900, color: phase.accent, letterSpacing: '0.05em' }}>{phase.num}</span>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* Success stats */
|
||
<div style={{ textAlign: 'center' }}>
|
||
<div style={{ fontFamily: FF, fontSize: 68, fontWeight: 900, color: phase.accent, lineHeight: 1, opacity: 0.9 }}>{fallbackText(phase.successApplications, '')}</div>
|
||
<div style={{ fontFamily: FF, fontSize: 8, fontWeight: 700, letterSpacing: '0.35em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.50)', marginTop: 4, marginBottom: 20 }}>{successApplicationsLabel}</div>
|
||
<div style={{ width: 1, height: 28, background: 'rgba(255,255,255,0.08)', margin: '0 auto 20px' }} />
|
||
<div style={{ fontFamily: FF, fontSize: 44, fontWeight: 900, color: '#fff', lineHeight: 1 }}>{fallbackText(phase.successWinners, '')}</div>
|
||
<div style={{ fontFamily: FF, fontSize: 8, fontWeight: 700, letterSpacing: '0.35em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.50)', marginTop: 4 }}>{successWinnersLabel}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Membership CTA – for active phases only */}
|
||
{phase.num !== '03' && (
|
||
<div style={{ textAlign: 'center' }}>
|
||
<div style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.08)', margin: '0 auto 20px' }} />
|
||
<Link
|
||
to={phase.membershipCta?.to || '/mitglied-werden'}
|
||
onMouseDown={e => e.stopPropagation()}
|
||
style={{
|
||
fontFamily: FF,
|
||
fontSize: 10,
|
||
fontWeight: 700,
|
||
letterSpacing: '0.14em',
|
||
textTransform: 'uppercase',
|
||
color: phase.accent,
|
||
border: `1.5px solid ${phase.accent}55`,
|
||
background: 'transparent',
|
||
textDecoration: 'none',
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
padding: '11px 22px',
|
||
transition: 'border-color 0.2s, color 0.2s',
|
||
}}
|
||
onMouseEnter={e => {
|
||
(e.currentTarget as HTMLElement).style.borderColor = phase.accent;
|
||
}}
|
||
onMouseLeave={e => {
|
||
(e.currentTarget as HTMLElement).style.borderColor = `${phase.accent}55`;
|
||
}}
|
||
>
|
||
{phase.membershipCta?.label} <ChevronRight size={13} />
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default Home;
|