import React, { useState, useEffect, useRef } from 'react'; import { Link } from '@/spa/router'; import { X, ArrowRight, Globe, Linkedin, Building2, MapPin, ExternalLink, Play } from 'lucide-react'; import SponsoringForm from '@/spa/components/forms/SponsoringForm'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute'; import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'; import { homeContent } from '@/spa/homeContent'; import { selectJuryDocs, sortJuryMembersQuoteFirst } from '@/spa/juryMembers'; import { netzwerkContent } from '@/spa/netzwerkContent'; import Image from '@/spa/components/ui/UnoptimizedImage' import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'; const FF = '"IBM Plex Sans", sans-serif'; const FB = '"Inter", sans-serif'; const NAVY = '#111D55'; const GOLD = '#EFBF04'; type NetzwerkCms = Omit, 'hero' | 'jury' | 'sponsoring'> & { hero?: Partial & { image?: unknown } jury?: Partial> & { members?: unknown[] | null } sponsoring?: Partial & { image?: unknown } } type HomeVideoCms = { hero?: { videoLabel?: string | null } videoModal?: { video?: unknown title?: string | null description?: string | null closeLabel?: string | null } } type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown } type JuryMember = (typeof netzwerkContent.jury.members)[number] & { active?: boolean | null imageAlt?: string | null imageUpload?: unknown organization?: string | null quote?: string | null sortOrder?: number | null } type Partner = (typeof netzwerkContent.partners.items)[number] & PartnerLogoData & { active?: boolean | null heroImg?: string linkedin?: string showOnHomepage?: boolean | null sortOrder?: number | null stableId?: string | null tier: 1 | 2 | 3 } type PartnerTier = (typeof netzwerkContent.partners.tiers)[number] type ExpertiseRow = (typeof netzwerkContent.expertise.rows)[number] type StatItem = (typeof netzwerkContent.juryIntro.stats)[number] type BenefitItem = (typeof netzwerkContent.sponsoring.benefits)[number] type PartnerModalCopy = typeof netzwerkContent.partners.modal const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const fallbackArray = (value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback; const managedImage = (upload: unknown, fallback: unknown) => mediaUrl(upload, fallbackText(fallback, '')) const deprecatedPartnerDescription = 'Partner in drei Kategorien: Hauptsponsoren, Medienpartner und weitere Sponsoren. Klicken für Details.' const visiblePartnerDescription = (value: unknown) => { const text = fallbackText(value, netzwerkContent.partners.description) return text.trim() === deprecatedPartnerDescription ? netzwerkContent.partners.description : text } const juryImageFallback = '/images/preistraeger-placeholder.svg' const sortedPartners = (partners: T[]) => partners .map((partner, index) => ({ partner, index })) .sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index)) .map(({ partner }) => partner) const normalizeJuryMember = (member: CmsRouteDoc): JuryMember => { const organization = fallbackText(member.organization, '') const rawRole = fallbackText(member.role, '') const role = rawRole && organization && rawRole !== organization ? `${rawRole} / ${organization}` : fallbackText(rawRole, organization) const image = member.image ?? member.imageUpload return { name: fallbackText(member.name, ''), role, bio: fallbackText(member.bio, ''), image: mediaUrl(image, juryImageFallback), imageAlt: fallbackText(member.imageAlt, fallbackText(member.name, 'Jury-Mitglied')), imageUpload: image, organization, quote: fallbackText(member.quote, ''), sortOrder: typeof member.sortOrder === 'number' ? member.sortOrder : undefined, active: member.active !== false, } } const hasJuryText = (value: unknown) => typeof value === 'string' && value.trim().length > 0 function Lines({ text, highlight }: { text: string; highlight?: string }) { return ( <> {text.split('\n').map((line, i) => { const highlighted = highlight && line.includes(highlight) return ( {i > 0 &&
} {highlighted ? ( <> {line.replace(highlight, '')}{highlight} ) : line}
) })} ) } // ── PersonCard (Schirmherrschaft) ───────────────────────────────────────────── interface PersonCardProps { imgSrc: string; imgAlt: string; name: string; titleLine1: string; titleLine2: string; institution: string; borderRight?: boolean; } function PersonCard({ imgSrc, imgAlt, name, titleLine1, titleLine2, institution, borderRight }: PersonCardProps) { const [hovered, setHovered] = useState(false); const isMobileCard = useIsMobile(); return (
setHovered(true)} onMouseLeave={() => setHovered(false)} >
{imgAlt}
{name}
{titleLine1}
{titleLine2}
{institution}
); } // ── JuryCard ────────────────────────────────────────────────────────────────── const JuryCard: React.FC<{ member: JuryMember }> = ({ member }) => { const [hovered, setHovered] = useState(false); const isMobileCard = useIsMobile(); const bio = hasJuryText(member.bio) ? member.bio : ''; const quote = hasJuryText(member.quote) ? member.quote : ''; return (
setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ display: 'flex', flexDirection: 'column', height: '100%', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.04)' : NAVY, transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '72vw', maxWidth: '72vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : { minWidth: 0, border: '1px solid rgba(255,255,255,0.07)' }) }} >
{fallbackText(member.imageAlt,
{member.name}
{member.role}
{bio ? (
{bio}
) : null} {quote ? (
{quote}
) : null}
); }; // ── JuryChairCard (Vorsitz – prominent) ─────────────────────────────────────── const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member, badge }) => { const [hovered, setHovered] = useState(false); const isMobileCard = useIsMobile(); const bio = hasJuryText(member.bio) ? member.bio : ''; const quote = hasJuryText(member.quote) ? member.quote : ''; return (
setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ display: 'grid', gridTemplateColumns: isMobileCard ? '1fr' : '380px 1fr', gap: 0, background: 'rgba(255,255,255,0.03)', border: `1px solid ${GOLD}40`, borderRadius: 20, overflow: 'hidden', }} >
{fallbackText(member.imageAlt,
{badge}
{member.name}
{member.role}
{bio ? (
{bio}
) : null} {quote ? (
{quote}
) : null}
); }; // ── PartnerCell ─────────────────────────────────────────────────────────────── // ── SponsorCell (tiered) ────────────────────────────────────────────────────── const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; detailLabel: string; onClick: () => void }> = ({ partner, variant, idx, cols, detailLabel, onClick }) => { const [hovered, setHovered] = useState(false); const isMobileCell = useIsMobile(); const lg = variant === 'lg'; const sm = variant === 'sm'; const lastCol = (idx % cols) === cols - 1; const logoHeight = isMobileCell ? (lg ? 76 : sm ? 54 : 62) : lg ? 92 : sm ? 62 : 76; return (
setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ padding: isMobileCell ? '26px 20px' : lg ? '44px 38px 40px' : sm ? '30px 28px' : '36px 32px', borderRight: !lastCol && !isMobileCell ? '1px solid rgba(3,9,58,0.08)' : 'none', borderBottom: '1px solid rgba(3,9,58,0.08)', display: 'flex', flexDirection: 'column', gap: lg ? 18 : 16, cursor: 'pointer', background: hovered ? '#E8E5E4' : 'transparent', transition: 'background 0.15s', position: 'relative', }} >
{partner.name}
{!sm &&
{partner.desc}
}
{detailLabel}
); }; // ── PartnerModal ───────────────────────────────────────────────────────────── function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: PartnerModalCopy; onClose: () => void }) { const accentColor = GOLD; useEffect(() => { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }, []); return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, zIndex: 500, background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, overflowY: 'auto', }} >
e.stopPropagation()} style={{ background: '#fff', width: '100%', maxWidth: 720, boxShadow: '0 32px 80px rgba(0,0,0,0.35)', overflow: 'hidden', flexShrink: 0, maxHeight: '92vh', overflowY: 'auto', }} > {/* ── Hero ── */}
{/* Background */} {partner.heroImg ? :
} {/* Gradient to white */}
{/* Social + Close – top right */}
{partner.linkedin && ( )} {partner.website && ( )}
{/* Logo box – bottom left */}
{/* ── Content ── */}

{partner.name}

{partner.desc}

{/* 3 info cards */}
{[ { Icon: Building2, label: fallbackText(copy.industryLabel, netzwerkContent.partners.modal.industryLabel), value: partner.industry }, { Icon: MapPin, label: fallbackText(copy.locationLabel, netzwerkContent.partners.modal.locationLabel), value: partner.location }, { Icon: Globe, label: fallbackText(copy.webLabel, netzwerkContent.partners.modal.webLabel), value: partner.website }, ].map(({ Icon, label, value }) => (
{label}
{value}
))}
{/* Separator */}
{/* Über section */}
{fallbackText(copy.aboutLabel, netzwerkContent.partners.modal.aboutLabel)}

{partner.fullDesc}

{/* CTAs */}
{partner.website && ( { const el = e.currentTarget as HTMLElement; el.style.background = '#FFD130'; el.style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }} onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = GOLD; el.style.boxShadow = 'none'; }} > {fallbackText(copy.websiteLabel, netzwerkContent.partners.modal.websiteLabel)} )}
{/* ── Footer strip ── */}
{fallbackText(copy.footerTitle, netzwerkContent.partners.modal.footerTitle)}
); } // ── Netzwerk ────────────────────────────────────────────────────────────────── const Netzwerk: React.FC = () => { const isMobile = useIsMobile(); const [selectedPartner, setSelectedPartner] = useState(null); const [showGreetingVideo, setShowGreetingVideo] = useState(false); const greetingVideoRef = useRef(null); const cms = (useCmsRoute()?.doc?.netzwerk || {}) as NetzwerkCms; const homePage = useCmsCollection('pages').find((page) => page.spaPath === '/' || page.slug === 'startseite' || page.slug === 'home'); const cmsPartners = useCmsCollection('partners') as unknown as Partner[]; const cmsJuryMembers = useCmsCollection('jury-mitglieder'); const home = (homePage?.home || {}) as HomeVideoCms; const homeVideoModal = home.videoModal || {}; const hero = { ...netzwerkContent.hero, ...(cms.hero || {}) }; const patronage = { ...netzwerkContent.patronage, ...(cms.patronage || {}) }; const juryIntro = { ...netzwerkContent.juryIntro, ...(cms.juryIntro || {}) }; const jury = { ...netzwerkContent.jury, ...(cms.jury || {}) }; const expertise = { ...netzwerkContent.expertise, ...(cms.expertise || {}) }; const partnerSection = { ...netzwerkContent.partners, ...(cms.partners || {}) }; const partnerModal = { ...netzwerkContent.partners.modal, ...((cms.partners as typeof netzwerkContent.partners | undefined)?.modal || {}) }; const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) }; const patronPeople = fallbackArray(patronage.people, netzwerkContent.patronage.people); const cmsJurySelection = cms.jury?.members; const hasCmsJurySelection = cmsJurySelection !== undefined; const sourcedJuryDocs = selectJuryDocs(cmsJurySelection, cmsJuryMembers); const normalizedJuryMembers = sourcedJuryDocs .map(normalizeJuryMember) .filter((member) => member.active !== false && member.name); const jurySourceMembers: JuryMember[] = normalizedJuryMembers.length || hasCmsJurySelection ? normalizedJuryMembers : netzwerkContent.jury.members; const juryMembers = sortJuryMembersQuoteFirst(jurySourceMembers); const expertiseRows = fallbackArray(expertise.rows, netzwerkContent.expertise.rows); const partners = sortedPartners(cmsPartners) .filter((partner) => partner.active !== false) .map((partner) => ({ ...partner, id: partner.stableId || partner.id, })); const partnerTiers = fallbackArray(partnerSection.tiers, netzwerkContent.partners.tiers); const mobileStats = fallbackArray(juryIntro.stats, netzwerkContent.juryIntro.stats); const desktopStats = fallbackArray(juryIntro.desktopStats, netzwerkContent.juryIntro.desktopStats); const sponsorBenefits = fallbackArray(sponsoring.benefits, netzwerkContent.sponsoring.benefits); const greetingVideoSrc = mediaUrl(homeVideoModal.video, ''); const greetingVideoTitle = fallbackText(homeVideoModal.title, homeContent.videoModal.title); const greetingVideoLabel = fallbackText(home.hero?.videoLabel, homeContent.hero.videoLabel); const greetingVideoCloseLabel = fallbackText(homeVideoModal.closeLabel, homeContent.videoModal.closeLabel); useEffect(() => { if (!showGreetingVideo || !greetingVideoSrc) return; greetingVideoRef.current?.play().catch(() => { // Browser autoplay rules can still require the native control click. }); }, [showGreetingVideo, greetingVideoSrc]); return (
{/* ── 1. HERO ──────────────────────────────────────────────────────────── */}
{mediaAlt(hero.image,
{fallbackText(hero.eyebrow, netzwerkContent.hero.eyebrow)}

{fallbackText(hero.description, netzwerkContent.hero.description)}

{/* ── 2. SCHIRMHERRSCHAFT ──────────────────────────────────────────────── */}
{fallbackText(patronage.eyebrow, netzwerkContent.patronage.eyebrow)}

{fallbackText(patronage.heading, netzwerkContent.patronage.heading)}

{fallbackText(patronage.description, netzwerkContent.patronage.description)}

{patronPeople.map((person, index) => ( ))}
{/* Grußwort – Ilse Aigner */}
{fallbackText(patronage.greetingEyebrow, netzwerkContent.patronage.greetingEyebrow)}
{fallbackText(patronage.greetingRole, netzwerkContent.patronage.greetingRole)}
{fallbackText(patronage.greetingName, netzwerkContent.patronage.greetingName)}
{fallbackText(patronage.greetingTitleLine1, netzwerkContent.patronage.greetingTitleLine1)}
{fallbackText(patronage.greetingTitleLine2, netzwerkContent.patronage.greetingTitleLine2)}
{fallbackText(patronage.greetingQuote, netzwerkContent.patronage.greetingQuote)}
{fallbackText(patronage.greetingAttribution, netzwerkContent.patronage.greetingAttribution)}
{greetingVideoSrc && (
)}
{/* ── 3. JURY-INTRO (editorial 2-col split) ────────────────────────────── */}
{!isMobile && } {isMobile && } {isMobile ? ( /* Mobile: one cohesive navy module – copy + compact stat strip */
{fallbackText(juryIntro.eyebrow, netzwerkContent.juryIntro.eyebrow)}

{fallbackText(juryIntro.description, netzwerkContent.juryIntro.description)}

{/* Stat strip */}
{mobileStats.map((s, i) => (
0 ? 14 : 0, borderLeft: i > 0 ? '1px solid rgba(255,255,255,0.14)' : 'none' }}>
{fallbackText(s.value, netzwerkContent.juryIntro.stats[i]?.value || '')}
{fallbackText(s.label, netzwerkContent.juryIntro.stats[i]?.label || '')}
))}
) : ( <> {/* Left – White, stats */}
{fallbackText(desktopStats[0]?.value, netzwerkContent.juryIntro.desktopStats[0].value)}
{fallbackText(desktopStats[0]?.label, netzwerkContent.juryIntro.desktopStats[0].label)}
{fallbackText(desktopStats[1]?.value, netzwerkContent.juryIntro.desktopStats[1].value)}
{fallbackText(desktopStats[1]?.label, netzwerkContent.juryIntro.desktopStats[1].label)}
{fallbackText(desktopStats[2]?.value, netzwerkContent.juryIntro.desktopStats[2].value)}
{fallbackText(desktopStats[2]?.label, netzwerkContent.juryIntro.desktopStats[2].label)}
{/* Right – Navy, editorial copy */}
{fallbackText(juryIntro.eyebrow, netzwerkContent.juryIntro.eyebrow)}

{fallbackText(juryIntro.description, netzwerkContent.juryIntro.description)}

)}
{/* ── 4. JURY-MITGLIEDER ───────────────────────────────────────────────── */}
{fallbackText(jury.eyebrow, netzwerkContent.jury.eyebrow)}

{fallbackText(jury.description, netzwerkContent.jury.description)}

{/* ── Prominenter Vorsitz-Bereich ─────────────────────────────────── */} {/* TODO: echten Jury-Vorsitzenden bestätigen (Name & Foto sind Platzhalter) */} {juryMembers.length ? (
) : null} {/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */}
{juryMembers.slice(1).map((member, idx) => ( ))}
{/* ── 5. HINTERGRUND & EXPERTISE ───────────────────────────────────────── */}
{fallbackText(expertise.eyebrow, netzwerkContent.expertise.eyebrow)}

{expertiseRows.map((row) => (
{row.num}
{row.label}
{row.body}
))}

{fallbackText(expertise.quote, netzwerkContent.expertise.quote)}

{fallbackText(expertise.quoteAttribution, netzwerkContent.expertise.quoteAttribution)}
{/* ── 6. PARTNER & SPONSOREN ───────────────────────────────────────────── */}
{fallbackText(partnerSection.eyebrow, netzwerkContent.partners.eyebrow)}

{visiblePartnerDescription(partnerSection.description)}

{/* Tiered grid – continuous, ranks marked by label bars */}
{partnerTiers.map(t => { const tier = Number(t.tier) as 1 | 2 | 3; const variant = tier === 1 ? 'lg' as const : tier === 2 ? 'md' as const : 'sm' as const; const cols = isMobile ? (tier === 1 ? 1 : 2) : (tier === 3 ? 4 : 2); const items = partners.filter(p => p.tier === tier); return (
{items.map((p, i) => ( setSelectedPartner(p)} /> ))}
); })}
{/* ── 7. SPONSORING-FORM ───────────────────────────────────────────────── */}
{/* Left – pitch */}
{fallbackText(sponsoring.eyebrow, netzwerkContent.sponsoring.eyebrow)}

{fallbackText(sponsoring.description, netzwerkContent.sponsoring.description)}

{sponsorBenefits.map((item) => (
{item.num}
{item.label}
{item.sub}
))}
{/* Center divider */} {!isMobile &&
} {/* Right – gold form panel */}
{!isMobile && (
{mediaAlt(sponsoring.image,
{fallbackText(sponsoring.imageLabel, netzwerkContent.sponsoring.imageLabel)}
)}
{/* ── 8. PARTNER MODAL ─────────────────────────────────────────────────── */} {selectedPartner && ( setSelectedPartner(null)} /> )} {/* ── 9. GRUSSWORT VIDEO MODAL ────────────────────────────────────────── */} {showGreetingVideo && greetingVideoSrc && (
{ if (e.target === e.currentTarget) setShowGreetingVideo(false); }} style={{ position: 'fixed', inset: 0, zIndex: 520, background: 'rgba(0,0,0,0.95)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: isMobile ? 16 : 24, }} >
)}
); }; export default Netzwerk;