feat: manage participation page via payload

This commit is contained in:
syntaxbullet
2026-06-22 10:40:10 +02:00
parent ae98269b03
commit 2e9ad5cdf8
9 changed files with 1886 additions and 360 deletions

View File

@@ -19,6 +19,7 @@
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"preload:contact-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-contact-page-cms.ts",
"preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts",
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"test": "pnpm run test:int && pnpm run test:e2e",

View File

@@ -10,6 +10,7 @@ import { MediaBlock } from '../../blocks/MediaBlock/config'
import { hero } from '@/heros/config'
import { contactFields } from './contactFields'
import { homeFields } from './homeFields'
import { participationFields } from './participationFields'
import { slugField } from 'payload'
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
@@ -39,8 +40,16 @@ const isContactPage = (_: unknown, siblingData?: { slug?: string; spaPath?: stri
return spaPath === '/kontakt' || slug === 'kontakt' || slug === 'contact' || title === 'kontakt' || title === 'contact'
}
const isParticipationPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
const slug = siblingData?.slug
const spaPath = siblingData?.spaPath
const title = siblingData?.title?.toLowerCase()
return spaPath === '/teilnahme' || slug === 'teilnahme' || slug === 'participation' || title === 'teilnahme' || title === 'participation'
}
const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
isHomePage(undefined, siblingData) || isContactPage(undefined, siblingData)
isHomePage(undefined, siblingData) || isContactPage(undefined, siblingData) || isParticipationPage(undefined, siblingData)
export const Pages: CollectionConfig<'pages'> = {
slug: 'pages',
@@ -122,6 +131,13 @@ export const Pages: CollectionConfig<'pages'> = {
fields: contactFields,
label: 'Contact Page',
},
{
admin: {
condition: (data) => isParticipationPage(undefined, data),
},
fields: participationFields,
label: 'Participation Page',
},
{
name: 'meta',
label: 'SEO',

View File

@@ -0,0 +1,400 @@
import type { Field } from 'payload'
import { participationContent } from '@/spa/participationContent'
const uploadField = (name: string, label: string, description?: string): Field => ({
name,
type: 'upload',
relationTo: 'media',
label,
admin: description ? { description } : undefined,
})
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 checkbox = (name: string, label: string, defaultValue = false): Field => ({
name,
type: 'checkbox',
label,
defaultValue,
})
const sectionAdmin = (description: string) => ({
description,
initCollapsed: true,
})
const linkGroup = (labelDefault: string, urlDefault: string): Field[] => [
text('label', 'Label', labelDefault),
text('url', 'URL', urlDefault),
]
const iconOptions = [
{ label: 'Alert circle', value: 'alertCircle' },
{ label: 'Arrow right', value: 'arrowRight' },
{ label: 'Building', value: 'building2' },
{ label: 'Check', value: 'check' },
{ label: 'Check circle', value: 'checkCircle2' },
{ label: 'File text', value: 'fileText' },
{ label: 'Map pin', value: 'mapPin' },
{ label: 'Search', value: 'search' },
{ label: 'Send', value: 'send' },
{ label: 'Star', value: 'star' },
{ label: 'Trending up', value: 'trendingUp' },
{ label: 'Trophy', value: 'trophy' },
{ label: 'User', value: 'user' },
{ label: 'User check', value: 'userCheck' },
{ label: 'User plus', value: 'userPlus' },
{ label: 'Users', value: 'users' },
]
const iconField = (defaultValue = 'star', dbName?: string): Field => ({
name: 'icon',
label: 'Icon',
type: 'select',
dbName,
defaultValue,
options: iconOptions,
})
const factFields: Field[] = [text('label', 'Label'), textarea('desc', 'Description')]
const ctaGroup = (name: string, label: string, labelDefault: string, urlDefault: string): Field => ({
name,
label,
type: 'group',
fields: linkGroup(labelDefault, urlDefault),
})
export const participationFields: Field[] = [
{
name: 'participation',
label: 'Participation page layout sections',
type: 'group',
admin: {
description:
'Edit the participation page in the same order it appears on the frontend. The generic Hero and Content tabs are hidden for this SPA-managed route.',
},
fields: [
{
name: 'hero',
label: '01 · Hero',
type: 'group',
admin: sectionAdmin('Top image, eyebrow, headline, and intro copy.'),
fields: [
uploadField('backgroundImage', 'Background image', `Current frontend image: /images/${participationContent.hero.backgroundImageFilename}`),
text('imageAlt', 'Image alt text', participationContent.hero.imageAlt),
text('eyebrow', 'Eyebrow', participationContent.hero.eyebrow),
textarea('heading', 'Headline', participationContent.hero.heading),
textarea('description', 'Intro paragraph', participationContent.hero.description),
],
},
{
name: 'process',
label: '02 · Weg zur Auszeichnung timeline',
type: 'group',
admin: sectionAdmin('Timeline section rendered directly after the hero.'),
fields: [
text('eyebrow', 'Eyebrow', participationContent.process.eyebrow),
textarea('heading', 'Heading', participationContent.process.heading),
textarea('description', 'Intro paragraph', participationContent.process.description),
text('scrollHint', 'Desktop scroll hint', participationContent.process.scrollHint),
text('progressLabel', 'Desktop progress label', participationContent.process.progressLabel),
{
name: 'steps',
label: 'Process steps',
type: 'array',
dbName: 'proc_steps',
defaultValue: participationContent.process.steps,
fields: [text('step', 'Step number'), text('title', 'Title'), textarea('desc', 'Description'), text('date', 'Date'), iconField('fileText')],
},
],
},
{
name: 'eligibility',
label: '03 · Eligibility section',
type: 'group',
admin: sectionAdmin('Cream eligibility section with criteria rows, notes, and CTAs.'),
fields: [
text('eyebrow', 'Eyebrow', participationContent.eligibility.eyebrow),
textarea('heading', 'Heading', participationContent.eligibility.heading),
textarea('description', 'Intro paragraph', participationContent.eligibility.description),
ctaGroup('primaryCta', 'Primary CTA', participationContent.eligibility.primaryCta.label, participationContent.eligibility.primaryCta.url),
ctaGroup('secondaryCta', 'Secondary CTA', participationContent.eligibility.secondaryCta.label, participationContent.eligibility.secondaryCta.url),
{
name: 'criteria',
label: 'Eligibility rows',
type: 'array',
dbName: 'elig_crit',
defaultValue: participationContent.eligibility.criteria,
fields: [text('num', 'Number'), text('title', 'Title'), textarea('body', 'Body'), iconField('mapPin')],
},
{
name: 'notes',
label: 'Additional notes',
type: 'array',
dbName: 'elig_notes',
defaultValue: participationContent.eligibility.notes,
fields: [text('title', 'Title'), textarea('body', 'Body'), iconField('building2')],
},
text('nudgeText', 'Bottom nudge text', participationContent.eligibility.nudgeText),
ctaGroup('nudgeCta', 'Bottom CTA', participationContent.eligibility.nudgeCta.label, participationContent.eligibility.nudgeCta.url),
],
},
{
name: 'evaluation',
label: '04 · Criteria / evaluation section',
type: 'group',
admin: sectionAdmin('Navy evaluation section with image, link, and criteria grid.'),
fields: [
uploadField('image', 'Image', `Current frontend image: /images/${participationContent.evaluation.imageFilename}`),
text('imageAlt', 'Image alt text', participationContent.evaluation.imageAlt),
text('eyebrow', 'Eyebrow', participationContent.evaluation.eyebrow),
textarea('heading', 'Heading', participationContent.evaluation.heading),
textarea('body', 'Body copy', participationContent.evaluation.body),
ctaGroup('link', 'External criteria link', participationContent.evaluation.link.label, participationContent.evaluation.link.url),
{
name: 'criteria',
label: 'Evaluation criteria',
type: 'array',
dbName: 'eval_crit',
defaultValue: participationContent.evaluation.criteria,
fields: factFields,
},
],
},
{
name: 'applicationWays',
label: '05 · Application paths',
type: 'group',
admin: sectionAdmin('Cream section explaining proposal, self-application, and special award paths.'),
fields: [
text('eyebrow', 'Eyebrow', participationContent.applicationWays.eyebrow),
textarea('heading', 'Heading', participationContent.applicationWays.heading),
textarea('description', 'Intro paragraph', participationContent.applicationWays.description),
text('recommendedLabel', 'Featured card label', participationContent.applicationWays.recommendedLabel),
{
name: 'institutions',
label: 'Proposal institutions',
type: 'array',
dbName: 'app_inst',
defaultValue: participationContent.applicationWays.institutions,
fields: [textarea('text', 'Institution')],
},
{
name: 'ways',
label: 'Application path cards',
type: 'array',
dbName: 'app_ways',
defaultValue: participationContent.applicationWays.ways,
fields: [
text('label', 'Path label'),
textarea('title', 'Title'),
textarea('desc', 'Description'),
iconField('send', 'ico'),
checkbox('featured', 'Featured card'),
{
name: 'listType',
label: 'Detail list type',
type: 'select',
dbName: 'lt',
defaultValue: 'facts',
options: [
{ label: 'Use proposal institutions list', value: 'institutions' },
{ label: 'Use facts below', value: 'facts' },
],
},
{
name: 'facts',
label: 'Fact rows',
type: 'array',
dbName: 'way_facts',
fields: factFields,
},
ctaGroup('cta', 'CTA', 'Zum Formular', '#bewerben'),
],
},
],
},
{
name: 'datesBanner',
label: '06 · Dates banner',
type: 'group',
admin: sectionAdmin('Gold important-dates banner. Mobile and desktop labels differ slightly in the current design.'),
fields: [
text('heading', 'Heading', participationContent.datesBanner.heading),
textarea('description', 'Desktop description', participationContent.datesBanner.description),
{
name: 'mobileItems',
label: 'Mobile date items',
type: 'array',
dbName: 'date_mob',
defaultValue: participationContent.datesBanner.mobileItems,
fields: [text('date', 'Date'), text('label', 'Label')],
},
{
name: 'desktopItems',
label: 'Desktop date items',
type: 'array',
dbName: 'date_desk',
defaultValue: participationContent.datesBanner.desktopItems,
fields: [text('date', 'Date'), text('label', 'Label')],
},
],
},
{
name: 'applicationForm',
label: '07 · Application form wrapper',
type: 'group',
admin: sectionAdmin('Navy/gold section containing the application form and PDF upload links.'),
fields: [
uploadField('image', 'Form image', `Current frontend image: /images/${participationContent.applicationForm.imageFilename}`),
text('imageAlt', 'Image alt text', participationContent.applicationForm.imageAlt),
text('imageCaption', 'Image caption', participationContent.applicationForm.imageCaption),
text('eyebrow', 'Eyebrow', participationContent.applicationForm.eyebrow),
textarea('heading', 'Heading', participationContent.applicationForm.heading),
textarea('description', 'Body copy', participationContent.applicationForm.description),
{
name: 'facts',
label: 'Fact rows',
type: 'array',
dbName: 'form_facts',
defaultValue: participationContent.applicationForm.facts,
fields: [text('num', 'Number'), text('label', 'Label'), textarea('desc', 'Description')],
},
text('formEyebrow', 'Form eyebrow', participationContent.applicationForm.formEyebrow),
ctaGroup('uploadCta', 'Top upload CTA', participationContent.applicationForm.uploadCta.label, participationContent.applicationForm.uploadCta.url),
textarea('uploadPrompt', 'Bottom upload prompt', participationContent.applicationForm.uploadPrompt),
ctaGroup('uploadFooterCta', 'Bottom upload CTA', participationContent.applicationForm.uploadFooterCta.label, participationContent.applicationForm.uploadFooterCta.url),
],
},
{
name: 'form',
label: '08 · Application form copy and logic labels',
type: 'group',
admin: sectionAdmin('Text, options, and eligibility thresholds used inside the multi-step application form.'),
fields: [
text('stepCompleteLabel', 'Completed step label', participationContent.form.stepCompleteLabel),
text('backLabel', 'Back button label', participationContent.form.backLabel),
text('nextLabel', 'Next button label', participationContent.form.nextLabel),
text('submitLabel', 'Submit button label', participationContent.form.submitLabel),
text('yesLabel', 'Yes option label', participationContent.form.yesLabel),
text('noLabel', 'No option label', participationContent.form.noLabel),
text('requiredSuffix', 'Required field suffix', participationContent.form.requiredSuffix),
{
name: 'validation',
label: 'Validation messages',
type: 'group',
fields: [
text('choose', 'Choose message', participationContent.form.validation.choose),
text('required', 'Required message', participationContent.form.validation.required),
text('invalidEmail', 'Invalid email message', participationContent.form.validation.invalidEmail),
],
},
{
name: 'typeStep',
label: 'Step 1 · Submission type',
type: 'group',
fields: [
text('eyebrow', 'Eyebrow', participationContent.form.typeStep.eyebrow),
text('heading', 'Heading', participationContent.form.typeStep.heading),
text('selfTitle', 'Self-application title', participationContent.form.typeStep.selfTitle),
textarea('selfDesc', 'Self-application description', participationContent.form.typeStep.selfDesc),
text('nominationTitle', 'Nomination title', participationContent.form.typeStep.nominationTitle),
textarea('nominationDesc', 'Nomination description', participationContent.form.typeStep.nominationDesc),
],
},
{
name: 'selfSteps',
label: 'Self-application step labels',
type: 'array',
dbName: 'self_steps',
defaultValue: participationContent.form.selfSteps,
fields: [text('label', 'Label'), iconField('trophy')],
},
{
name: 'nominationSteps',
label: 'Nomination step labels',
type: 'array',
dbName: 'nom_steps',
defaultValue: participationContent.form.nominationSteps,
fields: [text('label', 'Label'), iconField('trophy')],
},
{
name: 'employeeOptions',
label: 'Employee options',
type: 'array',
dbName: 'emp_opts',
defaultValue: participationContent.form.employeeOptions,
fields: [text('value', 'Value'), text('label', 'Label')],
},
{
name: 'eligibility',
label: 'Self-application eligibility step',
type: 'group',
fields: [
text('allowedEmployeeValues', 'Allowed employee option values, comma-separated', participationContent.form.eligibility.allowedEmployeeValues),
text('requiredBayernValue', 'Required Bavaria answer value', participationContent.form.eligibility.requiredBayernValue),
text('eyebrow', 'Eyebrow', participationContent.form.eligibility.eyebrow),
text('heading', 'Heading', participationContent.form.eligibility.heading),
text('employeesLabel', 'Employees field label', participationContent.form.eligibility.employeesLabel),
text('bayernLabel', 'Bavaria field label', participationContent.form.eligibility.bayernLabel),
text('ownerLabel', 'Owner field label', participationContent.form.eligibility.ownerLabel),
text('ineligibleHeading', 'Ineligible heading', participationContent.form.eligibility.ineligibleHeading),
textarea('ineligibleBody', 'Ineligible body', participationContent.form.eligibility.ineligibleBody),
text('ineligibleContactPrefix', 'Ineligible contact prefix', participationContent.form.eligibility.ineligibleContactPrefix),
text('ineligibleContactEmail', 'Ineligible contact email', participationContent.form.eligibility.ineligibleContactEmail),
],
},
{
name: 'selfContact',
label: 'Self-application contact step',
type: 'group',
fields: Object.entries(participationContent.form.selfContact).map(([name, value]) => text(name, name, value)),
},
{
name: 'selfSummary',
label: 'Self-application summary step',
type: 'group',
fields: Object.entries(participationContent.form.selfSummary).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationCompany',
label: 'Nomination company step',
type: 'group',
fields: Object.entries(participationContent.form.nominationCompany).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationContact',
label: 'Nomination contact step',
type: 'group',
fields: Object.entries(participationContent.form.nominationContact).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationSummary',
label: 'Nomination summary step',
type: 'group',
fields: Object.entries(participationContent.form.nominationSummary).map(([name, value]) => text(name, name, value)),
},
{
name: 'success',
label: 'Success state copy',
type: 'group',
fields: Object.entries(participationContent.form.success).map(([name, value]) => text(name, name, value)),
},
],
},
],
},
]

View File

@@ -488,6 +488,431 @@ export interface Page {
| null;
};
};
/**
* Edit the participation page in the same order it appears on the frontend. The generic Hero and Content tabs are hidden for this SPA-managed route.
*/
participation?: {
/**
* Top image, eyebrow, headline, and intro copy.
*/
hero?: {
/**
* Current frontend image: /images/preisuebergabe.jpg
*/
backgroundImage?: (number | null) | Media;
imageAlt?: string | null;
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
};
/**
* Timeline section rendered directly after the hero.
*/
process?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
scrollHint?: string | null;
progressLabel?: string | null;
steps?:
| {
step?: string | null;
title?: string | null;
desc?: string | null;
date?: string | null;
icon?:
| (
| 'alertCircle'
| 'arrowRight'
| 'building2'
| 'check'
| 'checkCircle2'
| 'fileText'
| 'mapPin'
| 'search'
| 'send'
| 'star'
| 'trendingUp'
| 'trophy'
| 'user'
| 'userCheck'
| 'userPlus'
| 'users'
)
| null;
id?: string | null;
}[]
| null;
};
/**
* Cream eligibility section with criteria rows, notes, and CTAs.
*/
eligibility?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
primaryCta?: {
label?: string | null;
url?: string | null;
};
secondaryCta?: {
label?: string | null;
url?: string | null;
};
criteria?:
| {
num?: string | null;
title?: string | null;
body?: string | null;
icon?:
| (
| 'alertCircle'
| 'arrowRight'
| 'building2'
| 'check'
| 'checkCircle2'
| 'fileText'
| 'mapPin'
| 'search'
| 'send'
| 'star'
| 'trendingUp'
| 'trophy'
| 'user'
| 'userCheck'
| 'userPlus'
| 'users'
)
| null;
id?: string | null;
}[]
| null;
notes?:
| {
title?: string | null;
body?: string | null;
icon?:
| (
| 'alertCircle'
| 'arrowRight'
| 'building2'
| 'check'
| 'checkCircle2'
| 'fileText'
| 'mapPin'
| 'search'
| 'send'
| 'star'
| 'trendingUp'
| 'trophy'
| 'user'
| 'userCheck'
| 'userPlus'
| 'users'
)
| null;
id?: string | null;
}[]
| null;
nudgeText?: string | null;
nudgeCta?: {
label?: string | null;
url?: string | null;
};
};
/**
* Navy evaluation section with image, link, and criteria grid.
*/
evaluation?: {
/**
* Current frontend image: /images/buehne-gewinner.jpg
*/
image?: (number | null) | Media;
imageAlt?: string | null;
eyebrow?: string | null;
heading?: string | null;
body?: string | null;
link?: {
label?: string | null;
url?: string | null;
};
criteria?:
| {
label?: string | null;
desc?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Cream section explaining proposal, self-application, and special award paths.
*/
applicationWays?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
recommendedLabel?: string | null;
institutions?:
| {
text?: string | null;
id?: string | null;
}[]
| null;
ways?:
| {
label?: string | null;
title?: string | null;
desc?: string | null;
icon?:
| (
| 'alertCircle'
| 'arrowRight'
| 'building2'
| 'check'
| 'checkCircle2'
| 'fileText'
| 'mapPin'
| 'search'
| 'send'
| 'star'
| 'trendingUp'
| 'trophy'
| 'user'
| 'userCheck'
| 'userPlus'
| 'users'
)
| null;
featured?: boolean | null;
listType?: ('institutions' | 'facts') | null;
facts?:
| {
label?: string | null;
desc?: string | null;
id?: string | null;
}[]
| null;
cta?: {
label?: string | null;
url?: string | null;
};
id?: string | null;
}[]
| null;
};
/**
* Gold important-dates banner. Mobile and desktop labels differ slightly in the current design.
*/
datesBanner?: {
heading?: string | null;
description?: string | null;
mobileItems?:
| {
date?: string | null;
label?: string | null;
id?: string | null;
}[]
| null;
desktopItems?:
| {
date?: string | null;
label?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Navy/gold section containing the application form and PDF upload links.
*/
applicationForm?: {
/**
* Current frontend image: /images/preistraeger-jubel.jpg
*/
image?: (number | null) | Media;
imageAlt?: string | null;
imageCaption?: string | null;
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
facts?:
| {
num?: string | null;
label?: string | null;
desc?: string | null;
id?: string | null;
}[]
| null;
formEyebrow?: string | null;
uploadCta?: {
label?: string | null;
url?: string | null;
};
uploadPrompt?: string | null;
uploadFooterCta?: {
label?: string | null;
url?: string | null;
};
};
/**
* Text, options, and eligibility thresholds used inside the multi-step application form.
*/
form?: {
stepCompleteLabel?: string | null;
backLabel?: string | null;
nextLabel?: string | null;
submitLabel?: string | null;
yesLabel?: string | null;
noLabel?: string | null;
requiredSuffix?: string | null;
validation?: {
choose?: string | null;
required?: string | null;
invalidEmail?: string | null;
};
typeStep?: {
eyebrow?: string | null;
heading?: string | null;
selfTitle?: string | null;
selfDesc?: string | null;
nominationTitle?: string | null;
nominationDesc?: string | null;
};
selfSteps?:
| {
label?: string | null;
icon?:
| (
| 'alertCircle'
| 'arrowRight'
| 'building2'
| 'check'
| 'checkCircle2'
| 'fileText'
| 'mapPin'
| 'search'
| 'send'
| 'star'
| 'trendingUp'
| 'trophy'
| 'user'
| 'userCheck'
| 'userPlus'
| 'users'
)
| null;
id?: string | null;
}[]
| null;
nominationSteps?:
| {
label?: string | null;
icon?:
| (
| 'alertCircle'
| 'arrowRight'
| 'building2'
| 'check'
| 'checkCircle2'
| 'fileText'
| 'mapPin'
| 'search'
| 'send'
| 'star'
| 'trendingUp'
| 'trophy'
| 'user'
| 'userCheck'
| 'userPlus'
| 'users'
)
| null;
id?: string | null;
}[]
| null;
employeeOptions?:
| {
value?: string | null;
label?: string | null;
id?: string | null;
}[]
| null;
eligibility?: {
allowedEmployeeValues?: string | null;
requiredBayernValue?: string | null;
eyebrow?: string | null;
heading?: string | null;
employeesLabel?: string | null;
bayernLabel?: string | null;
ownerLabel?: string | null;
ineligibleHeading?: string | null;
ineligibleBody?: string | null;
ineligibleContactPrefix?: string | null;
ineligibleContactEmail?: string | null;
};
selfContact?: {
eyebrow?: string | null;
heading?: string | null;
companyLabel?: string | null;
companyPlaceholder?: string | null;
nameLabel?: string | null;
namePlaceholder?: string | null;
emailLabel?: string | null;
emailPlaceholder?: string | null;
phoneLabel?: string | null;
phonePlaceholder?: string | null;
footnote?: string | null;
};
selfSummary?: {
eyebrow?: string | null;
heading?: string | null;
companyLabel?: string | null;
employeesLabel?: string | null;
locationLabel?: string | null;
locationPrefix?: string | null;
contactLabel?: string | null;
};
nominationCompany?: {
eyebrow?: string | null;
heading?: string | null;
companyLabel?: string | null;
companyPlaceholder?: string | null;
industryLabel?: string | null;
industryPlaceholder?: string | null;
locationLabel?: string | null;
locationPlaceholder?: string | null;
};
nominationContact?: {
eyebrow?: string | null;
heading?: string | null;
nameLabel?: string | null;
namePlaceholder?: string | null;
emailLabel?: string | null;
emailPlaceholder?: string | null;
relationshipLabel?: string | null;
relationshipPlaceholder?: string | null;
footnote?: string | null;
};
nominationSummary?: {
eyebrow?: string | null;
heading?: string | null;
companyLabel?: string | null;
industryLabel?: string | null;
locationLabel?: string | null;
nominatedByLabel?: string | null;
emptyValue?: string | null;
};
success?: {
selfHeading?: string | null;
nominationHeading?: string | null;
selfPrefix?: string | null;
selfMiddle?: string | null;
selfSuffix?: string | null;
nominationPrefix?: string | null;
nominationMiddle?: string | null;
nominationSuffix?: string | null;
};
};
};
meta?: {
title?: string | null;
/**
@@ -1764,6 +2189,331 @@ export interface PagesSelect<T extends boolean = true> {
};
};
};
participation?:
| T
| {
hero?:
| T
| {
backgroundImage?: T;
imageAlt?: T;
eyebrow?: T;
heading?: T;
description?: T;
};
process?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
scrollHint?: T;
progressLabel?: T;
steps?:
| T
| {
step?: T;
title?: T;
desc?: T;
date?: T;
icon?: T;
id?: T;
};
};
eligibility?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
primaryCta?:
| T
| {
label?: T;
url?: T;
};
secondaryCta?:
| T
| {
label?: T;
url?: T;
};
criteria?:
| T
| {
num?: T;
title?: T;
body?: T;
icon?: T;
id?: T;
};
notes?:
| T
| {
title?: T;
body?: T;
icon?: T;
id?: T;
};
nudgeText?: T;
nudgeCta?:
| T
| {
label?: T;
url?: T;
};
};
evaluation?:
| T
| {
image?: T;
imageAlt?: T;
eyebrow?: T;
heading?: T;
body?: T;
link?:
| T
| {
label?: T;
url?: T;
};
criteria?:
| T
| {
label?: T;
desc?: T;
id?: T;
};
};
applicationWays?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
recommendedLabel?: T;
institutions?:
| T
| {
text?: T;
id?: T;
};
ways?:
| T
| {
label?: T;
title?: T;
desc?: T;
icon?: T;
featured?: T;
listType?: T;
facts?:
| T
| {
label?: T;
desc?: T;
id?: T;
};
cta?:
| T
| {
label?: T;
url?: T;
};
id?: T;
};
};
datesBanner?:
| T
| {
heading?: T;
description?: T;
mobileItems?:
| T
| {
date?: T;
label?: T;
id?: T;
};
desktopItems?:
| T
| {
date?: T;
label?: T;
id?: T;
};
};
applicationForm?:
| T
| {
image?: T;
imageAlt?: T;
imageCaption?: T;
eyebrow?: T;
heading?: T;
description?: T;
facts?:
| T
| {
num?: T;
label?: T;
desc?: T;
id?: T;
};
formEyebrow?: T;
uploadCta?:
| T
| {
label?: T;
url?: T;
};
uploadPrompt?: T;
uploadFooterCta?:
| T
| {
label?: T;
url?: T;
};
};
form?:
| T
| {
stepCompleteLabel?: T;
backLabel?: T;
nextLabel?: T;
submitLabel?: T;
yesLabel?: T;
noLabel?: T;
requiredSuffix?: T;
validation?:
| T
| {
choose?: T;
required?: T;
invalidEmail?: T;
};
typeStep?:
| T
| {
eyebrow?: T;
heading?: T;
selfTitle?: T;
selfDesc?: T;
nominationTitle?: T;
nominationDesc?: T;
};
selfSteps?:
| T
| {
label?: T;
icon?: T;
id?: T;
};
nominationSteps?:
| T
| {
label?: T;
icon?: T;
id?: T;
};
employeeOptions?:
| T
| {
value?: T;
label?: T;
id?: T;
};
eligibility?:
| T
| {
allowedEmployeeValues?: T;
requiredBayernValue?: T;
eyebrow?: T;
heading?: T;
employeesLabel?: T;
bayernLabel?: T;
ownerLabel?: T;
ineligibleHeading?: T;
ineligibleBody?: T;
ineligibleContactPrefix?: T;
ineligibleContactEmail?: T;
};
selfContact?:
| T
| {
eyebrow?: T;
heading?: T;
companyLabel?: T;
companyPlaceholder?: T;
nameLabel?: T;
namePlaceholder?: T;
emailLabel?: T;
emailPlaceholder?: T;
phoneLabel?: T;
phonePlaceholder?: T;
footnote?: T;
};
selfSummary?:
| T
| {
eyebrow?: T;
heading?: T;
companyLabel?: T;
employeesLabel?: T;
locationLabel?: T;
locationPrefix?: T;
contactLabel?: T;
};
nominationCompany?:
| T
| {
eyebrow?: T;
heading?: T;
companyLabel?: T;
companyPlaceholder?: T;
industryLabel?: T;
industryPlaceholder?: T;
locationLabel?: T;
locationPlaceholder?: T;
};
nominationContact?:
| T
| {
eyebrow?: T;
heading?: T;
nameLabel?: T;
namePlaceholder?: T;
emailLabel?: T;
emailPlaceholder?: T;
relationshipLabel?: T;
relationshipPlaceholder?: T;
footnote?: T;
};
nominationSummary?:
| T
| {
eyebrow?: T;
heading?: T;
companyLabel?: T;
industryLabel?: T;
locationLabel?: T;
nominatedByLabel?: T;
emptyValue?: T;
};
success?:
| T
| {
selfHeading?: T;
nominationHeading?: T;
selfPrefix?: T;
selfMiddle?: T;
selfSuffix?: T;
nominationPrefix?: T;
nominationMiddle?: T;
nominationSuffix?: T;
};
};
};
meta?:
| T
| {

View File

@@ -0,0 +1,78 @@
import 'dotenv/config'
import config from '@payload-config'
import { getPayload, type Payload } from 'payload'
import { participationContent } from '@/spa/participationContent'
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: '/teilnahme' } }, { slug: { equals: 'teilnahme' } }, { slug: { equals: 'participation' } }],
} as never,
})
const page = pageResult.docs[0]
if (!page) throw new Error('Participation page not found. Expected spaPath=/teilnahme or slug=teilnahme/participation.')
const [heroImage, evaluationImage, formImage] = await Promise.all([
mediaByFilename(payload, participationContent.hero.backgroundImageFilename),
mediaByFilename(payload, participationContent.evaluation.imageFilename),
mediaByFilename(payload, participationContent.applicationForm.imageFilename),
])
const { backgroundImageFilename: _heroFilename, ...heroContent } = participationContent.hero
const { imageFilename: _evaluationFilename, ...evaluationContent } = participationContent.evaluation
const { imageFilename: _formFilename, ...applicationFormContent } = participationContent.applicationForm
await payload.update({
collection: 'pages',
id: page.id,
overrideAccess: true,
context: { disableRevalidate: true },
data: {
participation: {
hero: {
...heroContent,
backgroundImage: heroImage,
},
process: participationContent.process,
eligibility: participationContent.eligibility,
evaluation: {
...evaluationContent,
image: evaluationImage,
},
applicationWays: participationContent.applicationWays,
datesBanner: participationContent.datesBanner,
applicationForm: {
...applicationFormContent,
image: formImage,
},
form: participationContent.form,
},
} as never,
})
payload.logger.info(`Preloaded participation CMS fields for page ${page.id}`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})

View File

@@ -8,21 +8,36 @@ import {
ProcessCardTitle,
} from '@/spa/components/ui/process-timeline';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { participationContent } from '@/spa/participationContent';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
const STEPS = [
{ step: '01', title: 'Online-Einreichung', desc: 'Bewerbungsformular vollständig ausfüllen. Die Teilnahme ist kostenfrei und ohne bürokratischen Aufwand möglich.', Icon: FileText, date: 'Bis 30. Juni 2026' },
{ step: '02', title: 'Formale Vorprüfung', desc: 'Das Gremium sichtet alle Unterlagen auf Vollständigkeit, KMU-Konformität und regionale Zugehörigkeit im Freistaat.', Icon: Search, date: 'Juli 2026' },
{ step: '03', title: 'Audit & Jury-Sitzung',desc: 'Die besten Unternehmen werden durch unabhängige Experten-Audits vor Ort evaluiert und in der Jury-Sitzung bewertet.', Icon: UserPlus, date: 'Aug Sep 2026' },
{ step: '04', title: 'Gala-Preisverleihung',desc: 'Die Gewinner werden im Rahmen der feierlichen Gala in München vor geladenen Gästen aus Wirtschaft und Politik geehrt.', Icon: Trophy, date: 'Oktober 2026' },
];
type ProcessContent = Partial<typeof participationContent.process>;
export default function WegZurAuszeichnungSection() {
const ICONS = {
fileText: FileText,
search: Search,
userPlus: UserPlus,
trophy: Trophy,
};
const fallbackText = (value: unknown, fallback: string) =>
typeof value === 'string' && value.length > 0 ? value : fallback;
const fallbackArray = <T,>(value: unknown, fallback: T[]) =>
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
function Lines({ text }: { text: string }) {
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
}
export default function WegZurAuszeichnungSection({ content }: { content?: ProcessContent }) {
const isMobile = useIsMobile();
const section = { ...participationContent.process, ...(content || {}) };
const steps = fallbackArray(section.steps, participationContent.process.steps);
// ── MOBILE: stacked vertical list (no scroll-jack, no horizontal overflow) ──
if (isMobile) {
@@ -30,42 +45,42 @@ export default function WegZurAuszeichnungSection() {
<section id="schritte" style={{ background: NAVY, padding: '56px 22px' }}>
{/* Section header */}
<div style={{ marginBottom: 36 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>Schritt für Schritt</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(section.eyebrow, participationContent.process.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.9rem, 8vw, 2.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.05, margin: '0 0 16px' }}>
IHR WEG ZUM<br />PREIS.
<Lines text={fallbackText(section.heading, participationContent.process.heading)} />
</h2>
<p style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.65)', lineHeight: 1.7, margin: 0 }}>
Von der Einreichung bis zur Gala vier klar definierte Schritte auf dem Weg zur höchsten Auszeichnung des bayerischen Mittelstands.
{fallbackText(section.description, participationContent.process.description)}
</p>
</div>
{/* Stacked step cards */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{STEPS.map((item, index) => {
const Icon = item.Icon;
{steps.map((item, index) => {
const Icon = ICONS[item.icon as keyof typeof ICONS] || FileText;
return (
<div key={item.step} style={{ border: '1px solid rgba(239,191,4,0.25)', background: '#111D55', padding: '24px 22px' }}>
<div key={`${item.step}-${index}`} style={{ border: '1px solid rgba(239,191,4,0.25)', background: '#111D55', padding: '24px 22px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<div style={{ width: 44, height: 44, border: '1px solid rgba(239,191,4,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Icon size={18} style={{ color: GOLD }} strokeWidth={1.5} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.18em', textTransform: 'uppercase' }}>Schritt {item.step}</span>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.18em', textTransform: 'uppercase' }}>Schritt {fallbackText(item.step, '')}</span>
<span style={{ width: 1, height: 11, background: 'rgba(255,255,255,0.2)' }} />
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(255,255,255,0.62)', letterSpacing: '0.06em' }}>{item.date}</span>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(255,255,255,0.62)', letterSpacing: '0.06em' }}>{fallbackText(item.date, '')}</span>
</div>
<h3 style={{ fontFamily: FF, fontSize: '1.35rem', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: 0 }}>
{item.title}
{fallbackText(item.title, '')}
</h3>
</div>
</div>
<p style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.68)', lineHeight: 1.7, margin: 0 }}>
{item.desc}
{fallbackText(item.desc, '')}
</p>
{/* Step progress indicator */}
<div style={{ display: 'flex', gap: 4, marginTop: 20 }}>
{[0, 1, 2, 3].map(i => (
{steps.map((_, i) => (
<div key={i} style={{ height: 2, flex: i === index ? 2 : 1, background: i === index ? GOLD : 'rgba(255,255,255,0.1)' }} />
))}
</div>
@@ -84,13 +99,13 @@ export default function WegZurAuszeichnungSection() {
{/* Section header */}
<div style={{ padding: '56px 80px 40px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40, alignItems: 'flex-end', borderBottom: '1px solid rgba(255,255,255,0.07)', flexShrink: 0 }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 14 }}>Schritt für Schritt</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 14 }}>{fallbackText(section.eyebrow, participationContent.process.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.8rem, 3vw, 2.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0 }}>
IHR WEG ZUM<br />PREIS.
<Lines text={fallbackText(section.heading, participationContent.process.heading)} />
</h2>
</div>
<p style={{ fontFamily: FB, fontSize: 14, color: 'rgba(255,255,255,0.65)', lineHeight: 1.8, margin: 0 }}>
Von der Einreichung bis zur Gala vier klar definierte Schritte auf dem Weg zur höchsten Auszeichnung des bayerischen Mittelstands.
{fallbackText(section.description, participationContent.process.description)}
</p>
</div>
@@ -109,23 +124,23 @@ export default function WegZurAuszeichnungSection() {
))}
</div>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)' }}>
Scrollen zum Erkunden
{fallbackText(section.scrollHint, participationContent.process.scrollHint)}
</span>
{/* Progress bar */}
<div style={{ flex: 1, height: 1, background: 'rgba(255,255,255,0.07)', position: 'relative', overflow: 'hidden', maxWidth: 200 }}>
<div style={{ position: 'absolute', left: 0, top: 0, height: '100%', width: '25%', background: GOLD, opacity: 0.6 }} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, color: 'rgba(255,255,255,0.50)', letterSpacing: '0.1em' }}>01 / 04</span>
<span style={{ fontFamily: FF, fontSize: 9, color: 'rgba(255,255,255,0.50)', letterSpacing: '0.1em' }}>{fallbackText(section.progressLabel, participationContent.process.progressLabel)}</span>
</div>
{/* Cards strip */}
<div className="flex flex-nowrap flex-1 items-stretch px-20 pb-16 gap-0">
{STEPS.map((item, index) => {
const Icon = item.Icon;
{steps.map((item, index) => {
const Icon = ICONS[item.icon as keyof typeof ICONS] || FileText;
return (
<ProcessCard
key={item.step}
itemsLength={4}
key={`${item.step}-${index}`}
itemsLength={steps.length}
index={index}
variant="bmp"
className="min-w-[70%] max-w-[70%] flex-shrink-0"
@@ -136,7 +151,7 @@ export default function WegZurAuszeichnungSection() {
<Icon size={16} style={{ color: GOLD }} strokeWidth={1.5} />
</div>
<span style={{ fontFamily: FF, fontSize: 11, fontWeight: 900, color: 'rgba(239,191,4,0.4)', letterSpacing: '0.1em', writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>
{item.step}
{fallbackText(item.step, '')}
</span>
</ProcessCardTitle>
@@ -145,22 +160,22 @@ export default function WegZurAuszeichnungSection() {
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.22em', textTransform: 'uppercase' }}>
Schritt {item.step}
Schritt {fallbackText(item.step, '')}
</span>
<span style={{ width: 1, height: 12, background: 'rgba(255,255,255,0.2)' }} />
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(255,255,255,0.62)', letterSpacing: '0.08em' }}>{item.date}</span>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(255,255,255,0.62)', letterSpacing: '0.08em' }}>{fallbackText(item.date, '')}</span>
</div>
<div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20, opacity: 0.5 }} />
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.5rem, 2.5vw, 2.2rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: 0 }}>
{item.title}
{fallbackText(item.title, '')}
</h3>
</div>
<p style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.68)', lineHeight: 1.75, margin: 0 }}>
{item.desc}
{fallbackText(item.desc, '')}
</p>
{/* Step progress indicator */}
<div style={{ display: 'flex', gap: 4 }}>
{[0,1,2,3].map(i => (
{steps.map((_, i) => (
<div key={i} style={{ height: 2, flex: i === index ? 2 : 1, background: i === index ? GOLD : 'rgba(255,255,255,0.1)', transition: 'flex 0.3s' }} />
))}
</div>

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { ArrowRight, ArrowLeft, Check, Trophy, User, Send, Building2, AlertCircle } from 'lucide-react';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { participationContent } from '@/spa/participationContent';
const FF = '"IBM Plex Sans", sans-serif';
@@ -138,43 +139,55 @@ type FormData = {
nomBeziehung: string;
};
const STEPS_SELBST = [
{ label: 'Einreichung', icon: Trophy },
{ label: 'Schnell-Check',icon: Check },
{ label: 'Kontakt', icon: User },
{ label: 'Absenden', icon: Send },
];
const STEPS_VORSCHLAG = [
{ label: 'Einreichung', icon: Trophy },
{ label: 'Unternehmen', icon: Building2},
{ label: 'Nominierender',icon: User },
{ label: 'Absenden', icon: Send },
];
const MITARBEITER_OPTIONS = [
{ value: 'unter10', label: 'Unter 10' },
{ value: '10-50', label: '10 50' },
{ value: '51-200', label: '51 200' },
{ value: '201-500', label: '201 500'},
{ value: 'über500', label: 'Über 500' },
];
const variants = {
enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (dir: number) => ({ x: dir > 0 ? -40 : 40, opacity: 0 }),
};
type FormContent = Partial<typeof participationContent.form>;
type StepItem = { label?: string | null; icon?: string | null };
type EmployeeOption = { value?: string | null; label?: string | null };
const ICONS = {
alertCircle: AlertCircle,
arrowRight: ArrowRight,
building2: Building2,
check: Check,
send: Send,
trophy: Trophy,
user: User,
};
const fallbackText = (value: unknown, fallback: string) =>
typeof value === 'string' && value.length > 0 ? value : fallback;
const fallbackArray = <T,>(value: unknown, fallback: T[]) =>
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
const mergeFormContent = (content?: FormContent) => ({
...participationContent.form,
...(content || {}),
validation: { ...participationContent.form.validation, ...(content?.validation || {}) },
typeStep: { ...participationContent.form.typeStep, ...(content?.typeStep || {}) },
eligibility: { ...participationContent.form.eligibility, ...(content?.eligibility || {}) },
selfContact: { ...participationContent.form.selfContact, ...(content?.selfContact || {}) },
selfSummary: { ...participationContent.form.selfSummary, ...(content?.selfSummary || {}) },
nominationCompany: { ...participationContent.form.nominationCompany, ...(content?.nominationCompany || {}) },
nominationContact: { ...participationContent.form.nominationContact, ...(content?.nominationContact || {}) },
nominationSummary: { ...participationContent.form.nominationSummary, ...(content?.nominationSummary || {}) },
success: { ...participationContent.form.success, ...(content?.success || {}) },
});
/* ── Sub-components ───────────────────────────────────────────────────── */
function Field({ label, error, children, optional, c }: {
label: string; error?: string; children: React.ReactNode; optional?: boolean; c: C;
function Field({ label, error, children, optional, c, requiredSuffix }: {
label: string; error?: string; children: React.ReactNode; optional?: boolean; c: C; requiredSuffix: string;
}) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>
{label}{!optional && ' *'}
{label}{!optional && ` ${requiredSuffix}`}
</label>
{children}
{error && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{error}</span>}
@@ -242,28 +255,36 @@ function OptionButton({ selected, onClick, label, c, style }: {
);
}
function YesNo({ value, onChange, c }: {
value: 'ja' | 'nein' | ''; onChange: (v: 'ja' | 'nein') => void; c: C;
function YesNo({ value, onChange, c, yesLabel, noLabel }: {
value: 'ja' | 'nein' | ''; onChange: (v: 'ja' | 'nein') => void; c: C; yesLabel: string; noLabel: string;
}) {
const isMobile = useIsMobile();
const flexStyle = isMobile ? { flex: 1 } : undefined;
return (
<div style={{ display: 'flex', gap: 8 }}>
<OptionButton selected={value === 'ja'} onClick={() => onChange('ja')} label="Ja" c={c} style={flexStyle} />
<OptionButton selected={value === 'nein'} onClick={() => onChange('nein')} label="Nein" c={c} style={flexStyle} />
<OptionButton selected={value === 'ja'} onClick={() => onChange('ja')} label={yesLabel} c={c} style={flexStyle} />
<OptionButton selected={value === 'nein'} onClick={() => onChange('nein')} label={noLabel} c={c} style={flexStyle} />
</div>
);
}
function isEligible(data: FormData) {
return ['10-50', '51-200', '201-500'].includes(data.mitarbeiter) && data.standortBayern === 'ja';
function isEligible(data: FormData, formContent: ReturnType<typeof mergeFormContent>) {
const allowed = fallbackText(formContent.eligibility.allowedEmployeeValues, participationContent.form.eligibility.allowedEmployeeValues)
.split(',')
.map((item) => item.trim())
.filter(Boolean);
return allowed.includes(data.mitarbeiter) && data.standortBayern === fallbackText(formContent.eligibility.requiredBayernValue, 'ja');
}
/* ── Main component ───────────────────────────────────────────────────── */
export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'gold' }) {
export default function BewerbungsForm({ theme = 'dark', content }: { theme?: 'dark' | 'gold'; content?: FormContent }) {
const c = theme === 'gold' ? GOLD : DARK;
const isMobile = useIsMobile();
const formContent = mergeFormContent(content);
const selfSteps = fallbackArray<StepItem>(formContent.selfSteps, participationContent.form.selfSteps);
const nominationSteps = fallbackArray<StepItem>(formContent.nominationSteps, participationContent.form.nominationSteps);
const employeeOptions = fallbackArray<EmployeeOption>(formContent.employeeOptions, participationContent.form.employeeOptions);
const [step, setStep] = useState(0);
const [dir, setDir] = useState(1);
@@ -276,7 +297,7 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
});
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
const STEPS = data.type === 'vorschlag' ? STEPS_VORSCHLAG : STEPS_SELBST;
const STEPS = data.type === 'vorschlag' ? nominationSteps : selfSteps;
const progress = step === 0 ? 0 : (step / (STEPS.length - 1)) * 100;
const set = (k: keyof FormData, v: string) => {
@@ -288,25 +309,25 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
const e: typeof errors = {};
if (data.type === 'selbst') {
if (step === 1) {
if (!data.mitarbeiter) e.mitarbeiter = 'Bitte wählen';
if (!data.standortBayern) e.standortBayern = 'Bitte wählen';
if (!data.familiengeführt)e.familiengeführt= 'Bitte wählen';
if (!data.mitarbeiter) e.mitarbeiter = fallbackText(formContent.validation.choose, participationContent.form.validation.choose);
if (!data.standortBayern) e.standortBayern = fallbackText(formContent.validation.choose, participationContent.form.validation.choose);
if (!data.familiengeführt)e.familiengeführt= fallbackText(formContent.validation.choose, participationContent.form.validation.choose);
}
if (step === 2) {
if (!data.firmenname) e.firmenname = 'Pflichtfeld';
if (!data.kontaktName) e.kontaktName = 'Pflichtfeld';
if (!data.email) e.email = 'Pflichtfeld';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = 'Ungültige E-Mail';
if (!data.firmenname) e.firmenname = fallbackText(formContent.validation.required, participationContent.form.validation.required);
if (!data.kontaktName) e.kontaktName = fallbackText(formContent.validation.required, participationContent.form.validation.required);
if (!data.email) e.email = fallbackText(formContent.validation.required, participationContent.form.validation.required);
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = fallbackText(formContent.validation.invalidEmail, participationContent.form.validation.invalidEmail);
}
} else {
if (step === 1) {
if (!data.nomFirma) e.nomFirma = 'Pflichtfeld';
if (!data.nomBranche) e.nomBranche = 'Pflichtfeld';
if (!data.nomFirma) e.nomFirma = fallbackText(formContent.validation.required, participationContent.form.validation.required);
if (!data.nomBranche) e.nomBranche = fallbackText(formContent.validation.required, participationContent.form.validation.required);
}
if (step === 2) {
if (!data.nomName) e.nomName = 'Pflichtfeld';
if (!data.nomEmail) e.nomEmail = 'Pflichtfeld';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.nomEmail)) e.nomEmail = 'Ungültige E-Mail';
if (!data.nomName) e.nomName = fallbackText(formContent.validation.required, participationContent.form.validation.required);
if (!data.nomEmail) e.nomEmail = fallbackText(formContent.validation.required, participationContent.form.validation.required);
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.nomEmail)) e.nomEmail = fallbackText(formContent.validation.invalidEmail, participationContent.form.validation.invalidEmail);
}
}
setErrors(e);
@@ -325,12 +346,12 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
<Check size={22} color={c.successIconInner} strokeWidth={3} />
</div>
<h3 style={{ fontSize: 18, fontWeight: 900, color: c.successHeading, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 10 }}>
{data.type === 'selbst' ? 'Anfrage eingegangen' : 'Nominierung eingegangen'}
{data.type === 'selbst' ? fallbackText(formContent.success.selfHeading, participationContent.form.success.selfHeading) : fallbackText(formContent.success.nominationHeading, participationContent.form.success.nominationHeading)}
</h3>
<p style={{ fontSize: 13, color: c.successBody, maxWidth: 340, margin: '0 auto', lineHeight: 1.7 }}>
{data.type === 'selbst'
? <><span>Vielen Dank, </span><strong style={{ color: c.successName }}>{data.firmenname}</strong><span>. Wir senden Ihnen den vollständigen Fragebogen an </span><strong style={{ color: c.successAccent }}>{data.email}</strong>.</>
: <><span>Vielen Dank, </span><strong style={{ color: c.successName }}>{data.nomName}</strong><span>. Wir nehmen Kontakt mit </span><strong style={{ color: c.successAccent }}>{data.nomFirma}</strong><span> auf und informieren sie über Ihre Nominierung.</span></>
? <><span>{fallbackText(formContent.success.selfPrefix, participationContent.form.success.selfPrefix)}</span><strong style={{ color: c.successName }}>{data.firmenname}</strong><span>{fallbackText(formContent.success.selfMiddle, participationContent.form.success.selfMiddle)}</span><strong style={{ color: c.successAccent }}>{data.email}</strong><span>{fallbackText(formContent.success.selfSuffix, participationContent.form.success.selfSuffix)}</span></>
: <><span>{fallbackText(formContent.success.nominationPrefix, participationContent.form.success.nominationPrefix)}</span><strong style={{ color: c.successName }}>{data.nomName}</strong><span>{fallbackText(formContent.success.nominationMiddle, participationContent.form.success.nominationMiddle)}</span><strong style={{ color: c.successAccent }}>{data.nomFirma}</strong><span>{fallbackText(formContent.success.nominationSuffix, participationContent.form.success.nominationSuffix)}</span></>
}
</p>
</div>
@@ -349,7 +370,7 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{/* Step indicators */}
<div style={{ display: 'flex', borderBottom: `1px solid ${c.border}`, flexShrink: 0 }}>
{STEPS.map((s, i) => {
const Icon = s.icon;
const Icon = ICONS[s.icon as keyof typeof ICONS] || Trophy;
const done = i < step;
const active = i === step;
return (
@@ -363,7 +384,7 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
marginBottom: -1, transition: 'color 0.2s',
}}>
<Icon size={11} strokeWidth={active ? 2 : 1.5} />
<span>{done ? '✓' : s.label}</span>
<span>{done ? fallbackText(formContent.stepCompleteLabel, participationContent.form.stepCompleteLabel) : fallbackText(s.label, '')}</span>
</div>
);
})}
@@ -379,14 +400,14 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{/* ── Step 0: Typ-Wahl ── */}
{step === 0 && (
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 8 }}>Schritt 1</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 14 }}>Art der Einreichung</h3>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 8 }}>{fallbackText(formContent.typeStep.eyebrow, participationContent.form.typeStep.eyebrow)}</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 14 }}>{fallbackText(formContent.typeStep.heading, participationContent.form.typeStep.heading)}</h3>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 8 }}>
<TypeCard c={c} selected={data.type === 'selbst'} title="Eigenbewerbung"
desc="Ich bewerbe mein eigenes Unternehmen für den Bayerischen Mittelstandspreis."
<TypeCard c={c} selected={data.type === 'selbst'} title={fallbackText(formContent.typeStep.selfTitle, participationContent.form.typeStep.selfTitle)}
desc={fallbackText(formContent.typeStep.selfDesc, participationContent.form.typeStep.selfDesc)}
onClick={() => { set('type', 'selbst'); setDir(1); setStep(1); }} />
<TypeCard c={c} selected={data.type === 'vorschlag'} title="Vorschlag"
desc="Ich schlage ein anderes Unternehmen vor, das den Preis verdient."
<TypeCard c={c} selected={data.type === 'vorschlag'} title={fallbackText(formContent.typeStep.nominationTitle, participationContent.form.typeStep.nominationTitle)}
desc={fallbackText(formContent.typeStep.nominationDesc, participationContent.form.typeStep.nominationDesc)}
onClick={() => { set('type', 'vorschlag'); setDir(1); setStep(1); }} />
</div>
</div>
@@ -396,27 +417,27 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{step === 1 && data.type === 'selbst' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Schnell-Check</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Grundlegende Eignung</h3>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>{fallbackText(formContent.eligibility.eyebrow, participationContent.form.eligibility.eyebrow)}</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>{fallbackText(formContent.eligibility.heading, participationContent.form.eligibility.heading)}</h3>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>Wie viele Mitarbeiter hat Ihr Unternehmen? *</label>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>{fallbackText(formContent.eligibility.employeesLabel, participationContent.form.eligibility.employeesLabel)} {fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{MITARBEITER_OPTIONS.map(o => (
<OptionButton c={c} key={o.value} selected={data.mitarbeiter === o.value} onClick={() => set('mitarbeiter', o.value)} label={o.label} />
{employeeOptions.map(o => (
<OptionButton c={c} key={fallbackText(o.value, '')} selected={data.mitarbeiter === o.value} onClick={() => set('mitarbeiter', fallbackText(o.value, ''))} label={fallbackText(o.label, '')} />
))}
</div>
{errors.mitarbeiter && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{errors.mitarbeiter}</span>}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>Hat Ihr Unternehmen seinen Sitz in Bayern? *</label>
<YesNo c={c} value={data.standortBayern} onChange={v => set('standortBayern', v)} />
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>{fallbackText(formContent.eligibility.bayernLabel, participationContent.form.eligibility.bayernLabel)} {fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}</label>
<YesNo c={c} value={data.standortBayern} onChange={v => set('standortBayern', v)} yesLabel={fallbackText(formContent.yesLabel, participationContent.form.yesLabel)} noLabel={fallbackText(formContent.noLabel, participationContent.form.noLabel)} />
{errors.standortBayern && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{errors.standortBayern}</span>}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>Ist Ihr Unternehmen inhabergeführt oder familiengeführt? *</label>
<YesNo c={c} value={data.familiengeführt} onChange={v => set('familiengeführt', v)} />
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>{fallbackText(formContent.eligibility.ownerLabel, participationContent.form.eligibility.ownerLabel)} {fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}</label>
<YesNo c={c} value={data.familiengeführt} onChange={v => set('familiengeführt', v)} yesLabel={fallbackText(formContent.yesLabel, participationContent.form.yesLabel)} noLabel={fallbackText(formContent.noLabel, participationContent.form.noLabel)} />
{errors.familiengeführt && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{errors.familiengeführt}</span>}
</div>
</div>
@@ -425,42 +446,42 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{/* ── Eigenbewerbung Step 2: Kontakt / Ineligible ── */}
{step === 2 && data.type === 'selbst' && (() => {
if (!isEligible(data)) return (
if (!isEligible(data, formContent)) return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<AlertCircle size={18} color={c.alertIcon} />
<h3 style={{ fontFamily: FF, fontSize: 15, fontWeight: 800, color: c.alertTitle, textTransform: 'uppercase' }}>Leider nicht förderfähig</h3>
<h3 style={{ fontFamily: FF, fontSize: 15, fontWeight: 800, color: c.alertTitle, textTransform: 'uppercase' }}>{fallbackText(formContent.eligibility.ineligibleHeading, participationContent.form.eligibility.ineligibleHeading)}</h3>
</div>
<p style={{ fontFamily: FF, fontSize: 13, color: c.bodyText, lineHeight: 1.7 }}>
Der Bayerische Mittelstandspreis richtet sich an inhabergeführte Unternehmen mit 10500 Mitarbeitern und Sitz in Bayern. Ihr Unternehmen erfüllt diese Voraussetzungen aktuell nicht.
{fallbackText(formContent.eligibility.ineligibleBody, participationContent.form.eligibility.ineligibleBody)}
</p>
<p style={{ fontFamily: FF, fontSize: 13, color: c.mutedText, lineHeight: 1.7 }}>
Fragen? Schreiben Sie uns: <span style={{ color: c.alertEmail }}>info@bmp-bayern.de</span>
{fallbackText(formContent.eligibility.ineligibleContactPrefix, participationContent.form.eligibility.ineligibleContactPrefix)} <span style={{ color: c.alertEmail }}>{fallbackText(formContent.eligibility.ineligibleContactEmail, participationContent.form.eligibility.ineligibleContactEmail)}</span>
</p>
</div>
);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Kontakt</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Kontaktdaten</h3>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>{fallbackText(formContent.selfContact.eyebrow, participationContent.form.selfContact.eyebrow)}</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>{fallbackText(formContent.selfContact.heading, participationContent.form.selfContact.heading)}</h3>
</div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 14 }}>
<Field c={c} label="Firmenname" error={errors.firmenname}>
<StyledInput c={c} value={data.firmenname} onChange={e => set('firmenname', e.target.value)} placeholder="Muster GmbH" />
<Field c={c} label={fallbackText(formContent.selfContact.companyLabel, participationContent.form.selfContact.companyLabel)} error={errors.firmenname} requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} value={data.firmenname} onChange={e => set('firmenname', e.target.value)} placeholder={fallbackText(formContent.selfContact.companyPlaceholder, participationContent.form.selfContact.companyPlaceholder)} />
</Field>
<Field c={c} label="Ihr Name" error={errors.kontaktName}>
<StyledInput c={c} value={data.kontaktName} onChange={e => set('kontaktName', e.target.value)} placeholder="Vor- und Nachname" />
<Field c={c} label={fallbackText(formContent.selfContact.nameLabel, participationContent.form.selfContact.nameLabel)} error={errors.kontaktName} requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} value={data.kontaktName} onChange={e => set('kontaktName', e.target.value)} placeholder={fallbackText(formContent.selfContact.namePlaceholder, participationContent.form.selfContact.namePlaceholder)} />
</Field>
<Field c={c} label="E-Mail" error={errors.email}>
<StyledInput c={c} type="email" value={data.email} onChange={e => set('email', e.target.value)} placeholder="name@unternehmen.de" />
<Field c={c} label={fallbackText(formContent.selfContact.emailLabel, participationContent.form.selfContact.emailLabel)} error={errors.email} requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} type="email" value={data.email} onChange={e => set('email', e.target.value)} placeholder={fallbackText(formContent.selfContact.emailPlaceholder, participationContent.form.selfContact.emailPlaceholder)} />
</Field>
<Field c={c} label="Telefon" optional>
<StyledInput c={c} type="tel" value={data.telefon} onChange={e => set('telefon', e.target.value)} placeholder="+49 89 ..." />
<Field c={c} label={fallbackText(formContent.selfContact.phoneLabel, participationContent.form.selfContact.phoneLabel)} optional requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} type="tel" value={data.telefon} onChange={e => set('telefon', e.target.value)} placeholder={fallbackText(formContent.selfContact.phonePlaceholder, participationContent.form.selfContact.phonePlaceholder)} />
</Field>
</div>
<p style={{ fontFamily: FF, fontSize: 12, color: c.mutedText, lineHeight: 1.6 }}>
Wir senden Ihnen den vollständigen Fragebogen automatisch per E-Mail zu.
{fallbackText(formContent.selfContact.footnote, participationContent.form.selfContact.footnote)}
</p>
</div>
);
@@ -470,15 +491,15 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{step === 3 && data.type === 'selbst' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Zusammenfassung</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Bewerbung</h3>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>{fallbackText(formContent.selfSummary.eyebrow, participationContent.form.selfSummary.eyebrow)}</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>{fallbackText(formContent.selfSummary.heading, participationContent.form.selfSummary.heading)}</h3>
</div>
<div style={{ borderTop: `1px solid ${c.border}` }}>
{[
{ label: 'Unternehmen', value: data.firmenname },
{ label: 'Mitarbeiter', value: MITARBEITER_OPTIONS.find(o => o.value === data.mitarbeiter)?.label || '' },
{ label: 'Standort', value: `Bayern: ${data.standortBayern === 'ja' ? 'Ja' : 'Nein'}` },
{ label: 'Kontakt', value: `${data.kontaktName} · ${data.email}` },
{ label: fallbackText(formContent.selfSummary.companyLabel, participationContent.form.selfSummary.companyLabel), value: data.firmenname },
{ label: fallbackText(formContent.selfSummary.employeesLabel, participationContent.form.selfSummary.employeesLabel), value: employeeOptions.find(o => o.value === data.mitarbeiter)?.label || '' },
{ label: fallbackText(formContent.selfSummary.locationLabel, participationContent.form.selfSummary.locationLabel), value: `${fallbackText(formContent.selfSummary.locationPrefix, participationContent.form.selfSummary.locationPrefix)} ${data.standortBayern === 'ja' ? fallbackText(formContent.yesLabel, participationContent.form.yesLabel) : fallbackText(formContent.noLabel, participationContent.form.noLabel)}` },
{ label: fallbackText(formContent.selfSummary.contactLabel, participationContent.form.selfSummary.contactLabel), value: `${data.kontaktName} · ${data.email}` },
].map(row => (
<div key={row.label} style={{ display: 'grid', gridTemplateColumns: '90px 1fr', gap: 12, padding: '10px 0', borderBottom: `1px solid ${c.border}`, alignItems: 'baseline' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: c.rowLabel, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{row.label}</span>
@@ -493,18 +514,18 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{step === 1 && data.type === 'vorschlag' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Nominierung</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Nominiertes Unternehmen</h3>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>{fallbackText(formContent.nominationCompany.eyebrow, participationContent.form.nominationCompany.eyebrow)}</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>{fallbackText(formContent.nominationCompany.heading, participationContent.form.nominationCompany.heading)}</h3>
</div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 14 }}>
<Field c={c} label="Firmenname" error={errors.nomFirma}>
<StyledInput c={c} value={data.nomFirma} onChange={e => set('nomFirma', e.target.value)} placeholder="Muster GmbH" />
<Field c={c} label={fallbackText(formContent.nominationCompany.companyLabel, participationContent.form.nominationCompany.companyLabel)} error={errors.nomFirma} requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} value={data.nomFirma} onChange={e => set('nomFirma', e.target.value)} placeholder={fallbackText(formContent.nominationCompany.companyPlaceholder, participationContent.form.nominationCompany.companyPlaceholder)} />
</Field>
<Field c={c} label="Branche" error={errors.nomBranche}>
<StyledInput c={c} value={data.nomBranche} onChange={e => set('nomBranche', e.target.value)} placeholder="z. B. Maschinenbau" />
<Field c={c} label={fallbackText(formContent.nominationCompany.industryLabel, participationContent.form.nominationCompany.industryLabel)} error={errors.nomBranche} requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} value={data.nomBranche} onChange={e => set('nomBranche', e.target.value)} placeholder={fallbackText(formContent.nominationCompany.industryPlaceholder, participationContent.form.nominationCompany.industryPlaceholder)} />
</Field>
<Field c={c} label="Standort Bayern" optional>
<StyledInput c={c} value={data.nomStandort} onChange={e => set('nomStandort', e.target.value)} placeholder="z. B. München" />
<Field c={c} label={fallbackText(formContent.nominationCompany.locationLabel, participationContent.form.nominationCompany.locationLabel)} optional requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} value={data.nomStandort} onChange={e => set('nomStandort', e.target.value)} placeholder={fallbackText(formContent.nominationCompany.locationPlaceholder, participationContent.form.nominationCompany.locationPlaceholder)} />
</Field>
</div>
</div>
@@ -514,22 +535,22 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{step === 2 && data.type === 'vorschlag' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Nominierender</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Angaben</h3>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>{fallbackText(formContent.nominationContact.eyebrow, participationContent.form.nominationContact.eyebrow)}</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>{fallbackText(formContent.nominationContact.heading, participationContent.form.nominationContact.heading)}</h3>
</div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 14 }}>
<Field c={c} label="Ihr Name" error={errors.nomName}>
<StyledInput c={c} value={data.nomName} onChange={e => set('nomName', e.target.value)} placeholder="Vor- und Nachname" />
<Field c={c} label={fallbackText(formContent.nominationContact.nameLabel, participationContent.form.nominationContact.nameLabel)} error={errors.nomName} requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} value={data.nomName} onChange={e => set('nomName', e.target.value)} placeholder={fallbackText(formContent.nominationContact.namePlaceholder, participationContent.form.nominationContact.namePlaceholder)} />
</Field>
<Field c={c} label="Ihre E-Mail" error={errors.nomEmail}>
<StyledInput c={c} type="email" value={data.nomEmail} onChange={e => set('nomEmail', e.target.value)} placeholder="ihre@email.de" />
<Field c={c} label={fallbackText(formContent.nominationContact.emailLabel, participationContent.form.nominationContact.emailLabel)} error={errors.nomEmail} requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} type="email" value={data.nomEmail} onChange={e => set('nomEmail', e.target.value)} placeholder={fallbackText(formContent.nominationContact.emailPlaceholder, participationContent.form.nominationContact.emailPlaceholder)} />
</Field>
<Field c={c} label="Ihre Beziehung zum Unternehmen" optional>
<StyledInput c={c} value={data.nomBeziehung} onChange={e => set('nomBeziehung', e.target.value)} placeholder="z. B. Kunde, Partner, Bekannter" />
<Field c={c} label={fallbackText(formContent.nominationContact.relationshipLabel, participationContent.form.nominationContact.relationshipLabel)} optional requiredSuffix={fallbackText(formContent.requiredSuffix, participationContent.form.requiredSuffix)}>
<StyledInput c={c} value={data.nomBeziehung} onChange={e => set('nomBeziehung', e.target.value)} placeholder={fallbackText(formContent.nominationContact.relationshipPlaceholder, participationContent.form.nominationContact.relationshipPlaceholder)} />
</Field>
</div>
<p style={{ fontFamily: FF, fontSize: 12, color: c.mutedText, lineHeight: 1.6 }}>
Wir nehmen Kontakt mit dem nominierten Unternehmen auf und informieren es über Ihre Nominierung.
{fallbackText(formContent.nominationContact.footnote, participationContent.form.nominationContact.footnote)}
</p>
</div>
)}
@@ -538,15 +559,15 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
{step === 3 && data.type === 'vorschlag' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Zusammenfassung</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Nominierung</h3>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>{fallbackText(formContent.nominationSummary.eyebrow, participationContent.form.nominationSummary.eyebrow)}</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>{fallbackText(formContent.nominationSummary.heading, participationContent.form.nominationSummary.heading)}</h3>
</div>
<div style={{ borderTop: `1px solid ${c.border}` }}>
{[
{ label: 'Unternehmen', value: data.nomFirma },
{ label: 'Branche', value: data.nomBranche },
{ label: 'Standort', value: data.nomStandort || '' },
{ label: 'Nominiert von',value: `${data.nomName} · ${data.nomEmail}` },
{ label: fallbackText(formContent.nominationSummary.companyLabel, participationContent.form.nominationSummary.companyLabel), value: data.nomFirma },
{ label: fallbackText(formContent.nominationSummary.industryLabel, participationContent.form.nominationSummary.industryLabel), value: data.nomBranche },
{ label: fallbackText(formContent.nominationSummary.locationLabel, participationContent.form.nominationSummary.locationLabel), value: data.nomStandort || fallbackText(formContent.nominationSummary.emptyValue, participationContent.form.nominationSummary.emptyValue) },
{ label: fallbackText(formContent.nominationSummary.nominatedByLabel, participationContent.form.nominationSummary.nominatedByLabel),value: `${data.nomName} · ${data.nomEmail}` },
].map(row => (
<div key={row.label} style={{ display: 'grid', gridTemplateColumns: '100px 1fr', gap: 12, padding: '10px 0', borderBottom: `1px solid ${c.border}`, alignItems: 'baseline' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: c.rowLabel, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{row.label}</span>
@@ -574,11 +595,11 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
onMouseEnter={e => (e.currentTarget.style.color = c.backTextHover)}
onMouseLeave={e => (e.currentTarget.style.color = c.backText)}
>
<ArrowLeft size={12} /> Zurück
<ArrowLeft size={12} /> {fallbackText(formContent.backLabel, participationContent.form.backLabel)}
</button>
{step < STEPS.length - 1 ? (
step === 2 && data.type === 'selbst' && !isEligible(data) ? <div /> :
step === 2 && data.type === 'selbst' && !isEligible(data, formContent) ? <div /> :
<button type="button" onClick={next} style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase',
letterSpacing: '0.1em', color: c.btnText, background: c.btnBg,
@@ -588,7 +609,7 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = c.btnBgHover; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = c.btnBg; }}
>
Weiter <ArrowRight size={12} />
{fallbackText(formContent.nextLabel, participationContent.form.nextLabel)} <ArrowRight size={12} />
</button>
) : (
<button type="button" onClick={submit} style={{
@@ -600,7 +621,7 @@ export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'g
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = c.btnBgHover; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = c.btnBg; }}
>
Absenden <Send size={12} />
{fallbackText(formContent.submitLabel, participationContent.form.submitLabel)} <Send size={12} />
</button>
)}
</div>

View File

@@ -1,10 +1,13 @@
import React, { useRef } from 'react';
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star, type LucideIcon } from 'lucide-react';
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react';
import { Link } from '@/spa/router';
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useCmsRoute } from '@/spa/cmsRoute';
import { participationContent } from '@/spa/participationContent';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif';
@@ -13,9 +16,50 @@ const NAVY = '#111D55';
const GOLD = '#EFBF04';
const CREAM = '#E4E2E3';
type ParticipationCms = Partial<typeof participationContent> & {
hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown };
evaluation?: Partial<typeof participationContent.evaluation> & { image?: unknown };
applicationForm?: Partial<typeof participationContent.applicationForm> & { image?: unknown };
};
const ICONS = {
building2: Building2,
mapPin: MapPin,
send: Send,
star: Star,
trendingUp: TrendingUp,
userCheck: UserCheck,
users: Users,
};
const fallbackText = (value: unknown, fallback: string) =>
typeof value === 'string' && value.length > 0 ? value : fallback;
const fallbackArray = <T,>(value: unknown, fallback: T[]) =>
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
function Lines({ text }: { text: string }) {
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
}
const Participation: React.FC = () => {
const isMobile = useIsMobile();
const qualSectionRef = useRef<HTMLElement>(null);
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
const process = { ...participationContent.process, ...(cms.process || {}) };
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
const evaluation = { ...participationContent.evaluation, ...(cms.evaluation || {}) };
const applicationWays = { ...participationContent.applicationWays, ...(cms.applicationWays || {}) };
const datesBanner = { ...participationContent.datesBanner, ...(cms.datesBanner || {}) };
const applicationForm = { ...participationContent.applicationForm, ...(cms.applicationForm || {}) };
const form = { ...participationContent.form, ...(cms.form || {}) };
const eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
const evaluationCriteria = fallbackArray(evaluation.criteria, participationContent.evaluation.criteria);
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
const desktopDates = fallbackArray(datesBanner.desktopItems, participationContent.datesBanner.desktopItems);
const applicationFacts = fallbackArray(applicationForm.facts, participationContent.applicationForm.facts);
return (
<div className="animate-fade-in">
@@ -23,8 +67,8 @@ const Participation: React.FC = () => {
{/* ── HERO ─────────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', minHeight: '65vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: NAVY }}>
<Image unoptimized
src="/images/preisuebergabe.jpg"
alt="Teilnahme BMP"
src={mediaUrl(hero.backgroundImage, '/images/preisuebergabe.jpg')}
alt={mediaAlt(hero.backgroundImage, fallbackText(hero.imageAlt, participationContent.hero.imageAlt))}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
<div style={{ position: 'absolute', inset: 0, background: isMobile ? 'linear-gradient(to top, rgba(2,9,48,0.95) 0%, rgba(2,9,48,0.72) 38%, rgba(2,9,48,0.22) 72%, transparent 100%)' : 'linear-gradient(to right, #020930 0%, rgba(2,9,48,0.90) 38%, rgba(2,9,48,0.18) 65%, transparent 100%)' }} />
@@ -33,20 +77,20 @@ const Participation: React.FC = () => {
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '0 24px 48px' : '0 80px 80px', maxWidth: 800 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 24 }}>
<div style={{ width: 36, height: 2, background: GOLD }} />
<span style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: '#EFBF04' }}>Bewerbungsphase 2026</span>
<span style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: '#EFBF04' }}>{fallbackText(hero.eyebrow, participationContent.hero.eyebrow)}</span>
</div>
<h1 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.6rem, 6vw, 5rem)' : 'clamp(2.8rem, 6vw, 5rem)', fontWeight: 900, color: '#fff', lineHeight: 0.95, letterSpacing: '-0.03em', textTransform: 'uppercase', margin: '0 0 24px' }}>
GESTALTEN SIE<br />BAYERNS ZUKUNFT.
<Lines text={fallbackText(hero.heading, participationContent.hero.heading)} />
</h1>
<div style={{ width: 48, height: 2, background: GOLD, marginBottom: 24 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.6)', lineHeight: 1.7, maxWidth: 520, fontWeight: 300 }}>
Alles, was Sie für Ihre erfolgreiche Bewerbung zum Bayerischen Mittelstandspreis wissen müssen.
{fallbackText(hero.description, participationContent.hero.description)}
</p>
</div>
</section>
{/* ── TIMELINE direkt nach Hero ──────────────────────────────────────── */}
<WegZurAuszeichnungSection />
<WegZurAuszeichnungSection content={process} />
{/* ── QUALIFICATIONS ───────────────────────────────────────────────────── */}
<section id="voraussetzungen" ref={qualSectionRef} style={{ background: CREAM, overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
@@ -55,45 +99,40 @@ const Participation: React.FC = () => {
{/* Header */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 48, alignItems: 'flex-end', borderBottom: `1px solid #D0D5DD` }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>Berechtigung</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(eligibility.eyebrow, participationContent.eligibility.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.5vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0 }}>
WER KANN<br />TEILNEHMEN?
<Lines text={fallbackText(eligibility.heading, participationContent.eligibility.heading)} />
</h2>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: isMobile ? 'flex-start' : 'flex-end', justifyContent: 'space-between', gap: 24 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, flexShrink: 0 }}>
<Link
to="#bewerben"
to={fallbackText(eligibility.primaryCta?.url, participationContent.eligibility.primaryCta.url)}
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '11px 22px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, transition: 'opacity 0.2s', whiteSpace: 'nowrap' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '0.85')}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}
>
Jetzt bewerben <ArrowRight size={13} />
{fallbackText(eligibility.primaryCta?.label, participationContent.eligibility.primaryCta.label)} <ArrowRight size={13} />
</Link>
<Link
to="/mitglied-werden"
to={fallbackText(eligibility.secondaryCta?.url, participationContent.eligibility.secondaryCta.url)}
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: NAVY, border: `1.5px solid rgba(3,9,58,0.3)`, padding: '10px 22px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, transition: 'border-color 0.2s, color 0.2s', whiteSpace: 'nowrap' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = NAVY; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(3,9,58,0.3)'; }}
>
Mitglied werden
{fallbackText(eligibility.secondaryCta?.label, participationContent.eligibility.secondaryCta.label)}
</Link>
</div>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.8, margin: 0, textAlign: isMobile ? 'left' : 'right' }}>
Vier klar definierte Kriterien: Erfüllen Sie alle, bewerben Sie sich kostenlos und ohne bürokratischen Aufwand.
{fallbackText(eligibility.description, participationContent.eligibility.description)}
</p>
</div>
</div>
{/* 4 criteria as editorial rows */}
<div style={{ position: 'relative', zIndex: 1, borderBottom: `1px solid #D0D5DD` }}>
{[
{ num: '01', title: 'Standort Bayern', body: 'Sitz oder wesentliche Betriebsstätte im Freistaat Bayern.', Icon: MapPin },
{ num: '02', title: 'KMU-Größe', body: '50 bis 1.500 Mitarbeiter: klassischer Mittelstand im privatwirtschaftlichen Bereich.', Icon: Users },
{ num: '03', title: 'Marktreife', body: 'Mindestens drei vollständige Geschäftsjahre am Markt tätig.', Icon: TrendingUp },
{ num: '04', title: 'Privatwirtschaft', body: 'Keine überwiegende Zugehörigkeit zu staatlichen oder kommunalen Trägern.', Icon: Building2 },
].map((item, i, arr) => {
const Icon = item.Icon;
{eligibilityCriteria.map((item, i, arr) => {
const Icon = ICONS[item.icon as keyof typeof ICONS] || MapPin;
return (
<QualRow key={i} item={item} Icon={Icon} last={i === arr.length - 1} />
);
@@ -102,39 +141,35 @@ const Participation: React.FC = () => {
{/* Zusätzliche Hinweise: Verbund + Wiederbewerber */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '28px 24px' : '40px 80px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 48, borderBottom: '1px solid #D0D5DD' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14 }}>
<Building2 size={18} style={{ color: GOLD, flexShrink: 0, marginTop: 3 }} strokeWidth={1.7} />
{eligibilityNotes.map((note, i) => {
const Icon = ICONS[note.icon as keyof typeof ICONS] || Building2;
return (
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 14 }}>
<Icon size={18} style={{ color: GOLD, flexShrink: 0, marginTop: 3 }} strokeWidth={1.7} />
<div>
<div style={{ fontFamily: FF, fontSize: 13, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>Verbund &amp; Gemeinschaft</div>
<div style={{ fontFamily: FF, fontSize: 13, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>{fallbackText(note.title, '')}</div>
<p style={{ fontFamily: FB, fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, margin: 0 }}>
Oder Sie sind ein Verbund bzw. eine Gemeinschaft von mittelständischen Unternehmen oder von mittelständischen Unternehmen und Organisationen/Institutionen, die einen Schwerpunkt in Bayern hat.
</p>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14 }}>
<UserCheck size={18} style={{ color: GOLD, flexShrink: 0, marginTop: 3 }} strokeWidth={1.7} />
<div>
<div style={{ fontFamily: FF, fontSize: 13, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>Erneute Bewerbung</div>
<p style={{ fontFamily: FB, fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, margin: 0 }}>
Nominierte ohne Gewinn können sich sofort erneut bewerben. Preisträger können sich nach 7 Jahren erneut bewerben.
{fallbackText(note.body, '')}
</p>
</div>
</div>
);
})}
</div>
{/* CTA nudge */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '24px 24px' : '32px 80px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'center', justifyContent: 'space-between', gap: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<CheckCircle2 size={18} style={{ color: GOLD, flexShrink: 0 }} />
<span style={{ fontFamily: FF, fontSize: 18, fontWeight: 600, color: '#101828' }}>Alle 4 Punkte erfüllt? Dann jetzt kostenlos bewerben.</span>
<span style={{ fontFamily: FF, fontSize: 18, fontWeight: 600, color: '#101828' }}>{fallbackText(eligibility.nudgeText, participationContent.eligibility.nudgeText)}</span>
</div>
<Link
to="#bewerben"
to={fallbackText(eligibility.nudgeCta?.url, participationContent.eligibility.nudgeCta.url)}
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '13px 28px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, transition: 'opacity 0.2s', whiteSpace: 'nowrap' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '0.85')}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}
>
Zum Formular <ArrowRight size={13} />
{fallbackText(eligibility.nudgeCta?.label, participationContent.eligibility.nudgeCta.label)} <ArrowRight size={13} />
</Link>
</div>
</section>
@@ -148,8 +183,8 @@ const Participation: React.FC = () => {
{/* Left full-bleed image with gradient blend into navy on the right (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
<Image unoptimized
src="/images/buehne-gewinner.jpg"
alt="Jury-Bewertung BMP"
src={mediaUrl(evaluation.image, '/images/buehne-gewinner.jpg')}
alt={mediaAlt(evaluation.image, fallbackText(evaluation.imageAlt, participationContent.evaluation.imageAlt))}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
/>
{/* Dark overlay for depth */}
@@ -163,9 +198,9 @@ const Participation: React.FC = () => {
{/* Heading overlay mobile only */}
{isMobile && (
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>Bewertung</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(evaluation.eyebrow, participationContent.evaluation.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 9vw, 2.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 0.97, margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.6)' }}>
UNSERE<br />KRITERIEN.
<Lines text={fallbackText(evaluation.heading, participationContent.evaluation.heading)} />
</h2>
</div>
)}
@@ -175,40 +210,32 @@ const Participation: React.FC = () => {
<div style={{ padding: isMobile ? '32px 24px 40px' : '80px 80px 72px 64px', display: 'flex', flexDirection: 'column', justifyContent: 'center', borderBottom: '1px solid rgba(255,255,255,0.07)' }}>
{!isMobile && (
<>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>Bewertung</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(evaluation.eyebrow, participationContent.evaluation.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2.4rem, 4vw, 3.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 0.97, margin: '0 0 28px' }}>
UNSERE<br />KRITERIEN.
<Lines text={fallbackText(evaluation.heading, participationContent.evaluation.heading)} />
</h2>
</>
)}
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 28 }} />
<p style={{ fontFamily: FB, fontSize: 19, color: 'rgba(255,255,255,0.68)', lineHeight: 1.8, margin: 0, maxWidth: 420 }}>
Vier zentrale Säulen, bewertet durch eine vollständig unabhängige Expertenjury aus Wirtschaft, Wissenschaft und Gesellschaft.
<br />
<br />
Die Kriterien werden nicht gleich gewichtet.
<Lines text={fallbackText(evaluation.body, participationContent.evaluation.body)} />
</p>
<a
href="https://www.der-bayerische-mittelstandspreis.de/der-preis/kriterien/"
href={fallbackText(evaluation.link?.url, participationContent.evaluation.link.url)}
target="_blank"
rel="noopener noreferrer"
style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: GOLD, textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, marginTop: 28, borderBottom: `1px solid rgba(239,191,4,0.4)`, paddingBottom: 2, alignSelf: 'flex-start' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '0.8')}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}
>
Kriterien &amp; Bewertung <ArrowRight size={13} />
{fallbackText(evaluation.link?.label, participationContent.evaluation.link.label)} <ArrowRight size={13} />
</a>
</div>
</div>
{/* BOTTOM: full-width 4-column criteria grid */}
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(4, 1fr)', borderTop: '1px solid rgba(255,255,255,0.07)', position: 'relative' }}>
{[
{ title: 'Innovations\u00ADkraft', desc: 'Zukunftsweisende Produkte, Dienstleistungen oder interne Prozessinnovationen.' },
{ title: 'Nachhaltigkeit', desc: 'Ökologische Verantwortung und Ressourcen-Effizienz im operativen Kern.' },
{ title: 'Unternehmens\u00ADkultur', desc: 'Mitarbeiterbindung, Aus- und Weiterbildung sowie wertebasierte Führung.' },
{ title: 'Regionale Wurzeln', desc: 'Engagement am Standort Bayern und Beitrag zur regionalen Wertschöpfung.' },
].map((crit, i) => (
{evaluationCriteria.map((crit, i) => (
<CriteriaCell key={i} crit={crit} idx={i} />
))}
</div>
@@ -217,23 +244,19 @@ const Participation: React.FC = () => {
</section>
{/* ── BEWERBUNGSWEGE ───────────────────────────────────────────────────── */}
<BewerbungswegeSection />
<BewerbungswegeSection content={applicationWays} />
{/* ── DATES BANNER ─────────────────────────────────────────────────────── */}
<section id="fristen" style={{ background: GOLD }}>
{isMobile ? (
/* Mobile: slim horizontal bar */
<div style={{ padding: '24px 22px 26px' }}>
<div style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.2em', marginBottom: 18 }}>Wichtige Termine 2026</div>
<div style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.2em', marginBottom: 18 }}>{fallbackText(datesBanner.heading, participationContent.datesBanner.heading)}</div>
<div style={{ display: 'flex' }}>
{[
{ date: '30. Juni 2026', label: 'Bewerbungsschluss' },
{ date: 'August 2026', label: 'Nominierte' },
{ date: 'Oktober 2026', label: 'Gala-Verleihung' },
].map((d, i) => (
{mobileDates.map((d, i) => (
<div key={i} style={{ flex: 1, minWidth: 0, paddingLeft: i > 0 ? 14 : 0, borderLeft: i > 0 ? '1px solid rgba(3,9,58,0.2)' : 'none' }}>
<div style={{ fontFamily: FF, fontSize: 17, fontWeight: 900, color: '#101828', letterSpacing: '-0.02em', lineHeight: 1.12, marginBottom: 6 }}>{d.date}</div>
<div style={{ fontFamily: FF, fontSize: 9.5, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.55)', lineHeight: 1.25 }}>{d.label}</div>
<div style={{ fontFamily: FF, fontSize: 17, fontWeight: 900, color: '#101828', letterSpacing: '-0.02em', lineHeight: 1.12, marginBottom: 6 }}>{fallbackText(d.date, '')}</div>
<div style={{ fontFamily: FF, fontSize: 9.5, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.55)', lineHeight: 1.25 }}>{fallbackText(d.label, '')}</div>
</div>
))}
</div>
@@ -242,21 +265,17 @@ const Participation: React.FC = () => {
/* Desktop: full banner */
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1px 1fr 1px 1fr 1px 1fr', maxWidth: 1200, margin: '0 auto' }}>
<div style={{ padding: '48px 56px' }}>
<h3 style={{ fontFamily: FF, fontSize: 18, fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 6 }}>Wichtige Termine 2026</h3>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.6)', margin: 0 }}>Planen Sie Ihre Teilnahme rechtzeitig.</p>
<h3 style={{ fontFamily: FF, fontSize: 18, fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 6 }}>{fallbackText(datesBanner.heading, participationContent.datesBanner.heading)}</h3>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.6)', margin: 0 }}>{fallbackText(datesBanner.description, participationContent.datesBanner.description)}</p>
</div>
<div style={{ background: 'rgba(3,9,58,0.12)' }} />
{[
{ date: '30. Juni 2026', label: 'Bewerbungsschluss' },
{ date: 'August 2026', label: 'Bekanntgabe Nominierte' },
{ date: 'Oktober 2026', label: 'Gala-Verleihung' },
].map((d, i) => (
{desktopDates.map((d, i) => (
<React.Fragment key={i}>
<div style={{ padding: '48px 40px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ fontFamily: FF, fontSize: 'clamp(1.1rem, 1.8vw, 1.4rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.02em', marginBottom: 6 }}>{d.date}</div>
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.55)' }}>{d.label}</div>
<div style={{ fontFamily: FF, fontSize: 'clamp(1.1rem, 1.8vw, 1.4rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.02em', marginBottom: 6 }}>{fallbackText(d.date, '')}</div>
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.55)' }}>{fallbackText(d.label, '')}</div>
</div>
{i < 2 && <div style={{ background: 'rgba(3,9,58,0.12)' }} />}
{i < desktopDates.length - 1 && <div style={{ background: 'rgba(3,9,58,0.12)' }} />}
</React.Fragment>
))}
</div>
@@ -268,25 +287,21 @@ const Participation: React.FC = () => {
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr', flex: 1, minHeight: 0, overflow: isMobile ? 'visible' : 'hidden' }}>
{/* Left copy */}
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: isMobile ? 'visible' : 'hidden' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>Direktbewerbung</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(applicationForm.eyebrow, participationContent.applicationForm.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.2vw, 2.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.04, margin: '0 0 20px' }}>
BEWERBEN ODER<br />VORSCHLAGEN.
<Lines text={fallbackText(applicationForm.heading, participationContent.applicationForm.heading)} />
</h2>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.68)', lineHeight: 1.8, maxWidth: 400, marginBottom: 48 }}>
Sie können sich selbst bewerben oder ein herausragendes Unternehmen vorschlagen. Firmenverbunde können sich ebenfalls bewerben. Die Teilnahme ist vollständig kostenfrei.
{fallbackText(applicationForm.description, participationContent.applicationForm.description)}
</p>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
{[
{ num: '01', label: '0 EUR', desc: 'Teilnahmegebühr' },
{ num: '02', label: 'Schnell', desc: 'Ca. 3045 Minuten' },
{ num: '03', label: 'Mehrstufig', desc: 'Transparenter Prozess' },
].map((item, i) => (
{applicationFacts.map((item, i) => (
<div key={i} style={{ display: 'grid', gridTemplateColumns: '36px 1fr', gap: '0 16px', padding: '16px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', alignItems: 'center' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(239,191,4,0.5)', letterSpacing: '0.1em' }}>{item.num}</span>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(239,191,4,0.5)', letterSpacing: '0.1em' }}>{fallbackText(item.num, '')}</span>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{item.label}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(255,255,255,0.62)' }}>{item.desc}</span>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{fallbackText(item.label, '')}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(255,255,255,0.62)' }}>{fallbackText(item.desc, '')}</span>
</div>
</div>
))}
@@ -314,39 +329,39 @@ const Participation: React.FC = () => {
{!isMobile && (
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<Image unoptimized src="/images/preistraeger-jubel.jpg" alt="Preisverleihung 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
<Image unoptimized src={mediaUrl(applicationForm.image, '/images/preistraeger-jubel.jpg')} alt={mediaAlt(applicationForm.image, fallbackText(applicationForm.imageAlt, participationContent.applicationForm.imageAlt))} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 5, height: 5, borderRadius: '50%', background: 'rgba(17,29,85,0.7)' }} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(17,29,85,0.75)', textTransform: 'uppercase', letterSpacing: '0.2em' }}>Preisverleihung 2025</span>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(17,29,85,0.75)', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{fallbackText(applicationForm.imageCaption, participationContent.applicationForm.imageCaption)}</span>
</div>
</div>
)}
<div style={{ padding: isMobile ? '28px 20px' : '24px 48px 32px 40px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: isMobile ? 560 : 0, position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.28em', fontWeight: 700 }}>Bewerbung 2026</span>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.28em', fontWeight: 700 }}>{fallbackText(applicationForm.formEyebrow, participationContent.applicationForm.formEyebrow)}</span>
<Link
to="/formular-hochladen"
to={fallbackText(applicationForm.uploadCta?.url, participationContent.applicationForm.uploadCta.url)}
style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(17,29,85,0.55)', border: '1px solid rgba(17,29,85,0.2)', padding: '7px 14px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, transition: 'color 0.15s, border-color 0.15s', whiteSpace: 'nowrap', flexShrink: 0 }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = '#111D55'; (e.currentTarget as HTMLElement).style.borderColor = '#111D55'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'rgba(17,29,85,0.55)'; (e.currentTarget as HTMLElement).style.borderColor = 'rgba(17,29,85,0.2)'; }}
>
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M6 1v7M3 4l3-3 3 3M1 10h10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
PDF hochladen
{fallbackText(applicationForm.uploadCta?.label, participationContent.applicationForm.uploadCta.label)}
</Link>
</div>
<BewerbungsForm theme="gold" />
<BewerbungsForm theme="gold" content={form} />
</div>
</div>
</div>
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '16px 20px 20px' : '14px 56px 18px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, justifyContent: 'center', textAlign: 'center', borderTop: '1px solid rgba(255,255,255,0.06)', flexShrink: 0 }}>
<span style={{ fontFamily: FB, fontSize: 14, color: 'rgba(255,255,255,0.55)', lineHeight: 1.6, maxWidth: 520 }}>
Sie haben Ihre Unterlagen bereits als PDF vorbereitet? Laden Sie sie direkt hoch, statt das Formular auszufüllen.
{fallbackText(applicationForm.uploadPrompt, participationContent.applicationForm.uploadPrompt)}
</span>
<Link to="/formular-hochladen" style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(239,191,4,0.7)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, borderBottom: '1px solid rgba(239,191,4,0.3)', paddingBottom: 1 }}>
Formular hochladen
<Link to={fallbackText(applicationForm.uploadFooterCta?.url, participationContent.applicationForm.uploadFooterCta.url)} style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(239,191,4,0.7)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, borderBottom: '1px solid rgba(239,191,4,0.3)', paddingBottom: 1 }}>
{fallbackText(applicationForm.uploadFooterCta?.label, participationContent.applicationForm.uploadFooterCta.label)}
</Link>
</div>
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
@@ -408,31 +423,26 @@ function CriteriaCell({ crit, idx }: { crit: { title: string; desc: string }; id
// ── BewerbungswegeSection ─────────────────────────────────────────────────────
const INSTITUTIONEN = [
'Senatoren und Mitglieder der Verbandsgruppe „Wir Eigentümerunternehmer"',
'Bundesverband Deutscher Mittelstand (BM)',
'Europäisches Wirtschaftsforum (EWIF)',
'Union Mittelständischer Unternehmen (UMU)',
'HAM Hochschule für angewandtes Management und deren Regionalpartner',
'Mitglieder des Wirtschaftsbeirates Bayern',
'Ehemalige Preisträger des Bayerischen Mittelstandspreises',
];
type ApplicationWaysContent = Partial<typeof participationContent.applicationWays>;
function BewerbungswegeSection() {
function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }) {
const isMobile = useIsMobile();
const section = { ...participationContent.applicationWays, ...(content || {}) };
const institutions = fallbackArray(section.institutions, participationContent.applicationWays.institutions);
const ways = fallbackArray(section.ways, participationContent.applicationWays.ways);
return (
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
{/* Header */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid #D0D5DD' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>Bewerbung 2026</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(section.eyebrow, participationContent.applicationWays.eyebrow)}</span>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 16 : 48, alignItems: 'flex-end' }}>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.5vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0 }}>
HIER KÖNNEN SIE SICH<br />BEWERBEN ODER EIN<br />UNTERNEHMEN VORSCHLAGEN.
<Lines text={fallbackText(section.heading, participationContent.applicationWays.heading)} />
</h2>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}>
Als Unternehmen haben Sie zwei Möglichkeiten: Sie werden von einem unterstützenden Partner oder einer Institution vorgeschlagen, oder Sie ergreifen selbst die Initiative und füllen unsere Anmeldung aus. Die Teilnahme ist in allen Fällen kostenfrei.
{fallbackText(section.description, participationContent.applicationWays.description)}
</p>
</div>
</div>
@@ -441,47 +451,44 @@ function BewerbungswegeSection() {
<div style={{ position: 'relative', zIndex: 1, display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(3, 1fr)', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', gap: isMobile ? 14 : 0, padding: isMobile ? '28px 24px 36px' : 0 }}>
{/* ── MOBILE: unified premium cards ── */}
{isMobile && ([
{ weg: 'Weg 01', title: 'Vorschlag unterbreiten', desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.', Icon: Send, checkItems: INSTITUTIONEN, cta: 'Vorschlag unterbreiten', featured: false },
{ weg: 'Weg 02', title: 'Initiativbewerbung', desc: 'Für Unternehmer, die selbst die Initiative ergreifen.', Icon: UserCheck, kvItems: [ { k: '0 EUR', v: 'Teilnahmegebühr' }, { k: 'Eigeninitiative', v: 'Direkt ohne Intermediär' }, { k: 'Für Unternehmer', v: 'Inhabergeführte KMU in Bayern' }, { k: 'Deadline', v: '30. Juni 2026' } ], cta: 'Jetzt bewerben', featured: true },
{ weg: 'Weg 03 · Sonderpreis', title: 'Bavarian Future Award', desc: 'Sonderpreis, initiiert durch die Studierenden der HAM, Hochschule für angewandtes Management.', Icon: Star, kvItems: [ { k: 'Vorschlag möglich', v: 'Durch HAM & Partner' }, { k: 'Selbstbewerbung', v: 'Direkte Einreichung' }, { k: 'Initiiert von', v: 'Studierenden der HAM' }, { k: 'Kategorie', v: 'Zukunft & Innovation' } ], cta: 'Zum Formular', featured: false },
] as { weg: string; title: string; desc: string; Icon: LucideIcon; checkItems?: string[]; kvItems?: { k: string; v: string }[]; cta: string; featured: boolean }[]).map((w) => {
const Icon = w.Icon;
{isMobile && ways.map((w) => {
const Icon = ICONS[w.icon as keyof typeof ICONS] || Send;
const facts = fallbackArray<{ label?: string | null; desc?: string | null }>(w.facts, []);
return (
<div key={w.weg} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: w.featured ? `1.5px solid ${GOLD}` : '1px solid rgba(17,29,85,0.10)', boxShadow: w.featured ? '0 14px 34px rgba(239,191,4,0.20), 0 6px 16px rgba(3,9,58,0.10)' : '0 10px 28px rgba(3,9,58,0.08)' }}>
<div key={fallbackText(w.label, '')} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: w.featured ? `1.5px solid ${GOLD}` : '1px solid rgba(17,29,85,0.10)', boxShadow: w.featured ? '0 14px 34px rgba(239,191,4,0.20), 0 6px 16px rgba(3,9,58,0.10)' : '0 10px 28px rgba(3,9,58,0.08)' }}>
{/* Header */}
<div style={{ padding: '22px 22px 0', position: 'relative' }}>
{w.featured && (
<span style={{ position: 'absolute', top: 22, right: 22, fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '5px 10px', borderRadius: 100 }}>Empfohlen</span>
<span style={{ position: 'absolute', top: 22, right: 22, fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '5px 10px', borderRadius: 100 }}>{fallbackText(section.recommendedLabel, participationContent.applicationWays.recommendedLabel)}</span>
)}
<div style={{ width: 46, height: 46, borderRadius: 12, background: w.featured ? GOLD : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 18 }}>
<Icon size={20} style={{ color: w.featured ? '#101828' : GOLD }} strokeWidth={1.6} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: w.featured ? '#A87800' : 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 8 }}>{w.weg}</span>
<h3 style={{ fontFamily: FF, fontSize: '1.45rem', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 10px' }}>{w.title}</h3>
<p style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.55)', lineHeight: 1.6, margin: '0 0 20px' }}>{w.desc}</p>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: w.featured ? '#A87800' : 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 8 }}>{fallbackText(w.label, '')}</span>
<h3 style={{ fontFamily: FF, fontSize: '1.45rem', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 10px' }}><Lines text={fallbackText(w.title, '')} /></h3>
<p style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.55)', lineHeight: 1.6, margin: '0 0 20px' }}>{fallbackText(w.desc, '')}</p>
<div style={{ width: 36, height: 2, background: GOLD }} />
</div>
{/* Detail list */}
<div style={{ padding: '4px 22px 0', flex: 1 }}>
{w.checkItems && w.checkItems.map((inst, i) => (
{w.listType === 'institutions' && institutions.map((inst, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 0', borderBottom: '1px solid rgba(17,29,85,0.08)' }}>
<div style={{ width: 18, height: 18, borderRadius: 5, background: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, marginTop: 1 }}>
<svg width="9" height="9" viewBox="0 0 8 8"><polyline points="1,4 3,6 7,2" stroke="#101828" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.7)', lineHeight: 1.5 }}>{inst}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.7)', lineHeight: 1.5 }}>{fallbackText(inst.text, '')}</span>
</div>
))}
{w.kvItems && w.kvItems.map((item, i) => (
{w.listType !== 'institutions' && facts.map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12, padding: '13px 0', borderBottom: '1px solid rgba(17,29,85,0.08)' }}>
<span style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.06em', flexShrink: 0 }}>{item.k}</span>
<span style={{ fontFamily: FB, fontSize: 15, color: 'rgba(16,24,40,0.5)', textAlign: 'right' }}>{item.v}</span>
<span style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.06em', flexShrink: 0 }}>{fallbackText(item.label, '')}</span>
<span style={{ fontFamily: FB, fontSize: 15, color: 'rgba(16,24,40,0.5)', textAlign: 'right' }}>{fallbackText(item.desc, '')}</span>
</div>
))}
</div>
{/* CTA */}
<div style={{ padding: '18px 22px 22px' }}>
<a href="#bewerben" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, minHeight: 52, background: GOLD, color: '#101828', fontFamily: FF, fontSize: 14, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', textDecoration: 'none', borderRadius: 10 }}>{w.cta} <ArrowRight size={14} /></a>
<a href={fallbackText(w.cta?.url, '#bewerben')} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, minHeight: 52, background: GOLD, color: '#101828', fontFamily: FF, fontSize: 14, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', textDecoration: 'none', borderRadius: 10 }}>{fallbackText(w.cta?.label, 'Zum Formular')} <ArrowRight size={14} /></a>
</div>
</div>
);
@@ -490,114 +497,51 @@ function BewerbungswegeSection() {
{/* ── DESKTOP: original 3 columns ── */}
{!isMobile && (<>
{/* ── Weg A: Vorschlag ── */}
<div style={{ padding: isMobile ? '32px 24px' : '56px 48px 56px', borderRight: '1px solid #D0D5DD', display: 'flex', flexDirection: 'column', ...(isMobile ? { minWidth: '83vw', maxWidth: '83vw', flexShrink: 0, scrollSnapAlign: 'start' } : {}) }}>
<div style={{ width: 44, height: 44, background: NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
<Send size={18} style={{ color: GOLD }} strokeWidth={1.5} />
{ways.map((w, index) => {
const Icon = ICONS[w.icon as keyof typeof ICONS] || Send;
const isFeatured = Boolean(w.featured);
const facts = fallbackArray<{ label?: string | null; desc?: string | null }>(w.facts, []);
const isLast = index === ways.length - 1;
return (
<div key={`${w.label}-${index}`} style={{ padding: isMobile ? '32px 24px' : '56px 48px 56px', borderRight: !isLast ? '1px solid #D0D5DD' : 'none', display: 'flex', flexDirection: 'column', background: isFeatured ? NAVY : undefined, ...(isMobile ? { minWidth: '83vw', maxWidth: '83vw', flexShrink: 0, scrollSnapAlign: 'start' } : {}) }}>
<div style={{ width: 44, height: 44, background: isFeatured ? GOLD : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
<Icon size={18} style={{ color: isFeatured ? '#101828' : GOLD }} strokeWidth={1.5} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 10 }}>Weg 01</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.7rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: '0 0 8px' }}>
Vorschlag<br />unterbreiten
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: isFeatured ? 'rgba(255,255,255,0.35)' : 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 10 }}>{fallbackText(w.label, '')}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.7rem)', fontWeight: 900, color: isFeatured ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: '0 0 8px' }}>
<Lines text={fallbackText(w.title, '')} />
</h3>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, margin: '0 0 32px' }}>
Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.
<p style={{ fontFamily: FB, fontSize: 18, color: isFeatured ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.5)', lineHeight: 1.7, margin: '0 0 32px' }}>
{fallbackText(w.desc, '')}
</p>
{/* Institution list */}
<div style={{ borderTop: '1px solid #D0D5DD', flex: 1 }}>
{INSTITUTIONEN.map((inst, i) => (
<div style={{ borderTop: isFeatured ? '1px solid rgba(255,255,255,0.1)' : '1px solid #D0D5DD', flex: 1 }}>
{w.listType === 'institutions' ? institutions.map((inst, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 0', borderBottom: '1px solid #D0D5DD' }}>
<div style={{ width: 16, height: 16, background: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, marginTop: 2 }}>
<svg width="8" height="8" viewBox="0 0 8 8"><polyline points="1,4 3,6 7,2" stroke="#101828" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
<span style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.65)', lineHeight: 1.55 }}>{inst}</span>
<span style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.65)', lineHeight: 1.55 }}>{fallbackText(inst.text, '')}</span>
</div>
)) : facts.map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '14px 0', borderBottom: isFeatured ? '1px solid rgba(255,255,255,0.07)' : '1px solid #D0D5DD' }}>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: isFeatured ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '0.07em' }}>{fallbackText(item.label, '')}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: isFeatured ? 'rgba(255,255,255,0.50)' : 'rgba(16,24,40,0.45)' }}>{fallbackText(item.desc, '')}</span>
</div>
))}
</div>
<a
href="#bewerben"
href={fallbackText(w.cta?.url, '#bewerben')}
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '14px 24px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, marginTop: 36, alignSelf: 'flex-start', transition: 'opacity 0.15s' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '0.85')}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}
>
Vorschlag unterbreiten <ArrowRight size={13} />
</a>
</div>
{/* ── Weg B: Initiativbewerbung ── */}
<div style={{ padding: isMobile ? '32px 24px' : '56px 48px 56px', borderRight: '1px solid #D0D5DD', display: 'flex', flexDirection: 'column', background: NAVY, ...(isMobile ? { minWidth: '83vw', maxWidth: '83vw', flexShrink: 0, scrollSnapAlign: 'start' } : {}) }}>
<div style={{ width: 44, height: 44, background: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
<UserCheck size={18} style={{ color: '#101828' }} strokeWidth={1.5} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)', display: 'block', marginBottom: 10 }}>Weg 02</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.7rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: '0 0 8px' }}>
Initiativ­bewerbung
</h3>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.62)', lineHeight: 1.7, margin: '0 0 32px' }}>
Für Unternehmer, die selbst die Initiative ergreifen.
</p>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.1)', flex: 1 }}>
{[
{ label: '0 EUR', desc: 'Teilnahmegebühr' },
{ label: 'Eigeninitiative', desc: 'Direkt ohne Intermediär' },
{ label: 'Für Unternehmer', desc: 'Inhabergeführte KMU in Bayern' },
{ label: 'Deadline', desc: '30. Juni 2026' },
].map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '14px 0', borderBottom: '1px solid rgba(255,255,255,0.07)' }}>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.07em' }}>{item.label}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(255,255,255,0.50)' }}>{item.desc}</span>
</div>
))}
</div>
<a
href="#bewerben"
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '14px 24px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, marginTop: 36, alignSelf: 'flex-start', transition: 'opacity 0.15s' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '0.85')}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}
>
Jetzt bewerben <ArrowRight size={13} />
</a>
</div>
{/* ── Weg C: Sonderpreis ── */}
<div style={{ padding: isMobile ? '32px 24px' : '56px 48px 56px', display: 'flex', flexDirection: 'column', ...(isMobile ? { minWidth: '83vw', maxWidth: '83vw', flexShrink: 0, scrollSnapAlign: 'start' } : {}) }}>
<div style={{ width: 44, height: 44, background: NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
<Star size={18} style={{ color: GOLD }} strokeWidth={1.5} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: 'rgba(16,24,40,0.4)', display: 'block', marginBottom: 10 }}>Weg 03 · Sonderpreis</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.7rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: '0 0 8px' }}>
Bavarian<br />Future Award
</h3>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, margin: '0 0 32px' }}>
Sonderpreis, initiiert durch die Studierenden der HAM, Hochschule für angewandtes Management.
</p>
<div style={{ borderTop: '1px solid #D0D5DD', flex: 1 }}>
{[
{ label: 'Vorschlag möglich', desc: 'Durch HAM & Partner' },
{ label: 'Selbstbewerbung', desc: 'Direkte Einreichung' },
{ label: 'Initiiert von', desc: 'Studierenden der HAM' },
{ label: 'Kategorie', desc: 'Zukunft & Innovation' },
].map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '14px 0', borderBottom: '1px solid #D0D5DD' }}>
<span style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.07em' }}>{item.label}</span>
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.45)' }}>{item.desc}</span>
</div>
))}
</div>
<a
href="#bewerben"
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '14px 24px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, marginTop: 36, alignSelf: 'flex-start', transition: 'opacity 0.15s' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '0.85')}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}
>
Zum Formular <ArrowRight size={13} />
{fallbackText(w.cta?.label, 'Zum Formular')} <ArrowRight size={13} />
</a>
</div>
);
})}
</>)}

View File

@@ -0,0 +1,301 @@
export const participationContent = {
hero: {
backgroundImageFilename: 'preisuebergabe.jpg',
imageAlt: 'Teilnahme BMP',
eyebrow: 'Bewerbungsphase 2026',
heading: 'GESTALTEN SIE\nBAYERNS ZUKUNFT.',
description: 'Alles, was Sie für Ihre erfolgreiche Bewerbung zum Bayerischen Mittelstandspreis wissen müssen.',
},
process: {
eyebrow: 'Schritt für Schritt',
heading: 'IHR WEG ZUM\nPREIS.',
description:
'Von der Einreichung bis zur Gala vier klar definierte Schritte auf dem Weg zur höchsten Auszeichnung des bayerischen Mittelstands.',
scrollHint: 'Scrollen zum Erkunden',
progressLabel: '01 / 04',
steps: [
{
step: '01',
title: 'Online-Einreichung',
desc: 'Bewerbungsformular vollständig ausfüllen. Die Teilnahme ist kostenfrei und ohne bürokratischen Aufwand möglich.',
icon: 'fileText',
date: 'Bis 30. Juni 2026',
},
{
step: '02',
title: 'Formale Vorprüfung',
desc: 'Das Gremium sichtet alle Unterlagen auf Vollständigkeit, KMU-Konformität und regionale Zugehörigkeit im Freistaat.',
icon: 'search',
date: 'Juli 2026',
},
{
step: '03',
title: 'Audit & Jury-Sitzung',
desc: 'Die besten Unternehmen werden durch unabhängige Experten-Audits vor Ort evaluiert und in der Jury-Sitzung bewertet.',
icon: 'userPlus',
date: 'Aug Sep 2026',
},
{
step: '04',
title: 'Gala-Preisverleihung',
desc: 'Die Gewinner werden im Rahmen der feierlichen Gala in München vor geladenen Gästen aus Wirtschaft und Politik geehrt.',
icon: 'trophy',
date: 'Oktober 2026',
},
],
},
eligibility: {
eyebrow: 'Berechtigung',
heading: 'WER KANN\nTEILNEHMEN?',
primaryCta: { label: 'Jetzt bewerben', url: '#bewerben' },
secondaryCta: { label: 'Mitglied werden', url: '/mitglied-werden' },
description:
'Vier klar definierte Kriterien: Erfüllen Sie alle, bewerben Sie sich kostenlos und ohne bürokratischen Aufwand.',
criteria: [
{ num: '01', title: 'Standort Bayern', body: 'Sitz oder wesentliche Betriebsstätte im Freistaat Bayern.', icon: 'mapPin' },
{ num: '02', title: 'KMU-Größe', body: '50 bis 1.500 Mitarbeiter: klassischer Mittelstand im privatwirtschaftlichen Bereich.', icon: 'users' },
{ num: '03', title: 'Marktreife', body: 'Mindestens drei vollständige Geschäftsjahre am Markt tätig.', icon: 'trendingUp' },
{ num: '04', title: 'Privatwirtschaft', body: 'Keine überwiegende Zugehörigkeit zu staatlichen oder kommunalen Trägern.', icon: 'building2' },
],
notes: [
{
title: 'Verbund & Gemeinschaft',
body:
'Oder Sie sind ein Verbund bzw. eine Gemeinschaft von mittelständischen Unternehmen oder von mittelständischen Unternehmen und Organisationen/Institutionen, die einen Schwerpunkt in Bayern hat.',
icon: 'building2',
},
{
title: 'Erneute Bewerbung',
body: 'Nominierte ohne Gewinn können sich sofort erneut bewerben. Preisträger können sich nach 7 Jahren erneut bewerben.',
icon: 'userCheck',
},
],
nudgeText: 'Alle 4 Punkte erfüllt? Dann jetzt kostenlos bewerben.',
nudgeCta: { label: 'Zum Formular', url: '#bewerben' },
},
evaluation: {
imageFilename: 'buehne-gewinner.jpg',
imageAlt: 'Jury-Bewertung BMP',
eyebrow: 'Bewertung',
heading: 'UNSERE\nKRITERIEN.',
body:
'Vier zentrale Säulen, bewertet durch eine vollständig unabhängige Expertenjury aus Wirtschaft, Wissenschaft und Gesellschaft.\n\nDie Kriterien werden nicht gleich gewichtet.',
link: {
label: 'Kriterien & Bewertung',
url: 'https://www.der-bayerische-mittelstandspreis.de/der-preis/kriterien/',
},
criteria: [
{ title: 'Innovations­kraft', desc: 'Zukunftsweisende Produkte, Dienstleistungen oder interne Prozessinnovationen.' },
{ title: 'Nachhaltigkeit', desc: 'Ökologische Verantwortung und Ressourcen-Effizienz im operativen Kern.' },
{ title: 'Unternehmens­kultur', desc: 'Mitarbeiterbindung, Aus- und Weiterbildung sowie wertebasierte Führung.' },
{ title: 'Regionale Wurzeln', desc: 'Engagement am Standort Bayern und Beitrag zur regionalen Wertschöpfung.' },
],
},
applicationWays: {
eyebrow: 'Bewerbung 2026',
heading: 'HIER KÖNNEN SIE SICH\nBEWERBEN ODER EIN\nUNTERNEHMEN VORSCHLAGEN.',
description:
'Als Unternehmen haben Sie zwei Möglichkeiten: Sie werden von einem unterstützenden Partner oder einer Institution vorgeschlagen, oder Sie ergreifen selbst die Initiative und füllen unsere Anmeldung aus. Die Teilnahme ist in allen Fällen kostenfrei.',
institutions: [
'Senatoren und Mitglieder der Verbandsgruppe „Wir Eigentümerunternehmer"',
'Bundesverband Deutscher Mittelstand (BM)',
'Europäisches Wirtschaftsforum (EWIF)',
'Union Mittelständischer Unternehmen (UMU)',
'HAM Hochschule für angewandtes Management und deren Regionalpartner',
'Mitglieder des Wirtschaftsbeirates Bayern',
'Ehemalige Preisträger des Bayerischen Mittelstandspreises',
].map((text) => ({ text })),
ways: [
{
label: 'Weg 01',
title: 'Vorschlag\nunterbreiten',
desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.',
icon: 'send',
featured: false,
cta: { label: 'Vorschlag unterbreiten', url: '#bewerben' },
listType: 'institutions',
facts: [],
},
{
label: 'Weg 02',
title: 'Initiativ­bewerbung',
desc: 'Für Unternehmer, die selbst die Initiative ergreifen.',
icon: 'userCheck',
featured: true,
cta: { label: 'Jetzt bewerben', url: '#bewerben' },
listType: 'facts',
facts: [
{ label: '0 EUR', desc: 'Teilnahmegebühr' },
{ label: 'Eigeninitiative', desc: 'Direkt ohne Intermediär' },
{ label: 'Für Unternehmer', desc: 'Inhabergeführte KMU in Bayern' },
{ label: 'Deadline', desc: '30. Juni 2026' },
],
},
{
label: 'Weg 03 · Sonderpreis',
title: 'Bavarian\nFuture Award',
desc: 'Sonderpreis, initiiert durch die Studierenden der HAM, Hochschule für angewandtes Management.',
icon: 'star',
featured: false,
cta: { label: 'Zum Formular', url: '#bewerben' },
listType: 'facts',
facts: [
{ label: 'Vorschlag möglich', desc: 'Durch HAM & Partner' },
{ label: 'Selbstbewerbung', desc: 'Direkte Einreichung' },
{ label: 'Initiiert von', desc: 'Studierenden der HAM' },
{ label: 'Kategorie', desc: 'Zukunft & Innovation' },
],
},
],
recommendedLabel: 'Empfohlen',
},
datesBanner: {
heading: 'Wichtige Termine 2026',
description: 'Planen Sie Ihre Teilnahme rechtzeitig.',
mobileItems: [
{ date: '30. Juni 2026', label: 'Bewerbungsschluss' },
{ date: 'August 2026', label: 'Nominierte' },
{ date: 'Oktober 2026', label: 'Gala-Verleihung' },
],
desktopItems: [
{ date: '30. Juni 2026', label: 'Bewerbungsschluss' },
{ date: 'August 2026', label: 'Bekanntgabe Nominierte' },
{ date: 'Oktober 2026', label: 'Gala-Verleihung' },
],
},
applicationForm: {
imageFilename: 'preistraeger-jubel.jpg',
imageAlt: 'Preisverleihung 2025',
imageCaption: 'Preisverleihung 2025',
eyebrow: 'Direktbewerbung',
heading: 'BEWERBEN ODER\nVORSCHLAGEN.',
description:
'Sie können sich selbst bewerben oder ein herausragendes Unternehmen vorschlagen. Firmenverbunde können sich ebenfalls bewerben. Die Teilnahme ist vollständig kostenfrei.',
facts: [
{ num: '01', label: '0 EUR', desc: 'Teilnahmegebühr' },
{ num: '02', label: 'Schnell', desc: 'Ca. 3045 Minuten' },
{ num: '03', label: 'Mehrstufig', desc: 'Transparenter Prozess' },
],
formEyebrow: 'Bewerbung 2026',
uploadCta: { label: 'PDF hochladen', url: '/formular-hochladen' },
uploadPrompt: 'Sie haben Ihre Unterlagen bereits als PDF vorbereitet? Laden Sie sie direkt hoch, statt das Formular auszufüllen.',
uploadFooterCta: { label: 'Formular hochladen →', url: '/formular-hochladen' },
},
form: {
stepCompleteLabel: '✓',
backLabel: 'Zurück',
nextLabel: 'Weiter',
submitLabel: 'Absenden',
yesLabel: 'Ja',
noLabel: 'Nein',
requiredSuffix: '*',
validation: {
choose: 'Bitte wählen',
required: 'Pflichtfeld',
invalidEmail: 'Ungültige E-Mail',
},
typeStep: {
eyebrow: 'Schritt 1',
heading: 'Art der Einreichung',
selfTitle: 'Eigenbewerbung',
selfDesc: 'Ich bewerbe mein eigenes Unternehmen für den Bayerischen Mittelstandspreis.',
nominationTitle: 'Vorschlag',
nominationDesc: 'Ich schlage ein anderes Unternehmen vor, das den Preis verdient.',
},
selfSteps: [
{ label: 'Einreichung', icon: 'trophy' },
{ label: 'Schnell-Check', icon: 'check' },
{ label: 'Kontakt', icon: 'user' },
{ label: 'Absenden', icon: 'send' },
],
nominationSteps: [
{ label: 'Einreichung', icon: 'trophy' },
{ label: 'Unternehmen', icon: 'building2' },
{ label: 'Nominierender', icon: 'user' },
{ label: 'Absenden', icon: 'send' },
],
employeeOptions: [
{ value: 'unter10', label: 'Unter 10' },
{ value: '10-50', label: '10 50' },
{ value: '51-200', label: '51 200' },
{ value: '201-500', label: '201 500' },
{ value: 'über500', label: 'Über 500' },
],
eligibility: {
allowedEmployeeValues: '10-50,51-200,201-500',
requiredBayernValue: 'ja',
eyebrow: 'Schnell-Check',
heading: 'Grundlegende Eignung',
employeesLabel: 'Wie viele Mitarbeiter hat Ihr Unternehmen?',
bayernLabel: 'Hat Ihr Unternehmen seinen Sitz in Bayern?',
ownerLabel: 'Ist Ihr Unternehmen inhabergeführt oder familiengeführt?',
ineligibleHeading: 'Leider nicht förderfähig',
ineligibleBody:
'Der Bayerische Mittelstandspreis richtet sich an inhabergeführte Unternehmen mit 10500 Mitarbeitern und Sitz in Bayern. Ihr Unternehmen erfüllt diese Voraussetzungen aktuell nicht.',
ineligibleContactPrefix: 'Fragen? Schreiben Sie uns:',
ineligibleContactEmail: 'info@bmp-bayern.de',
},
selfContact: {
eyebrow: 'Kontakt',
heading: 'Ihre Kontaktdaten',
companyLabel: 'Firmenname',
companyPlaceholder: 'Muster GmbH',
nameLabel: 'Ihr Name',
namePlaceholder: 'Vor- und Nachname',
emailLabel: 'E-Mail',
emailPlaceholder: 'name@unternehmen.de',
phoneLabel: 'Telefon',
phonePlaceholder: '+49 89 ...',
footnote: 'Wir senden Ihnen den vollständigen Fragebogen automatisch per E-Mail zu.',
},
selfSummary: {
eyebrow: 'Zusammenfassung',
heading: 'Ihre Bewerbung',
companyLabel: 'Unternehmen',
employeesLabel: 'Mitarbeiter',
locationLabel: 'Standort',
locationPrefix: 'Bayern:',
contactLabel: 'Kontakt',
},
nominationCompany: {
eyebrow: 'Nominierung',
heading: 'Nominiertes Unternehmen',
companyLabel: 'Firmenname',
companyPlaceholder: 'Muster GmbH',
industryLabel: 'Branche',
industryPlaceholder: 'z. B. Maschinenbau',
locationLabel: 'Standort Bayern',
locationPlaceholder: 'z. B. München',
},
nominationContact: {
eyebrow: 'Nominierender',
heading: 'Ihre Angaben',
nameLabel: 'Ihr Name',
namePlaceholder: 'Vor- und Nachname',
emailLabel: 'Ihre E-Mail',
emailPlaceholder: 'ihre@email.de',
relationshipLabel: 'Ihre Beziehung zum Unternehmen',
relationshipPlaceholder: 'z. B. Kunde, Partner, Bekannter',
footnote: 'Wir nehmen Kontakt mit dem nominierten Unternehmen auf und informieren es über Ihre Nominierung.',
},
nominationSummary: {
eyebrow: 'Zusammenfassung',
heading: 'Ihre Nominierung',
companyLabel: 'Unternehmen',
industryLabel: 'Branche',
locationLabel: 'Standort',
nominatedByLabel: 'Nominiert von',
emptyValue: '',
},
success: {
selfHeading: 'Anfrage eingegangen',
nominationHeading: 'Nominierung eingegangen',
selfPrefix: 'Vielen Dank, ',
selfMiddle: '. Wir senden Ihnen den vollständigen Fragebogen an ',
selfSuffix: '.',
nominationPrefix: 'Vielen Dank, ',
nominationMiddle: '. Wir nehmen Kontakt mit ',
nominationSuffix: ' auf und informieren sie über Ihre Nominierung.',
},
},
}