feat: update Preistraeger page to include a simple winner grid for 2023 and remove unused components
fix: change featured status for participation content and remove deprecated award card feat: add migration for new participation phase form fields and backfill existing data feat: create AwardsGridSection component for displaying awards with improved layout feat: implement NewsletterInterestForm component for newsletter sign-up with validation
This commit is contained in:
@@ -60,6 +60,36 @@ const iconField = (defaultValue = 'star'): Field => ({
|
||||
options: iconOptions,
|
||||
})
|
||||
|
||||
const newsletterVariantFields = (
|
||||
defaults: typeof homeContent.application.newsletterPhaseEvaluation,
|
||||
benefitsDbName: string,
|
||||
): Field[] => [
|
||||
text('eyebrow', 'Left eyebrow', defaults.eyebrow),
|
||||
textarea('heading', 'Left heading', defaults.heading),
|
||||
textarea('body', 'Left body copy', defaults.body),
|
||||
{
|
||||
name: 'benefits',
|
||||
label: 'Benefit rows',
|
||||
type: 'array',
|
||||
dbName: benefitsDbName,
|
||||
defaultValue: defaults.benefits,
|
||||
fields: [text('num', 'Number'), text('label', 'Label'), text('desc', 'Description')],
|
||||
},
|
||||
text('formEyebrow', 'Form eyebrow', defaults.formEyebrow),
|
||||
text('formTitle', 'Form title', defaults.formTitle),
|
||||
textarea('formBody', 'Form body copy', defaults.formBody),
|
||||
text('emailLabel', 'Email label', defaults.emailLabel),
|
||||
text('emailPlaceholder', 'Email placeholder', defaults.emailPlaceholder),
|
||||
text('submitLabel', 'Submit button label', defaults.submitLabel),
|
||||
textarea('privacy', 'Privacy note', defaults.privacy),
|
||||
text('validationRequired', 'Required email validation message', defaults.validationRequired),
|
||||
text('validationInvalid', 'Invalid email validation message', defaults.validationInvalid),
|
||||
text('successHeading', 'Success heading', defaults.successHeading),
|
||||
textarea('successBody', 'Success body copy', defaults.successBody),
|
||||
text('footnote', 'Footnote normal text', defaults.footnote),
|
||||
text('footnoteStrong', 'Footnote highlighted text', defaults.footnoteStrong),
|
||||
]
|
||||
|
||||
export const homeFields: Field[] = [
|
||||
{
|
||||
name: 'home',
|
||||
@@ -236,6 +266,20 @@ export const homeFields: Field[] = [
|
||||
text('formTitle', 'Form title', homeContent.application.formTitle),
|
||||
text('footnote', 'Footnote normal text', homeContent.application.footnote),
|
||||
text('footnoteStrong', 'Footnote highlighted text', homeContent.application.footnoteStrong),
|
||||
{
|
||||
name: 'newsletterPhaseEvaluation',
|
||||
label: 'Phase 02 · Newsletter form',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Homepage newsletter copy used when the active application phase is Auswertung / Jury bewertet.'),
|
||||
fields: newsletterVariantFields(homeContent.application.newsletterPhaseEvaluation, 'home_news_eval_bens'),
|
||||
},
|
||||
{
|
||||
name: 'newsletterPhaseCompleted',
|
||||
label: 'Phase 03 · Newsletter form',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Homepage newsletter copy used when the active application phase is Preisverleihung abgeschlossen.'),
|
||||
fields: newsletterVariantFields(homeContent.application.newsletterPhaseCompleted, 'home_news_done_bens'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -76,6 +76,35 @@ const ctaGroup = (name: string, label: string, labelDefault: string, urlDefault:
|
||||
type: 'group',
|
||||
fields: linkGroup(labelDefault, urlDefault),
|
||||
})
|
||||
const newsletterVariantFields = (
|
||||
defaults: typeof participationContent.applicationForm.newsletterPhaseEvaluation,
|
||||
benefitsDbName: string,
|
||||
): Field[] => [
|
||||
text('eyebrow', 'Left eyebrow', defaults.eyebrow),
|
||||
textarea('heading', 'Left heading', defaults.heading),
|
||||
textarea('body', 'Left body copy', defaults.body),
|
||||
{
|
||||
name: 'benefits',
|
||||
label: 'Benefit rows',
|
||||
type: 'array',
|
||||
dbName: benefitsDbName,
|
||||
defaultValue: defaults.benefits,
|
||||
fields: [text('num', 'Number'), text('label', 'Label'), text('desc', 'Description')],
|
||||
},
|
||||
text('formEyebrow', 'Form eyebrow', defaults.formEyebrow),
|
||||
text('formTitle', 'Form title', defaults.formTitle),
|
||||
textarea('formBody', 'Form body copy', defaults.formBody),
|
||||
text('emailLabel', 'Email label', defaults.emailLabel),
|
||||
text('emailPlaceholder', 'Email placeholder', defaults.emailPlaceholder),
|
||||
text('submitLabel', 'Submit button label', defaults.submitLabel),
|
||||
textarea('privacy', 'Privacy note', defaults.privacy),
|
||||
text('validationRequired', 'Required email validation message', defaults.validationRequired),
|
||||
text('validationInvalid', 'Invalid email validation message', defaults.validationInvalid),
|
||||
text('successHeading', 'Success heading', defaults.successHeading),
|
||||
textarea('successBody', 'Success body copy', defaults.successBody),
|
||||
text('footnote', 'Footnote normal text', defaults.footnote),
|
||||
text('footnoteStrong', 'Footnote highlighted text', defaults.footnoteStrong),
|
||||
]
|
||||
const awardCardDefaults = participationContent.awardsGrid.cards.map(({ imageFilename: _imageFilename, ...card }) => card)
|
||||
|
||||
export const participationFields: Field[] = [
|
||||
@@ -158,7 +187,7 @@ export const participationFields: Field[] = [
|
||||
name: 'applicationWays',
|
||||
label: '04 · Application paths',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Cream section explaining proposal, self-application, and special award paths.'),
|
||||
admin: sectionAdmin('Cream section explaining proposal and self-application paths.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', participationContent.applicationWays.eyebrow),
|
||||
textarea('heading', 'Heading', participationContent.applicationWays.heading),
|
||||
@@ -270,7 +299,7 @@ export const participationFields: Field[] = [
|
||||
name: 'applicationForm',
|
||||
label: '07 · Application form wrapper',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Navy/gold section containing the application form and PDF upload links.'),
|
||||
admin: sectionAdmin('Navy/gold section containing the phase-aware application form or newsletter prompt.'),
|
||||
fields: [
|
||||
uploadField('image', 'Form image', `Current frontend image: /images/${participationContent.applicationForm.imageFilename}`),
|
||||
text('imageAlt', 'Image alt text', participationContent.applicationForm.imageAlt),
|
||||
@@ -287,9 +316,24 @@ export const participationFields: Field[] = [
|
||||
fields: [text('num', 'Number'), text('label', 'Label'), textarea('desc', 'Description')],
|
||||
},
|
||||
text('formEyebrow', 'Form eyebrow', participationContent.applicationForm.formEyebrow),
|
||||
text('formTitle', 'Form title', participationContent.applicationForm.formTitle),
|
||||
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: 'newsletterPhaseEvaluation',
|
||||
label: 'Phase 02 · Newsletter form',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Participation page newsletter copy used when the active application phase is Auswertung / Jury bewertet.'),
|
||||
fields: newsletterVariantFields(participationContent.applicationForm.newsletterPhaseEvaluation, 'part_news_eval_bens'),
|
||||
},
|
||||
{
|
||||
name: 'newsletterPhaseCompleted',
|
||||
label: 'Phase 03 · Newsletter form',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Participation page newsletter copy used when the active application phase is Preisverleihung abgeschlossen.'),
|
||||
fields: newsletterVariantFields(participationContent.applicationForm.newsletterPhaseCompleted, 'part_news_done_bens'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
|
||||
|
||||
type MigrationDB = MigrateUpArgs['db']
|
||||
|
||||
type TextColumn = {
|
||||
name: string
|
||||
defaultValue: string
|
||||
}
|
||||
|
||||
type BenefitRow = {
|
||||
num: string
|
||||
label: string
|
||||
desc: string
|
||||
}
|
||||
|
||||
const participationPageWhere = "(`spa_path` = '/teilnahme' OR `slug` IN ('teilnahme', 'participation'))"
|
||||
|
||||
const textColumns: TextColumn[] = [
|
||||
{ name: 'participation_application_form_form_title', defaultValue: 'Kostenlos bewerben' },
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_eyebrow', defaultValue: 'Updates erhalten' },
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_heading', defaultValue: 'DIE BEWERBUNG\nIST GESCHLOSSEN.' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_evaluation_body',
|
||||
defaultValue:
|
||||
'Die Bewerbungsphase 2026 ist geschlossen. Hinterlassen Sie Ihre E-Mail und erhalten Sie Updates zur Juryphase, zur Preisverleihung und zur nächsten Runde.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_form_eyebrow', defaultValue: 'Newsletter abonnieren' },
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_form_title', defaultValue: 'Updates erhalten' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_evaluation_form_body',
|
||||
defaultValue:
|
||||
'Tragen Sie Ihre E-Mail ein und erhalten Sie relevante Informationen zum weiteren Verlauf des Bayerischen Mittelstandspreises.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_email_label', defaultValue: 'E-Mail-Adresse' },
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_email_placeholder', defaultValue: 'name@unternehmen.de' },
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_submit_label', defaultValue: 'Eintragen' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_evaluation_privacy',
|
||||
defaultValue: 'Kein Spam. Nur relevante Informationen rund um den Bayerischen Mittelstandspreis.',
|
||||
},
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_evaluation_validation_required',
|
||||
defaultValue: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||
},
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_evaluation_validation_invalid',
|
||||
defaultValue: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_success_heading', defaultValue: 'Danke' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_evaluation_success_body',
|
||||
defaultValue: 'Ihre E-Mail wurde für BMP-Updates eingetragen.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_footnote', defaultValue: 'BMP-Updates -' },
|
||||
{ name: 'participation_application_form_newsletter_phase_evaluation_footnote_strong', defaultValue: 'Juryphase und Termine' },
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_eyebrow', defaultValue: 'Nächste Runde' },
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_heading', defaultValue: 'DEN START\nNICHT\nVERPASSEN.' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_completed_body',
|
||||
defaultValue:
|
||||
'Der aktuelle Preisjahrgang ist abgeschlossen. Hinterlassen Sie Ihre E-Mail und wir informieren Sie, sobald es Neuigkeiten zur nächsten Bewerbungsphase gibt.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_form_eyebrow', defaultValue: 'Newsletter abonnieren' },
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_form_title', defaultValue: 'Informiert bleiben' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_completed_form_body',
|
||||
defaultValue: 'Tragen Sie sich ein und erhalten Sie Hinweise zum Bewerbungsstart, zu Veranstaltungen und zu ausgezeichneten Unternehmen.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_email_label', defaultValue: 'E-Mail-Adresse' },
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_email_placeholder', defaultValue: 'name@unternehmen.de' },
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_submit_label', defaultValue: 'Abonnieren' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_completed_privacy',
|
||||
defaultValue: 'Sie erhalten nur BMP-relevante Informationen. Eine Abmeldung ist jederzeit möglich.',
|
||||
},
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_completed_validation_required',
|
||||
defaultValue: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||
},
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_completed_validation_invalid',
|
||||
defaultValue: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_success_heading', defaultValue: 'Angemeldet' },
|
||||
{
|
||||
name: 'participation_application_form_newsletter_phase_completed_success_body',
|
||||
defaultValue: 'Danke. Sie erhalten BMP-Updates an die angegebene E-Mail-Adresse.',
|
||||
},
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_footnote', defaultValue: 'Bewerbungsstart im Blick -' },
|
||||
{ name: 'participation_application_form_newsletter_phase_completed_footnote_strong', defaultValue: 'BMP-Updates erhalten' },
|
||||
]
|
||||
|
||||
const evaluationBenefits: BenefitRow[] = [
|
||||
{ num: '01', label: 'Juryphase', desc: 'Updates zum Verlauf' },
|
||||
{ num: '02', label: 'Preisverleihung', desc: 'Termine und Einblicke' },
|
||||
{ num: '03', label: 'Nächste Runde', desc: 'Start nicht verpassen' },
|
||||
]
|
||||
|
||||
const completedBenefits: BenefitRow[] = [
|
||||
{ num: '01', label: 'Startsignal', desc: 'Neue Bewerbungsphase' },
|
||||
{ num: '02', label: 'BMP-News', desc: 'Preisträger und Termine' },
|
||||
{ num: '03', label: 'Erinnerung', desc: 'Rechtzeitig vorbereitet' },
|
||||
]
|
||||
|
||||
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
|
||||
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
|
||||
|
||||
async function tableExists(db: MigrationDB, tableName: string) {
|
||||
const rows = (await db.all(
|
||||
sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
|
||||
)) as Array<{ name: string }>
|
||||
|
||||
return rows.length > 0
|
||||
}
|
||||
|
||||
async function columnExists(db: MigrationDB, tableName: string, columnName: string) {
|
||||
if (!(await tableExists(db, tableName))) return false
|
||||
|
||||
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
|
||||
|
||||
return columns.some((column) => column.name === columnName)
|
||||
}
|
||||
|
||||
async function addTextColumn(db: MigrationDB, tableName: string, column: TextColumn) {
|
||||
if (await columnExists(db, tableName, column.name)) return
|
||||
|
||||
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} ADD ${ident(column.name)} text DEFAULT ${literal(column.defaultValue)};`))
|
||||
}
|
||||
|
||||
async function dropColumn(db: MigrationDB, tableName: string, columnName: string) {
|
||||
if (!(await columnExists(db, tableName, columnName))) return
|
||||
|
||||
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} DROP COLUMN ${ident(columnName)};`))
|
||||
}
|
||||
|
||||
async function backfillParticipationTextColumn(db: MigrationDB, column: TextColumn) {
|
||||
if (!(await columnExists(db, 'pages', column.name))) return
|
||||
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`UPDATE ${ident('pages')} SET ${ident(column.name)} = ${literal(column.defaultValue)} WHERE ${ident(column.name)} IS NULL AND ${participationPageWhere};`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async function createBenefitTables(db: MigrationDB, tableName: string, versionTableName: string) {
|
||||
await db.run(sql.raw(`CREATE TABLE IF NOT EXISTS ${ident(tableName)} (
|
||||
${ident('_order')} integer NOT NULL,
|
||||
${ident('_parent_id')} integer NOT NULL,
|
||||
${ident('id')} text PRIMARY KEY NOT NULL,
|
||||
${ident('num')} text,
|
||||
${ident('label')} text,
|
||||
${ident('desc')} text,
|
||||
FOREIGN KEY (${ident('_parent_id')}) REFERENCES ${ident('pages')}(${ident('id')}) ON UPDATE no action ON DELETE cascade
|
||||
);`))
|
||||
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_order_idx`)} ON ${ident(tableName)} (${ident('_order')});`))
|
||||
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_parent_id_idx`)} ON ${ident(tableName)} (${ident('_parent_id')});`))
|
||||
|
||||
await db.run(sql.raw(`CREATE TABLE IF NOT EXISTS ${ident(versionTableName)} (
|
||||
${ident('_order')} integer NOT NULL,
|
||||
${ident('_parent_id')} integer NOT NULL,
|
||||
${ident('id')} integer PRIMARY KEY NOT NULL,
|
||||
${ident('num')} text,
|
||||
${ident('label')} text,
|
||||
${ident('desc')} text,
|
||||
${ident('_uuid')} text,
|
||||
FOREIGN KEY (${ident('_parent_id')}) REFERENCES ${ident('_pages_v')}(${ident('id')}) ON UPDATE no action ON DELETE cascade
|
||||
);`))
|
||||
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${versionTableName}_order_idx`)} ON ${ident(versionTableName)} (${ident('_order')});`))
|
||||
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${versionTableName}_parent_id_idx`)} ON ${ident(versionTableName)} (${ident('_parent_id')});`))
|
||||
}
|
||||
|
||||
async function seedBenefitRows(db: MigrationDB, tableName: string, rows: BenefitRow[]) {
|
||||
if (!(await tableExists(db, tableName))) return
|
||||
|
||||
for (const [index, row] of rows.entries()) {
|
||||
await db.run(
|
||||
sql.raw(`INSERT INTO ${ident(tableName)} (${ident('_order')}, ${ident('_parent_id')}, ${ident('id')}, ${ident('num')}, ${ident('label')}, ${ident('desc')})
|
||||
SELECT ${index + 1}, ${ident('pages')}.${ident('id')}, lower(hex(randomblob(12))), ${literal(row.num)}, ${literal(row.label)}, ${literal(row.desc)}
|
||||
FROM ${ident('pages')}
|
||||
WHERE ${participationPageWhere}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${ident(tableName)}
|
||||
WHERE ${ident('_parent_id')} = ${ident('pages')}.${ident('id')}
|
||||
AND ${ident('_order')} = ${index + 1}
|
||||
);`),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
|
||||
for (const column of textColumns) {
|
||||
await addTextColumn(db, 'pages', column)
|
||||
await addTextColumn(db, '_pages_v', {
|
||||
name: `version_${column.name}`,
|
||||
defaultValue: column.defaultValue,
|
||||
})
|
||||
await backfillParticipationTextColumn(db, column)
|
||||
}
|
||||
|
||||
await createBenefitTables(db, 'part_news_eval_bens', '_part_news_eval_bens_v')
|
||||
await createBenefitTables(db, 'part_news_done_bens', '_part_news_done_bens_v')
|
||||
await seedBenefitRows(db, 'part_news_eval_bens', evaluationBenefits)
|
||||
await seedBenefitRows(db, 'part_news_done_bens', completedBenefits)
|
||||
}
|
||||
|
||||
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
|
||||
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident('part_news_eval_bens')};`))
|
||||
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident('_part_news_eval_bens_v')};`))
|
||||
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident('part_news_done_bens')};`))
|
||||
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident('_part_news_done_bens_v')};`))
|
||||
|
||||
for (const column of [...textColumns].reverse()) {
|
||||
await dropColumn(db, 'pages', column.name)
|
||||
await dropColumn(db, '_pages_v', `version_${column.name}`)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import * as migration_20260630_165002_participation_awards_grid from './20260630
|
||||
import * as migration_20260630_172754_network_partner_logo_uploads from './20260630_172754_network_partner_logo_uploads';
|
||||
import * as migration_20260630_175614_partners_collection from './20260630_175614_partners_collection';
|
||||
import * as migration_20260630_182219_prune_unused_admin_collections from './20260630_182219_prune_unused_admin_collections';
|
||||
import * as migration_20260701_163206_participation_phase_form_fields from './20260701_163206_participation_phase_form_fields';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -30,4 +31,9 @@ export const migrations = [
|
||||
down: migration_20260630_182219_prune_unused_admin_collections.down,
|
||||
name: '20260630_182219_prune_unused_admin_collections',
|
||||
},
|
||||
{
|
||||
up: migration_20260701_163206_participation_phase_form_fields.up,
|
||||
down: migration_20260701_163206_participation_phase_form_fields.down,
|
||||
name: '20260701_163206_participation_phase_form_fields',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -380,6 +380,64 @@ export interface Page {
|
||||
formTitle?: string | null;
|
||||
footnote?: string | null;
|
||||
footnoteStrong?: string | null;
|
||||
/**
|
||||
* Homepage newsletter copy used when the active application phase is Auswertung / Jury bewertet.
|
||||
*/
|
||||
newsletterPhaseEvaluation?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
body?: string | null;
|
||||
benefits?:
|
||||
| {
|
||||
num?: string | null;
|
||||
label?: string | null;
|
||||
desc?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
formEyebrow?: string | null;
|
||||
formTitle?: string | null;
|
||||
formBody?: string | null;
|
||||
emailLabel?: string | null;
|
||||
emailPlaceholder?: string | null;
|
||||
submitLabel?: string | null;
|
||||
privacy?: string | null;
|
||||
validationRequired?: string | null;
|
||||
validationInvalid?: string | null;
|
||||
successHeading?: string | null;
|
||||
successBody?: string | null;
|
||||
footnote?: string | null;
|
||||
footnoteStrong?: string | null;
|
||||
};
|
||||
/**
|
||||
* Homepage newsletter copy used when the active application phase is Preisverleihung abgeschlossen.
|
||||
*/
|
||||
newsletterPhaseCompleted?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
body?: string | null;
|
||||
benefits?:
|
||||
| {
|
||||
num?: string | null;
|
||||
label?: string | null;
|
||||
desc?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
formEyebrow?: string | null;
|
||||
formTitle?: string | null;
|
||||
formBody?: string | null;
|
||||
emailLabel?: string | null;
|
||||
emailPlaceholder?: string | null;
|
||||
submitLabel?: string | null;
|
||||
privacy?: string | null;
|
||||
validationRequired?: string | null;
|
||||
validationInvalid?: string | null;
|
||||
successHeading?: string | null;
|
||||
successBody?: string | null;
|
||||
footnote?: string | null;
|
||||
footnoteStrong?: string | null;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Text, options, and eligibility thresholds used inside the home page multi-step application form.
|
||||
@@ -808,7 +866,7 @@ export interface Page {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Cream section explaining proposal, self-application, and special award paths.
|
||||
* Cream section explaining proposal and self-application paths.
|
||||
*/
|
||||
applicationWays?: {
|
||||
eyebrow?: string | null;
|
||||
@@ -915,7 +973,7 @@ export interface Page {
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Navy/gold section containing the application form and PDF upload links.
|
||||
* Navy/gold section containing the phase-aware application form or newsletter prompt.
|
||||
*/
|
||||
applicationForm?: {
|
||||
/**
|
||||
@@ -936,6 +994,7 @@ export interface Page {
|
||||
}[]
|
||||
| null;
|
||||
formEyebrow?: string | null;
|
||||
formTitle?: string | null;
|
||||
uploadCta?: {
|
||||
label?: string | null;
|
||||
url?: string | null;
|
||||
@@ -945,6 +1004,64 @@ export interface Page {
|
||||
label?: string | null;
|
||||
url?: string | null;
|
||||
};
|
||||
/**
|
||||
* Participation page newsletter copy used when the active application phase is Auswertung / Jury bewertet.
|
||||
*/
|
||||
newsletterPhaseEvaluation?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
body?: string | null;
|
||||
benefits?:
|
||||
| {
|
||||
num?: string | null;
|
||||
label?: string | null;
|
||||
desc?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
formEyebrow?: string | null;
|
||||
formTitle?: string | null;
|
||||
formBody?: string | null;
|
||||
emailLabel?: string | null;
|
||||
emailPlaceholder?: string | null;
|
||||
submitLabel?: string | null;
|
||||
privacy?: string | null;
|
||||
validationRequired?: string | null;
|
||||
validationInvalid?: string | null;
|
||||
successHeading?: string | null;
|
||||
successBody?: string | null;
|
||||
footnote?: string | null;
|
||||
footnoteStrong?: string | null;
|
||||
};
|
||||
/**
|
||||
* Participation page newsletter copy used when the active application phase is Preisverleihung abgeschlossen.
|
||||
*/
|
||||
newsletterPhaseCompleted?: {
|
||||
eyebrow?: string | null;
|
||||
heading?: string | null;
|
||||
body?: string | null;
|
||||
benefits?:
|
||||
| {
|
||||
num?: string | null;
|
||||
label?: string | null;
|
||||
desc?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
formEyebrow?: string | null;
|
||||
formTitle?: string | null;
|
||||
formBody?: string | null;
|
||||
emailLabel?: string | null;
|
||||
emailPlaceholder?: string | null;
|
||||
submitLabel?: string | null;
|
||||
privacy?: string | null;
|
||||
validationRequired?: string | null;
|
||||
validationInvalid?: string | null;
|
||||
successHeading?: string | null;
|
||||
successBody?: string | null;
|
||||
footnote?: string | null;
|
||||
footnoteStrong?: string | null;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Text, options, and eligibility thresholds used inside the multi-step application form.
|
||||
@@ -3097,6 +3214,62 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
formTitle?: T;
|
||||
footnote?: T;
|
||||
footnoteStrong?: T;
|
||||
newsletterPhaseEvaluation?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
body?: T;
|
||||
benefits?:
|
||||
| T
|
||||
| {
|
||||
num?: T;
|
||||
label?: T;
|
||||
desc?: T;
|
||||
id?: T;
|
||||
};
|
||||
formEyebrow?: T;
|
||||
formTitle?: T;
|
||||
formBody?: T;
|
||||
emailLabel?: T;
|
||||
emailPlaceholder?: T;
|
||||
submitLabel?: T;
|
||||
privacy?: T;
|
||||
validationRequired?: T;
|
||||
validationInvalid?: T;
|
||||
successHeading?: T;
|
||||
successBody?: T;
|
||||
footnote?: T;
|
||||
footnoteStrong?: T;
|
||||
};
|
||||
newsletterPhaseCompleted?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
body?: T;
|
||||
benefits?:
|
||||
| T
|
||||
| {
|
||||
num?: T;
|
||||
label?: T;
|
||||
desc?: T;
|
||||
id?: T;
|
||||
};
|
||||
formEyebrow?: T;
|
||||
formTitle?: T;
|
||||
formBody?: T;
|
||||
emailLabel?: T;
|
||||
emailPlaceholder?: T;
|
||||
submitLabel?: T;
|
||||
privacy?: T;
|
||||
validationRequired?: T;
|
||||
validationInvalid?: T;
|
||||
successHeading?: T;
|
||||
successBody?: T;
|
||||
footnote?: T;
|
||||
footnoteStrong?: T;
|
||||
};
|
||||
};
|
||||
form?:
|
||||
| T
|
||||
@@ -3550,6 +3723,7 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
id?: T;
|
||||
};
|
||||
formEyebrow?: T;
|
||||
formTitle?: T;
|
||||
uploadCta?:
|
||||
| T
|
||||
| {
|
||||
@@ -3563,6 +3737,62 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
label?: T;
|
||||
url?: T;
|
||||
};
|
||||
newsletterPhaseEvaluation?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
body?: T;
|
||||
benefits?:
|
||||
| T
|
||||
| {
|
||||
num?: T;
|
||||
label?: T;
|
||||
desc?: T;
|
||||
id?: T;
|
||||
};
|
||||
formEyebrow?: T;
|
||||
formTitle?: T;
|
||||
formBody?: T;
|
||||
emailLabel?: T;
|
||||
emailPlaceholder?: T;
|
||||
submitLabel?: T;
|
||||
privacy?: T;
|
||||
validationRequired?: T;
|
||||
validationInvalid?: T;
|
||||
successHeading?: T;
|
||||
successBody?: T;
|
||||
footnote?: T;
|
||||
footnoteStrong?: T;
|
||||
};
|
||||
newsletterPhaseCompleted?:
|
||||
| T
|
||||
| {
|
||||
eyebrow?: T;
|
||||
heading?: T;
|
||||
body?: T;
|
||||
benefits?:
|
||||
| T
|
||||
| {
|
||||
num?: T;
|
||||
label?: T;
|
||||
desc?: T;
|
||||
id?: T;
|
||||
};
|
||||
formEyebrow?: T;
|
||||
formTitle?: T;
|
||||
formBody?: T;
|
||||
emailLabel?: T;
|
||||
emailPlaceholder?: T;
|
||||
submitLabel?: T;
|
||||
privacy?: T;
|
||||
validationRequired?: T;
|
||||
validationInvalid?: T;
|
||||
successHeading?: T;
|
||||
successBody?: T;
|
||||
footnote?: T;
|
||||
footnoteStrong?: T;
|
||||
};
|
||||
};
|
||||
form?:
|
||||
| T
|
||||
|
||||
158
src/spa/components/AwardsGridSection.tsx
Normal file
158
src/spa/components/AwardsGridSection.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { ArrowRight, Mail } from 'lucide-react';
|
||||
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage';
|
||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import { participationContent } from '@/spa/participationContent';
|
||||
import { Link } from '@/spa/router';
|
||||
|
||||
const FF = '"IBM Plex Sans", sans-serif';
|
||||
const FB = '"Inter", sans-serif';
|
||||
const NAVY = '#111D55';
|
||||
const GOLD = '#EFBF04';
|
||||
|
||||
type AwardCard = (typeof participationContent.awardsGrid.cards)[number];
|
||||
type AwardCardCms = Partial<AwardCard> & { image?: unknown };
|
||||
|
||||
export type AwardsGridContent = Partial<typeof participationContent.awardsGrid> & {
|
||||
cards?: AwardCardCms[];
|
||||
};
|
||||
|
||||
const fallbackText = (value: unknown, fallback: string) =>
|
||||
typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||
|
||||
const mergeAwardCards = (value: unknown, fallback: AwardCard[]): (AwardCard & { image?: unknown })[] => {
|
||||
const cards = Array.isArray(value) && value.length > 0 ? (value as AwardCardCms[]) : fallback;
|
||||
return cards.map((card, index) => ({ ...(fallback[index] || fallback[0]), ...card }));
|
||||
};
|
||||
|
||||
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 AwardsGridSection({ content }: { content?: AwardsGridContent }) {
|
||||
const isMobile = useIsMobile();
|
||||
const section = { ...participationContent.awardsGrid, ...(content || {}) };
|
||||
const cards = mergeAwardCards(section.cards, participationContent.awardsGrid.cards);
|
||||
|
||||
return (
|
||||
<section id="auszeichnungen" style={{ background: '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
|
||||
<MunichSkylineBg />
|
||||
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>
|
||||
{fallbackText(section.eyebrow, participationContent.awardsGrid.eyebrow)}
|
||||
</span>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.9fr) minmax(0, 1fr)', gap: isMobile ? 16 : 56, alignItems: '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, overflowWrap: 'anywhere' }}>
|
||||
<Lines text={fallbackText(section.heading, participationContent.awardsGrid.heading)} />
|
||||
</h2>
|
||||
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}>
|
||||
{fallbackText(section.description, participationContent.awardsGrid.description)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))', borderBottom: '1px solid rgba(3,9,58,0.08)' }}>
|
||||
{cards.map((card, index) => {
|
||||
const imageFilename = fallbackText(card.imageFilename, participationContent.awardsGrid.cards[index]?.imageFilename || participationContent.awardsGrid.cards[0].imageFilename);
|
||||
const isContainedImage = imageFilename.endsWith('.png') || imageFilename.includes('roland-berger');
|
||||
const isLast = index === cards.length - 1;
|
||||
|
||||
return (
|
||||
<article
|
||||
key={`${fallbackText(card.title, 'award')}-${index}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: isMobile ? 'auto' : 620,
|
||||
borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
|
||||
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
|
||||
background: index === 1 ? NAVY : 'rgba(255,255,255,0.72)',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: isMobile ? 220 : 260, background: imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
|
||||
<Image unoptimized
|
||||
src={mediaUrl(card.image, `/images/${imageFilename}`)}
|
||||
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: isContainedImage ? 'contain' : 'cover',
|
||||
objectPosition: index === 1 ? 'center top' : 'center',
|
||||
padding: imageFilename.endsWith('.png') ? 22 : 0,
|
||||
boxSizing: 'border-box',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: isMobile ? '28px 24px 32px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.26em', textTransform: 'uppercase', color: index === 1 ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}>
|
||||
{fallbackText(card.label, '')}
|
||||
</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.25rem, 2vw, 1.7rem)', fontWeight: 900, color: index === 1 ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 16px', overflowWrap: 'anywhere' }}>
|
||||
<Lines text={fallbackText(card.title, '')} />
|
||||
</h3>
|
||||
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 22 }} />
|
||||
<p style={{ fontFamily: FB, fontSize: 18, color: index === 1 ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.55)', lineHeight: 1.7, margin: 0, flex: 1 }}>
|
||||
{fallbackText(card.description, '')}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, marginTop: 32 }}>
|
||||
<Link
|
||||
to={fallbackText(card.articleCta?.url, '#bewerben')}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '0 18px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
background: GOLD,
|
||||
color: '#101828',
|
||||
fontFamily: FF,
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{fallbackText(card.articleCta?.label, 'Artikel lesen')} <ArrowRight size={13} />
|
||||
</Link>
|
||||
<a
|
||||
href={fallbackText(card.mailtoCta?.url, `mailto:${participationContent.form.eligibility.ineligibleContactEmail}`)}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '0 18px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
color: index === 1 ? '#fff' : NAVY,
|
||||
border: `1px solid ${index === 1 ? 'rgba(255,255,255,0.22)' : 'rgba(3,9,58,0.18)'}`,
|
||||
fontFamily: FF,
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Mail size={13} /> {fallbackText(card.mailtoCta?.label, 'Kontakt aufnehmen')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
159
src/spa/components/forms/NewsletterInterestForm.tsx
Normal file
159
src/spa/components/forms/NewsletterInterestForm.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Check, Mail } from 'lucide-react';
|
||||
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
|
||||
const FF = '"IBM Plex Sans", sans-serif';
|
||||
|
||||
export type NewsletterBenefit = { num: string; label: string; desc: string };
|
||||
|
||||
export type NewsletterInterestCopy = {
|
||||
eyebrow: string;
|
||||
heading: string;
|
||||
body: string;
|
||||
benefits: readonly NewsletterBenefit[];
|
||||
formEyebrow: string;
|
||||
formTitle: string;
|
||||
formBody: string;
|
||||
emailLabel: string;
|
||||
emailPlaceholder: string;
|
||||
submitLabel: string;
|
||||
privacy: string;
|
||||
validationRequired: string;
|
||||
validationInvalid: string;
|
||||
successHeading: string;
|
||||
successBody: string;
|
||||
footnote: string;
|
||||
footnoteStrong: string;
|
||||
};
|
||||
|
||||
export type NewsletterInterestCms = Partial<Omit<NewsletterInterestCopy, 'benefits'>> & {
|
||||
benefits?: unknown;
|
||||
};
|
||||
|
||||
export function NewsletterInterestForm({ copy }: { copy: NewsletterInterestCopy }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const trimmedEmail = email.trim();
|
||||
|
||||
if (!trimmedEmail) {
|
||||
setError(copy.validationRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) {
|
||||
setError(copy.validationInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div
|
||||
aria-live="polite"
|
||||
style={{
|
||||
borderTop: '1px solid rgba(17,29,85,0.15)',
|
||||
paddingTop: isMobile ? 28 : 36,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 52, height: 52, background: '#111D55', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 22 }}>
|
||||
<Check size={22} color="#EFBF04" strokeWidth={3} />
|
||||
</div>
|
||||
<h4 style={{ fontFamily: FF, fontSize: isMobile ? 20 : 24, fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: '0 0 12px' }}>
|
||||
{copy.successHeading}
|
||||
</h4>
|
||||
<p style={{ fontFamily: FF, fontSize: 16, color: 'rgba(17,29,85,0.66)', lineHeight: 1.7, margin: 0, maxWidth: 430 }}>
|
||||
{copy.successBody}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={submit}
|
||||
style={{
|
||||
borderTop: '1px solid rgba(17,29,85,0.15)',
|
||||
paddingTop: isMobile ? 24 : 30,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<p style={{ fontFamily: FF, fontSize: 16, color: 'rgba(17,29,85,0.68)', lineHeight: 1.75, margin: '0 0 28px', maxWidth: 520 }}>
|
||||
{copy.formBody}
|
||||
</p>
|
||||
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: 'rgba(17,29,85,0.5)', marginBottom: 8 }}>
|
||||
{copy.emailLabel}
|
||||
</label>
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, width: '100%', maxWidth: 560 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
if (error) setError('');
|
||||
}}
|
||||
placeholder={copy.emailPlaceholder}
|
||||
aria-invalid={Boolean(error)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 50,
|
||||
padding: '0 16px',
|
||||
border: `1.5px solid ${error ? '#b91c1c' : 'rgba(17,29,85,0.22)'}`,
|
||||
background: 'rgba(255,255,255,0.55)',
|
||||
color: '#111D55',
|
||||
fontFamily: FF,
|
||||
fontSize: 16,
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
{error && <div style={{ fontFamily: FF, fontSize: 12, color: '#b91c1c', marginTop: 7 }}>{error}</div>}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
height: 50,
|
||||
padding: '0 24px',
|
||||
border: 'none',
|
||||
background: '#111D55',
|
||||
color: '#fff',
|
||||
fontFamily: FF,
|
||||
fontSize: 13,
|
||||
fontWeight: 800,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{copy.submitLabel} <Mail size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ fontFamily: FF, fontSize: 13, color: 'rgba(17,29,85,0.48)', lineHeight: 1.6, margin: '18px 0 0', maxWidth: 520 }}>
|
||||
{copy.privacy}
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Link, useLocation } from '@/spa/router';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import LoadingScreen from '@/spa/components/ui/LoadingScreen';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import { useApplicationPhase } from '@/spa/cmsRoute';
|
||||
import {
|
||||
defaultFooterData,
|
||||
defaultHeaderData,
|
||||
@@ -37,6 +38,7 @@ const LINE = 'rgba(239,191,4,0.35)';
|
||||
const FF_DISPLAY = '"IBM Plex Sans", sans-serif';
|
||||
const FF_BODY = '"Inter", sans-serif';
|
||||
const HIDE_MEMBERSHIP_FOOTER_ENTRY = true;
|
||||
const APPLICATION_PHASE_OPEN = '0';
|
||||
|
||||
const isMembershipFooterLink = (link?: { label?: string; path?: string }) =>
|
||||
link?.path === '/mitglied-werden' || link?.label?.trim().toLowerCase() === 'mitglied werden';
|
||||
@@ -72,6 +74,8 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
||||
const footerCtas = footer.ctas;
|
||||
const fallbackHeaderCtas = defaultHeaderData.ctas;
|
||||
const fallbackFooterCtas = defaultFooterData.ctas;
|
||||
const applicationPhase = useApplicationPhase();
|
||||
const showFloatingApplicationCta = applicationPhase?.activePhase === APPLICATION_PHASE_OPEN;
|
||||
const footerMembershipCta = footerCtas[1] || fallbackFooterCtas[1];
|
||||
const showFooterMembershipCta =
|
||||
!HIDE_MEMBERSHIP_FOOTER_ENTRY || !isMembershipFooterLink(footerMembershipCta);
|
||||
@@ -565,7 +569,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
||||
|
||||
|
||||
{/* ── FLOATING CTA ─────────────────────────────── */}
|
||||
{location.pathname === '/' && !formVisible && !isMobile && (
|
||||
{location.pathname === '/' && showFloatingApplicationCta && !formVisible && !isMobile && (
|
||||
<Link
|
||||
to="/teilnahme"
|
||||
style={{
|
||||
|
||||
@@ -35,6 +35,23 @@ const processCardVariants = cva("flex border backdrop-blur-lg", {
|
||||
},
|
||||
})
|
||||
|
||||
const subscribeToViewportWidth = (onStoreChange: () => void) => {
|
||||
window.addEventListener("resize", onStoreChange)
|
||||
return () => window.removeEventListener("resize", onStoreChange)
|
||||
}
|
||||
|
||||
const getViewportWidth = () => window.innerWidth
|
||||
|
||||
const getServerViewportWidth = () => 0
|
||||
|
||||
function useViewportWidth() {
|
||||
return React.useSyncExternalStore(
|
||||
subscribeToViewportWidth,
|
||||
getViewportWidth,
|
||||
getServerViewportWidth
|
||||
)
|
||||
}
|
||||
|
||||
interface ContainerScrollContextValue {
|
||||
scrollYProgress: MotionValue<number>
|
||||
}
|
||||
@@ -128,13 +145,13 @@ export const ProcessCard: React.FC<ProcessCardProps> = ({
|
||||
const { scrollYProgress } = useContainerScrollContext()
|
||||
const start = index / itemsLength
|
||||
const end = start + 1 / itemsLength
|
||||
const innerWidth = typeof window === "undefined" ? 0 : window.innerWidth
|
||||
const viewportWidth = useViewportWidth()
|
||||
const [ref, { width }] = useMeasure()
|
||||
|
||||
const x = useTransform(
|
||||
scrollYProgress,
|
||||
[start, end],
|
||||
[innerWidth, -((width ?? 0) * index) + 64 * index]
|
||||
[viewportWidth, -((width ?? 0) * index) + 64 * index]
|
||||
)
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
@@ -111,6 +111,56 @@ export const homeContent = {
|
||||
formTitle: 'Kostenlos bewerben',
|
||||
footnote: 'Kostenlos und unverbindlich –',
|
||||
footnoteStrong: 'keine Teilnahmegebühr',
|
||||
newsletterPhaseEvaluation: {
|
||||
eyebrow: 'Updates erhalten',
|
||||
heading: 'BLEIBEN\nSIE AUF\nDEM LAUFENDEN.',
|
||||
body:
|
||||
'Die Bewerbungsphase 2026 ist geschlossen. Hinterlassen Sie Ihre E-Mail und erhalten Sie Updates zur Juryphase, zur Preisverleihung und zum Start der nächsten Runde.',
|
||||
benefits: [
|
||||
{ num: '01', label: 'Juryphase', desc: 'Updates zum Verlauf' },
|
||||
{ num: '02', label: 'Preisverleihung', desc: 'Einladungen und Termine' },
|
||||
{ num: '03', label: 'Nächste Runde', desc: 'Start nicht verpassen' },
|
||||
],
|
||||
formEyebrow: 'Newsletter abonnieren',
|
||||
formTitle: 'Updates erhalten',
|
||||
formBody:
|
||||
'Tragen Sie Ihre E-Mail ein und erhalten Sie Updates zu Terminen, Preisträgern und zur nächsten Bewerbungsphase.',
|
||||
emailLabel: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'name@unternehmen.de',
|
||||
submitLabel: 'Eintragen',
|
||||
privacy: 'Kein Spam. Nur relevante Informationen rund um den Bayerischen Mittelstandspreis.',
|
||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||
successHeading: 'Danke',
|
||||
successBody: 'Ihre E-Mail wurde für BMP-Updates eingetragen.',
|
||||
footnote: 'BMP-Updates -',
|
||||
footnoteStrong: 'Termine und nächste Runde',
|
||||
},
|
||||
newsletterPhaseCompleted: {
|
||||
eyebrow: 'Nächste Runde',
|
||||
heading: 'DEN START\nNICHT\nVERPASSEN.',
|
||||
body:
|
||||
'Der aktuelle Preisjahrgang ist abgeschlossen. Hinterlassen Sie Ihre E-Mail und wir informieren Sie, sobald es Neuigkeiten zur nächsten Bewerbungsphase gibt.',
|
||||
benefits: [
|
||||
{ num: '01', label: 'Startsignal', desc: 'Neue Bewerbungsphase' },
|
||||
{ num: '02', label: 'BMP-News', desc: 'Preisträger und Termine' },
|
||||
{ num: '03', label: 'Erinnerung', desc: 'Rechtzeitig vorbereitet' },
|
||||
],
|
||||
formEyebrow: 'Newsletter abonnieren',
|
||||
formTitle: 'Informiert bleiben',
|
||||
formBody:
|
||||
'Tragen Sie sich ein und erhalten Sie Hinweise zum Bewerbungsstart, zu Veranstaltungen und zu ausgezeichneten Unternehmen.',
|
||||
emailLabel: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'name@unternehmen.de',
|
||||
submitLabel: 'Abonnieren',
|
||||
privacy: 'Sie erhalten nur BMP-relevante Informationen. Eine Abmeldung ist jederzeit möglich.',
|
||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||
successHeading: 'Angemeldet',
|
||||
successBody: 'Danke. Sie erhalten BMP-Updates an die angegebene E-Mail-Adresse.',
|
||||
footnote: 'Bewerbungsstart im Blick -',
|
||||
footnoteStrong: 'BMP-Updates erhalten',
|
||||
},
|
||||
},
|
||||
form: participationContent.form,
|
||||
videoModal: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { ChevronRight, Star } from 'lucide-react'
|
||||
|
||||
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection'
|
||||
import TestimonialsSection from '@/spa/components/TestimonialsSection'
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||
@@ -61,6 +62,12 @@ const paragraphs = (value: unknown, fallback: string[]) =>
|
||||
? (value as Array<string | TextRow>).map((item) => (typeof item === 'string' ? item : fallbackText(item.text, ''))).filter(Boolean)
|
||||
: fallback
|
||||
|
||||
const isParticipationPage = (page: CmsRouteDoc) => {
|
||||
const title = String(page.title || '').toLowerCase()
|
||||
|
||||
return page.spaPath === '/teilnahme' || page.slug === 'teilnahme' || page.slug === 'participation' || title === 'teilnahme' || title === 'participation'
|
||||
}
|
||||
|
||||
function HighlightedText({ text, highlight }: { text: string; highlight: string }) {
|
||||
const index = text.indexOf(highlight)
|
||||
if (!highlight || index < 0) return <>{text}</>
|
||||
@@ -115,6 +122,7 @@ const About: React.FC = () => {
|
||||
const isMobile = useIsMobile()
|
||||
const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms
|
||||
const cmsWinners = useCmsCollection('preistraeger')
|
||||
const cmsPages = useCmsCollection('pages')
|
||||
const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
|
||||
const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
|
||||
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
|
||||
@@ -124,6 +132,8 @@ const About: React.FC = () => {
|
||||
const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
|
||||
const values = { ...aboutContent.values, ...(cms.values || {}) }
|
||||
const cta = { ...aboutContent.cta, ...(cms.cta || {}) }
|
||||
const participationPage = cmsPages.find(isParticipationPage)
|
||||
const participationAwardsGrid = (participationPage?.participation as { awardsGrid?: AwardsGridContent } | undefined)?.awardsGrid
|
||||
const showSecondaryCta = !HIDE_MEMBERSHIP_ABOUT_CTA || !isMembershipCta(cta.secondaryCta)
|
||||
const highlightFallbackImage = mediaUrl(
|
||||
highlights.fallbackImage,
|
||||
@@ -407,6 +417,8 @@ const About: React.FC = () => {
|
||||
))}
|
||||
</section>
|
||||
|
||||
<AwardsGridSection content={participationAwardsGrid} />
|
||||
|
||||
<section style={{ background: NAVY, padding: isMobile ? '48px 24px' : '100px 80px', position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '60%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.3 }} />
|
||||
<div style={{ position: 'absolute', bottom: 0, left: '50%', transform: 'translateX(-50%)', width: '40%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.15 }} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Link } from '@/spa/router';
|
||||
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
|
||||
import { WINNERS } from '@/spa/data/winners';
|
||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
||||
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
|
||||
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
@@ -55,6 +56,8 @@ type HomeSectionCms = CmsRecord & {
|
||||
imageAlt?: string | null;
|
||||
imageLabel?: string | null;
|
||||
items?: unknown;
|
||||
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
||||
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
||||
note?: string | null;
|
||||
quote?: string | null;
|
||||
stats?: unknown;
|
||||
@@ -116,6 +119,41 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
|
||||
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
|
||||
|
||||
const HIDE_MEMBERSHIP_HOME_HERO_CTA = true;
|
||||
const APPLICATION_PHASE_OPEN = '0';
|
||||
|
||||
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
|
||||
'1': homeContent.application.newsletterPhaseEvaluation,
|
||||
'2': homeContent.application.newsletterPhaseCompleted,
|
||||
};
|
||||
|
||||
const mergeNewsletterInterestCopy = (
|
||||
value: NewsletterInterestCms | null | undefined,
|
||||
fallback: NewsletterInterestCopy,
|
||||
): NewsletterInterestCopy => ({
|
||||
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
|
||||
heading: fallbackText(value?.heading, fallback.heading),
|
||||
body: fallbackText(value?.body, fallback.body),
|
||||
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
|
||||
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
|
||||
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
|
||||
formBody: fallbackText(value?.formBody, fallback.formBody),
|
||||
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
|
||||
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
|
||||
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
|
||||
privacy: fallbackText(value?.privacy, fallback.privacy),
|
||||
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
|
||||
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
|
||||
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
|
||||
successBody: fallbackText(value?.successBody, fallback.successBody),
|
||||
footnote: fallbackText(value?.footnote, fallback.footnote),
|
||||
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
|
||||
});
|
||||
|
||||
const getNewsletterInterestCopy = (phase: string | undefined, application: HomeSectionCms) => {
|
||||
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
|
||||
const cmsCopy = phase === '1' ? application.newsletterPhaseEvaluation : application.newsletterPhaseCompleted;
|
||||
return mergeNewsletterInterestCopy(cmsCopy, fallback);
|
||||
};
|
||||
|
||||
const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | null) =>
|
||||
cta?.url === '/mitglied-werden' ||
|
||||
@@ -238,6 +276,9 @@ const Home: React.FC = () => {
|
||||
const form = home.form || {};
|
||||
const videoModal = home.videoModal || {};
|
||||
const applicationPhase = useApplicationPhase();
|
||||
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
||||
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
||||
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, application);
|
||||
const cmsPreistraeger = useCmsCollection('preistraeger');
|
||||
const selectedWinners = fallbackArray(winnersSection.featured, []);
|
||||
const heroSecondaryCta = {
|
||||
@@ -260,6 +301,7 @@ const Home: React.FC = () => {
|
||||
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
|
||||
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
|
||||
const applicationBenefits = fallbackArray(application.benefits, homeContent.application.benefits);
|
||||
const finalSectionBenefits = showApplicationForm ? applicationBenefits : newsletterCopy.benefits;
|
||||
const winnerFallbackImage = mediaUrl(winnersSection.fallbackImage, `/images/${homeContent.winners.fallbackImageFilename}`);
|
||||
const videoTitle = fallbackText(videoModal.title, homeContent.videoModal.title);
|
||||
const videoSrc = mediaUrl(videoModal.video, '');
|
||||
@@ -354,6 +396,9 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Status Phase Slider */}
|
||||
<StatusSlider data={applicationPhase} />
|
||||
|
||||
{/* Schnell-Check Section */}
|
||||
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||||
<MunichSkylineBg />
|
||||
@@ -503,9 +548,6 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Status Phase Slider */}
|
||||
<StatusSlider data={applicationPhase} />
|
||||
|
||||
{/* Winners Grid – neue Preisträger-Übersicht */}
|
||||
<section style={{ background: '#111D55' }}>
|
||||
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', justifyContent: 'space-between', gap: isMobile ? 16 : 0 }}>
|
||||
@@ -617,8 +659,8 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section – with embedded form */}
|
||||
<section style={{ background: '#24366A', position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : 'calc(100vh - 60px)', display: 'flex', flexDirection: 'column' }} id="bewerben">
|
||||
{/* CTA Section – with phase-aware embedded form */}
|
||||
<section style={{ background: '#24366A', position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : showApplicationForm ? 'calc(100vh - 60px)' : 'auto', minHeight: isMobile ? undefined : showApplicationForm ? undefined : 560, display: 'flex', flexDirection: 'column' }} id="bewerben">
|
||||
{/* Subtle radial glow behind left copy */}
|
||||
<div style={{ position: 'absolute', left: -120, top: '50%', transform: 'translateY(-50%)', width: 600, height: 600, borderRadius: '50%', background: 'radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%)', pointerEvents: 'none' }} />
|
||||
|
||||
@@ -626,18 +668,18 @@ const Home: React.FC = () => {
|
||||
|
||||
{/* Left – pitch copy */}
|
||||
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: 'hidden' }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.36em', fontWeight: 700, display: 'block', marginBottom: 16, opacity: 0.8 }}>{fallbackText(application.eyebrow, homeContent.application.eyebrow)}</span>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.36em', fontWeight: 700, display: 'block', marginBottom: 16, opacity: 0.8 }}>{showApplicationForm ? fallbackText(application.eyebrow, homeContent.application.eyebrow) : newsletterCopy.eyebrow}</span>
|
||||
<h2 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.6rem)' : 'clamp(1.8rem, 2.8vw, 2.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 1.0, marginBottom: 16 }}>
|
||||
<Lines text={fallbackText(application.heading, homeContent.application.heading)} />
|
||||
<Lines text={showApplicationForm ? fallbackText(application.heading, homeContent.application.heading) : newsletterCopy.heading} />
|
||||
</h2>
|
||||
<div style={{ width: 32, height: 2, background: '#EFBF04', marginBottom: 20 }} />
|
||||
<p style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.6)', lineHeight: 1.7, marginBottom: 32 }}>
|
||||
{fallbackText(application.body, homeContent.application.body)}
|
||||
{showApplicationForm ? fallbackText(application.body, homeContent.application.body) : newsletterCopy.body}
|
||||
</p>
|
||||
|
||||
{/* Benefit rows */}
|
||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
{applicationBenefits.map(item => (
|
||||
{finalSectionBenefits.map(item => (
|
||||
<div key={item.num} style={{ display: 'grid', gridTemplateColumns: '24px 1fr', gap: '0 14px', padding: '13px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', alignItems: 'center' }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, color: 'rgba(239,191,4,0.55)', letterSpacing: '0.1em' }}>{item.num}</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
|
||||
@@ -687,10 +729,10 @@ const Home: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Form area */}
|
||||
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{fallbackText(application.formEyebrow, homeContent.application.formEyebrow)}</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{fallbackText(application.formTitle, homeContent.application.formTitle)}</h3>
|
||||
<BewerbungsForm theme="gold" content={form} />
|
||||
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile && showApplicationForm ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{showApplicationForm ? fallbackText(application.formEyebrow, homeContent.application.formEyebrow) : newsletterCopy.formEyebrow}</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{showApplicationForm ? fallbackText(application.formTitle, homeContent.application.formTitle) : newsletterCopy.formTitle}</h3>
|
||||
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -698,8 +740,8 @@ const Home: React.FC = () => {
|
||||
|
||||
{/* Footnote row */}
|
||||
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '16px 24px 20px' : '14px 56px 18px', display: 'flex', flexWrap: 'wrap', justifyContent: 'center', textAlign: 'center', borderTop: '1px solid rgba(255,255,255,0.06)', flexShrink: 0 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)', marginRight: isMobile ? 6 : 10 }}>{fallbackText(application.footnote, homeContent.application.footnote)}</span>
|
||||
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: 'rgba(239,191,4,0.8)' }}>{fallbackText(application.footnoteStrong, homeContent.application.footnoteStrong)}</span>
|
||||
<span style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)', marginRight: isMobile ? 6 : 10 }}>{showApplicationForm ? fallbackText(application.footnote, homeContent.application.footnote) : newsletterCopy.footnote}</span>
|
||||
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: 'rgba(239,191,4,0.8)' }}>{showApplicationForm ? fallbackText(application.footnoteStrong, homeContent.application.footnoteStrong) : newsletterCopy.footnoteStrong}</span>
|
||||
</div>
|
||||
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
||||
</section>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star, Mail } 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 { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
|
||||
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
|
||||
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 { useApplicationPhase, useCmsRoute } from '@/spa/cmsRoute';
|
||||
import { participationContent } from '@/spa/participationContent';
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||
|
||||
@@ -18,12 +20,15 @@ const CREAM = '#E4E2E3';
|
||||
|
||||
type ParticipationCms = Partial<typeof participationContent> & {
|
||||
hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown };
|
||||
awardsGrid?: Partial<typeof participationContent.awardsGrid> & { cards?: AwardCardCms[] };
|
||||
applicationForm?: Partial<typeof participationContent.applicationForm> & { image?: unknown };
|
||||
awardsGrid?: AwardsGridContent;
|
||||
applicationForm?: ParticipationApplicationFormCms;
|
||||
};
|
||||
|
||||
type AwardCard = (typeof participationContent.awardsGrid.cards)[number];
|
||||
type AwardCardCms = Partial<AwardCard> & { image?: unknown };
|
||||
type ParticipationApplicationFormCms = Partial<typeof participationContent.applicationForm> & {
|
||||
image?: unknown;
|
||||
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
||||
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
||||
};
|
||||
|
||||
const ICONS = {
|
||||
building2: Building2,
|
||||
@@ -38,12 +43,50 @@ const ICONS = {
|
||||
const fallbackText = (value: unknown, fallback: string) =>
|
||||
typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||
|
||||
const fallbackArray = <T,>(value: unknown, fallback: T[]) =>
|
||||
const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
|
||||
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
|
||||
|
||||
const mergeAwardCards = (value: unknown, fallback: AwardCard[]): (AwardCard & { image?: unknown })[] => {
|
||||
const cards = Array.isArray(value) && value.length > 0 ? (value as AwardCardCms[]) : fallback;
|
||||
return cards.map((card, index) => ({ ...(fallback[index] || fallback[0]), ...card }));
|
||||
const APPLICATION_PHASE_OPEN = '0';
|
||||
|
||||
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
|
||||
'1': participationContent.applicationForm.newsletterPhaseEvaluation,
|
||||
'2': participationContent.applicationForm.newsletterPhaseCompleted,
|
||||
};
|
||||
|
||||
const mergeNewsletterInterestCopy = (
|
||||
value: NewsletterInterestCms | null | undefined,
|
||||
fallback: NewsletterInterestCopy,
|
||||
): NewsletterInterestCopy => ({
|
||||
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
|
||||
heading: fallbackText(value?.heading, fallback.heading),
|
||||
body: fallbackText(value?.body, fallback.body),
|
||||
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
|
||||
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
|
||||
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
|
||||
formBody: fallbackText(value?.formBody, fallback.formBody),
|
||||
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
|
||||
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
|
||||
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
|
||||
privacy: fallbackText(value?.privacy, fallback.privacy),
|
||||
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
|
||||
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
|
||||
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
|
||||
successBody: fallbackText(value?.successBody, fallback.successBody),
|
||||
footnote: fallbackText(value?.footnote, fallback.footnote),
|
||||
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
|
||||
});
|
||||
|
||||
const getNewsletterInterestCopy = (phase: string | undefined, applicationForm: ParticipationApplicationFormCms) => {
|
||||
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
|
||||
const cmsCopy = phase === '1' ? applicationForm.newsletterPhaseEvaluation : applicationForm.newsletterPhaseCompleted;
|
||||
return mergeNewsletterInterestCopy(cmsCopy, fallback);
|
||||
};
|
||||
|
||||
const isFutureAwardApplicationWay = (value: { label?: unknown; title?: unknown }) => {
|
||||
const label = typeof value.label === 'string' ? value.label.toLowerCase() : '';
|
||||
const title = typeof value.title === 'string' ? value.title.toLowerCase() : '';
|
||||
|
||||
return label.includes('sonderpreis') || title.includes('bavarian') || title.includes('future award');
|
||||
};
|
||||
|
||||
function Lines({ text }: { text: string }) {
|
||||
@@ -54,6 +97,7 @@ const Participation: React.FC = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const qualSectionRef = useRef<HTMLElement>(null);
|
||||
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
|
||||
const applicationPhase = useApplicationPhase();
|
||||
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
|
||||
const process = { ...participationContent.process, ...(cms.process || {}) };
|
||||
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
|
||||
@@ -62,11 +106,15 @@ const Participation: React.FC = () => {
|
||||
const awardsGrid = { ...participationContent.awardsGrid, ...(cms.awardsGrid || {}) };
|
||||
const applicationForm = { ...participationContent.applicationForm, ...(cms.applicationForm || {}) };
|
||||
const form = { ...participationContent.form, ...(cms.form || {}) };
|
||||
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
||||
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
||||
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, applicationForm);
|
||||
const eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
|
||||
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
|
||||
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
|
||||
const desktopDates = fallbackArray(datesBanner.desktopItems, participationContent.datesBanner.desktopItems);
|
||||
const applicationFacts = fallbackArray(applicationForm.facts, participationContent.applicationForm.facts);
|
||||
const finalSectionFacts = showApplicationForm ? applicationFacts : newsletterCopy.benefits;
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in">
|
||||
@@ -224,21 +272,21 @@ const Participation: React.FC = () => {
|
||||
<AwardsGridSection content={awardsGrid} />
|
||||
|
||||
{/* ── FORM SECTION ─────────────────────────────────────────────────────── */}
|
||||
<section style={{ background: NAVY, position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : 'calc(100vh - 60px)', display: 'flex', flexDirection: 'column' }} id="bewerben">
|
||||
<section style={{ background: NAVY, position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : showApplicationForm ? 'calc(100vh - 60px)' : 'auto', minHeight: isMobile ? undefined : showApplicationForm ? undefined : 560, display: 'flex', flexDirection: 'column' }} id="bewerben">
|
||||
<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 }}>{fallbackText(applicationForm.eyebrow, participationContent.applicationForm.eyebrow)}</span>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{showApplicationForm ? fallbackText(applicationForm.eyebrow, participationContent.applicationForm.eyebrow) : newsletterCopy.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' }}>
|
||||
<Lines text={fallbackText(applicationForm.heading, participationContent.applicationForm.heading)} />
|
||||
<Lines text={showApplicationForm ? fallbackText(applicationForm.heading, participationContent.applicationForm.heading) : newsletterCopy.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 }}>
|
||||
{fallbackText(applicationForm.description, participationContent.applicationForm.description)}
|
||||
{showApplicationForm ? fallbackText(applicationForm.description, participationContent.applicationForm.description) : newsletterCopy.body}
|
||||
</p>
|
||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
{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' }}>
|
||||
{finalSectionFacts.map((item, i) => (
|
||||
<div key={`${fallbackText(item.num, String(i))}-${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' }}>{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' }}>{fallbackText(item.label, '')}</span>
|
||||
@@ -279,31 +327,51 @@ const Participation: React.FC = () => {
|
||||
</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 }}>{fallbackText(applicationForm.formEyebrow, participationContent.applicationForm.formEyebrow)}</span>
|
||||
<Link
|
||||
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>
|
||||
{fallbackText(applicationForm.uploadCta?.label, participationContent.applicationForm.uploadCta.label)}
|
||||
</Link>
|
||||
</div>
|
||||
<BewerbungsForm theme="gold" content={form} />
|
||||
<div style={{ padding: isMobile ? '28px 20px' : '24px 48px 32px 40px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: isMobile && showApplicationForm ? 560 : 0, position: 'relative', zIndex: 1 }}>
|
||||
{showApplicationForm ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 10 }}>
|
||||
<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={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>
|
||||
{fallbackText(applicationForm.uploadCta?.label, participationContent.applicationForm.uploadCta.label)}
|
||||
</Link>
|
||||
</div>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{fallbackText(applicationForm.formTitle, participationContent.applicationForm.formTitle)}</h3>
|
||||
<BewerbungsForm theme="gold" content={form} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{newsletterCopy.formEyebrow}</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{newsletterCopy.formTitle}</h3>
|
||||
<NewsletterInterestForm copy={newsletterCopy} />
|
||||
</>
|
||||
)}
|
||||
</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 }}>
|
||||
{fallbackText(applicationForm.uploadPrompt, participationContent.applicationForm.uploadPrompt)}
|
||||
</span>
|
||||
<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>
|
||||
{showApplicationForm ? (
|
||||
<>
|
||||
<span style={{ fontFamily: FB, fontSize: 14, color: 'rgba(255,255,255,0.55)', lineHeight: 1.6, maxWidth: 520 }}>
|
||||
{fallbackText(applicationForm.uploadPrompt, participationContent.applicationForm.uploadPrompt)}
|
||||
</span>
|
||||
<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 style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: isMobile ? '0 6px' : '0 10px' }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)' }}>{newsletterCopy.footnote}</span>
|
||||
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: 'rgba(239,191,4,0.8)' }}>{newsletterCopy.footnoteStrong}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
||||
</section>
|
||||
@@ -349,7 +417,9 @@ 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);
|
||||
const ways = fallbackArray(section.ways, participationContent.applicationWays.ways).filter(
|
||||
(way) => !isFutureAwardApplicationWay(way),
|
||||
);
|
||||
const fallbackCta = { ...participationContent.applicationWays.fallbackCta, ...(section.fallbackCta || {}) };
|
||||
return (
|
||||
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
|
||||
@@ -368,24 +438,21 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3 Wege – premium swipe cards on mobile, 3-col grid on desktop */}
|
||||
<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 }}>
|
||||
{/* Wege – premium swipe cards on mobile, equal grid columns on desktop */}
|
||||
<div style={{ position: 'relative', zIndex: 1, display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : `repeat(${Math.max(ways.length, 1)}, minmax(0, 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 && 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={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)' }}>
|
||||
<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: '1px solid rgba(17,29,85,0.10)', boxShadow: '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 }}>{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 style={{ width: 46, height: 46, borderRadius: 12, background: NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 18 }}>
|
||||
<Icon size={20} style={{ color: 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 }}>{fallbackText(w.label, '')}</span>
|
||||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: '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 }} />
|
||||
@@ -420,23 +487,22 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
|
||||
|
||||
{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 key={`${w.label}-${index}`} style={{ padding: isMobile ? '32px 24px' : '56px 48px 56px', borderRight: !isLast ? '1px solid #D0D5DD' : 'none', display: 'flex', flexDirection: 'column', background: '#fff', ...(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 }}>
|
||||
<Icon size={18} style={{ color: GOLD }} strokeWidth={1.5} />
|
||||
</div>
|
||||
<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' }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '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: '#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: isFeatured ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.5)', lineHeight: 1.7, margin: '0 0 32px' }}>
|
||||
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, margin: '0 0 32px' }}>
|
||||
{fallbackText(w.desc, '')}
|
||||
</p>
|
||||
|
||||
<div style={{ borderTop: isFeatured ? '1px solid rgba(255,255,255,0.1)' : '1px solid #D0D5DD', flex: 1 }}>
|
||||
<div style={{ borderTop: '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 }}>
|
||||
@@ -445,9 +511,9 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
|
||||
<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 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' }}>{fallbackText(item.label, '')}</span>
|
||||
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.45)' }}>{fallbackText(item.desc, '')}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -471,131 +537,4 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
|
||||
);
|
||||
}
|
||||
|
||||
type AwardsGridContent = Partial<typeof participationContent.awardsGrid>;
|
||||
|
||||
function AwardsGridSection({ content }: { content?: AwardsGridContent }) {
|
||||
const isMobile = useIsMobile();
|
||||
const section = { ...participationContent.awardsGrid, ...(content || {}) };
|
||||
const cards = mergeAwardCards(section.cards, participationContent.awardsGrid.cards);
|
||||
|
||||
return (
|
||||
<section id="auszeichnungen" style={{ background: '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
|
||||
<MunichSkylineBg />
|
||||
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>
|
||||
{fallbackText(section.eyebrow, participationContent.awardsGrid.eyebrow)}
|
||||
</span>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.9fr) minmax(0, 1fr)', gap: isMobile ? 16 : 56, alignItems: '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, overflowWrap: 'anywhere' }}>
|
||||
<Lines text={fallbackText(section.heading, participationContent.awardsGrid.heading)} />
|
||||
</h2>
|
||||
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}>
|
||||
{fallbackText(section.description, participationContent.awardsGrid.description)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))', borderBottom: '1px solid rgba(3,9,58,0.08)' }}>
|
||||
{cards.map((card, index) => {
|
||||
const imageFilename = fallbackText(card.imageFilename, participationContent.awardsGrid.cards[index]?.imageFilename || participationContent.awardsGrid.cards[0].imageFilename);
|
||||
const isContainedImage = imageFilename.endsWith('.png') || imageFilename.includes('roland-berger');
|
||||
const isLast = index === cards.length - 1;
|
||||
|
||||
return (
|
||||
<article
|
||||
key={`${fallbackText(card.title, 'award')}-${index}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: isMobile ? 'auto' : 620,
|
||||
borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
|
||||
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
|
||||
background: index === 1 ? NAVY : 'rgba(255,255,255,0.72)',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: isMobile ? 220 : 260, background: imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
|
||||
<Image unoptimized
|
||||
src={mediaUrl(card.image, `/images/${imageFilename}`)}
|
||||
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: isContainedImage ? 'contain' : 'cover',
|
||||
objectPosition: index === 1 ? 'center top' : 'center',
|
||||
padding: imageFilename.endsWith('.png') ? 22 : 0,
|
||||
boxSizing: 'border-box',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: isMobile ? '28px 24px 32px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.26em', textTransform: 'uppercase', color: index === 1 ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}>
|
||||
{fallbackText(card.label, '')}
|
||||
</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.25rem, 2vw, 1.7rem)', fontWeight: 900, color: index === 1 ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 16px', overflowWrap: 'anywhere' }}>
|
||||
<Lines text={fallbackText(card.title, '')} />
|
||||
</h3>
|
||||
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 22 }} />
|
||||
<p style={{ fontFamily: FB, fontSize: 18, color: index === 1 ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.55)', lineHeight: 1.7, margin: 0, flex: 1 }}>
|
||||
{fallbackText(card.description, '')}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, marginTop: 32 }}>
|
||||
<Link
|
||||
to={fallbackText(card.articleCta?.url, '#bewerben')}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '0 18px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
background: GOLD,
|
||||
color: '#101828',
|
||||
fontFamily: FF,
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{fallbackText(card.articleCta?.label, 'Artikel lesen')} <ArrowRight size={13} />
|
||||
</Link>
|
||||
<a
|
||||
href={fallbackText(card.mailtoCta?.url, `mailto:${participationContent.form.eligibility.ineligibleContactEmail}`)}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '0 18px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
color: index === 1 ? '#fff' : NAVY,
|
||||
border: `1px solid ${index === 1 ? 'rgba(255,255,255,0.22)' : 'rgba(3,9,58,0.18)'}`,
|
||||
fontFamily: FF,
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Mail size={13} /> {fallbackText(card.mailtoCta?.label, 'Kontakt aufnehmen')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Participation;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Link } from '@/spa/router';
|
||||
import { Trophy, ChevronRight, ArrowRight } from 'lucide-react';
|
||||
import { Trophy, ArrowRight } from 'lucide-react';
|
||||
import { WINNERS, type Winner } from '@/spa/data/winners';
|
||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
@@ -14,6 +14,7 @@ const GOLD = '#EFBF04';
|
||||
const BORDER = '#D0D5DD';
|
||||
const GRAY = '#666666';
|
||||
const BG_ALT = '#E4E2E3';
|
||||
const SIMPLE_LIST_YEAR = 2023;
|
||||
|
||||
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
|
||||
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
|
||||
@@ -69,6 +70,7 @@ export default function Preistraeger() {
|
||||
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
|
||||
|
||||
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]);
|
||||
const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR;
|
||||
|
||||
return (
|
||||
<div style={{ background: '#fff', minHeight: '100vh' }}>
|
||||
@@ -129,6 +131,8 @@ export default function Preistraeger() {
|
||||
<div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}>
|
||||
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
|
||||
</div>
|
||||
) : usesSimpleWinnerGrid ? (
|
||||
<SimpleWinnerGrid winners={filtered} />
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
@@ -165,38 +169,6 @@ export default function Preistraeger() {
|
||||
>
|
||||
{fallbackText(cta.primaryCta?.label, preistraegerIndexContent.cta.primaryCta.label)} <ArrowRight size={14} />
|
||||
</Link>
|
||||
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : undefined, flexWrap: isMobile ? 'wrap' : 'nowrap', gap: 8 }}>
|
||||
<span style={{ width: 20, height: 1, background: 'rgba(239,191,4,0.3)', display: 'inline-block' }} />
|
||||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 11, color: 'rgba(16,24,40,0.4)' }}>{fallbackText(cta.secondaryPrefix, preistraegerIndexContent.cta.secondaryPrefix)}</span>
|
||||
<Link
|
||||
to={fallbackText(cta.secondaryCta?.url, preistraegerIndexContent.cta.secondaryCta.url)}
|
||||
style={{
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
fontSize: 11,
|
||||
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,
|
||||
transition: 'color 0.15s, borderColor 0.15s',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLElement).style.color = '#EFBF04';
|
||||
(e.currentTarget as HTMLElement).style.borderBottomColor = '#EFBF04';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
(e.currentTarget as HTMLElement).style.color = 'rgba(239,191,4,0.7)';
|
||||
(e.currentTarget as HTMLElement).style.borderBottomColor = 'rgba(239,191,4,0.3)';
|
||||
}}
|
||||
>
|
||||
{fallbackText(cta.secondaryCta?.label, preistraegerIndexContent.cta.secondaryCta.label)} <ChevronRight size={10} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -257,3 +229,91 @@ function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function SimpleWinnerGrid({ winners }: { winners: Winner[] }) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#F7F7F8',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))',
|
||||
gap: isMobile ? 12 : 16,
|
||||
padding: isMobile ? '24px' : '40px 80px 56px',
|
||||
}}>
|
||||
{winners.map(winner => (
|
||||
<SimpleWinnerCard key={winner.id} winner={winner} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SimpleWinnerCard({ winner }: { winner: Winner }) {
|
||||
const isMobile = useIsMobile();
|
||||
const hasDescription = winner.shortDesc.trim().length > 0;
|
||||
|
||||
return (
|
||||
<article
|
||||
style={{
|
||||
background: '#fff',
|
||||
border: `1px solid ${BORDER}`,
|
||||
color: '#101828',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: isMobile ? 0 : 220,
|
||||
padding: isMobile ? '20px' : '24px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 18 }}>
|
||||
<span style={{
|
||||
color: GOLD,
|
||||
fontFamily: FF,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
{winner.year} · {winner.type}
|
||||
</span>
|
||||
{winner.category ? (
|
||||
<span style={{
|
||||
border: `1px solid ${BORDER}`,
|
||||
color: GRAY,
|
||||
fontFamily: FF,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
padding: '5px 8px',
|
||||
}}>
|
||||
{winner.category}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<h3 style={{
|
||||
color: NAVY,
|
||||
fontFamily: FF,
|
||||
fontSize: isMobile ? 20 : 22,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.18,
|
||||
margin: 0,
|
||||
overflowWrap: 'anywhere',
|
||||
}}>
|
||||
{winner.name}
|
||||
</h3>
|
||||
|
||||
{hasDescription ? (
|
||||
<p style={{
|
||||
color: GRAY,
|
||||
fontFamily: FF,
|
||||
fontSize: 15,
|
||||
lineHeight: 1.55,
|
||||
margin: '16px 0 0',
|
||||
overflowWrap: 'anywhere',
|
||||
}}>
|
||||
{winner.shortDesc}
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export const participationContent = {
|
||||
title: 'Initiativbewerbung',
|
||||
desc: 'Für Unternehmer, die selbst die Initiative ergreifen.',
|
||||
icon: 'userCheck',
|
||||
featured: true,
|
||||
featured: false,
|
||||
cta: { label: 'Jetzt bewerben', url: '#bewerben' },
|
||||
listType: 'facts',
|
||||
facts: [
|
||||
@@ -117,21 +117,6 @@ export const participationContent = {
|
||||
{ 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',
|
||||
},
|
||||
@@ -164,9 +149,60 @@ export const participationContent = {
|
||||
{ num: '03', label: 'Mehrstufig', desc: 'Transparenter Prozess' },
|
||||
],
|
||||
formEyebrow: 'Bewerbung 2026',
|
||||
formTitle: 'Kostenlos bewerben',
|
||||
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' },
|
||||
newsletterPhaseEvaluation: {
|
||||
eyebrow: 'Updates erhalten',
|
||||
heading: 'DIE BEWERBUNG\nIST GESCHLOSSEN.',
|
||||
body:
|
||||
'Die Bewerbungsphase 2026 ist geschlossen. Hinterlassen Sie Ihre E-Mail und erhalten Sie Updates zur Juryphase, zur Preisverleihung und zur nächsten Runde.',
|
||||
benefits: [
|
||||
{ num: '01', label: 'Juryphase', desc: 'Updates zum Verlauf' },
|
||||
{ num: '02', label: 'Preisverleihung', desc: 'Termine und Einblicke' },
|
||||
{ num: '03', label: 'Nächste Runde', desc: 'Start nicht verpassen' },
|
||||
],
|
||||
formEyebrow: 'Newsletter abonnieren',
|
||||
formTitle: 'Updates erhalten',
|
||||
formBody:
|
||||
'Tragen Sie Ihre E-Mail ein und erhalten Sie relevante Informationen zum weiteren Verlauf des Bayerischen Mittelstandspreises.',
|
||||
emailLabel: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'name@unternehmen.de',
|
||||
submitLabel: 'Eintragen',
|
||||
privacy: 'Kein Spam. Nur relevante Informationen rund um den Bayerischen Mittelstandspreis.',
|
||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||
successHeading: 'Danke',
|
||||
successBody: 'Ihre E-Mail wurde für BMP-Updates eingetragen.',
|
||||
footnote: 'BMP-Updates -',
|
||||
footnoteStrong: 'Juryphase und Termine',
|
||||
},
|
||||
newsletterPhaseCompleted: {
|
||||
eyebrow: 'Nächste Runde',
|
||||
heading: 'DEN START\nNICHT\nVERPASSEN.',
|
||||
body:
|
||||
'Der aktuelle Preisjahrgang ist abgeschlossen. Hinterlassen Sie Ihre E-Mail und wir informieren Sie, sobald es Neuigkeiten zur nächsten Bewerbungsphase gibt.',
|
||||
benefits: [
|
||||
{ num: '01', label: 'Startsignal', desc: 'Neue Bewerbungsphase' },
|
||||
{ num: '02', label: 'BMP-News', desc: 'Preisträger und Termine' },
|
||||
{ num: '03', label: 'Erinnerung', desc: 'Rechtzeitig vorbereitet' },
|
||||
],
|
||||
formEyebrow: 'Newsletter abonnieren',
|
||||
formTitle: 'Informiert bleiben',
|
||||
formBody:
|
||||
'Tragen Sie sich ein und erhalten Sie Hinweise zum Bewerbungsstart, zu Veranstaltungen und zu ausgezeichneten Unternehmen.',
|
||||
emailLabel: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'name@unternehmen.de',
|
||||
submitLabel: 'Abonnieren',
|
||||
privacy: 'Sie erhalten nur BMP-relevante Informationen. Eine Abmeldung ist jederzeit möglich.',
|
||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||
successHeading: 'Angemeldet',
|
||||
successBody: 'Danke. Sie erhalten BMP-Updates an die angegebene E-Mail-Adresse.',
|
||||
footnote: 'Bewerbungsstart im Blick -',
|
||||
footnoteStrong: 'BMP-Updates erhalten',
|
||||
},
|
||||
},
|
||||
form: {
|
||||
stepCompleteLabel: '✓',
|
||||
|
||||
Reference in New Issue
Block a user