Files
bmp-website-2026/src/spa/pages/MitgliedWerden.tsx
2026-06-22 11:44:35 +02:00

1377 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import { Link } from '@/spa/router';
import { ChevronRight, ArrowLeft, Users, Star, Eye, Heart } from 'lucide-react';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import MembershipWizard from '@/spa/components/membership/MembershipWizard';
import { useCmsRoute } from '@/spa/cmsRoute';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { mitgliedWerdenContent } from '@/spa/mitgliedWerdenContent';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55';
const GOLD = '#EFBF04';
const CREAM = '#EFE5E3';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
const BLUE = '#4A8FC9';
// ── Benefit card with hover accent line ──────────────────────────────────────
function BenefitCard({
icon,
title,
body,
isLast,
isMobile = false,
}: {
icon: React.ReactNode;
title: string;
body: string;
isLast: boolean;
isMobile?: boolean;
}) {
const [hovered, setHovered] = useState(false);
return (
<div
style={{
padding: isMobile ? '28px 24px' : '40px 36px',
borderRight: isLast || isMobile ? 'none' : '1px solid rgba(3,9,58,0.08)',
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.08)' : undefined,
borderTop: `3px solid ${hovered ? GOLD : 'transparent'}`,
transition: 'border-top-color 0.25s, background 0.25s',
cursor: 'default',
background: hovered ? 'rgba(3,9,58,0.02)' : 'transparent',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<div style={{ marginBottom: 20 }}>{icon}</div>
<div
style={{
fontFamily: FF,
fontSize: 16,
fontWeight: 700,
textTransform: 'uppercase' as const,
letterSpacing: '0.07em',
color: '#101828',
marginBottom: 12,
}}
>
{title}
</div>
<div
style={{
fontFamily: FB,
fontSize: 18,
color: 'rgba(16,24,40,0.55)',
lineHeight: 1.7,
}}
>
{body}
</div>
</div>
);
}
// ── Beitragsrechner ───────────────────────────────────────────────────────────
type MitgliedWerdenCms = Partial<typeof mitgliedWerdenContent> & {
hero?: Partial<typeof mitgliedWerdenContent.hero> & { image?: unknown }
formIntro?: Partial<typeof mitgliedWerdenContent.formIntro> & { image?: unknown }
applicationOptions?: Partial<typeof mitgliedWerdenContent.applicationOptions> & {
items?: Array<Partial<(typeof mitgliedWerdenContent.applicationOptions.items)[number]> & { file?: unknown }>
}
}
type PricingContent = typeof mitgliedWerdenContent.pricing
type ApplicationOption = (typeof mitgliedWerdenContent.applicationOptions.items)[number] & { file?: unknown }
type FaqItem = (typeof mitgliedWerdenContent.faq.items)[number]
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
const fallbackNumber = (value: unknown, fallback: number) => typeof value === 'number' && Number.isFinite(value) ? value : fallback;
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
function formatEurLocal(n: number) {
return n.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });
}
function TextLines({ text }: { text: string }) {
return (
<>
{text.split('\n').map((line, i) => (
<React.Fragment key={`${line}-${i}`}>
{i > 0 && <br />}
{line}
</React.Fragment>
))}
</>
)
}
function BeitragsrechnerSection({ isMobile, content }: { isMobile: boolean; content: Partial<PricingContent> }) {
const [selected, setSelected] = useState('');
const [tableOpen, setTableOpen] = useState(false);
const fallback = mitgliedWerdenContent.pricing;
const tiers = fallbackArray(content.tiers, fallback.tiers);
const vatRate = fallbackNumber(content.vatRate, fallback.vatRate);
const admissionFeeNet = fallbackNumber(content.admissionFeeNet, fallback.admissionFeeNet);
const tier = tiers.find(t => t.label === selected);
const annualNet = tier?.annual ?? 0;
const annualGross = annualNet * (1 + vatRate);
const admissionGross = admissionFeeNet * (1 + vatRate);
const today = new Date();
const monthsLeft = 12 - today.getMonth();
const anteilig = annualGross / 12 * monthsLeft + admissionGross;
return (
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px' : '80px' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.32em', color: BLUE, display: 'block', marginBottom: 16 }}>
{fallbackText(content.eyebrow, fallback.eyebrow)}
</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: '0 0 20px' }}>
{fallbackText(content.heading, fallback.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 40, alignItems: 'start' }}>
{/* Left: dropdown + result */}
<div>
<label style={{ display: 'block', fontFamily: FF, fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: 'rgba(16,24,40,0.5)', marginBottom: 10 }}>
{fallbackText(content.employeeLabel, fallback.employeeLabel)}
</label>
<select
value={selected}
onChange={e => setSelected(e.target.value)}
style={{
width: '100%',
padding: '14px 18px',
background: '#fff',
border: `2px solid ${selected ? GOLD : 'rgba(16,24,40,0.15)'}`,
borderRadius: 8,
fontFamily: FF,
fontSize: 16,
fontWeight: 600,
color: selected ? '#101828' : 'rgba(16,24,40,0.4)',
cursor: 'pointer',
outline: 'none',
appearance: 'none',
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23EFBF04' stroke-width='1.8' fill='none' stroke-linecap='round'/%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 16px center',
transition: 'border-color 0.2s',
}}
>
<option value="">{fallbackText(content.selectPlaceholder, fallback.selectPlaceholder)}</option>
{tiers.map(t => (
<option key={t.label} value={t.label}>{t.label} {fallbackText(content.optionSuffix, fallback.optionSuffix)}</option>
))}
</select>
{/* Result card */}
<div style={{
marginTop: 24,
background: NAVY,
borderRadius: 12,
padding: '24px 28px',
opacity: selected ? 1 : 0,
transform: selected ? 'translateY(0)' : 'translateY(8px)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: selected ? 'auto' : 'none',
}}>
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.15em', color: GOLD, marginBottom: 18 }}>
{fallbackText(content.resultEyebrow, fallback.resultEyebrow)}
</div>
{[
[fallbackText(content.annualNetLabel, fallback.annualNetLabel), formatEurLocal(annualNet)],
[fallbackText(content.annualGrossLabel, fallback.annualGrossLabel), formatEurLocal(annualGross)],
[fallbackText(content.admissionGrossLabel, fallback.admissionGrossLabel), formatEurLocal(admissionGross)],
].map(([l, v]) => (
<div key={l} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, fontSize: 14 }}>
<span style={{ color: 'rgba(255,255,255,0.5)', fontFamily: FF }}>{l}</span>
<span style={{ color: '#fff', fontWeight: 600, fontFamily: FF }}>{v}</span>
</div>
))}
<div style={{ height: 1, background: 'rgba(239,191,4,0.3)', margin: '14px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 15 }}>
<span style={{ color: GOLD, fontWeight: 700, fontFamily: FF }}>{fallbackText(content.totalYearOneLabel, fallback.totalYearOneLabel)}</span>
<span style={{ color: GOLD, fontWeight: 900, fontFamily: FF, fontSize: 18 }}>{formatEurLocal(anteilig)}</span>
</div>
<div style={{ marginTop: 12, fontSize: 11, color: 'rgba(255,255,255,0.3)', fontFamily: FF, lineHeight: 1.5 }}>
{fallbackText(content.footnote, fallback.footnote)}
</div>
</div>
<a
href={fallbackText(content.ctaHref, fallback.ctaHref)}
style={{
display: 'block',
marginTop: 20,
background: GOLD,
color: '#101828',
fontFamily: FF,
fontSize: 14,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.15em',
padding: '14px 28px',
textDecoration: 'none',
textAlign: 'center',
borderRadius: 4,
transition: 'background 0.2s, 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.6)';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.background = GOLD;
(e.currentTarget as HTMLElement).style.boxShadow = 'none';
}}
>
{fallbackText(content.ctaLabel, fallback.ctaLabel)}
</a>
</div>
{/* Right: Staffeltabelle */}
<div>
<button
onClick={() => setTableOpen(!tableOpen)}
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
background: '#fff',
border: '1px solid rgba(16,24,40,0.12)',
borderBottom: tableOpen ? 'none' : '1px solid rgba(16,24,40,0.12)',
borderRadius: tableOpen ? '8px 8px 0 0' : 8,
padding: '14px 18px',
cursor: 'pointer',
fontFamily: FF,
fontSize: 13,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.1em',
color: '#101828',
}}
>
{fallbackText(content.tableToggleLabel, fallback.tableToggleLabel)}
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ transform: tableOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.25s' }}>
<path d="M2 4l5 5 5-5" stroke={GOLD} strokeWidth="1.8" strokeLinecap="round" />
</svg>
</button>
{tableOpen && (
<div style={{ background: '#fff', border: '1px solid rgba(16,24,40,0.12)', borderTop: 'none', borderRadius: '0 0 8px 8px', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: FF, tableLayout: isMobile ? 'fixed' : 'auto' }}>
<thead>
<tr style={{ background: NAVY }}>
<th style={{ padding: isMobile ? '10px 10px' : '12px 18px', textAlign: 'left', fontSize: 10, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', color: GOLD }}>{fallbackText(content.tableEmployeeHeader, fallback.tableEmployeeHeader)}</th>
<th style={{ padding: isMobile ? '10px 10px' : '12px 18px', textAlign: 'right', fontSize: 10, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', color: GOLD }}>{fallbackText(content.tableNetHeader, fallback.tableNetHeader)}</th>
<th style={{ padding: isMobile ? '10px 10px' : '12px 18px', textAlign: 'right', fontSize: 10, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', color: GOLD }}>{fallbackText(content.tableGrossHeader, fallback.tableGrossHeader)}</th>
</tr>
</thead>
<tbody>
{tiers.map((t, i) => (
<tr key={t.label} style={{ background: i % 2 === 0 ? 'rgba(239,191,4,0.04)' : '#fff', borderBottom: '1px solid rgba(16,24,40,0.06)' }}>
<td style={{ padding: isMobile ? '10px 10px' : '12px 18px', fontSize: isMobile ? 13 : 14, color: '#101828', fontWeight: selected === t.label ? 700 : 400 }}>{t.label}</td>
<td style={{ padding: isMobile ? '10px 10px' : '12px 18px', fontSize: isMobile ? 13 : 14, textAlign: 'right', color: 'rgba(16,24,40,0.6)' }}>{formatEurLocal(t.annual)}</td>
<td style={{ padding: isMobile ? '10px 10px' : '12px 18px', fontSize: isMobile ? 13 : 14, textAlign: 'right', fontWeight: 600, color: selected === t.label ? NAVY : '#101828' }}>{formatEurLocal(t.annual * (1 + vatRate))}</td>
</tr>
))}
</tbody>
</table>
<div style={{ padding: '10px 18px', fontSize: 11, color: 'rgba(16,24,40,0.4)', fontFamily: FF, borderTop: '1px solid rgba(16,24,40,0.06)' }}>
{fallbackText(content.tableNotePrefix, fallback.tableNotePrefix)} {formatEurLocal(admissionGross)} {fallbackText(content.tableNoteSuffix, fallback.tableNoteSuffix)}
</div>
</div>
)}
</div>
</div>
</div>
</section>
);
}
// ── PDF Download Section – editorial split strip ──────────────────────────────
function PDFDownloadSection({ isMobile, content }: { isMobile: boolean; content: Partial<typeof mitgliedWerdenContent.applicationOptions> }) {
const [hover, setHover] = useState<'online' | 'pdf' | null>(null);
const items = fallbackArray(content.items, mitgliedWerdenContent.applicationOptions.items) as ApplicationOption[];
const online = items[0] || mitgliedWerdenContent.applicationOptions.items[0];
const pdf = items[1] || mitgliedWerdenContent.applicationOptions.items[1];
const pdfHref = mediaUrl(pdf.file, fallbackText(pdf.href, mitgliedWerdenContent.applicationOptions.items[1].href));
return (
<section style={{
background: CREAM,
borderTop: '1px solid rgba(16,24,40,0.08)',
borderBottom: '1px solid rgba(16,24,40,0.08)',
position: 'relative',
overflow: 'hidden',
}}>
{/* Gold top rule */}
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: `linear-gradient(90deg, ${GOLD} 0%, rgba(239,191,4,0.3) 60%, transparent 100%)` }} />
<div style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : '1fr 1px 1fr',
minHeight: isMobile ? undefined : 140,
}}>
{/* Left – online */}
<a
href={fallbackText(online.href, mitgliedWerdenContent.applicationOptions.items[0].href)}
style={{
display: 'flex',
alignItems: 'center',
gap: 24,
padding: isMobile ? '32px 24px' : '40px 52px',
textDecoration: 'none',
background: hover === 'online' ? 'rgba(17,29,85,0.04)' : 'transparent',
transition: 'background 0.3s ease',
cursor: 'pointer',
}}
onMouseEnter={() => setHover('online')}
onMouseLeave={() => setHover(null)}
>
{/* Number */}
<div style={{
flexShrink: 0,
fontFamily: FF,
fontSize: 'clamp(2.4rem, 4vw, 3.6rem)',
fontWeight: 900,
color: hover === 'online' ? GOLD : 'rgba(16,24,40,0.08)',
lineHeight: 1,
letterSpacing: '-0.04em',
transition: 'color 0.3s ease',
userSelect: 'none',
}}>
{fallbackText(online.number, mitgliedWerdenContent.applicationOptions.items[0].number)}
</div>
<div style={{ flex: 1 }}>
<div style={{
fontFamily: FF,
fontSize: 9,
fontWeight: 700,
letterSpacing: '0.28em',
textTransform: 'uppercase',
color: BLUE,
marginBottom: 8,
}}>
{fallbackText(online.eyebrow, mitgliedWerdenContent.applicationOptions.items[0].eyebrow)}
</div>
<div style={{
fontFamily: FF,
fontSize: 'clamp(1rem, 1.6vw, 1.2rem)',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.04em',
color: '#101828',
marginBottom: 6,
}}>
{fallbackText(online.title, mitgliedWerdenContent.applicationOptions.items[0].title)}
</div>
<div style={{
fontFamily: FB,
fontSize: 13,
color: 'rgba(16,24,40,0.45)',
lineHeight: 1.6,
}}>
{fallbackText(online.description, mitgliedWerdenContent.applicationOptions.items[0].description)}
</div>
</div>
{/* Arrow */}
<div style={{
flexShrink: 0,
width: 36,
height: 36,
border: `1px solid ${hover === 'online' ? GOLD : 'rgba(16,24,40,0.15)'}`,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'border-color 0.3s, background 0.3s, transform 0.3s',
background: hover === 'online' ? GOLD : 'transparent',
transform: hover === 'online' ? 'translateX(4px)' : 'none',
}}>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M3 7h8M8 4l3 3-3 3" stroke={hover === 'online' ? NAVY : 'rgba(16,24,40,0.5)'} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</a>
{/* Divider */}
{!isMobile && (
<div style={{ background: 'rgba(16,24,40,0.08)', position: 'relative' }}>
{/* Gold dot at center */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 6,
height: 6,
background: GOLD,
borderRadius: '50%',
}} />
</div>
)}
{/* Right – PDF */}
<a
href={pdfHref}
download={pdf.download || undefined}
style={{
display: 'flex',
alignItems: 'center',
gap: 24,
padding: isMobile ? '32px 24px' : '40px 52px',
textDecoration: 'none',
background: hover === 'pdf' ? 'rgba(17,29,85,0.04)' : 'transparent',
borderTop: isMobile ? '1px solid rgba(16,24,40,0.08)' : 'none',
transition: 'background 0.3s ease',
cursor: 'pointer',
}}
onMouseEnter={() => setHover('pdf')}
onMouseLeave={() => setHover(null)}
>
{/* Number */}
<div style={{
flexShrink: 0,
fontFamily: FF,
fontSize: 'clamp(2.4rem, 4vw, 3.6rem)',
fontWeight: 900,
color: hover === 'pdf' ? GOLD : 'rgba(16,24,40,0.08)',
lineHeight: 1,
letterSpacing: '-0.04em',
transition: 'color 0.3s ease',
userSelect: 'none',
}}>
{fallbackText(pdf.number, mitgliedWerdenContent.applicationOptions.items[1].number)}
</div>
<div style={{ flex: 1 }}>
<div style={{
fontFamily: FF,
fontSize: 9,
fontWeight: 700,
letterSpacing: '0.28em',
textTransform: 'uppercase',
color: BLUE,
marginBottom: 8,
}}>
{fallbackText(pdf.eyebrow, mitgliedWerdenContent.applicationOptions.items[1].eyebrow)}
</div>
<div style={{
fontFamily: FF,
fontSize: 'clamp(1rem, 1.6vw, 1.2rem)',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.04em',
color: '#101828',
marginBottom: 6,
}}>
{fallbackText(pdf.title, mitgliedWerdenContent.applicationOptions.items[1].title)}
</div>
<div style={{
fontFamily: FB,
fontSize: 13,
color: 'rgba(16,24,40,0.45)',
lineHeight: 1.6,
}}>
{fallbackText(pdf.description, mitgliedWerdenContent.applicationOptions.items[1].description)}
</div>
</div>
{/* Download icon */}
<div style={{
flexShrink: 0,
width: 36,
height: 36,
border: `1px solid ${hover === 'pdf' ? GOLD : 'rgba(16,24,40,0.15)'}`,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'border-color 0.3s, background 0.3s, transform 0.3s',
background: hover === 'pdf' ? GOLD : 'transparent',
transform: hover === 'pdf' ? 'translateY(2px)' : 'none',
}}>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M7 2v7M4 6l3 3 3-3M2 11h10" stroke={hover === 'pdf' ? NAVY : 'rgba(16,24,40,0.5)'} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</a>
</div>
</section>
);
}
// ── FAQ Section ───────────────────────────────────────────────────────────────
function FAQSection({ isMobile, content }: { isMobile: boolean; content: Partial<typeof mitgliedWerdenContent.faq> }) {
const [openIdx, setOpenIdx] = useState<number | null>(null);
const items = fallbackArray(content.items, mitgliedWerdenContent.faq.items) as FaqItem[];
return (
<section style={{ background: CREAM, padding: isMobile ? '48px 24px' : '80px' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.32em', color: BLUE, display: 'block', marginBottom: 16 }}>
{fallbackText(content.eyebrow, mitgliedWerdenContent.faq.eyebrow)}
</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: '0 0 20px' }}>
{fallbackText(content.heading, mitgliedWerdenContent.faq.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
<div style={{ maxWidth: 800 }}>
{items.map((item, i) => (
<div key={i} style={{ borderBottom: '1px solid rgba(16,24,40,0.1)' }}>
<button
onClick={() => setOpenIdx(openIdx === i ? null : i)}
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '20px 0',
background: 'transparent',
border: 'none',
cursor: 'pointer',
textAlign: 'left',
gap: 16,
}}
>
<span style={{
fontFamily: FF,
fontSize: 16,
fontWeight: openIdx === i ? 700 : 600,
color: openIdx === i ? NAVY : '#101828',
transition: 'color 0.2s',
lineHeight: 1.4,
}}>
{item.q}
</span>
<svg
width="16" height="16" viewBox="0 0 16 16" fill="none"
style={{ flexShrink: 0, transform: openIdx === i ? 'rotate(180deg)' : 'none', transition: 'transform 0.25s' }}
>
<path d="M3 5l5 5 5-5" stroke={openIdx === i ? GOLD : 'rgba(16,24,40,0.4)'} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{openIdx === i && (
<div style={{
padding: '0 0 20px',
fontFamily: FB,
fontSize: 17,
color: 'rgba(16,24,40,0.6)',
lineHeight: 1.8,
borderLeft: `3px solid ${GOLD}`,
paddingLeft: 20,
marginLeft: 2,
}}>
{item.a}
</div>
)}
</div>
))}
</div>
</section>
);
}
// ── Quote card ────────────────────────────────────────────────────────────────
function QuoteCard({
quote,
initials,
name,
role,
}: {
quote: string;
initials: string;
name: string;
role: string;
}) {
return (
<div
style={{
background: CREAM,
padding: '32px',
borderTop: `3px solid ${GOLD}`,
display: 'flex',
flexDirection: 'column' as const,
}}
>
<p
style={{
fontFamily: FB,
fontSize: 18,
fontStyle: 'italic',
color: 'rgba(16,24,40,0.7)',
lineHeight: 1.75,
margin: '0 0 24px',
flex: 1,
}}
>
„{quote}“
</p>
<div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20 }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div
style={{
width: 40,
height: 40,
background: NAVY,
color: GOLD,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: FF,
fontSize: 16,
fontWeight: 700,
flexShrink: 0,
}}
>
{initials}
</div>
<div>
<div
style={{
fontFamily: FF,
fontSize: 16,
fontWeight: 700,
color: '#101828',
marginBottom: 2,
}}
>
{name}
</div>
<div
style={{
fontFamily: FB,
fontSize: 16,
color: 'rgba(16,24,40,0.45)',
textTransform: 'uppercase' as const,
letterSpacing: '0.08em',
}}
>
{role}
</div>
</div>
</div>
</div>
);
}
// ── Main component ────────────────────────────────────────────────────────────
const MitgliedWerden: React.FC = () => {
const isMobile = useIsMobile();
const cms = (useCmsRoute()?.doc?.mitgliedWerden || {}) as MitgliedWerdenCms;
const header = (cms.header || {}) as Partial<typeof mitgliedWerdenContent.header>;
const hero = (cms.hero || {}) as NonNullable<MitgliedWerdenCms['hero']>;
const benefits = (cms.benefits || {}) as Partial<typeof mitgliedWerdenContent.benefits>;
const about = (cms.about || {}) as Partial<typeof mitgliedWerdenContent.about>;
const pricing = (cms.pricing || {}) as Partial<typeof mitgliedWerdenContent.pricing>;
const applicationOptions = (cms.applicationOptions || {}) as NonNullable<MitgliedWerdenCms['applicationOptions']>;
const formIntro = (cms.formIntro || {}) as NonNullable<MitgliedWerdenCms['formIntro']>;
const testimonials = (cms.testimonials || {}) as Partial<typeof mitgliedWerdenContent.testimonials>;
const faq = (cms.faq || {}) as Partial<typeof mitgliedWerdenContent.faq>;
const bottomCta = (cms.bottomCta || {}) as Partial<typeof mitgliedWerdenContent.bottomCta>;
const heroStats = fallbackArray(hero.stats, mitgliedWerdenContent.hero.stats);
const benefitCards = fallbackArray(benefits.cards, mitgliedWerdenContent.benefits.cards);
const aboutParagraphs = fallbackArray(
about.paragraphs,
mitgliedWerdenContent.about.paragraphs,
).map((item) => typeof item === 'string' ? item : fallbackText((item as { text?: string }).text, ''));
const aboutFacts = fallbackArray(about.facts, mitgliedWerdenContent.about.facts);
const formPromises = fallbackArray(formIntro.promises, mitgliedWerdenContent.formIntro.promises);
const testimonialItems = fallbackArray(testimonials.items, mitgliedWerdenContent.testimonials.items);
const iconMap: Record<string, React.ReactNode> = {
users: <Users size={24} color={NAVY} strokeWidth={1.5} />,
star: <Star size={24} color={NAVY} strokeWidth={1.5} />,
eye: <Eye size={24} color={NAVY} strokeWidth={1.5} />,
heart: <Heart size={24} color={NAVY} strokeWidth={1.5} />,
};
return (
<div className="animate-fade-in">
{/* ── SECTION 1 – Minimal Header ──────────────────────────────────────── */}
<header
style={{
background: NAVY,
padding: isMobile ? '16px 24px' : '18px 80px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: '1px solid rgba(239,191,4,0.25)',
}}
>
<span
style={{
fontFamily: FF,
fontSize: 14,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.1em',
color: GOLD,
}}
>
{fallbackText(header.brandLabel, mitgliedWerdenContent.header.brandLabel)}
</span>
<Link
to={fallbackText(header.backUrl, mitgliedWerdenContent.header.backUrl)}
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.15em',
color: 'rgba(255,255,255,0.4)',
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
gap: 6,
}}
>
<ArrowLeft size={12} />
{fallbackText(header.backLabel, mitgliedWerdenContent.header.backLabel)}
</Link>
</header>
{/* ── SECTION 2 – Hero ────────────────────────────────────────────────── */}
<section
style={{
background: NAVY,
padding: isMobile ? '64px 24px 48px' : '100px 80px 80px',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Background image */}
<Image unoptimized
src={mediaUrl(hero.image, `/images/${mitgliedWerdenContent.hero.imageFilename}`)}
alt={mediaAlt(hero.image, fallbackText(hero.imageAlt, mitgliedWerdenContent.hero.imageAlt))}
aria-hidden="true"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }}
/>
{/* Dark overlay so text remains readable */}
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to right, rgba(17,29,85,0.92) 0%, rgba(17,29,85,0.75) 55%, rgba(17,29,85,0.45) 100%)' }} />
<div
style={{
position: 'relative',
zIndex: 1,
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : '1fr auto',
alignItems: 'center',
gap: isMobile ? 40 : 60,
}}
>
{/* Left col */}
<div style={{ maxWidth: 760 }}>
<span
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.35em',
textTransform: 'uppercase',
color: GOLD,
display: 'block',
marginBottom: 24,
}}
>
{fallbackText(hero.eyebrow, mitgliedWerdenContent.hero.eyebrow)}
</span>
<h1
style={{
fontFamily: FF,
fontSize: isMobile ? 'clamp(2rem, 9vw, 2.6rem)' : 'clamp(2.8rem, 6vw, 5rem)',
fontWeight: 900,
color: '#fff',
textTransform: 'uppercase',
lineHeight: 0.95,
letterSpacing: '-0.03em',
margin: '0 0 28px',
}}
>
{fallbackText(hero.heading, mitgliedWerdenContent.hero.heading)}
</h1>
<div style={{ width: 48, height: 3, background: GOLD, marginBottom: 28 }} />
<p
style={{
fontFamily: FB,
fontSize: 18,
fontWeight: 300,
color: 'rgba(255,255,255,0.55)',
maxWidth: 520,
lineHeight: 1.75,
margin: 0,
}}
>
<TextLines text={fallbackText(hero.description, mitgliedWerdenContent.hero.description)} />
</p>
</div>
{/* Right col – typographic stat block */}
{!isMobile && (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
paddingRight: 16,
}}
>
{heroStats.map((stat, index) => (
<React.Fragment key={`${stat.value}-${stat.label}`}>
<div style={{ textAlign: 'right' }}>
<div
style={{
fontFamily: FF,
fontSize: index === 0 ? 72 : 48,
fontWeight: 900,
color: index === 0 ? GOLD : '#fff',
lineHeight: 1,
letterSpacing: index === 0 ? '-0.03em' : '-0.02em',
}}
>
{stat.value}
</div>
<div
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.3em',
color: 'rgba(255,255,255,0.35)',
marginTop: 4,
}}
>
{stat.label}
</div>
</div>
{index < heroStats.length - 1 && (
<div
style={{
width: '100%',
height: 1,
background: 'rgba(255,255,255,0.1)',
margin: '32px 0',
}}
/>
)}
</React.Fragment>
))}
</div>
)}
</div>
{/* Gold bottom gradient rule */}
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 2,
background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.3), transparent)`,
}}
/>
</section>
{/* ── SECTION 3 – Benefit cards ───────────────────────────────────────── */}
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
<div
style={{
position: 'relative',
zIndex: 1,
padding: isMobile ? '48px 24px' : '80px 80px 72px',
}}
>
<span
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.32em',
color: BLUE,
display: 'block',
marginBottom: 16,
}}
>
{fallbackText(benefits.eyebrow, mitgliedWerdenContent.benefits.eyebrow)}
</span>
<h2
style={{
fontFamily: FF,
fontSize: 'clamp(2rem, 4vw, 3rem)',
fontWeight: 900,
color: '#101828',
textTransform: 'uppercase',
letterSpacing: '-0.025em',
lineHeight: 1.03,
margin: '0 0 20px',
}}
>
{fallbackText(benefits.heading, mitgliedWerdenContent.benefits.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
<div
style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'repeat(4, 1fr)',
borderTop: '1px solid rgba(3,9,58,0.1)',
}}
>
{benefitCards.map((card, index) => (
<BenefitCard
key={`${card.title}-${index}`}
icon={iconMap[card.icon] || iconMap.users}
title={card.title}
body={card.body}
isLast={index === benefitCards.length - 1}
isMobile={isMobile}
/>
))}
</div>
</div>
</section>
{/* ── SECTION 4 – Über den Verein ─────────────────────────────────────── */}
<section
style={{
background: '#fff',
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : '45% 55%',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Left col */}
<div style={{ padding: isMobile ? '48px 24px' : '80px 64px' }}>
<span
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.32em',
color: BLUE,
display: 'block',
marginBottom: 20,
}}
>
{fallbackText(about.eyebrow, mitgliedWerdenContent.about.eyebrow)}
</span>
<h2
style={{
fontFamily: FF,
fontSize: 'clamp(2rem, 3.5vw, 3rem)',
fontWeight: 900,
color: '#101828',
textTransform: 'uppercase',
letterSpacing: '-0.025em',
lineHeight: 1.03,
margin: '0 0 20px',
}}
>
{fallbackText(about.heading, mitgliedWerdenContent.about.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 32 }} />
{aboutParagraphs.map((paragraph, index) => (
<p
key={`${paragraph}-${index}`}
style={{
fontFamily: FB,
fontSize: 18,
color: 'rgba(16,24,40,0.55)',
lineHeight: 1.85,
margin: index === aboutParagraphs.length - 1 ? '0 0 40px' : index === 0 ? '0 0 20px' : '0 0 40px',
}}
>
{paragraph}
</p>
))}
<div style={{ width: 40, height: 1, background: GOLD, marginBottom: 16 }} />
<span
style={{
fontFamily: FB,
fontSize: 17,
color: 'rgba(16,24,40,0.5)',
letterSpacing: '0.05em',
}}
>
{fallbackText(about.descriptor, mitgliedWerdenContent.about.descriptor)}
</span>
</div>
{/* Right col */}
<div
style={{
padding: isMobile ? '0 24px 48px' : '80px 64px',
borderLeft: isMobile ? 'none' : '1px solid rgba(16,24,40,0.08)',
borderTop: isMobile ? '1px solid rgba(16,24,40,0.08)' : 'none',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
>
{aboutFacts.map((item, i) => (
<div
key={i}
style={{
display: 'grid',
gridTemplateColumns: '40px 1fr',
gap: '0 16px',
padding: '20px 0',
borderBottom: '1px solid rgba(16,24,40,0.08)',
alignItems: 'start',
}}
>
<span
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
color: GOLD,
letterSpacing: '0.1em',
paddingTop: 2,
}}
>
{item.num}
</span>
<div>
<div
style={{
fontFamily: FF,
fontSize: 16,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.07em',
color: '#101828',
marginBottom: 6,
}}
>
{item.label}
</div>
<div
style={{
fontFamily: FB,
fontSize: 18,
color: 'rgba(16,24,40,0.5)',
lineHeight: 1.6,
}}
>
{item.body}
</div>
</div>
</div>
))}
</div>
</section>
{/* ── SECTION 5 – Beitragsrechner ─────────────────────────────────────── */}
<BeitragsrechnerSection isMobile={isMobile} content={pricing} />
{/* ── Antrag-Wahl Strip ────────────────────────────────────────────────── */}
<PDFDownloadSection isMobile={isMobile} content={applicationOptions} />
{/* ── SECTION 6 – Formular ────────────────────────────────────────────── */}
<section
id="mitglied-formular"
style={{ background: NAVY, position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : 'calc(100vh - 60px)', display: 'flex', flexDirection: 'column' }}
>
<div
style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr',
flex: 1,
minHeight: 0,
overflow: isMobile ? 'visible' : 'hidden',
}}
>
{/* Left – pitch */}
<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,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.32em',
color: GOLD,
display: 'block',
marginBottom: 20,
}}
>
{fallbackText(formIntro.eyebrow, mitgliedWerdenContent.formIntro.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',
}}
>
{fallbackText(formIntro.heading, mitgliedWerdenContent.formIntro.heading)}
</h2>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
<p
style={{
fontFamily: FB,
fontSize: 18,
color: 'rgba(255,255,255,0.5)',
lineHeight: 1.8,
maxWidth: 400,
marginBottom: 48,
}}
>
<TextLines text={fallbackText(formIntro.description, mitgliedWerdenContent.formIntro.description)} />
</p>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
{formPromises.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',
}}
>
<span
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
color: 'rgba(239,191,4,0.6)',
letterSpacing: '0.1em',
}}
>
{item.num}
</span>
<div
style={{
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
justifyContent: 'space-between',
alignItems: isMobile ? 'flex-start' : 'baseline',
gap: isMobile ? 4 : 0,
}}
>
<span
style={{
fontFamily: FF,
fontSize: 15,
fontWeight: 700,
color: '#fff',
textTransform: 'uppercase',
letterSpacing: '0.08em',
}}
>
{item.label}
</span>
<span
style={{
fontFamily: FB,
fontSize: isMobile ? 15 : 17,
color: 'rgba(255,255,255,0.3)',
}}
>
{item.desc}
</span>
</div>
</div>
))}
</div>
</div>
{/* Center divider */}
<div
style={{
background: 'rgba(255,255,255,0.07)',
display: isMobile ? 'none' : undefined,
}}
/>
{/* Right – wizard */}
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(0,0,0,0.18)', overflowY: isMobile ? 'visible' : 'auto', minHeight: 0 }}>
{!isMobile && (
<div style={{ height: 180, position: 'relative', overflow: 'hidden', flexShrink: 0 }}>
<Image unoptimized src={mediaUrl(formIntro.image, `/images/${mitgliedWerdenContent.formIntro.imageFilename}`)} alt={mediaAlt(formIntro.image, fallbackText(formIntro.imageAlt, mitgliedWerdenContent.formIntro.imageAlt))} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(17,29,85,0.5) 0%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.45) 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, background: GOLD }} />
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 5, height: 5, borderRadius: '50%', background: GOLD }} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(255,255,255,0.85)', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{fallbackText(formIntro.imageLabel, mitgliedWerdenContent.formIntro.imageLabel)}</span>
</div>
</div>
)}
<div style={{ padding: isMobile ? '24px' : '28px 40px 32px', flex: 1, overflowY: isMobile ? 'visible' : 'auto' }}>
<MembershipWizard content={cms.wizard} pricing={pricing} />
</div>
</div>
</div>
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
</section>
{/* ── SECTION 7 – Testimonials ────────────────────────────────────────── */}
<section style={{ background: '#fff', position: 'relative', overflow: 'hidden' }}>
<div style={{ padding: isMobile ? '48px 24px' : '80px' }}>
<span
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.32em',
color: BLUE,
display: 'block',
marginBottom: 16,
}}
>
{fallbackText(testimonials.eyebrow, mitgliedWerdenContent.testimonials.eyebrow)}
</span>
<h2
style={{
fontFamily: FF,
fontSize: 'clamp(2rem, 4vw, 3rem)',
fontWeight: 900,
color: '#101828',
textTransform: 'uppercase',
letterSpacing: '-0.025em',
lineHeight: 1.03,
margin: '0 0 20px',
}}
>
{fallbackText(testimonials.heading, mitgliedWerdenContent.testimonials.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
<div
style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)',
gap: 24,
}}
>
{testimonialItems.map((item, index) => (
<QuoteCard key={`${item.name}-${index}`} quote={item.quote} initials={item.initials} name={item.name} role={item.role} />
))}
</div>
</div>
</section>
{/* ── FAQ ─────────────────────────────────────────────────────────────── */}
<FAQSection isMobile={isMobile} content={faq} />
{/* ── SECTION 8 – Bottom CTA ──────────────────────────────────────────── */}
<section
style={{
background: NAVY,
padding: isMobile ? '48px 24px' : '80px',
textAlign: 'center',
}}
>
<span
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.32em',
color: GOLD,
display: 'block',
marginBottom: 20,
}}
>
{fallbackText(bottomCta.eyebrow, mitgliedWerdenContent.bottomCta.eyebrow)}
</span>
<h2
style={{
fontFamily: FF,
fontSize: 'clamp(2rem, 4vw, 3.2rem)',
fontWeight: 900,
color: '#fff',
textTransform: 'uppercase',
letterSpacing: '-0.025em',
lineHeight: 1.03,
margin: '0 auto 20px',
maxWidth: 640,
}}
>
{fallbackText(bottomCta.heading, mitgliedWerdenContent.bottomCta.heading)}
</h2>
<div
style={{
width: 40,
height: 2,
background: GOLD,
margin: '0 auto 40px',
}}
/>
<a
href={fallbackText(bottomCta.ctaHref, mitgliedWerdenContent.bottomCta.ctaHref)}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 10,
background: GOLD,
color: '#101828',
fontFamily: FF,
fontSize: 17,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.15em',
padding: '16px 36px',
textDecoration: 'none',
transition: 'background 0.2s, 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 = GOLD;
(e.currentTarget as HTMLElement).style.boxShadow = 'none';
}}
>
{fallbackText(bottomCta.ctaLabel, mitgliedWerdenContent.bottomCta.ctaLabel)}
<ChevronRight size={14} />
</a>
</section>
</div>
);
};
export default MitgliedWerden;