feat: manage membership page via payload
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"preload:formular-upload-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-formular-upload-page-cms.ts",
|
||||
"preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts",
|
||||
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",
|
||||
"preload:mitglied-werden-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-mitglied-werden-page-cms.ts",
|
||||
"preload:netzwerk-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-netzwerk-page-cms.ts",
|
||||
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
|
||||
"preload:preistraeger-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-preistraeger-index-page-cms.ts",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { datenschutzFields } from './datenschutzFields'
|
||||
import { formularUploadFields } from './formularUploadFields'
|
||||
import { homeFields } from './homeFields'
|
||||
import { impressumFields } from './impressumFields'
|
||||
import { mitgliedWerdenFields } from './mitgliedWerdenFields'
|
||||
import { netzwerkFields } from './netzwerkFields'
|
||||
import { participationFields } from './participationFields'
|
||||
import { preistraegerIndexFields } from './preistraegerIndexFields'
|
||||
@@ -111,6 +112,14 @@ const isNetzwerkPage = (_: unknown, siblingData?: { slug?: string; spaPath?: str
|
||||
return spaPath === '/netzwerk' || slug === 'netzwerk' || slug === 'network' || title === 'netzwerk' || title === 'network'
|
||||
}
|
||||
|
||||
const isMitgliedWerdenPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
|
||||
const slug = siblingData?.slug
|
||||
const spaPath = siblingData?.spaPath
|
||||
const title = siblingData?.title?.toLowerCase()
|
||||
|
||||
return spaPath === '/mitglied-werden' || slug === 'mitglied-werden' || slug === 'membership' || title === 'mitglied werden'
|
||||
}
|
||||
|
||||
const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
|
||||
isHomePage(undefined, siblingData) ||
|
||||
isContactPage(undefined, siblingData) ||
|
||||
@@ -121,7 +130,8 @@ const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: s
|
||||
isPreistraegerIndexPage(undefined, siblingData) ||
|
||||
isPressIndexPage(undefined, siblingData) ||
|
||||
isFormularUploadPage(undefined, siblingData) ||
|
||||
isNetzwerkPage(undefined, siblingData)
|
||||
isNetzwerkPage(undefined, siblingData) ||
|
||||
isMitgliedWerdenPage(undefined, siblingData)
|
||||
|
||||
export const Pages: CollectionConfig<'pages'> = {
|
||||
slug: 'pages',
|
||||
@@ -259,6 +269,13 @@ export const Pages: CollectionConfig<'pages'> = {
|
||||
fields: netzwerkFields,
|
||||
label: 'Network Page',
|
||||
},
|
||||
{
|
||||
admin: {
|
||||
condition: (data) => isMitgliedWerdenPage(undefined, data),
|
||||
},
|
||||
fields: mitgliedWerdenFields,
|
||||
label: 'Membership Page',
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
label: 'SEO',
|
||||
|
||||
303
src/collections/Pages/mitgliedWerdenFields.ts
Normal file
303
src/collections/Pages/mitgliedWerdenFields.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { mitgliedWerdenContent } from '@/spa/mitgliedWerdenContent'
|
||||
|
||||
const text = (name: string, label: string, defaultValue?: string): Field => ({
|
||||
name,
|
||||
type: 'text',
|
||||
label,
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const textarea = (name: string, label: string, defaultValue?: string): Field => ({
|
||||
name,
|
||||
type: 'textarea',
|
||||
label,
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const numberField = (name: string, label: string, defaultValue?: number): Field => ({
|
||||
name,
|
||||
type: 'number',
|
||||
label,
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const checkbox = (name: string, label: string, defaultValue?: boolean): Field => ({
|
||||
name,
|
||||
type: 'checkbox',
|
||||
label,
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const uploadField = (name: string, label: string, description?: string): Field => ({
|
||||
name,
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label,
|
||||
admin: description ? { description } : undefined,
|
||||
})
|
||||
|
||||
const sectionAdmin = (description: string) => ({
|
||||
description,
|
||||
initCollapsed: true,
|
||||
})
|
||||
|
||||
const statFields: Field[] = [text('value', 'Value'), text('label', 'Label')]
|
||||
const factFields: Field[] = [text('num', 'Number'), text('label', 'Label'), textarea('body', 'Body')]
|
||||
const faqFields: Field[] = [text('q', 'Question'), textarea('a', 'Answer')]
|
||||
const testimonialFields: Field[] = [
|
||||
textarea('quote', 'Quote'),
|
||||
text('initials', 'Initials'),
|
||||
text('name', 'Name'),
|
||||
text('role', 'Role'),
|
||||
]
|
||||
|
||||
export const mitgliedWerdenFields: Field[] = [
|
||||
{
|
||||
name: 'mitgliedWerden',
|
||||
label: 'Membership page content',
|
||||
type: 'group',
|
||||
admin: {
|
||||
description:
|
||||
'Edit the Mitglied werden page sections, pricing model, application choices, FAQ, testimonials, and wizard copy.',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'header',
|
||||
label: '00 · Header',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Small top bar brand and return link.'),
|
||||
fields: [
|
||||
text('brandLabel', 'Brand label', mitgliedWerdenContent.header.brandLabel),
|
||||
text('backLabel', 'Back link label', mitgliedWerdenContent.header.backLabel),
|
||||
text('backUrl', 'Back link URL', mitgliedWerdenContent.header.backUrl),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'hero',
|
||||
label: '01 · Hero',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Hero image, headline, description, and desktop stats.'),
|
||||
fields: [
|
||||
uploadField('image', 'Hero image', `Current frontend image: /images/${mitgliedWerdenContent.hero.imageFilename}`),
|
||||
text('imageAlt', 'Image alt text', mitgliedWerdenContent.hero.imageAlt),
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.hero.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.hero.heading),
|
||||
textarea('description', 'Description', mitgliedWerdenContent.hero.description),
|
||||
{
|
||||
name: 'stats',
|
||||
label: 'Stats',
|
||||
type: 'array',
|
||||
dbName: 'member_hero_stats',
|
||||
defaultValue: mitgliedWerdenContent.hero.stats,
|
||||
fields: statFields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'benefits',
|
||||
label: '02 · Benefits',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Benefit section heading and cards. Icon choices map to Lucide icons in the SPA.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.benefits.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.benefits.heading),
|
||||
{
|
||||
name: 'cards',
|
||||
label: 'Cards',
|
||||
type: 'array',
|
||||
dbName: 'member_benefits',
|
||||
defaultValue: mitgliedWerdenContent.benefits.cards,
|
||||
fields: [
|
||||
{
|
||||
name: 'icon',
|
||||
label: 'Icon',
|
||||
type: 'select',
|
||||
defaultValue: 'users',
|
||||
options: [
|
||||
{ label: 'Users', value: 'users' },
|
||||
{ label: 'Star', value: 'star' },
|
||||
{ label: 'Eye', value: 'eye' },
|
||||
{ label: 'Heart', value: 'heart' },
|
||||
],
|
||||
},
|
||||
text('title', 'Title'),
|
||||
textarea('body', 'Body'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
label: '03 · About',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Association explainer, descriptor, and fact rows.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.about.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.about.heading),
|
||||
{
|
||||
name: 'paragraphs',
|
||||
label: 'Paragraphs',
|
||||
type: 'array',
|
||||
dbName: 'member_about_copy',
|
||||
defaultValue: mitgliedWerdenContent.about.paragraphs.map((text) => ({ text })),
|
||||
fields: [textarea('text', 'Text')],
|
||||
},
|
||||
text('descriptor', 'Descriptor', mitgliedWerdenContent.about.descriptor),
|
||||
{
|
||||
name: 'facts',
|
||||
label: 'Facts',
|
||||
type: 'array',
|
||||
dbName: 'member_about_facts',
|
||||
defaultValue: mitgliedWerdenContent.about.facts,
|
||||
fields: factFields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'pricing',
|
||||
label: '04 · Pricing calculator',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Calculator labels, table copy, VAT rate, admission fee, and tier rows.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.pricing.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.pricing.heading),
|
||||
text('employeeLabel', 'Employee select label', mitgliedWerdenContent.pricing.employeeLabel),
|
||||
text('selectPlaceholder', 'Select placeholder', mitgliedWerdenContent.pricing.selectPlaceholder),
|
||||
text('optionSuffix', 'Option suffix', mitgliedWerdenContent.pricing.optionSuffix),
|
||||
text('resultEyebrow', 'Result eyebrow', mitgliedWerdenContent.pricing.resultEyebrow),
|
||||
text('annualNetLabel', 'Annual net label', mitgliedWerdenContent.pricing.annualNetLabel),
|
||||
text('annualGrossLabel', 'Annual gross label', mitgliedWerdenContent.pricing.annualGrossLabel),
|
||||
text('annualGrossShortLabel', 'Annual gross short label', mitgliedWerdenContent.pricing.annualGrossShortLabel),
|
||||
text('admissionGrossLabel', 'Admission gross label', mitgliedWerdenContent.pricing.admissionGrossLabel),
|
||||
text('totalYearOneLabel', 'Total year-one label', mitgliedWerdenContent.pricing.totalYearOneLabel),
|
||||
textarea('footnote', 'Footnote', mitgliedWerdenContent.pricing.footnote),
|
||||
text('ctaLabel', 'CTA label', mitgliedWerdenContent.pricing.ctaLabel),
|
||||
text('ctaHref', 'CTA URL', mitgliedWerdenContent.pricing.ctaHref),
|
||||
text('tableToggleLabel', 'Table toggle label', mitgliedWerdenContent.pricing.tableToggleLabel),
|
||||
text('tableEmployeeHeader', 'Table employees header', mitgliedWerdenContent.pricing.tableEmployeeHeader),
|
||||
text('tableNetHeader', 'Table net header', mitgliedWerdenContent.pricing.tableNetHeader),
|
||||
text('tableGrossHeader', 'Table gross header', mitgliedWerdenContent.pricing.tableGrossHeader),
|
||||
text('tableNotePrefix', 'Table note prefix', mitgliedWerdenContent.pricing.tableNotePrefix),
|
||||
text('tableNoteSuffix', 'Table note suffix', mitgliedWerdenContent.pricing.tableNoteSuffix),
|
||||
numberField('vatRate', 'VAT rate', mitgliedWerdenContent.pricing.vatRate),
|
||||
numberField('admissionFeeNet', 'Admission fee net', mitgliedWerdenContent.pricing.admissionFeeNet),
|
||||
{
|
||||
name: 'tiers',
|
||||
label: 'Tiers',
|
||||
type: 'array',
|
||||
dbName: 'member_pricing',
|
||||
defaultValue: mitgliedWerdenContent.pricing.tiers,
|
||||
fields: [numberField('max', 'Maximum employees'), text('label', 'Label'), numberField('annual', 'Annual net amount')],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'applicationOptions',
|
||||
label: '05 · Application options',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Online and PDF application strip.'),
|
||||
fields: [
|
||||
{
|
||||
name: 'items',
|
||||
label: 'Options',
|
||||
type: 'array',
|
||||
dbName: 'member_app_options',
|
||||
defaultValue: mitgliedWerdenContent.applicationOptions.items,
|
||||
fields: [
|
||||
text('id', 'Stable ID'),
|
||||
text('number', 'Number'),
|
||||
text('eyebrow', 'Eyebrow'),
|
||||
text('title', 'Title'),
|
||||
textarea('description', 'Description'),
|
||||
text('href', 'URL'),
|
||||
uploadField('file', 'Download file'),
|
||||
checkbox('download', 'Download link'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'formIntro',
|
||||
label: '06 · Form intro',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Left-hand pitch copy and wizard image/caption.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.formIntro.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.formIntro.heading),
|
||||
textarea('description', 'Description', mitgliedWerdenContent.formIntro.description),
|
||||
{
|
||||
name: 'promises',
|
||||
label: 'Promise rows',
|
||||
type: 'array',
|
||||
dbName: 'member_promises',
|
||||
defaultValue: mitgliedWerdenContent.formIntro.promises,
|
||||
fields: [text('num', 'Number'), text('label', 'Label'), text('desc', 'Description')],
|
||||
},
|
||||
uploadField('image', 'Wizard image', `Current frontend image: /images/${mitgliedWerdenContent.formIntro.imageFilename}`),
|
||||
text('imageAlt', 'Image alt text', mitgliedWerdenContent.formIntro.imageAlt),
|
||||
text('imageLabel', 'Image caption', mitgliedWerdenContent.formIntro.imageLabel),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'wizard',
|
||||
label: '07 · Wizard copy JSON',
|
||||
type: 'json',
|
||||
admin: {
|
||||
description:
|
||||
'Deep wizard labels, options, validation messages, legal text, consent copy, success copy, and duplicated pricing defaults. Keep the object shape aligned with src/spa/mitgliedWerdenContent.ts.',
|
||||
},
|
||||
defaultValue: mitgliedWerdenContent.wizard,
|
||||
},
|
||||
{
|
||||
name: 'testimonials',
|
||||
label: '08 · Testimonials',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Member quotes and attribution cards.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.testimonials.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.testimonials.heading),
|
||||
{
|
||||
name: 'items',
|
||||
label: 'Quotes',
|
||||
type: 'array',
|
||||
dbName: 'member_quotes',
|
||||
defaultValue: mitgliedWerdenContent.testimonials.items,
|
||||
fields: testimonialFields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'faq',
|
||||
label: '09 · FAQ',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('FAQ heading and question/answer rows.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.faq.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.faq.heading),
|
||||
{
|
||||
name: 'items',
|
||||
label: 'Items',
|
||||
type: 'array',
|
||||
dbName: 'member_faq',
|
||||
defaultValue: mitgliedWerdenContent.faq.items,
|
||||
fields: faqFields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'bottomCta',
|
||||
label: '10 · Bottom CTA',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Final call-to-action band.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', mitgliedWerdenContent.bottomCta.eyebrow),
|
||||
textarea('heading', 'Heading', mitgliedWerdenContent.bottomCta.heading),
|
||||
text('ctaLabel', 'CTA label', mitgliedWerdenContent.bottomCta.ctaLabel),
|
||||
text('ctaHref', 'CTA URL', mitgliedWerdenContent.bottomCta.ctaHref),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1629,6 +1629,201 @@ export interface Page {
|
||||
footerText?: string | null;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Edit the Mitglied werden page sections, pricing model, application choices, FAQ, testimonials, and wizard copy.
|
||||
*/
|
||||
mitgliedWerden?: {
|
||||
/**
|
||||
* Small top bar brand and return link.
|
||||
*/
|
||||
header?: {
|
||||
brandLabel?: string | null;
|
||||
backLabel?: string | null;
|
||||
backUrl?: string | null;
|
||||
};
|
||||
/**
|
||||
* Hero image, headline, description, and desktop stats.
|
||||
*/
|
||||
hero?: {
|
||||
/**
|
||||
* Current frontend image: /images/mitglied-hero.jpg
|
||||
*/
|
||||
image?: (number | null) | Media;
|
||||
imageAlt?: string | null;
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
description?: string | null;
|
||||
stats?:
|
||||
| {
|
||||
value?: string | null;
|
||||
label?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Benefit section heading and cards. Icon choices map to Lucide icons in the SPA.
|
||||
*/
|
||||
benefits?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
cards?:
|
||||
| {
|
||||
icon?: ('users' | 'star' | 'eye' | 'heart') | null;
|
||||
title?: string | null;
|
||||
body?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Association explainer, descriptor, and fact rows.
|
||||
*/
|
||||
about?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
paragraphs?:
|
||||
| {
|
||||
text?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
descriptor?: string | null;
|
||||
facts?:
|
||||
| {
|
||||
num?: string | null;
|
||||
label?: string | null;
|
||||
body?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Calculator labels, table copy, VAT rate, admission fee, and tier rows.
|
||||
*/
|
||||
pricing?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
employeeLabel?: string | null;
|
||||
selectPlaceholder?: string | null;
|
||||
optionSuffix?: string | null;
|
||||
resultEyebrow?: string | null;
|
||||
annualNetLabel?: string | null;
|
||||
annualGrossLabel?: string | null;
|
||||
annualGrossShortLabel?: string | null;
|
||||
admissionGrossLabel?: string | null;
|
||||
totalYearOneLabel?: string | null;
|
||||
footnote?: string | null;
|
||||
ctaLabel?: string | null;
|
||||
ctaHref?: string | null;
|
||||
tableToggleLabel?: string | null;
|
||||
tableEmployeeHeader?: string | null;
|
||||
tableNetHeader?: string | null;
|
||||
tableGrossHeader?: string | null;
|
||||
tableNotePrefix?: string | null;
|
||||
tableNoteSuffix?: string | null;
|
||||
vatRate?: number | null;
|
||||
admissionFeeNet?: number | null;
|
||||
tiers?:
|
||||
| {
|
||||
max?: number | null;
|
||||
label?: string | null;
|
||||
annual?: number | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Online and PDF application strip.
|
||||
*/
|
||||
applicationOptions?: {
|
||||
items?:
|
||||
| {
|
||||
id?: string | null;
|
||||
number?: string | null;
|
||||
eyebrow?: string | null;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
href?: string | null;
|
||||
file?: (number | null) | Media;
|
||||
download?: boolean | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Left-hand pitch copy and wizard image/caption.
|
||||
*/
|
||||
formIntro?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
description?: string | null;
|
||||
promises?:
|
||||
| {
|
||||
num?: string | null;
|
||||
label?: string | null;
|
||||
desc?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* Current frontend image: /images/gewinner-gruppenfoto.jpg
|
||||
*/
|
||||
image?: (number | null) | Media;
|
||||
imageAlt?: string | null;
|
||||
imageLabel?: string | null;
|
||||
};
|
||||
/**
|
||||
* Deep wizard labels, options, validation messages, legal text, consent copy, success copy, and duplicated pricing defaults. Keep the object shape aligned with src/spa/mitgliedWerdenContent.ts.
|
||||
*/
|
||||
wizard?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* Member quotes and attribution cards.
|
||||
*/
|
||||
testimonials?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
items?:
|
||||
| {
|
||||
quote?: string | null;
|
||||
initials?: string | null;
|
||||
name?: string | null;
|
||||
role?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* FAQ heading and question/answer rows.
|
||||
*/
|
||||
faq?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
items?:
|
||||
| {
|
||||
q?: string | null;
|
||||
a?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Final call-to-action band.
|
||||
*/
|
||||
bottomCta?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
ctaLabel?: string | null;
|
||||
ctaHref?: string | null;
|
||||
};
|
||||
};
|
||||
meta?: {
|
||||
title?: string | null;
|
||||
/**
|
||||
@@ -3867,6 +4062,173 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
footerText?: T;
|
||||
};
|
||||
};
|
||||
mitgliedWerden?:
|
||||
| T
|
||||
| {
|
||||
header?:
|
||||
| T
|
||||
| {
|
||||
brandLabel?: T;
|
||||
backLabel?: T;
|
||||
backUrl?: T;
|
||||
};
|
||||
hero?:
|
||||
| T
|
||||
| {
|
||||
image?: T;
|
||||
imageAlt?: T;
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
description?: T;
|
||||
stats?:
|
||||
| T
|
||||
| {
|
||||
value?: T;
|
||||
label?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
benefits?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
cards?:
|
||||
| T
|
||||
| {
|
||||
icon?: T;
|
||||
title?: T;
|
||||
body?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
about?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
paragraphs?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
descriptor?: T;
|
||||
facts?:
|
||||
| T
|
||||
| {
|
||||
num?: T;
|
||||
label?: T;
|
||||
body?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
pricing?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
employeeLabel?: T;
|
||||
selectPlaceholder?: T;
|
||||
optionSuffix?: T;
|
||||
resultEyebrow?: T;
|
||||
annualNetLabel?: T;
|
||||
annualGrossLabel?: T;
|
||||
annualGrossShortLabel?: T;
|
||||
admissionGrossLabel?: T;
|
||||
totalYearOneLabel?: T;
|
||||
footnote?: T;
|
||||
ctaLabel?: T;
|
||||
ctaHref?: T;
|
||||
tableToggleLabel?: T;
|
||||
tableEmployeeHeader?: T;
|
||||
tableNetHeader?: T;
|
||||
tableGrossHeader?: T;
|
||||
tableNotePrefix?: T;
|
||||
tableNoteSuffix?: T;
|
||||
vatRate?: T;
|
||||
admissionFeeNet?: T;
|
||||
tiers?:
|
||||
| T
|
||||
| {
|
||||
max?: T;
|
||||
label?: T;
|
||||
annual?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
applicationOptions?:
|
||||
| T
|
||||
| {
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
number?: T;
|
||||
eyebrow?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
href?: T;
|
||||
file?: T;
|
||||
download?: T;
|
||||
};
|
||||
};
|
||||
formIntro?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
description?: T;
|
||||
promises?:
|
||||
| T
|
||||
| {
|
||||
num?: T;
|
||||
label?: T;
|
||||
desc?: T;
|
||||
id?: T;
|
||||
};
|
||||
image?: T;
|
||||
imageAlt?: T;
|
||||
imageLabel?: T;
|
||||
};
|
||||
wizard?: T;
|
||||
testimonials?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
quote?: T;
|
||||
initials?: T;
|
||||
name?: T;
|
||||
role?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
faq?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
q?: T;
|
||||
a?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
bottomCta?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
ctaLabel?: T;
|
||||
ctaHref?: T;
|
||||
};
|
||||
};
|
||||
meta?:
|
||||
| T
|
||||
| {
|
||||
|
||||
86
src/scripts/preload-mitglied-werden-page-cms.ts
Normal file
86
src/scripts/preload-mitglied-werden-page-cms.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
|
||||
import { mitgliedWerdenContent } from '@/spa/mitgliedWerdenContent'
|
||||
|
||||
const mediaByFilename = async (payload: Payload, filename: string) => {
|
||||
const result = await payload.find({
|
||||
collection: 'media',
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
where: { filename: { equals: filename } },
|
||||
})
|
||||
return result.docs[0]?.id
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const pageResult = await payload.find({
|
||||
collection: 'pages',
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
draft: true,
|
||||
overrideAccess: true,
|
||||
where: {
|
||||
or: [
|
||||
{ spaPath: { equals: '/mitglied-werden' } },
|
||||
{ slug: { equals: 'mitglied-werden' } },
|
||||
{ slug: { equals: 'membership' } },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const page = pageResult.docs[0]
|
||||
if (!page) {
|
||||
throw new Error('Mitglied werden page not found. Expected spaPath=/mitglied-werden or slug=mitglied-werden/membership.')
|
||||
}
|
||||
|
||||
const heroImage = await mediaByFilename(payload, mitgliedWerdenContent.hero.imageFilename)
|
||||
const formImage = await mediaByFilename(payload, mitgliedWerdenContent.formIntro.imageFilename)
|
||||
const applicationItems = await Promise.all(
|
||||
mitgliedWerdenContent.applicationOptions.items.map(async (item) => {
|
||||
const { fileFilename, ...rest } = item
|
||||
return {
|
||||
...rest,
|
||||
file: fileFilename ? await mediaByFilename(payload, fileFilename) : undefined,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const { imageFilename: _heroFilename, ...heroContent } = mitgliedWerdenContent.hero
|
||||
const { imageFilename: _formFilename, ...formIntroContent } = mitgliedWerdenContent.formIntro
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: page.id,
|
||||
overrideAccess: true,
|
||||
context: { disableRevalidate: true },
|
||||
data: {
|
||||
mitgliedWerden: {
|
||||
...mitgliedWerdenContent,
|
||||
hero: {
|
||||
...heroContent,
|
||||
image: heroImage,
|
||||
},
|
||||
applicationOptions: {
|
||||
...mitgliedWerdenContent.applicationOptions,
|
||||
items: applicationItems,
|
||||
},
|
||||
formIntro: {
|
||||
...formIntroContent,
|
||||
image: formImage,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Mitglied werden CMS fields for page ${page.id}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useIsMobile } from "@/spa/hooks/useIsMobile";
|
||||
import { mitgliedWerdenContent } from "@/spa/mitgliedWerdenContent";
|
||||
|
||||
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -7,25 +8,7 @@ const NAVY = "#111D55";
|
||||
const GOLD = "#EFBF04";
|
||||
const WHITE = "#fff";
|
||||
|
||||
const TIERS = [
|
||||
{ max: 10, label: "bis 10", annual: 480 },
|
||||
{ max: 20, label: "bis 20", annual: 600 },
|
||||
{ max: 50, label: "bis 50", annual: 900 },
|
||||
{ max: 100, label: "bis 100", annual: 1200 },
|
||||
{ max: 250, label: "bis 250", annual: 2400 },
|
||||
{ max: 1000, label: "bis 1.000", annual: 3600 },
|
||||
];
|
||||
|
||||
const VAT = 0.19;
|
||||
const ADMISSION = 500;
|
||||
|
||||
const STEPS = [
|
||||
{ id: 1, label: "Unternehmen" },
|
||||
{ id: 2, label: "Kontakt" },
|
||||
{ id: 3, label: "Details" },
|
||||
{ id: 4, label: "Zahlung" },
|
||||
{ id: 5, label: "Abschluss" },
|
||||
];
|
||||
const DEFAULT_WIZARD = mitgliedWerdenContent.wizard;
|
||||
|
||||
// ─── TYPES ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -59,6 +42,44 @@ interface FormData {
|
||||
}
|
||||
|
||||
type Errors = Partial<Record<keyof FormData, string>>;
|
||||
type WizardCopy = typeof mitgliedWerdenContent.wizard;
|
||||
type PricingCopy = typeof mitgliedWerdenContent.pricing;
|
||||
type WizardTier = WizardCopy["tiers"][number];
|
||||
|
||||
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;
|
||||
const optionsFromStrings = (items: string[]) => items.map((v) => ({ value: v, label: v }));
|
||||
|
||||
function mergeWizardCopy(content?: Partial<WizardCopy>, pricing?: Partial<PricingCopy>): WizardCopy {
|
||||
return {
|
||||
...DEFAULT_WIZARD,
|
||||
...content,
|
||||
nav: { ...DEFAULT_WIZARD.nav, ...content?.nav },
|
||||
validation: { ...DEFAULT_WIZARD.validation, ...content?.validation },
|
||||
pricingSummary: { ...DEFAULT_WIZARD.pricingSummary, ...content?.pricingSummary },
|
||||
fields: {
|
||||
company: { ...DEFAULT_WIZARD.fields.company, ...content?.fields?.company },
|
||||
contact: { ...DEFAULT_WIZARD.fields.contact, ...content?.fields?.contact },
|
||||
details: { ...DEFAULT_WIZARD.fields.details, ...content?.fields?.details },
|
||||
payment: { ...DEFAULT_WIZARD.fields.payment, ...content?.fields?.payment },
|
||||
summary: {
|
||||
...DEFAULT_WIZARD.fields.summary,
|
||||
...content?.fields?.summary,
|
||||
labels: { ...DEFAULT_WIZARD.fields.summary.labels, ...content?.fields?.summary?.labels },
|
||||
consents: { ...DEFAULT_WIZARD.fields.summary.consents, ...content?.fields?.summary?.consents },
|
||||
},
|
||||
},
|
||||
success: {
|
||||
...DEFAULT_WIZARD.success,
|
||||
...content?.success,
|
||||
steps: fallbackArray(content?.success?.steps, DEFAULT_WIZARD.success.steps),
|
||||
},
|
||||
steps: fallbackArray(content?.steps, DEFAULT_WIZARD.steps),
|
||||
tiers: fallbackArray(pricing?.tiers || content?.tiers, DEFAULT_WIZARD.tiers),
|
||||
vatRate: fallbackNumber(pricing?.vatRate ?? content?.vatRate, DEFAULT_WIZARD.vatRate),
|
||||
admissionFeeNet: fallbackNumber(pricing?.admissionFeeNet ?? content?.admissionFeeNet, DEFAULT_WIZARD.admissionFeeNet),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── HELPERS ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -72,11 +93,11 @@ function formatEur(n: number): string {
|
||||
return n.toLocaleString("de-DE", { style: "currency", currency: "EUR" });
|
||||
}
|
||||
|
||||
function calcBeitrag(mitarbeiter: string) {
|
||||
const tier = TIERS.find((t) => t.label === mitarbeiter) ?? TIERS[0];
|
||||
function calcBeitrag(mitarbeiter: string, tiers: WizardTier[], vatRate: number, admissionFeeNet: number) {
|
||||
const tier = tiers.find((t) => t.label === mitarbeiter) ?? tiers[0];
|
||||
const annualNet = tier.annual;
|
||||
const annualGross = annualNet * (1 + VAT);
|
||||
const admissionGross = ADMISSION * (1 + 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,39 +126,40 @@ function maskIBAN(iban: string): string {
|
||||
return clean.slice(0, 4) + " **** **** **** " + clean.slice(-4);
|
||||
}
|
||||
|
||||
function validateStep(step: number, data: FormData): Errors {
|
||||
function validateStep(step: number, data: FormData, copy: WizardCopy): Errors {
|
||||
const e: Errors = {};
|
||||
const validation = copy.validation;
|
||||
if (step === 1) {
|
||||
if (!data.firmenname.trim()) e.firmenname = "Pflichtfeld";
|
||||
if (!data.rechtsform) e.rechtsform = "Bitte wählen";
|
||||
if (!data.strasse.trim()) e.strasse = "Pflichtfeld";
|
||||
if (!/^\d{5}$/.test(data.plz)) e.plz = "5-stellige PLZ erforderlich";
|
||||
if (!data.ort.trim()) e.ort = "Pflichtfeld";
|
||||
if (!data.mitarbeiter) e.mitarbeiter = "Bitte wählen";
|
||||
if (!data.firmenname.trim()) e.firmenname = validation.required;
|
||||
if (!data.rechtsform) e.rechtsform = validation.select;
|
||||
if (!data.strasse.trim()) e.strasse = validation.required;
|
||||
if (!/^\d{5}$/.test(data.plz)) e.plz = validation.postalCode;
|
||||
if (!data.ort.trim()) e.ort = validation.required;
|
||||
if (!data.mitarbeiter) e.mitarbeiter = validation.select;
|
||||
}
|
||||
if (step === 2) {
|
||||
if (!data.anrede) e.anrede = "Bitte wählen";
|
||||
if (!data.vorname.trim()) e.vorname = "Pflichtfeld";
|
||||
if (!data.nachname.trim()) e.nachname = "Pflichtfeld";
|
||||
if (!data.position.trim()) e.position = "Pflichtfeld";
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = "Gültige E-Mail erforderlich";
|
||||
if (!data.telefon.trim()) e.telefon = "Pflichtfeld";
|
||||
if (!data.anrede) e.anrede = validation.select;
|
||||
if (!data.vorname.trim()) e.vorname = validation.required;
|
||||
if (!data.nachname.trim()) e.nachname = validation.required;
|
||||
if (!data.position.trim()) e.position = validation.required;
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = validation.email;
|
||||
if (!data.telefon.trim()) e.telefon = validation.required;
|
||||
}
|
||||
if (step === 3) {
|
||||
if (!data.eintrittsdatum) e.eintrittsdatum = "Pflichtfeld";
|
||||
if (!data.aufmerksam) e.aufmerksam = "Bitte wählen";
|
||||
if (!data.eintrittsdatum) e.eintrittsdatum = validation.required;
|
||||
if (!data.aufmerksam) e.aufmerksam = validation.select;
|
||||
}
|
||||
if (step === 4) {
|
||||
if (!data.kontoinhaber.trim()) e.kontoinhaber = "Pflichtfeld";
|
||||
if (!validateIBAN(data.iban)) e.iban = "Ungültige IBAN (DE, 22 Zeichen)";
|
||||
if (!data.bic.trim()) e.bic = "Pflichtfeld";
|
||||
if (!data.bank.trim()) e.bank = "Pflichtfeld";
|
||||
if (!data.sepaMandat) e.sepaMandat = "Bitte SEPA-Mandat bestätigen";
|
||||
if (!data.kontoinhaber.trim()) e.kontoinhaber = validation.required;
|
||||
if (!validateIBAN(data.iban)) e.iban = validation.iban;
|
||||
if (!data.bic.trim()) e.bic = validation.required;
|
||||
if (!data.bank.trim()) e.bank = validation.required;
|
||||
if (!data.sepaMandat) e.sepaMandat = validation.sepa;
|
||||
}
|
||||
if (step === 5) {
|
||||
if (!data.datenschutz) e.datenschutz = "Pflichtfeld";
|
||||
if (!data.satzung) e.satzung = "Pflichtfeld";
|
||||
if (!data.widerruf) e.widerruf = "Pflichtfeld";
|
||||
if (!data.datenschutz) e.datenschutz = validation.required;
|
||||
if (!data.satzung) e.satzung = validation.required;
|
||||
if (!data.widerruf) e.widerruf = validation.required;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
@@ -176,9 +198,10 @@ const fieldBase: React.CSSProperties = {
|
||||
};
|
||||
|
||||
function Field({
|
||||
label, required, error, children,
|
||||
label, required, error, children, requiredSuffix = DEFAULT_WIZARD.requiredSuffix,
|
||||
}: {
|
||||
label: string; required?: boolean; error?: string; children: React.ReactNode;
|
||||
requiredSuffix?: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
@@ -192,7 +215,7 @@ function Field({
|
||||
marginBottom: 7,
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
{label}{required && <span style={{ color: GOLD, marginLeft: 3 }}>*</span>}
|
||||
{label}{required && <span style={{ color: GOLD, marginLeft: 3 }}>{requiredSuffix}</span>}
|
||||
</label>
|
||||
{children}
|
||||
<FieldError msg={error} />
|
||||
@@ -306,10 +329,11 @@ function Textarea({
|
||||
}
|
||||
|
||||
function Checkbox({
|
||||
checked, onChange, label, error, optional,
|
||||
checked, onChange, label, error, optional, optionalSuffix = DEFAULT_WIZARD.optionalSuffix,
|
||||
}: {
|
||||
checked: boolean; onChange: (v: boolean) => void; label: React.ReactNode;
|
||||
error?: string; optional?: boolean;
|
||||
optionalSuffix?: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
@@ -349,7 +373,7 @@ function Checkbox({
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
{label}
|
||||
{optional && <span style={{ color: "rgba(255,255,255,0.35)", marginLeft: 6, fontSize: 11 }}>(optional)</span>}
|
||||
{optional && <span style={{ color: "rgba(255,255,255,0.35)", marginLeft: 6, fontSize: 11 }}>{optionalSuffix}</span>}
|
||||
</span>
|
||||
</label>
|
||||
{error && <FieldError msg={error} />}
|
||||
@@ -406,7 +430,7 @@ function Accordion({ title, children }: { title: string; children: React.ReactNo
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewRow({ label, value }: { label: string; value: string }) {
|
||||
function ReviewRow({ label, value, fallback = DEFAULT_WIZARD.reviewFallback }: { label: string; value: string; fallback?: string }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
@@ -418,7 +442,7 @@ function ReviewRow({ label, value }: { label: string; value: string }) {
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ color: WHITE, fontFamily: '"IBM Plex Sans", sans-serif', fontWeight: 500 }}>
|
||||
{value || "–"}
|
||||
{value || fallback}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -426,7 +450,7 @@ function ReviewRow({ label, value }: { label: string; value: string }) {
|
||||
|
||||
// ─── PROGRESS BAR ─────────────────────────────────────────────────────────────
|
||||
|
||||
function ProgressBar({ current, total }: { current: number; total: number }) {
|
||||
function ProgressBar({ current, total, steps }: { current: number; total: number; steps: WizardCopy["steps"] }) {
|
||||
const isMobile = useIsMobile();
|
||||
return (
|
||||
<div style={{ marginBottom: isMobile ? 24 : 36 }}>
|
||||
@@ -468,7 +492,7 @@ function ProgressBar({ current, total }: { current: number; total: number }) {
|
||||
}} />
|
||||
|
||||
{/* Dots */}
|
||||
{STEPS.map((step) => {
|
||||
{steps.map((step) => {
|
||||
const done = step.id < current;
|
||||
const active = step.id === current;
|
||||
return (
|
||||
@@ -523,11 +547,11 @@ function ProgressBar({ current, total }: { current: number; total: number }) {
|
||||
color: GOLD,
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
{STEPS.find((s) => s.id === current)?.label} · {current}/{total}
|
||||
{steps.find((s) => s.id === current)?.label} · {current}/{total}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex" }}>
|
||||
{STEPS.map((step) => {
|
||||
{steps.map((step) => {
|
||||
const done = step.id < current;
|
||||
const active = step.id === current;
|
||||
return (
|
||||
@@ -554,7 +578,7 @@ function ProgressBar({ current, total }: { current: number; total: number }) {
|
||||
|
||||
// ─── STEP HEADING ─────────────────────────────────────────────────────────────
|
||||
|
||||
function StepHeading({ step, title, subtitle }: { step: number; title: string; subtitle?: string }) {
|
||||
function StepHeading({ step, total, title, subtitle, template }: { step: number; total: number; title: string; subtitle?: string; template: string }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{
|
||||
@@ -577,7 +601,7 @@ function StepHeading({ step, title, subtitle }: { step: number; title: string; s
|
||||
color: GOLD,
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
Schritt {step} von 5
|
||||
{template.replace('{step}', String(step)).replace('{total}', String(total))}
|
||||
</span>
|
||||
</div>
|
||||
<h3 style={{
|
||||
@@ -607,12 +631,13 @@ function StepHeading({ step, title, subtitle }: { step: number; title: string; s
|
||||
// ─── NAV BUTTONS ─────────────────────────────────────────────────────────────
|
||||
|
||||
function NavButtons({
|
||||
step, onBack, onNext, onSubmit, disabled,
|
||||
step, total, onBack, onNext, onSubmit, disabled, copy,
|
||||
}: {
|
||||
step: number; onBack: () => void; onNext: () => void; onSubmit: () => void; disabled?: boolean;
|
||||
total: number; copy: WizardCopy;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const isLast = step === 5;
|
||||
const isLast = step === total;
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
@@ -650,7 +675,7 @@ function NavButtons({
|
||||
(e.target as HTMLButtonElement).style.color = "rgba(255,255,255,0.6)";
|
||||
}}
|
||||
>
|
||||
← Zurück
|
||||
{copy.nav.backLabel}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -683,7 +708,7 @@ function NavButtons({
|
||||
if (!disabled) (e.target as HTMLButtonElement).style.background = GOLD;
|
||||
}}
|
||||
>
|
||||
{isLast ? "Mitgliedschaftsantrag verbindlich einreichen" : "Weiter →"}
|
||||
{isLast ? copy.nav.submitLabel : copy.nav.nextLabel}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -691,48 +716,46 @@ function NavButtons({
|
||||
|
||||
// ─── STEP 1 – UNTERNEHMEN ─────────────────────────────────────────────────────
|
||||
|
||||
function Step1({ data, onChange, errors }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors;
|
||||
function Step1({ data, onChange, errors, copy }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors; copy: WizardCopy;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const rechtsformen = [
|
||||
"GmbH","GmbH & Co. KG","AG","UG","KG","OHG","GbR",
|
||||
"Einzelunternehmen","e.K.","Sonstige",
|
||||
].map((v) => ({ value: v, label: v }));
|
||||
const fields = copy.fields.company;
|
||||
const rechtsformen = optionsFromStrings(fallbackArray(fields.legalForms, DEFAULT_WIZARD.fields.company.legalForms));
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading step={1} title="Ihr Unternehmen" subtitle="Angaben zum antragstellenden Unternehmen" />
|
||||
<StepHeading step={1} total={copy.steps.length} title={fields.title} subtitle={fields.subtitle} template={copy.stepCounterTemplate} />
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "0 16px" }}>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="Firmenname" required error={errors.firmenname}>
|
||||
<Input value={data.firmenname} onChange={(v) => onChange("firmenname", v)} placeholder="Muster GmbH" error={errors.firmenname} />
|
||||
<Field label={fields.companyNameLabel} required error={errors.firmenname} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.firmenname} onChange={(v) => onChange("firmenname", v)} placeholder={fields.companyNamePlaceholder} error={errors.firmenname} />
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="Rechtsform" required error={errors.rechtsform}>
|
||||
<Field label={fields.legalFormLabel} required error={errors.rechtsform} requiredSuffix={copy.requiredSuffix}>
|
||||
<Select value={data.rechtsform} onChange={(v) => onChange("rechtsform", v)}
|
||||
options={rechtsformen} placeholder="Bitte wählen" error={errors.rechtsform} />
|
||||
options={rechtsformen} placeholder={copy.validation.select} error={errors.rechtsform} />
|
||||
</Field>
|
||||
<Field label="Anzahl Mitarbeiter" required error={errors.mitarbeiter}>
|
||||
<Field label={fields.employeesLabel} required error={errors.mitarbeiter} requiredSuffix={copy.requiredSuffix}>
|
||||
<Select value={data.mitarbeiter} onChange={(v) => onChange("mitarbeiter", v)}
|
||||
options={TIERS.map((t) => ({ value: t.label, label: t.label }))}
|
||||
placeholder="Bitte wählen" error={errors.mitarbeiter} />
|
||||
options={copy.tiers.map((t) => ({ value: t.label, label: t.label }))}
|
||||
placeholder={copy.validation.select} error={errors.mitarbeiter} />
|
||||
</Field>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="Straße & Hausnummer" required error={errors.strasse}>
|
||||
<Input value={data.strasse} onChange={(v) => onChange("strasse", v)} placeholder="Musterstraße 42" error={errors.strasse} />
|
||||
<Field label={fields.streetLabel} required error={errors.strasse} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.strasse} onChange={(v) => onChange("strasse", v)} placeholder={fields.streetPlaceholder} error={errors.strasse} />
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="PLZ" required error={errors.plz}>
|
||||
<Input value={data.plz} onChange={(v) => onChange("plz", v)} placeholder="12345" maxLength={5} error={errors.plz} />
|
||||
<Field label={fields.postalCodeLabel} required error={errors.plz} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.plz} onChange={(v) => onChange("plz", v)} placeholder={fields.postalCodePlaceholder} maxLength={5} error={errors.plz} />
|
||||
</Field>
|
||||
<Field label="Ort" required error={errors.ort}>
|
||||
<Input value={data.ort} onChange={(v) => onChange("ort", v)} placeholder="Berlin" error={errors.ort} />
|
||||
<Field label={fields.cityLabel} required error={errors.ort} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.ort} onChange={(v) => onChange("ort", v)} placeholder={fields.cityPlaceholder} error={errors.ort} />
|
||||
</Field>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="Website">
|
||||
<Input value={data.website} onChange={(v) => onChange("website", v)} placeholder="https://www.ihrewebsite.de" type="url" />
|
||||
<Field label={fields.websiteLabel} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.website} onChange={(v) => onChange("website", v)} placeholder={fields.websitePlaceholder} type="url" />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -742,40 +765,41 @@ function Step1({ data, onChange, errors }: {
|
||||
|
||||
// ─── STEP 2 – KONTAKT ─────────────────────────────────────────────────────────
|
||||
|
||||
function Step2({ data, onChange, errors }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors;
|
||||
function Step2({ data, onChange, errors, copy }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors; copy: WizardCopy;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const fields = copy.fields.contact;
|
||||
return (
|
||||
<>
|
||||
<StepHeading step={2} title="Kontaktperson" subtitle="Ansprechpartner für die Mitgliedschaft" />
|
||||
<StepHeading step={2} total={copy.steps.length} title={fields.title} subtitle={fields.subtitle} template={copy.stepCounterTemplate} />
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "0 16px" }}>
|
||||
<Field label="Anrede" required error={errors.anrede}>
|
||||
<Field label={fields.salutationLabel} required error={errors.anrede} requiredSuffix={copy.requiredSuffix}>
|
||||
<Select value={data.anrede} onChange={(v) => onChange("anrede", v)}
|
||||
options={[{value:"Herr",label:"Herr"},{value:"Frau",label:"Frau"},{value:"Divers",label:"Divers"}]}
|
||||
placeholder="Bitte wählen" error={errors.anrede} />
|
||||
options={optionsFromStrings(fallbackArray(fields.salutations, DEFAULT_WIZARD.fields.contact.salutations))}
|
||||
placeholder={copy.validation.select} error={errors.anrede} />
|
||||
</Field>
|
||||
{!isMobile && <div />} {/* spacer (desktop only) */}
|
||||
<Field label="Vorname" required error={errors.vorname}>
|
||||
<Input value={data.vorname} onChange={(v) => onChange("vorname", v)} placeholder="Max" error={errors.vorname} />
|
||||
<Field label={fields.firstNameLabel} required error={errors.vorname} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.vorname} onChange={(v) => onChange("vorname", v)} placeholder={fields.firstNamePlaceholder} error={errors.vorname} />
|
||||
</Field>
|
||||
<Field label="Nachname" required error={errors.nachname}>
|
||||
<Input value={data.nachname} onChange={(v) => onChange("nachname", v)} placeholder="Mustermann" error={errors.nachname} />
|
||||
<Field label={fields.lastNameLabel} required error={errors.nachname} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.nachname} onChange={(v) => onChange("nachname", v)} placeholder={fields.lastNamePlaceholder} error={errors.nachname} />
|
||||
</Field>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="Position / Funktion" required error={errors.position}>
|
||||
<Input value={data.position} onChange={(v) => onChange("position", v)} placeholder="z. B. Geschäftsführer" error={errors.position} />
|
||||
<Field label={fields.positionLabel} required error={errors.position} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.position} onChange={(v) => onChange("position", v)} placeholder={fields.positionPlaceholder} error={errors.position} />
|
||||
</Field>
|
||||
</div>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="E-Mail-Adresse" required error={errors.email}>
|
||||
<Input value={data.email} onChange={(v) => onChange("email", v)} placeholder="max@muster.de" type="email" error={errors.email} />
|
||||
<Field label={fields.emailLabel} required error={errors.email} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.email} onChange={(v) => onChange("email", v)} placeholder={fields.emailPlaceholder} type="email" error={errors.email} />
|
||||
</Field>
|
||||
</div>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="Telefon" required error={errors.telefon}>
|
||||
<Input value={data.telefon} onChange={(v) => onChange("telefon", v)} placeholder="+49 30 123456" type="tel" error={errors.telefon} />
|
||||
<Field label={fields.phoneLabel} required error={errors.telefon} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.telefon} onChange={(v) => onChange("telefon", v)} placeholder={fields.phonePlaceholder} type="tel" error={errors.telefon} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -785,14 +809,16 @@ function Step2({ data, onChange, errors }: {
|
||||
|
||||
// ─── STEP 3 – DETAILS ─────────────────────────────────────────────────────────
|
||||
|
||||
function Step3({ data, onChange, errors }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors;
|
||||
function Step3({ data, onChange, errors, copy }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors; copy: WizardCopy;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const beitrag = calcBeitrag(data.mitarbeiter);
|
||||
const fields = copy.fields.details;
|
||||
const summary = copy.pricingSummary;
|
||||
const beitrag = calcBeitrag(data.mitarbeiter, copy.tiers, copy.vatRate, copy.admissionFeeNet);
|
||||
return (
|
||||
<>
|
||||
<StepHeading step={3} title="Mitgliedschaft & Details" subtitle="Beitragsübersicht und Eintrittsdatum" />
|
||||
<StepHeading step={3} total={copy.steps.length} title={fields.title} subtitle={fields.subtitle} template={copy.stepCounterTemplate} />
|
||||
|
||||
{/* Contribution card */}
|
||||
{data.mitarbeiter && (
|
||||
@@ -808,12 +834,12 @@ function Step3({ data, onChange, errors }: {
|
||||
textTransform: "uppercase", color: GOLD, marginBottom: 12,
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
Beitragsübersicht
|
||||
{summary.eyebrow}
|
||||
</div>
|
||||
{[
|
||||
["Jahresbeitrag (netto)", formatEur(beitrag.annualNet)],
|
||||
["Jahresbeitrag (brutto)", formatEur(beitrag.annualGross)],
|
||||
["Aufnahmegebühr (brutto)", formatEur(beitrag.admissionGross)],
|
||||
[summary.annualNetLabel, formatEur(beitrag.annualNet)],
|
||||
[summary.annualGrossLabel, formatEur(beitrag.annualGross)],
|
||||
[summary.admissionGrossLabel, formatEur(beitrag.admissionGross)],
|
||||
].map(([l, v]) => (
|
||||
<div key={l} style={{ display: "flex", justifyContent: "space-between", marginBottom: 8, fontSize: 13 }}>
|
||||
<span style={{ color: "rgba(255,255,255,0.5)", fontFamily: '"IBM Plex Sans", sans-serif' }}>{l}</span>
|
||||
@@ -825,7 +851,7 @@ function Step3({ data, onChange, errors }: {
|
||||
}} />
|
||||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 14 }}>
|
||||
<span style={{ color: GOLD, fontWeight: 600, fontFamily: '"IBM Plex Sans", sans-serif' }}>
|
||||
Gesamt Jahr 1 (anteilig)
|
||||
{summary.totalYearOneLabel}
|
||||
</span>
|
||||
<span style={{ color: GOLD, fontWeight: 700, fontFamily: '"IBM Plex Sans", sans-serif' }}>
|
||||
{formatEur(beitrag.anteilig)}
|
||||
@@ -835,19 +861,18 @@ function Step3({ data, onChange, errors }: {
|
||||
)}
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "0 16px" }}>
|
||||
<Field label="Gewünschtes Eintrittsdatum" required error={errors.eintrittsdatum}>
|
||||
<Field label={fields.entryDateLabel} required error={errors.eintrittsdatum} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.eintrittsdatum} onChange={(v) => onChange("eintrittsdatum", v)} type="date" error={errors.eintrittsdatum} />
|
||||
</Field>
|
||||
<Field label="Wie wurden Sie aufmerksam?" required error={errors.aufmerksam}>
|
||||
<Field label={fields.referralLabel} required error={errors.aufmerksam} requiredSuffix={copy.requiredSuffix}>
|
||||
<Select value={data.aufmerksam} onChange={(v) => onChange("aufmerksam", v)}
|
||||
options={["Empfehlung","Veranstaltung","Google","Social Media","Presse","Sonstiges"]
|
||||
.map((v) => ({ value: v, label: v }))}
|
||||
placeholder="Bitte wählen" error={errors.aufmerksam} />
|
||||
options={optionsFromStrings(fallbackArray(fields.referralOptions, DEFAULT_WIZARD.fields.details.referralOptions))}
|
||||
placeholder={copy.validation.select} error={errors.aufmerksam} />
|
||||
</Field>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="Ihre Motivation (optional)">
|
||||
<Field label={fields.motivationLabel} requiredSuffix={copy.requiredSuffix}>
|
||||
<Textarea value={data.motivation} onChange={(v) => onChange("motivation", v)}
|
||||
placeholder="Was erhoffen Sie sich von der Mitgliedschaft beim BMP e.V.?" maxLength={500} />
|
||||
placeholder={fields.motivationPlaceholder} maxLength={fields.motivationMaxLength} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -857,15 +882,16 @@ function Step3({ data, onChange, errors }: {
|
||||
|
||||
// ─── STEP 4 – SEPA ────────────────────────────────────────────────────────────
|
||||
|
||||
function Step4({ data, onChange, errors }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors;
|
||||
function Step4({ data, onChange, errors, copy }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors; copy: WizardCopy;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const ibanIsValid = validateIBAN(data.iban);
|
||||
const fields = copy.fields.payment;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading step={4} title="SEPA-Lastschriftmandat" subtitle="Bankverbindung für den Beitragseinzug" />
|
||||
<StepHeading step={4} total={copy.steps.length} title={fields.title} subtitle={fields.subtitle} template={copy.stepCounterTemplate} />
|
||||
|
||||
{/* Mandate text */}
|
||||
<div style={{
|
||||
@@ -881,45 +907,46 @@ function Step4({ data, onChange, errors }: {
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
Ich ermächtige den BMP e.V., Zahlungen von meinem Konto mittels Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die vom BMP e.V. auf mein Konto gezogenen Lastschriften einzulösen. Hinweis: Ich kann innerhalb von acht Wochen, beginnend mit dem Belastungsdatum, die Erstattung des belasteten Betrages verlangen. Es gelten dabei die mit meinem Kreditinstitut vereinbarten Bedingungen. Die Mandatsreferenz wird separat mitgeteilt. Gläubiger-Identifikationsnummer: DE98ZZZ09999999999.
|
||||
{fields.mandateText}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "0 16px" }}>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="Kontoinhaber" required error={errors.kontoinhaber}>
|
||||
<Input value={data.kontoinhaber} onChange={(v) => onChange("kontoinhaber", v)} placeholder="Max Mustermann" error={errors.kontoinhaber} />
|
||||
<Field label={fields.accountHolderLabel} required error={errors.kontoinhaber} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.kontoinhaber} onChange={(v) => onChange("kontoinhaber", v)} placeholder={fields.accountHolderPlaceholder} error={errors.kontoinhaber} />
|
||||
</Field>
|
||||
</div>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<Field label="IBAN" required error={errors.iban}>
|
||||
<Field label={fields.ibanLabel} required error={errors.iban} requiredSuffix={copy.requiredSuffix}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Input value={data.iban} onChange={(v) => onChange("iban", v.toUpperCase())}
|
||||
placeholder="DE00 0000 0000 0000 0000 00" error={errors.iban} />
|
||||
placeholder={fields.ibanPlaceholder} error={errors.iban} />
|
||||
{data.iban.replace(/\s/g,"").length > 4 && (
|
||||
<div style={{
|
||||
position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)",
|
||||
fontSize: 11, fontWeight: 700,
|
||||
color: ibanIsValid ? "#4ade80" : "rgba(239,191,4,0.6)",
|
||||
}}>
|
||||
{ibanIsValid ? "✓ OK" : "…"}
|
||||
{ibanIsValid ? fields.ibanValidLabel : fields.ibanPendingLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="BIC" required error={errors.bic}>
|
||||
<Field label={fields.bicLabel} required error={errors.bic} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.bic} onChange={(v) => onChange("bic", v.toUpperCase())}
|
||||
placeholder={ibanIsValid ? "Wird ausgefüllt" : "BELADEBEXXX"} error={errors.bic} />
|
||||
placeholder={ibanIsValid ? fields.bicAutoPlaceholder : fields.bicPlaceholder} error={errors.bic} />
|
||||
</Field>
|
||||
<Field label="Bank / Kreditinstitut" required error={errors.bank}>
|
||||
<Input value={data.bank} onChange={(v) => onChange("bank", v)} placeholder="Berliner Sparkasse" error={errors.bank} />
|
||||
<Field label={fields.bankLabel} required error={errors.bank} requiredSuffix={copy.requiredSuffix}>
|
||||
<Input value={data.bank} onChange={(v) => onChange("bank", v)} placeholder={fields.bankPlaceholder} error={errors.bank} />
|
||||
</Field>
|
||||
<div style={{ gridColumn: "1 / -1", marginTop: 4 }}>
|
||||
<Checkbox
|
||||
checked={data.sepaMandat}
|
||||
onChange={(v) => onChange("sepaMandat", v)}
|
||||
label="Ich bestätige das SEPA-Lastschriftmandat und ermächtige den BMP e.V. zum Einzug."
|
||||
label={fields.sepaCheckboxLabel}
|
||||
error={errors.sepaMandat}
|
||||
optionalSuffix={copy.optionalSuffix}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -929,35 +956,38 @@ function Step4({ data, onChange, errors }: {
|
||||
|
||||
// ─── STEP 5 – SUMMARY + CONSENT ───────────────────────────────────────────────
|
||||
|
||||
function Step5({ data, onChange, errors }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors;
|
||||
function Step5({ data, onChange, errors, copy }: {
|
||||
data: FormData; onChange: (k: keyof FormData, v: string | boolean) => void; errors: Errors; copy: WizardCopy;
|
||||
}) {
|
||||
const fields = copy.fields.summary;
|
||||
const labels = fields.labels;
|
||||
const consents = fields.consents;
|
||||
return (
|
||||
<>
|
||||
<StepHeading step={5} title="Zusammenfassung & Abschluss" subtitle="Bitte prüfen Sie Ihre Angaben" />
|
||||
<StepHeading step={5} total={copy.steps.length} title={fields.title} subtitle={fields.subtitle} template={copy.stepCounterTemplate} />
|
||||
|
||||
<Accordion title="Unternehmen">
|
||||
<ReviewRow label="Firmenname" value={data.firmenname} />
|
||||
<ReviewRow label="Rechtsform" value={data.rechtsform} />
|
||||
<ReviewRow label="Adresse" value={`${data.strasse}, ${data.plz} ${data.ort}`} />
|
||||
<ReviewRow label="Mitarbeiter" value={data.mitarbeiter} />
|
||||
<ReviewRow label="Website" value={data.website || "–"} />
|
||||
<Accordion title={fields.companyAccordion}>
|
||||
<ReviewRow label={labels.companyName} value={data.firmenname} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.legalForm} value={data.rechtsform} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.address} value={`${data.strasse}, ${data.plz} ${data.ort}`} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.employees} value={data.mitarbeiter} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.website} value={data.website || copy.reviewFallback} fallback={copy.reviewFallback} />
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Kontaktperson">
|
||||
<ReviewRow label="Name" value={`${data.anrede} ${data.vorname} ${data.nachname}`} />
|
||||
<ReviewRow label="Position" value={data.position} />
|
||||
<ReviewRow label="E-Mail" value={data.email} />
|
||||
<ReviewRow label="Telefon" value={data.telefon} />
|
||||
<Accordion title={fields.contactAccordion}>
|
||||
<ReviewRow label={labels.name} value={`${data.anrede} ${data.vorname} ${data.nachname}`} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.position} value={data.position} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.email} value={data.email} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.phone} value={data.telefon} fallback={copy.reviewFallback} />
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Zahlung">
|
||||
<ReviewRow label="Kontoinhaber" value={data.kontoinhaber} />
|
||||
<ReviewRow label="IBAN" value={maskIBAN(data.iban)} />
|
||||
<ReviewRow label="BIC" value={data.bic} />
|
||||
<ReviewRow label="Bank" value={data.bank} />
|
||||
<ReviewRow label="Eintrittsdatum" value={data.eintrittsdatum} />
|
||||
<ReviewRow label="Jahresbeitrag" value={formatEur(calcBeitrag(data.mitarbeiter).annualGross)} />
|
||||
<Accordion title={fields.paymentAccordion}>
|
||||
<ReviewRow label={labels.accountHolder} value={data.kontoinhaber} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.iban} value={maskIBAN(data.iban)} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.bic} value={data.bic} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.bank} value={data.bank} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.entryDate} value={data.eintrittsdatum} fallback={copy.reviewFallback} />
|
||||
<ReviewRow label={labels.annualContribution} value={formatEur(calcBeitrag(data.mitarbeiter, copy.tiers, copy.vatRate, copy.admissionFeeNet).annualGross)} fallback={copy.reviewFallback} />
|
||||
</Accordion>
|
||||
|
||||
<div style={{ marginTop: 20, marginBottom: 4 }}>
|
||||
@@ -966,17 +996,17 @@ function Step5({ data, onChange, errors }: {
|
||||
textTransform: "uppercase", color: "rgba(255,255,255,0.4)",
|
||||
marginBottom: 14, fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
Pflichtbestätigungen
|
||||
{fields.requiredTitle}
|
||||
</div>
|
||||
<Checkbox checked={data.satzung} onChange={(v) => onChange("satzung", v)}
|
||||
label="Ich habe die Satzung des BMP e.V. gelesen und erkenne sie an."
|
||||
error={errors.satzung} />
|
||||
label={consents.satzung}
|
||||
error={errors.satzung} optionalSuffix={copy.optionalSuffix} />
|
||||
<Checkbox checked={data.datenschutz} onChange={(v) => onChange("datenschutz", v)}
|
||||
label="Ich habe die Datenschutzerklärung gelesen und stimme zu."
|
||||
error={errors.datenschutz} />
|
||||
label={consents.datenschutz}
|
||||
error={errors.datenschutz} optionalSuffix={copy.optionalSuffix} />
|
||||
<Checkbox checked={data.widerruf} onChange={(v) => onChange("widerruf", v)}
|
||||
label="Ich habe die Kündigungsfrist zur Kenntnis genommen (1 Jahr zum Jahresende)."
|
||||
error={errors.widerruf} />
|
||||
label={consents.widerruf}
|
||||
error={errors.widerruf} optionalSuffix={copy.optionalSuffix} />
|
||||
|
||||
<div style={{
|
||||
height: 1, background: "rgba(255,255,255,0.08)",
|
||||
@@ -987,14 +1017,14 @@ function Step5({ data, onChange, errors }: {
|
||||
textTransform: "uppercase", color: "rgba(255,255,255,0.4)",
|
||||
marginBottom: 14, fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
Optional
|
||||
{fields.optionalTitle}
|
||||
</div>
|
||||
<Checkbox checked={data.newsletter} onChange={(v) => onChange("newsletter", v)}
|
||||
label="Ich möchte elektronische Informationen, Einladungen und den Newsletter erhalten."
|
||||
optional />
|
||||
label={consents.newsletter}
|
||||
optional optionalSuffix={copy.optionalSuffix} />
|
||||
<Checkbox checked={data.websitePub} onChange={(v) => onChange("websitePub", v)}
|
||||
label="Meinen Firmennamen auf der Website des BMP e.V. als Mitglied veröffentlichen."
|
||||
optional />
|
||||
label={consents.websitePub}
|
||||
optional optionalSuffix={copy.optionalSuffix} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -1002,7 +1032,8 @@ function Step5({ data, onChange, errors }: {
|
||||
|
||||
// ─── SUCCESS SCREEN ───────────────────────────────────────────────────────────
|
||||
|
||||
function SuccessScreen() {
|
||||
function SuccessScreen({ copy }: { copy: WizardCopy }) {
|
||||
const success = copy.success;
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
@@ -1046,7 +1077,7 @@ function SuccessScreen() {
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
letterSpacing: "-0.02em",
|
||||
}}>
|
||||
Ihr Antrag ist eingegangen.
|
||||
{success.heading}
|
||||
</h3>
|
||||
<p style={{
|
||||
margin: "0 0 28px",
|
||||
@@ -1055,7 +1086,7 @@ function SuccessScreen() {
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
lineHeight: 1.6,
|
||||
}}>
|
||||
Wir melden uns innerhalb von 5 Werktagen bei Ihnen.
|
||||
{success.message}
|
||||
</p>
|
||||
|
||||
<div style={{
|
||||
@@ -1070,13 +1101,9 @@ function SuccessScreen() {
|
||||
textTransform: "uppercase", color: GOLD, marginBottom: 14,
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
}}>
|
||||
Ihre nächsten Schritte
|
||||
{success.nextStepsTitle}
|
||||
</div>
|
||||
{[
|
||||
["1", "Bestätigung per E-Mail", "Sie erhalten in Kürze eine Eingangsbestätigung."],
|
||||
["2", "Prüfung durch den Vorstand", "Ihr Antrag wird in der nächsten Sitzung behandelt."],
|
||||
["3", "Willkommen im BMP e.V.", "Nach Bestätigung erhalten Sie Ihre Mitgliedsdaten."],
|
||||
].map(([num, title, desc]) => (
|
||||
{success.steps.map(({ num, title, desc }) => (
|
||||
<div key={num} style={{ display: "flex", gap: 14, marginBottom: 14 }}>
|
||||
<div style={{
|
||||
flexShrink: 0,
|
||||
@@ -1118,8 +1145,9 @@ function SuccessScreen() {
|
||||
|
||||
// ─── MAIN COMPONENT ───────────────────────────────────────────────────────────
|
||||
|
||||
function MembershipWizard() {
|
||||
function MembershipWizard({ content, pricing }: { content?: Partial<WizardCopy>; pricing?: Partial<PricingCopy> }) {
|
||||
const isMobile = useIsMobile();
|
||||
const copy = mergeWizardCopy(content, pricing);
|
||||
const [step, setStep] = useState(1);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
@@ -1172,10 +1200,10 @@ function MembershipWizard() {
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const errs = validateStep(step, data);
|
||||
const errs = validateStep(step, data, copy);
|
||||
if (Object.keys(errs).length > 0) { setErrors(errs); return; }
|
||||
setErrors({});
|
||||
if (step < 5) animateTransition(step + 1, "forward");
|
||||
if (step < copy.steps.length) animateTransition(step + 1, "forward");
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
@@ -1184,7 +1212,7 @@ function MembershipWizard() {
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const errs = validateStep(5, data);
|
||||
const errs = validateStep(copy.steps.length, data, copy);
|
||||
if (Object.keys(errs).length > 0) { setErrors(errs); return; }
|
||||
setErrors({});
|
||||
setSubmitted(true);
|
||||
@@ -1222,25 +1250,27 @@ function MembershipWizard() {
|
||||
`}</style>
|
||||
|
||||
{submitted ? (
|
||||
<SuccessScreen />
|
||||
<SuccessScreen copy={copy} />
|
||||
) : (
|
||||
<>
|
||||
<ProgressBar current={step} total={STEPS.length} />
|
||||
<ProgressBar current={step} total={copy.steps.length} steps={copy.steps} />
|
||||
|
||||
<div ref={contentRef} style={{ minHeight: isMobile ? 300 : 380 }}>
|
||||
{step === 1 && <Step1 data={data} onChange={onChange} errors={errors} />}
|
||||
{step === 2 && <Step2 data={data} onChange={onChange} errors={errors} />}
|
||||
{step === 3 && <Step3 data={data} onChange={onChange} errors={errors} />}
|
||||
{step === 4 && <Step4 data={data} onChange={onChange} errors={errors} />}
|
||||
{step === 5 && <Step5 data={data} onChange={onChange} errors={errors} />}
|
||||
{step === 1 && <Step1 data={data} onChange={onChange} errors={errors} copy={copy} />}
|
||||
{step === 2 && <Step2 data={data} onChange={onChange} errors={errors} copy={copy} />}
|
||||
{step === 3 && <Step3 data={data} onChange={onChange} errors={errors} copy={copy} />}
|
||||
{step === 4 && <Step4 data={data} onChange={onChange} errors={errors} copy={copy} />}
|
||||
{step === 5 && <Step5 data={data} onChange={onChange} errors={errors} copy={copy} />}
|
||||
</div>
|
||||
|
||||
<NavButtons
|
||||
step={step}
|
||||
total={copy.steps.length}
|
||||
onBack={handleBack}
|
||||
onNext={handleNext}
|
||||
onSubmit={handleSubmit}
|
||||
disabled={submitDisabled}
|
||||
copy={copy}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
347
src/spa/mitgliedWerdenContent.ts
Normal file
347
src/spa/mitgliedWerdenContent.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
export const mitgliedWerdenContent = {
|
||||
header: {
|
||||
brandLabel: 'BMP 2026',
|
||||
backLabel: 'Zurück zur Website',
|
||||
backUrl: '/',
|
||||
},
|
||||
hero: {
|
||||
imageFilename: 'mitglied-hero.jpg',
|
||||
imageAlt: 'BMP Mitgliedschaft',
|
||||
eyebrow: 'Vereinsmitgliedschaft',
|
||||
heading: 'DEIN WEG ZU UNS.',
|
||||
description:
|
||||
'Der Bayerische Mittelstandspreis lebt vom Engagement seiner Mitglieder.\nEine Mitgliedschaft ist Unterstützung, damit der Preis weiterhin unabhängig\nund kostenlos umgesetzt werden kann.',
|
||||
stats: [
|
||||
{ value: '120+', label: 'Mitglieder' },
|
||||
{ value: '20+', label: 'Jahre' },
|
||||
{ value: '500 €', label: 'Aufnahme' },
|
||||
],
|
||||
},
|
||||
benefits: {
|
||||
eyebrow: 'Deine Vorteile',
|
||||
heading: 'WAS DU GEWINNST.',
|
||||
cards: [
|
||||
{
|
||||
icon: 'users',
|
||||
title: 'Netzwerk',
|
||||
body: 'Direkter Zugang zu Jury-Mitgliedern, Partnern und Entscheidern aus dem bayerischen Mittelstand.',
|
||||
},
|
||||
{
|
||||
icon: 'star',
|
||||
title: 'Unterstützung',
|
||||
body: 'Mit deiner Mitgliedschaft sorgst du dafür, dass der Preis unabhängig und kostenlos vergeben werden kann.',
|
||||
},
|
||||
{
|
||||
icon: 'eye',
|
||||
title: 'Sichtbarkeit',
|
||||
body: 'Als Vereinsmitglied wirst du im BMP-Ökosystem sichtbar – auf Events, in Publikationen und online.',
|
||||
},
|
||||
{
|
||||
icon: 'heart',
|
||||
title: 'Wirkung',
|
||||
body: 'Leiste einen konkreten Beitrag für den bayerischen Mittelstand und die Menschen dahinter.',
|
||||
},
|
||||
],
|
||||
},
|
||||
about: {
|
||||
eyebrow: 'Der Verein',
|
||||
heading: 'WER WIR SIND.',
|
||||
paragraphs: [
|
||||
'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.',
|
||||
'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.',
|
||||
'Veranstalter ist der EWIF – Europäisches Wirtschaftsforum. Die Mitgliedschaft läuft über den EWIF.',
|
||||
],
|
||||
descriptor: 'Bayerischer Mittelstandspreis e.V., eingetragener gemeinnütziger Verein',
|
||||
facts: [
|
||||
{ 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' },
|
||||
],
|
||||
},
|
||||
pricing: {
|
||||
eyebrow: 'Mitgliedsbeitrag',
|
||||
heading: 'IHR BEITRAG.',
|
||||
employeeLabel: 'Anzahl Mitarbeiter wählen',
|
||||
selectPlaceholder: 'Bitte wählen…',
|
||||
optionSuffix: 'Mitarbeiter',
|
||||
resultEyebrow: 'Beitragsübersicht',
|
||||
annualNetLabel: 'Jahresbeitrag (netto)',
|
||||
annualGrossLabel: 'Jahresbeitrag (19 % MwSt.)',
|
||||
annualGrossShortLabel: 'Jahresbeitrag (brutto)',
|
||||
admissionGrossLabel: 'Aufnahmegebühr (brutto)',
|
||||
totalYearOneLabel: 'Gesamt Jahr 1 (anteilig)',
|
||||
footnote:
|
||||
'* Anteilig ab nächstem Monatsersten bis 31.12. des laufenden Jahres. Ab dem Folgejahr gilt der volle Jahresbeitrag.',
|
||||
ctaLabel: 'Jetzt Mitglied werden →',
|
||||
ctaHref: '#mitglied-formular',
|
||||
tableToggleLabel: 'Vollständige Beitragsstaffel',
|
||||
tableEmployeeHeader: 'Mitarbeiter',
|
||||
tableNetHeader: 'Netto / Jahr',
|
||||
tableGrossHeader: 'Brutto / Jahr',
|
||||
tableNotePrefix: 'Zzgl. einmalige Aufnahmegebühr',
|
||||
tableNoteSuffix: '(brutto) im ersten Jahr. Alle Angaben inkl. 19 % MwSt.',
|
||||
vatRate: 0.19,
|
||||
admissionFeeNet: 500,
|
||||
tiers: [
|
||||
{ max: 10, label: 'bis 10', annual: 480 },
|
||||
{ max: 20, label: 'bis 20', annual: 600 },
|
||||
{ max: 50, label: 'bis 50', annual: 900 },
|
||||
{ max: 100, label: 'bis 100', annual: 1200 },
|
||||
{ max: 250, label: 'bis 250', annual: 2400 },
|
||||
{ max: 1000, label: 'bis 1.000', annual: 3600 },
|
||||
],
|
||||
},
|
||||
applicationOptions: {
|
||||
items: [
|
||||
{
|
||||
id: 'online',
|
||||
number: '01',
|
||||
eyebrow: 'Online',
|
||||
title: 'Antrag digital ausfüllen',
|
||||
description: 'In wenigen Schritten direkt auf dieser Seite',
|
||||
href: '#mitglied-formular',
|
||||
download: false,
|
||||
},
|
||||
{
|
||||
id: 'pdf',
|
||||
number: '02',
|
||||
eyebrow: 'Per Post',
|
||||
title: 'PDF herunterladen',
|
||||
description: 'Antrag ausdrucken und unterschrieben einsenden',
|
||||
href: '/aufnahmeantrag-bmp.pdf',
|
||||
fileFilename: 'aufnahmeantrag-bmp.pdf',
|
||||
download: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
formIntro: {
|
||||
eyebrow: 'Jetzt beitreten',
|
||||
heading: 'DU BIST DABEI.',
|
||||
description:
|
||||
'Fülle das Formular aus und wir melden uns innerhalb von 48 Stunden bei dir.\nDie Mitgliedschaft beginnt mit Bestätigung durch den Vorstand.',
|
||||
promises: [
|
||||
{ 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' },
|
||||
],
|
||||
imageFilename: 'gewinner-gruppenfoto.jpg',
|
||||
imageAlt: 'BMP Mitglieder',
|
||||
imageLabel: 'BMP Mitglieder 2025',
|
||||
},
|
||||
testimonials: {
|
||||
eyebrow: 'Stimmen der Mitglieder',
|
||||
heading: 'WAS MITGLIEDER SAGEN.',
|
||||
items: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
faq: {
|
||||
eyebrow: 'Häufige Fragen',
|
||||
heading: '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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
bottomCta: {
|
||||
eyebrow: 'Werde Teil des BMP',
|
||||
heading: 'UNTERSTÜTZE. NETZWERKE. WIRKE.',
|
||||
ctaLabel: 'Jetzt Mitglied werden',
|
||||
ctaHref: '#mitglied-formular',
|
||||
},
|
||||
wizard: {
|
||||
requiredSuffix: '*',
|
||||
optionalSuffix: '(optional)',
|
||||
stepCounterTemplate: 'Schritt {step} von {total}',
|
||||
reviewFallback: '–',
|
||||
nav: {
|
||||
backLabel: '← Zurück',
|
||||
nextLabel: 'Weiter →',
|
||||
submitLabel: 'Mitgliedschaftsantrag verbindlich einreichen',
|
||||
},
|
||||
steps: [
|
||||
{ id: 1, label: 'Unternehmen' },
|
||||
{ id: 2, label: 'Kontakt' },
|
||||
{ id: 3, label: 'Details' },
|
||||
{ id: 4, label: 'Zahlung' },
|
||||
{ id: 5, label: 'Abschluss' },
|
||||
],
|
||||
validation: {
|
||||
required: 'Pflichtfeld',
|
||||
select: 'Bitte wählen',
|
||||
postalCode: '5-stellige PLZ erforderlich',
|
||||
email: 'Gültige E-Mail erforderlich',
|
||||
iban: 'Ungültige IBAN (DE, 22 Zeichen)',
|
||||
sepa: 'Bitte SEPA-Mandat bestätigen',
|
||||
},
|
||||
tiers: [
|
||||
{ max: 10, label: 'bis 10', annual: 480 },
|
||||
{ max: 20, label: 'bis 20', annual: 600 },
|
||||
{ max: 50, label: 'bis 50', annual: 900 },
|
||||
{ max: 100, label: 'bis 100', annual: 1200 },
|
||||
{ max: 250, label: 'bis 250', annual: 2400 },
|
||||
{ max: 1000, label: 'bis 1.000', annual: 3600 },
|
||||
],
|
||||
vatRate: 0.19,
|
||||
admissionFeeNet: 500,
|
||||
pricingSummary: {
|
||||
eyebrow: 'Beitragsübersicht',
|
||||
annualNetLabel: 'Jahresbeitrag (netto)',
|
||||
annualGrossLabel: 'Jahresbeitrag (brutto)',
|
||||
admissionGrossLabel: 'Aufnahmegebühr (brutto)',
|
||||
totalYearOneLabel: 'Gesamt Jahr 1 (anteilig)',
|
||||
},
|
||||
fields: {
|
||||
company: {
|
||||
title: 'Ihr Unternehmen',
|
||||
subtitle: 'Angaben zum antragstellenden Unternehmen',
|
||||
legalForms: ['GmbH', 'GmbH & Co. KG', 'AG', 'UG', 'KG', 'OHG', 'GbR', 'Einzelunternehmen', 'e.K.', 'Sonstige'],
|
||||
companyNameLabel: 'Firmenname',
|
||||
companyNamePlaceholder: 'Muster GmbH',
|
||||
legalFormLabel: 'Rechtsform',
|
||||
employeesLabel: 'Anzahl Mitarbeiter',
|
||||
streetLabel: 'Straße & Hausnummer',
|
||||
streetPlaceholder: 'Musterstraße 42',
|
||||
postalCodeLabel: 'PLZ',
|
||||
postalCodePlaceholder: '12345',
|
||||
cityLabel: 'Ort',
|
||||
cityPlaceholder: 'Berlin',
|
||||
websiteLabel: 'Website',
|
||||
websitePlaceholder: 'https://www.ihrewebsite.de',
|
||||
},
|
||||
contact: {
|
||||
title: 'Kontaktperson',
|
||||
subtitle: 'Ansprechpartner für die Mitgliedschaft',
|
||||
salutations: ['Herr', 'Frau', 'Divers'],
|
||||
salutationLabel: 'Anrede',
|
||||
firstNameLabel: 'Vorname',
|
||||
firstNamePlaceholder: 'Max',
|
||||
lastNameLabel: 'Nachname',
|
||||
lastNamePlaceholder: 'Mustermann',
|
||||
positionLabel: 'Position / Funktion',
|
||||
positionPlaceholder: 'z. B. Geschäftsführer',
|
||||
emailLabel: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'max@muster.de',
|
||||
phoneLabel: 'Telefon',
|
||||
phonePlaceholder: '+49 30 123456',
|
||||
},
|
||||
details: {
|
||||
title: 'Mitgliedschaft & Details',
|
||||
subtitle: 'Beitragsübersicht und Eintrittsdatum',
|
||||
entryDateLabel: 'Gewünschtes Eintrittsdatum',
|
||||
referralLabel: 'Wie wurden Sie aufmerksam?',
|
||||
referralOptions: ['Empfehlung', 'Veranstaltung', 'Google', 'Social Media', 'Presse', 'Sonstiges'],
|
||||
motivationLabel: 'Ihre Motivation (optional)',
|
||||
motivationPlaceholder: 'Was erhoffen Sie sich von der Mitgliedschaft beim BMP e.V.?',
|
||||
motivationMaxLength: 500,
|
||||
},
|
||||
payment: {
|
||||
title: 'SEPA-Lastschriftmandat',
|
||||
subtitle: 'Bankverbindung für den Beitragseinzug',
|
||||
mandateText:
|
||||
'Ich ermächtige den BMP e.V., Zahlungen von meinem Konto mittels Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die vom BMP e.V. auf mein Konto gezogenen Lastschriften einzulösen. Hinweis: Ich kann innerhalb von acht Wochen, beginnend mit dem Belastungsdatum, die Erstattung des belasteten Betrages verlangen. Es gelten dabei die mit meinem Kreditinstitut vereinbarten Bedingungen. Die Mandatsreferenz wird separat mitgeteilt. Gläubiger-Identifikationsnummer: DE98ZZZ09999999999.',
|
||||
accountHolderLabel: 'Kontoinhaber',
|
||||
accountHolderPlaceholder: 'Max Mustermann',
|
||||
ibanLabel: 'IBAN',
|
||||
ibanPlaceholder: 'DE00 0000 0000 0000 0000 00',
|
||||
ibanValidLabel: '✓ OK',
|
||||
ibanPendingLabel: '…',
|
||||
bicLabel: 'BIC',
|
||||
bicPlaceholder: 'BELADEBEXXX',
|
||||
bicAutoPlaceholder: 'Wird ausgefüllt',
|
||||
bankLabel: 'Bank / Kreditinstitut',
|
||||
bankPlaceholder: 'Berliner Sparkasse',
|
||||
sepaCheckboxLabel: 'Ich bestätige das SEPA-Lastschriftmandat und ermächtige den BMP e.V. zum Einzug.',
|
||||
},
|
||||
summary: {
|
||||
title: 'Zusammenfassung & Abschluss',
|
||||
subtitle: 'Bitte prüfen Sie Ihre Angaben',
|
||||
companyAccordion: 'Unternehmen',
|
||||
contactAccordion: 'Kontaktperson',
|
||||
paymentAccordion: 'Zahlung',
|
||||
requiredTitle: 'Pflichtbestätigungen',
|
||||
optionalTitle: 'Optional',
|
||||
labels: {
|
||||
companyName: 'Firmenname',
|
||||
legalForm: 'Rechtsform',
|
||||
address: 'Adresse',
|
||||
employees: 'Mitarbeiter',
|
||||
website: 'Website',
|
||||
name: 'Name',
|
||||
position: 'Position',
|
||||
email: 'E-Mail',
|
||||
phone: 'Telefon',
|
||||
accountHolder: 'Kontoinhaber',
|
||||
iban: 'IBAN',
|
||||
bic: 'BIC',
|
||||
bank: 'Bank',
|
||||
entryDate: 'Eintrittsdatum',
|
||||
annualContribution: 'Jahresbeitrag',
|
||||
},
|
||||
consents: {
|
||||
satzung: 'Ich habe die Satzung des BMP e.V. gelesen und erkenne sie an.',
|
||||
datenschutz: 'Ich habe die Datenschutzerklärung gelesen und stimme zu.',
|
||||
widerruf: 'Ich habe die Kündigungsfrist zur Kenntnis genommen (1 Jahr zum Jahresende).',
|
||||
newsletter: 'Ich möchte elektronische Informationen, Einladungen und den Newsletter erhalten.',
|
||||
websitePub: 'Meinen Firmennamen auf der Website des BMP e.V. als Mitglied veröffentlichen.',
|
||||
},
|
||||
},
|
||||
},
|
||||
success: {
|
||||
heading: 'Ihr Antrag ist eingegangen.',
|
||||
message: 'Wir melden uns innerhalb von 5 Werktagen bei Ihnen.',
|
||||
nextStepsTitle: 'Ihre nächsten Schritte',
|
||||
steps: [
|
||||
{ num: '1', title: 'Bestätigung per E-Mail', desc: 'Sie erhalten in Kürze eine Eingangsbestätigung.' },
|
||||
{ num: '2', title: 'Prüfung durch den Vorstand', desc: 'Ihr Antrag wird in der nächsten Sitzung behandelt.' },
|
||||
{ num: '3', title: 'Willkommen im BMP e.V.', desc: 'Nach Bestätigung erhalten Sie Ihre Mitgliedsdaten.' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user