feat: manage membership page via payload

This commit is contained in:
syntaxbullet
2026-06-22 11:44:35 +02:00
parent c35e45499e
commit 5bd1977c63
8 changed files with 1506 additions and 464 deletions

View File

@@ -4,6 +4,9 @@ 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';
@@ -73,29 +76,51 @@ function BenefitCard({
// ── Beitragsrechner ───────────────────────────────────────────────────────────
const BEITRAGS_TIERS = [
{ label: 'bis 10', annual: 480 },
{ label: 'bis 20', annual: 600 },
{ label: 'bis 50', annual: 900 },
{ label: 'bis 100', annual: 1200 },
{ label: 'bis 250', annual: 2400 },
{ label: 'bis 1.000', annual: 3600 },
];
const BEITRAGS_VAT = 0.19;
const BEITRAGS_ADMISSION = 500;
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 BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
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 = BEITRAGS_TIERS.find(t => t.label === selected);
const tier = tiers.find(t => t.label === selected);
const annualNet = tier?.annual ?? 0;
const annualGross = annualNet * (1 + BEITRAGS_VAT);
const admissionGross = BEITRAGS_ADMISSION * (1 + BEITRAGS_VAT);
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;
@@ -105,10 +130,10 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
<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 }}>
Mitgliedsbeitrag
{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' }}>
IHR BEITRAG.
{fallbackText(content.heading, fallback.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
@@ -116,7 +141,7 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
{/* 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 }}>
Anzahl Mitarbeiter wählen
{fallbackText(content.employeeLabel, fallback.employeeLabel)}
</label>
<select
value={selected}
@@ -140,9 +165,9 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
transition: 'border-color 0.2s',
}}
>
<option value="">Bitte wählen…</option>
{BEITRAGS_TIERS.map(t => (
<option key={t.label} value={t.label}>{t.label} Mitarbeiter</option>
<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>
@@ -158,12 +183,12 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
pointerEvents: selected ? 'auto' : 'none',
}}>
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.15em', color: GOLD, marginBottom: 18 }}>
Beitragsübersicht
{fallbackText(content.resultEyebrow, fallback.resultEyebrow)}
</div>
{[
['Jahresbeitrag (netto)', formatEurLocal(annualNet)],
['Jahresbeitrag (19 % MwSt.)', formatEurLocal(annualGross)],
['Aufnahmegebühr (brutto)', formatEurLocal(admissionGross)],
[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>
@@ -172,16 +197,16 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
))}
<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 }}>Gesamt Jahr 1 (anteilig)</span>
<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 }}>
* Anteilig ab nächstem Monatsersten bis 31.12. des laufenden Jahres. Ab dem Folgejahr gilt der volle Jahresbeitrag.
{fallbackText(content.footnote, fallback.footnote)}
</div>
</div>
<a
href="#mitglied-formular"
href={fallbackText(content.ctaHref, fallback.ctaHref)}
style={{
display: 'block',
marginTop: 20,
@@ -207,7 +232,7 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
(e.currentTarget as HTMLElement).style.boxShadow = 'none';
}}
>
Jetzt Mitglied werden →
{fallbackText(content.ctaLabel, fallback.ctaLabel)}
</a>
</div>
@@ -234,7 +259,7 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
color: '#101828',
}}
>
Vollständige Beitragsstaffel
{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>
@@ -244,23 +269,23 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
<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 }}>Mitarbeiter</th>
<th style={{ padding: isMobile ? '10px 10px' : '12px 18px', textAlign: 'right', fontSize: 10, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', color: GOLD }}>Netto / Jahr</th>
<th style={{ padding: isMobile ? '10px 10px' : '12px 18px', textAlign: 'right', fontSize: 10, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', color: GOLD }}>Brutto / Jahr</th>
<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>
{BEITRAGS_TIERS.map((t, i) => (
{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 + BEITRAGS_VAT))}</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)' }}>
Zzgl. einmalige Aufnahmegebühr {formatEurLocal(BEITRAGS_ADMISSION * (1 + BEITRAGS_VAT))} (brutto) im ersten Jahr. Alle Angaben inkl. 19 % MwSt.
{fallbackText(content.tableNotePrefix, fallback.tableNotePrefix)} {formatEurLocal(admissionGross)} {fallbackText(content.tableNoteSuffix, fallback.tableNoteSuffix)}
</div>
</div>
)}
@@ -273,8 +298,12 @@ function BeitragsrechnerSection({ isMobile }: { isMobile: boolean }) {
// ── PDF Download Section – editorial split strip ──────────────────────────────
function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
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={{
@@ -294,7 +323,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
}}>
{/* Left – online */}
<a
href="#mitglied-formular"
href={fallbackText(online.href, mitgliedWerdenContent.applicationOptions.items[0].href)}
style={{
display: 'flex',
alignItems: 'center',
@@ -320,7 +349,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
transition: 'color 0.3s ease',
userSelect: 'none',
}}>
01
{fallbackText(online.number, mitgliedWerdenContent.applicationOptions.items[0].number)}
</div>
<div style={{ flex: 1 }}>
@@ -333,7 +362,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
color: BLUE,
marginBottom: 8,
}}>
Online
{fallbackText(online.eyebrow, mitgliedWerdenContent.applicationOptions.items[0].eyebrow)}
</div>
<div style={{
fontFamily: FF,
@@ -344,7 +373,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
color: '#101828',
marginBottom: 6,
}}>
Antrag digital ausfüllen
{fallbackText(online.title, mitgliedWerdenContent.applicationOptions.items[0].title)}
</div>
<div style={{
fontFamily: FB,
@@ -352,7 +381,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
color: 'rgba(16,24,40,0.45)',
lineHeight: 1.6,
}}>
In wenigen Schritten direkt auf dieser Seite
{fallbackText(online.description, mitgliedWerdenContent.applicationOptions.items[0].description)}
</div>
</div>
@@ -395,8 +424,8 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
{/* Right – PDF */}
<a
href="/aufnahmeantrag-bmp.pdf"
download
href={pdfHref}
download={pdf.download || undefined}
style={{
display: 'flex',
alignItems: 'center',
@@ -423,7 +452,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
transition: 'color 0.3s ease',
userSelect: 'none',
}}>
02
{fallbackText(pdf.number, mitgliedWerdenContent.applicationOptions.items[1].number)}
</div>
<div style={{ flex: 1 }}>
@@ -436,7 +465,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
color: BLUE,
marginBottom: 8,
}}>
Per Post
{fallbackText(pdf.eyebrow, mitgliedWerdenContent.applicationOptions.items[1].eyebrow)}
</div>
<div style={{
fontFamily: FF,
@@ -447,7 +476,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
color: '#101828',
marginBottom: 6,
}}>
PDF herunterladen
{fallbackText(pdf.title, mitgliedWerdenContent.applicationOptions.items[1].title)}
</div>
<div style={{
fontFamily: FB,
@@ -455,7 +484,7 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
color: 'rgba(16,24,40,0.45)',
lineHeight: 1.6,
}}>
Antrag ausdrucken und unterschrieben einsenden
{fallbackText(pdf.description, mitgliedWerdenContent.applicationOptions.items[1].description)}
</div>
</div>
@@ -485,31 +514,22 @@ function PDFDownloadSection({ isMobile }: { isMobile: boolean }) {
// ── FAQ Section ───────────────────────────────────────────────────────────────
const FAQ_ITEMS = [
{ q: 'Wer kann Mitglied werden?', a: 'Mitglied können natürliche und juristische Personen werden, die die Ziele des BMP e.V. unterstützen. Dazu zählen Unternehmen jeder Rechtsform, Verbände, Institutionen sowie Einzelpersonen.' },
{ q: 'Wie hoch ist der Mitgliedsbeitrag?', a: 'Der Jahresbeitrag richtet sich nach der Anzahl Ihrer Mitarbeiter und liegt zwischen 480 € und 3.600 € netto. Hinzu kommt eine einmalige Aufnahmegebühr von 500 € netto. Alle Beträge zzgl. 19 % MwSt.' },
{ q: 'Wie läuft das Aufnahmeverfahren ab?', a: 'Nach Eingang Ihres Antrags wird dieser in der nächsten Vorstandssitzung beraten. Sie erhalten innerhalb von 5 Werktagen eine Eingangsbestätigung. Die formelle Aufnahme erfolgt durch Beschluss des Vorstands.' },
{ q: 'Wie wird der Beitrag eingezogen?', a: 'Der Mitgliedsbeitrag wird jährlich per SEPA-Lastschrift eingezogen. Im ersten Jahr wird der Beitrag anteilig ab dem nächsten Monatsersten bis Jahresende berechnet.' },
{ q: 'Wie kann ich die Mitgliedschaft kündigen?', a: 'Die Mitgliedschaft kann mit einer Frist von einem Jahr zum Jahresende schriftlich gekündigt werden. Die Kündigung ist an den Vorstand des BMP e.V. zu richten.' },
{ q: 'Welche Rechte habe ich als Mitglied?', a: 'Als Mitglied haben Sie Stimmrecht in der Mitgliederversammlung, können Anträge stellen und aktiv an der Vereinsarbeit teilnehmen. Sie erhalten Zugang zu Events, dem Mitgliedernetzwerk und allen Publikationen.' },
{ q: 'Kann ich den Beitrag von der Steuer absetzen?', a: 'Da es sich um einen Förderbeitrag an einen gemeinnützigen Verein handelt, ist der Mitgliedsbeitrag in der Regel als Betriebsausgabe absetzbar. Bitte sprechen Sie dazu mit Ihrem Steuerberater.' },
];
function FAQSection({ isMobile }: { isMobile: boolean }) {
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 }}>
Häufige Fragen
{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' }}>
FAQ.
{fallbackText(content.heading, mitgliedWerdenContent.faq.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
<div style={{ maxWidth: 800 }}>
{FAQ_ITEMS.map((item, i) => (
{items.map((item, i) => (
<div key={i} style={{ borderBottom: '1px solid rgba(16,24,40,0.1)' }}>
<button
onClick={() => setOpenIdx(openIdx === i ? null : i)}
@@ -652,6 +672,32 @@ function QuoteCard({
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">
@@ -677,10 +723,10 @@ const MitgliedWerden: React.FC = () => {
color: GOLD,
}}
>
BMP 2026
{fallbackText(header.brandLabel, mitgliedWerdenContent.header.brandLabel)}
</span>
<Link
to="/"
to={fallbackText(header.backUrl, mitgliedWerdenContent.header.backUrl)}
style={{
fontFamily: FF,
fontSize: 10,
@@ -695,7 +741,7 @@ const MitgliedWerden: React.FC = () => {
}}
>
<ArrowLeft size={12} />
Zurück zur Website
{fallbackText(header.backLabel, mitgliedWerdenContent.header.backLabel)}
</Link>
</header>
@@ -710,8 +756,8 @@ const MitgliedWerden: React.FC = () => {
>
{/* Background image */}
<Image unoptimized
src="/images/mitglied-hero.jpg"
alt=""
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%' }}
/>
@@ -741,7 +787,7 @@ const MitgliedWerden: React.FC = () => {
marginBottom: 24,
}}
>
Vereinsmitgliedschaft
{fallbackText(hero.eyebrow, mitgliedWerdenContent.hero.eyebrow)}
</span>
<h1
style={{
@@ -755,7 +801,7 @@ const MitgliedWerden: React.FC = () => {
margin: '0 0 28px',
}}
>
DEIN WEG ZU UNS.
{fallbackText(hero.heading, mitgliedWerdenContent.hero.heading)}
</h1>
<div style={{ width: 48, height: 3, background: GOLD, marginBottom: 28 }} />
<p
@@ -769,9 +815,7 @@ const MitgliedWerden: React.FC = () => {
margin: 0,
}}
>
Der Bayerische Mittelstandspreis lebt vom Engagement seiner Mitglieder.
Eine Mitgliedschaft ist Unterstützung, damit der Preis weiterhin unabhängig
und kostenlos umgesetzt werden kann.
<TextLines text={fallbackText(hero.description, mitgliedWerdenContent.hero.description)} />
</p>
</div>
@@ -785,18 +829,20 @@ const MitgliedWerden: React.FC = () => {
paddingRight: 16,
}}
>
{heroStats.map((stat, index) => (
<React.Fragment key={`${stat.value}-${stat.label}`}>
<div style={{ textAlign: 'right' }}>
<div
style={{
fontFamily: FF,
fontSize: 72,
fontSize: index === 0 ? 72 : 48,
fontWeight: 900,
color: GOLD,
color: index === 0 ? GOLD : '#fff',
lineHeight: 1,
letterSpacing: '-0.03em',
letterSpacing: index === 0 ? '-0.03em' : '-0.02em',
}}
>
120+
{stat.value}
</div>
<div
style={{
@@ -809,10 +855,11 @@ const MitgliedWerden: React.FC = () => {
marginTop: 4,
}}
>
Mitglieder
{stat.label}
</div>
</div>
{index < heroStats.length - 1 && (
<div
style={{
width: '100%',
@@ -821,71 +868,9 @@ const MitgliedWerden: React.FC = () => {
margin: '32px 0',
}}
/>
<div style={{ textAlign: 'right' }}>
<div
style={{
fontFamily: FF,
fontSize: 48,
fontWeight: 900,
color: '#fff',
lineHeight: 1,
letterSpacing: '-0.02em',
}}
>
20+
</div>
<div
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.3em',
color: 'rgba(255,255,255,0.35)',
marginTop: 4,
}}
>
Jahre
</div>
</div>
<div
style={{
width: '100%',
height: 1,
background: 'rgba(255,255,255,0.1)',
margin: '32px 0',
}}
/>
<div style={{ textAlign: 'right' }}>
<div
style={{
fontFamily: FF,
fontSize: 48,
fontWeight: 900,
color: '#fff',
lineHeight: 1,
letterSpacing: '-0.02em',
}}
>
500 €
</div>
<div
style={{
fontFamily: FF,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.3em',
color: 'rgba(255,255,255,0.35)',
marginTop: 4,
}}
>
Aufnahme
</div>
</div>
)}
</React.Fragment>
))}
</div>
)}
</div>
@@ -925,7 +910,7 @@ const MitgliedWerden: React.FC = () => {
marginBottom: 16,
}}
>
Deine Vorteile
{fallbackText(benefits.eyebrow, mitgliedWerdenContent.benefits.eyebrow)}
</span>
<h2
style={{
@@ -939,7 +924,7 @@ const MitgliedWerden: React.FC = () => {
margin: '0 0 20px',
}}
>
WAS DU GEWINNST.
{fallbackText(benefits.heading, mitgliedWerdenContent.benefits.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
@@ -950,34 +935,16 @@ const MitgliedWerden: React.FC = () => {
borderTop: '1px solid rgba(3,9,58,0.1)',
}}
>
<BenefitCard
icon={<Users size={24} color={NAVY} strokeWidth={1.5} />}
title="Netzwerk"
body="Direkter Zugang zu Jury-Mitgliedern, Partnern und Entscheidern aus dem bayerischen Mittelstand."
isLast={false}
isMobile={isMobile}
/>
<BenefitCard
icon={<Star size={24} color={NAVY} strokeWidth={1.5} />}
title="Unterstützung"
body="Mit deiner Mitgliedschaft sorgst du dafür, dass der Preis unabhängig und kostenlos vergeben werden kann."
isLast={false}
isMobile={isMobile}
/>
<BenefitCard
icon={<Eye size={24} color={NAVY} strokeWidth={1.5} />}
title="Sichtbarkeit"
body="Als Vereinsmitglied wirst du im BMP-Ökosystem sichtbar – auf Events, in Publikationen und online."
isLast={false}
isMobile={isMobile}
/>
<BenefitCard
icon={<Heart size={24} color={NAVY} strokeWidth={1.5} />}
title="Wirkung"
body="Leiste einen konkreten Beitrag für den bayerischen Mittelstand und die Menschen dahinter."
isLast={true}
isMobile={isMobile}
/>
{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>
@@ -1006,7 +973,7 @@ const MitgliedWerden: React.FC = () => {
marginBottom: 20,
}}
>
Der Verein
{fallbackText(about.eyebrow, mitgliedWerdenContent.about.eyebrow)}
</span>
<h2
style={{
@@ -1020,46 +987,23 @@ const MitgliedWerden: React.FC = () => {
margin: '0 0 20px',
}}
>
WER WIR SIND.
{fallbackText(about.heading, mitgliedWerdenContent.about.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 32 }} />
<p
style={{
fontFamily: FB,
fontSize: 18,
color: 'rgba(16,24,40,0.55)',
lineHeight: 1.85,
margin: '0 0 20px',
}}
>
Der Bayerische Mittelstandspreis e.V. ist ein gemeinnütziger Verein, der seit über
zwei Jahrzehnten die Exzellenz im bayerischen Mittelstand fördert und sichtbar macht.
</p>
<p
style={{
fontFamily: FB,
fontSize: 18,
color: 'rgba(16,24,40,0.55)',
lineHeight: 1.85,
margin: '0 0 40px',
}}
>
Unsere Mitglieder sind Unternehmer:innen, Wissenschaftler:innen, Verbände und
Einzelpersonen – verbunden durch das Ziel, den Mittelstand als Rückgrat der
bayerischen Wirtschaft zu stärken.
</p>
<p
style={{
fontFamily: FB,
fontSize: 18,
color: 'rgba(16,24,40,0.55)',
lineHeight: 1.85,
margin: '0 0 40px',
}}
>
Veranstalter ist der EWIF – Europäisches Wirtschaftsforum. Die Mitgliedschaft
läuft über den EWIF.
</p>
{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={{
@@ -1069,7 +1013,7 @@ const MitgliedWerden: React.FC = () => {
letterSpacing: '0.05em',
}}
>
Bayerischer Mittelstandspreis e.V., eingetragener gemeinnütziger Verein
{fallbackText(about.descriptor, mitgliedWerdenContent.about.descriptor)}
</span>
</div>
@@ -1084,23 +1028,7 @@ const MitgliedWerden: React.FC = () => {
justifyContent: 'center',
}}
>
{[
{
num: '01',
label: 'Gemeinnützig',
body: 'Alle Aktivitäten dienen ausschließlich dem Vereinszweck',
},
{
num: '02',
label: 'Ehrenamtlich',
body: 'Vorstand und Jury arbeiten ohne Vergütung',
},
{
num: '03',
label: 'Transparent',
body: 'Jahresbericht und Satzung öffentlich einsehbar',
},
].map((item, i) => (
{aboutFacts.map((item, i) => (
<div
key={i}
style={{
@@ -1155,10 +1083,10 @@ const MitgliedWerden: React.FC = () => {
</section>
{/* ── SECTION 5 – Beitragsrechner ─────────────────────────────────────── */}
<BeitragsrechnerSection isMobile={isMobile} />
<BeitragsrechnerSection isMobile={isMobile} content={pricing} />
{/* ── Antrag-Wahl Strip ────────────────────────────────────────────────── */}
<PDFDownloadSection isMobile={isMobile} />
<PDFDownloadSection isMobile={isMobile} content={applicationOptions} />
{/* ── SECTION 6 – Formular ────────────────────────────────────────────── */}
<section
@@ -1196,7 +1124,7 @@ const MitgliedWerden: React.FC = () => {
marginBottom: 20,
}}
>
Jetzt beitreten
{fallbackText(formIntro.eyebrow, mitgliedWerdenContent.formIntro.eyebrow)}
</span>
<h2
style={{
@@ -1210,7 +1138,7 @@ const MitgliedWerden: React.FC = () => {
margin: '0 0 20px',
}}
>
DU BIST DABEI.
{fallbackText(formIntro.heading, mitgliedWerdenContent.formIntro.heading)}
</h2>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
<p
@@ -1223,27 +1151,10 @@ const MitgliedWerden: React.FC = () => {
marginBottom: 48,
}}
>
Fülle das Formular aus und wir melden uns innerhalb von 48 Stunden bei dir.
Die Mitgliedschaft beginnt mit Bestätigung durch den Vorstand.
<TextLines text={fallbackText(formIntro.description, mitgliedWerdenContent.formIntro.description)} />
</p>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
{[
{
num: '01',
label: 'Sofort-Zugang',
desc: 'Netzwerk-Plattform ab Aufnahme',
},
{
num: '02',
label: 'Willkommenspaket',
desc: 'Mitgliedsausweis + Jahresbericht',
},
{
num: '03',
label: 'Event-Einladung',
desc: 'Nächste BMP-Veranstaltung inklusive',
},
].map((item, i) => (
{formPromises.map((item, i) => (
<div
key={i}
style={{
@@ -1314,17 +1225,17 @@ const MitgliedWerden: React.FC = () => {
<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="/images/gewinner-gruppenfoto.jpg" alt="BMP Mitglieder" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
<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' }}>BMP Mitglieder 2025</span>
<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 />
<MembershipWizard content={cms.wizard} pricing={pricing} />
</div>
</div>
</div>
@@ -1346,7 +1257,7 @@ const MitgliedWerden: React.FC = () => {
marginBottom: 16,
}}
>
Stimmen der Mitglieder
{fallbackText(testimonials.eyebrow, mitgliedWerdenContent.testimonials.eyebrow)}
</span>
<h2
style={{
@@ -1360,7 +1271,7 @@ const MitgliedWerden: React.FC = () => {
margin: '0 0 20px',
}}
>
WAS MITGLIEDER SAGEN.
{fallbackText(testimonials.heading, mitgliedWerdenContent.testimonials.heading)}
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 48 }} />
@@ -1371,30 +1282,15 @@ const MitgliedWerden: React.FC = () => {
gap: 24,
}}
>
<QuoteCard
quote="Die Mitgliedschaft hat mir Zugang zu einem Netzwerk gegeben, das ich sonst nie erreicht hätte. Der BMP verbindet Menschen, die wirklich etwas bewegen wollen."
initials="MH"
name="Markus Hoffmann"
role="Unternehmer, München"
/>
<QuoteCard
quote="Als Fördermitglied unterstütze ich, dass der Preis unabhängig und kostenlos vergeben werden kann. Dieser Beitrag für den Mittelstand ist mir viel wert."
initials="SK"
name="Sandra Keller"
role="Verbandsvertreterin"
/>
<QuoteCard
quote="Der Verein ist das Rückgrat des Awards – ohne die Mitglieder gibt es keinen BMP. Ich bin stolz, Teil davon zu sein."
initials="TR"
name="Thomas Riedl"
role="Jury-Alumni"
/>
{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} />
<FAQSection isMobile={isMobile} content={faq} />
{/* ── SECTION 8 – Bottom CTA ──────────────────────────────────────────── */}
<section
@@ -1415,8 +1311,8 @@ const MitgliedWerden: React.FC = () => {
display: 'block',
marginBottom: 20,
}}
>
Werde Teil des BMP
>
{fallbackText(bottomCta.eyebrow, mitgliedWerdenContent.bottomCta.eyebrow)}
</span>
<h2
style={{
@@ -1431,7 +1327,7 @@ const MitgliedWerden: React.FC = () => {
maxWidth: 640,
}}
>
UNTERSTÜTZE. NETZWERKE. WIRKE.
{fallbackText(bottomCta.heading, mitgliedWerdenContent.bottomCta.heading)}
</h2>
<div
style={{
@@ -1442,7 +1338,7 @@ const MitgliedWerden: React.FC = () => {
}}
/>
<a
href="#mitglied-formular"
href={fallbackText(bottomCta.ctaHref, mitgliedWerdenContent.bottomCta.ctaHref)}
style={{
display: 'inline-flex',
alignItems: 'center',
@@ -1468,7 +1364,7 @@ const MitgliedWerden: React.FC = () => {
(e.currentTarget as HTMLElement).style.boxShadow = 'none';
}}
>
Jetzt Mitglied werden
{fallbackText(bottomCta.ctaLabel, mitgliedWerdenContent.bottomCta.ctaLabel)}
<ChevronRight size={14} />
</a>
</section>