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,
|
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[] = [
|
export const homeFields: Field[] = [
|
||||||
{
|
{
|
||||||
name: 'home',
|
name: 'home',
|
||||||
@@ -236,6 +266,20 @@ export const homeFields: Field[] = [
|
|||||||
text('formTitle', 'Form title', homeContent.application.formTitle),
|
text('formTitle', 'Form title', homeContent.application.formTitle),
|
||||||
text('footnote', 'Footnote normal text', homeContent.application.footnote),
|
text('footnote', 'Footnote normal text', homeContent.application.footnote),
|
||||||
text('footnoteStrong', 'Footnote highlighted text', homeContent.application.footnoteStrong),
|
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',
|
type: 'group',
|
||||||
fields: linkGroup(labelDefault, urlDefault),
|
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)
|
const awardCardDefaults = participationContent.awardsGrid.cards.map(({ imageFilename: _imageFilename, ...card }) => card)
|
||||||
|
|
||||||
export const participationFields: Field[] = [
|
export const participationFields: Field[] = [
|
||||||
@@ -158,7 +187,7 @@ export const participationFields: Field[] = [
|
|||||||
name: 'applicationWays',
|
name: 'applicationWays',
|
||||||
label: '04 · Application paths',
|
label: '04 · Application paths',
|
||||||
type: 'group',
|
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: [
|
fields: [
|
||||||
text('eyebrow', 'Eyebrow', participationContent.applicationWays.eyebrow),
|
text('eyebrow', 'Eyebrow', participationContent.applicationWays.eyebrow),
|
||||||
textarea('heading', 'Heading', participationContent.applicationWays.heading),
|
textarea('heading', 'Heading', participationContent.applicationWays.heading),
|
||||||
@@ -270,7 +299,7 @@ export const participationFields: Field[] = [
|
|||||||
name: 'applicationForm',
|
name: 'applicationForm',
|
||||||
label: '07 · Application form wrapper',
|
label: '07 · Application form wrapper',
|
||||||
type: 'group',
|
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: [
|
fields: [
|
||||||
uploadField('image', 'Form image', `Current frontend image: /images/${participationContent.applicationForm.imageFilename}`),
|
uploadField('image', 'Form image', `Current frontend image: /images/${participationContent.applicationForm.imageFilename}`),
|
||||||
text('imageAlt', 'Image alt text', participationContent.applicationForm.imageAlt),
|
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')],
|
fields: [text('num', 'Number'), text('label', 'Label'), textarea('desc', 'Description')],
|
||||||
},
|
},
|
||||||
text('formEyebrow', 'Form eyebrow', participationContent.applicationForm.formEyebrow),
|
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),
|
ctaGroup('uploadCta', 'Top upload CTA', participationContent.applicationForm.uploadCta.label, participationContent.applicationForm.uploadCta.url),
|
||||||
textarea('uploadPrompt', 'Bottom upload prompt', participationContent.applicationForm.uploadPrompt),
|
textarea('uploadPrompt', 'Bottom upload prompt', participationContent.applicationForm.uploadPrompt),
|
||||||
ctaGroup('uploadFooterCta', 'Bottom upload CTA', participationContent.applicationForm.uploadFooterCta.label, participationContent.applicationForm.uploadFooterCta.url),
|
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_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_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_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 = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -30,4 +31,9 @@ export const migrations = [
|
|||||||
down: migration_20260630_182219_prune_unused_admin_collections.down,
|
down: migration_20260630_182219_prune_unused_admin_collections.down,
|
||||||
name: '20260630_182219_prune_unused_admin_collections',
|
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;
|
formTitle?: string | null;
|
||||||
footnote?: string | null;
|
footnote?: string | null;
|
||||||
footnoteStrong?: 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.
|
* 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?: {
|
applicationWays?: {
|
||||||
eyebrow?: string | null;
|
eyebrow?: string | null;
|
||||||
@@ -915,7 +973,7 @@ export interface Page {
|
|||||||
| null;
|
| 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?: {
|
applicationForm?: {
|
||||||
/**
|
/**
|
||||||
@@ -936,6 +994,7 @@ export interface Page {
|
|||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
formEyebrow?: string | null;
|
formEyebrow?: string | null;
|
||||||
|
formTitle?: string | null;
|
||||||
uploadCta?: {
|
uploadCta?: {
|
||||||
label?: string | null;
|
label?: string | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -945,6 +1004,64 @@ export interface Page {
|
|||||||
label?: string | null;
|
label?: string | null;
|
||||||
url?: 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.
|
* 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;
|
formTitle?: T;
|
||||||
footnote?: T;
|
footnote?: T;
|
||||||
footnoteStrong?: 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?:
|
form?:
|
||||||
| T
|
| T
|
||||||
@@ -3550,6 +3723,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
formEyebrow?: T;
|
formEyebrow?: T;
|
||||||
|
formTitle?: T;
|
||||||
uploadCta?:
|
uploadCta?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -3563,6 +3737,62 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
label?: T;
|
label?: T;
|
||||||
url?: 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?:
|
form?:
|
||||||
| T
|
| 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 { ArrowRight } from 'lucide-react';
|
||||||
import LoadingScreen from '@/spa/components/ui/LoadingScreen';
|
import LoadingScreen from '@/spa/components/ui/LoadingScreen';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import { useApplicationPhase } from '@/spa/cmsRoute';
|
||||||
import {
|
import {
|
||||||
defaultFooterData,
|
defaultFooterData,
|
||||||
defaultHeaderData,
|
defaultHeaderData,
|
||||||
@@ -37,6 +38,7 @@ const LINE = 'rgba(239,191,4,0.35)';
|
|||||||
const FF_DISPLAY = '"IBM Plex Sans", sans-serif';
|
const FF_DISPLAY = '"IBM Plex Sans", sans-serif';
|
||||||
const FF_BODY = '"Inter", sans-serif';
|
const FF_BODY = '"Inter", sans-serif';
|
||||||
const HIDE_MEMBERSHIP_FOOTER_ENTRY = true;
|
const HIDE_MEMBERSHIP_FOOTER_ENTRY = true;
|
||||||
|
const APPLICATION_PHASE_OPEN = '0';
|
||||||
|
|
||||||
const isMembershipFooterLink = (link?: { label?: string; path?: string }) =>
|
const isMembershipFooterLink = (link?: { label?: string; path?: string }) =>
|
||||||
link?.path === '/mitglied-werden' || link?.label?.trim().toLowerCase() === 'mitglied werden';
|
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 footerCtas = footer.ctas;
|
||||||
const fallbackHeaderCtas = defaultHeaderData.ctas;
|
const fallbackHeaderCtas = defaultHeaderData.ctas;
|
||||||
const fallbackFooterCtas = defaultFooterData.ctas;
|
const fallbackFooterCtas = defaultFooterData.ctas;
|
||||||
|
const applicationPhase = useApplicationPhase();
|
||||||
|
const showFloatingApplicationCta = applicationPhase?.activePhase === APPLICATION_PHASE_OPEN;
|
||||||
const footerMembershipCta = footerCtas[1] || fallbackFooterCtas[1];
|
const footerMembershipCta = footerCtas[1] || fallbackFooterCtas[1];
|
||||||
const showFooterMembershipCta =
|
const showFooterMembershipCta =
|
||||||
!HIDE_MEMBERSHIP_FOOTER_ENTRY || !isMembershipFooterLink(footerMembershipCta);
|
!HIDE_MEMBERSHIP_FOOTER_ENTRY || !isMembershipFooterLink(footerMembershipCta);
|
||||||
@@ -565,7 +569,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
|
|
||||||
|
|
||||||
{/* ── FLOATING CTA ─────────────────────────────── */}
|
{/* ── FLOATING CTA ─────────────────────────────── */}
|
||||||
{location.pathname === '/' && !formVisible && !isMobile && (
|
{location.pathname === '/' && showFloatingApplicationCta && !formVisible && !isMobile && (
|
||||||
<Link
|
<Link
|
||||||
to="/teilnahme"
|
to="/teilnahme"
|
||||||
style={{
|
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 {
|
interface ContainerScrollContextValue {
|
||||||
scrollYProgress: MotionValue<number>
|
scrollYProgress: MotionValue<number>
|
||||||
}
|
}
|
||||||
@@ -128,13 +145,13 @@ export const ProcessCard: React.FC<ProcessCardProps> = ({
|
|||||||
const { scrollYProgress } = useContainerScrollContext()
|
const { scrollYProgress } = useContainerScrollContext()
|
||||||
const start = index / itemsLength
|
const start = index / itemsLength
|
||||||
const end = start + 1 / itemsLength
|
const end = start + 1 / itemsLength
|
||||||
const innerWidth = typeof window === "undefined" ? 0 : window.innerWidth
|
const viewportWidth = useViewportWidth()
|
||||||
const [ref, { width }] = useMeasure()
|
const [ref, { width }] = useMeasure()
|
||||||
|
|
||||||
const x = useTransform(
|
const x = useTransform(
|
||||||
scrollYProgress,
|
scrollYProgress,
|
||||||
[start, end],
|
[start, end],
|
||||||
[innerWidth, -((width ?? 0) * index) + 64 * index]
|
[viewportWidth, -((width ?? 0) * index) + 64 * index]
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -111,6 +111,56 @@ export const homeContent = {
|
|||||||
formTitle: 'Kostenlos bewerben',
|
formTitle: 'Kostenlos bewerben',
|
||||||
footnote: 'Kostenlos und unverbindlich –',
|
footnote: 'Kostenlos und unverbindlich –',
|
||||||
footnoteStrong: 'keine Teilnahmegebühr',
|
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,
|
form: participationContent.form,
|
||||||
videoModal: {
|
videoModal: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { ChevronRight, Star } from 'lucide-react'
|
import { ChevronRight, Star } from 'lucide-react'
|
||||||
|
|
||||||
|
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection'
|
||||||
import TestimonialsSection from '@/spa/components/TestimonialsSection'
|
import TestimonialsSection from '@/spa/components/TestimonialsSection'
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
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)
|
? (value as Array<string | TextRow>).map((item) => (typeof item === 'string' ? item : fallbackText(item.text, ''))).filter(Boolean)
|
||||||
: fallback
|
: 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 }) {
|
function HighlightedText({ text, highlight }: { text: string; highlight: string }) {
|
||||||
const index = text.indexOf(highlight)
|
const index = text.indexOf(highlight)
|
||||||
if (!highlight || index < 0) return <>{text}</>
|
if (!highlight || index < 0) return <>{text}</>
|
||||||
@@ -115,6 +122,7 @@ const About: React.FC = () => {
|
|||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms
|
const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms
|
||||||
const cmsWinners = useCmsCollection('preistraeger')
|
const cmsWinners = useCmsCollection('preistraeger')
|
||||||
|
const cmsPages = useCmsCollection('pages')
|
||||||
const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
|
const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
|
||||||
const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
|
const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
|
||||||
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
|
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
|
||||||
@@ -124,6 +132,8 @@ const About: React.FC = () => {
|
|||||||
const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
|
const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
|
||||||
const values = { ...aboutContent.values, ...(cms.values || {}) }
|
const values = { ...aboutContent.values, ...(cms.values || {}) }
|
||||||
const cta = { ...aboutContent.cta, ...(cms.cta || {}) }
|
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 showSecondaryCta = !HIDE_MEMBERSHIP_ABOUT_CTA || !isMembershipCta(cta.secondaryCta)
|
||||||
const highlightFallbackImage = mediaUrl(
|
const highlightFallbackImage = mediaUrl(
|
||||||
highlights.fallbackImage,
|
highlights.fallbackImage,
|
||||||
@@ -407,6 +417,8 @@ const About: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<AwardsGridSection content={participationAwardsGrid} />
|
||||||
|
|
||||||
<section style={{ background: NAVY, padding: isMobile ? '48px 24px' : '100px 80px', position: 'relative', overflow: 'hidden' }}>
|
<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', 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 }} />
|
<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 { PartnerTicker } from '@/spa/components/ui/partner-ticker';
|
||||||
import { WINNERS } from '@/spa/data/winners';
|
import { WINNERS } from '@/spa/data/winners';
|
||||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
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 TestimonialsSection from '@/spa/components/TestimonialsSection';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
@@ -55,6 +56,8 @@ type HomeSectionCms = CmsRecord & {
|
|||||||
imageAlt?: string | null;
|
imageAlt?: string | null;
|
||||||
imageLabel?: string | null;
|
imageLabel?: string | null;
|
||||||
items?: unknown;
|
items?: unknown;
|
||||||
|
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
||||||
|
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
quote?: string | null;
|
quote?: string | null;
|
||||||
stats?: unknown;
|
stats?: unknown;
|
||||||
@@ -116,6 +119,41 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
|
|||||||
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
|
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
|
||||||
|
|
||||||
const HIDE_MEMBERSHIP_HOME_HERO_CTA = true;
|
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) =>
|
const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | null) =>
|
||||||
cta?.url === '/mitglied-werden' ||
|
cta?.url === '/mitglied-werden' ||
|
||||||
@@ -238,6 +276,9 @@ const Home: React.FC = () => {
|
|||||||
const form = home.form || {};
|
const form = home.form || {};
|
||||||
const videoModal = home.videoModal || {};
|
const videoModal = home.videoModal || {};
|
||||||
const applicationPhase = useApplicationPhase();
|
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 cmsPreistraeger = useCmsCollection('preistraeger');
|
||||||
const selectedWinners = fallbackArray(winnersSection.featured, []);
|
const selectedWinners = fallbackArray(winnersSection.featured, []);
|
||||||
const heroSecondaryCta = {
|
const heroSecondaryCta = {
|
||||||
@@ -260,6 +301,7 @@ const Home: React.FC = () => {
|
|||||||
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
|
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
|
||||||
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
|
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
|
||||||
const applicationBenefits = fallbackArray(application.benefits, homeContent.application.benefits);
|
const applicationBenefits = fallbackArray(application.benefits, homeContent.application.benefits);
|
||||||
|
const finalSectionBenefits = showApplicationForm ? applicationBenefits : newsletterCopy.benefits;
|
||||||
const winnerFallbackImage = mediaUrl(winnersSection.fallbackImage, `/images/${homeContent.winners.fallbackImageFilename}`);
|
const winnerFallbackImage = mediaUrl(winnersSection.fallbackImage, `/images/${homeContent.winners.fallbackImageFilename}`);
|
||||||
const videoTitle = fallbackText(videoModal.title, homeContent.videoModal.title);
|
const videoTitle = fallbackText(videoModal.title, homeContent.videoModal.title);
|
||||||
const videoSrc = mediaUrl(videoModal.video, '');
|
const videoSrc = mediaUrl(videoModal.video, '');
|
||||||
@@ -354,6 +396,9 @@ const Home: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Status Phase Slider */}
|
||||||
|
<StatusSlider data={applicationPhase} />
|
||||||
|
|
||||||
{/* Schnell-Check Section */}
|
{/* Schnell-Check Section */}
|
||||||
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||||||
<MunichSkylineBg />
|
<MunichSkylineBg />
|
||||||
@@ -503,9 +548,6 @@ const Home: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Status Phase Slider */}
|
|
||||||
<StatusSlider data={applicationPhase} />
|
|
||||||
|
|
||||||
{/* Winners Grid – neue Preisträger-Übersicht */}
|
{/* Winners Grid – neue Preisträger-Übersicht */}
|
||||||
<section style={{ background: '#111D55' }}>
|
<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 }}>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CTA Section – with embedded form */}
|
{/* CTA Section – with phase-aware embedded form */}
|
||||||
<section style={{ background: '#24366A', position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : 'calc(100vh - 60px)', display: 'flex', flexDirection: 'column' }} id="bewerben">
|
<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 */}
|
{/* 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' }} />
|
<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 */}
|
{/* Left – pitch copy */}
|
||||||
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: 'hidden' }}>
|
<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 }}>
|
<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>
|
</h2>
|
||||||
<div style={{ width: 32, height: 2, background: '#EFBF04', marginBottom: 20 }} />
|
<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 }}>
|
<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>
|
</p>
|
||||||
|
|
||||||
{/* Benefit rows */}
|
{/* Benefit rows */}
|
||||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
<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' }}>
|
<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>
|
<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 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
|
||||||
@@ -687,10 +729,10 @@ const Home: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Form area */}
|
{/* 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 }}>
|
<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 }}>{fallbackText(application.formEyebrow, homeContent.application.formEyebrow)}</span>
|
<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 }}>{fallbackText(application.formTitle, homeContent.application.formTitle)}</h3>
|
<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>
|
||||||
<BewerbungsForm theme="gold" content={form} />
|
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -698,8 +740,8 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
{/* Footnote row */}
|
{/* 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 }}>
|
<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, 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)' }}>{fallbackText(application.footnoteStrong, homeContent.application.footnoteStrong)}</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>
|
||||||
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { useRef } from 'react';
|
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 { Link } from '@/spa/router';
|
||||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
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 WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
import { useApplicationPhase, useCmsRoute } from '@/spa/cmsRoute';
|
||||||
import { participationContent } from '@/spa/participationContent';
|
import { participationContent } from '@/spa/participationContent';
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
@@ -18,12 +20,15 @@ const CREAM = '#E4E2E3';
|
|||||||
|
|
||||||
type ParticipationCms = Partial<typeof participationContent> & {
|
type ParticipationCms = Partial<typeof participationContent> & {
|
||||||
hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown };
|
hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown };
|
||||||
awardsGrid?: Partial<typeof participationContent.awardsGrid> & { cards?: AwardCardCms[] };
|
awardsGrid?: AwardsGridContent;
|
||||||
applicationForm?: Partial<typeof participationContent.applicationForm> & { image?: unknown };
|
applicationForm?: ParticipationApplicationFormCms;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AwardCard = (typeof participationContent.awardsGrid.cards)[number];
|
type ParticipationApplicationFormCms = Partial<typeof participationContent.applicationForm> & {
|
||||||
type AwardCardCms = Partial<AwardCard> & { image?: unknown };
|
image?: unknown;
|
||||||
|
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
||||||
|
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
||||||
|
};
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
building2: Building2,
|
building2: Building2,
|
||||||
@@ -38,12 +43,50 @@ const ICONS = {
|
|||||||
const fallbackText = (value: unknown, fallback: string) =>
|
const fallbackText = (value: unknown, fallback: string) =>
|
||||||
typeof value === 'string' && value.length > 0 ? value : fallback;
|
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;
|
Array.isArray(value) && value.length > 0 ? (value as T[]) : fallback;
|
||||||
|
|
||||||
const mergeAwardCards = (value: unknown, fallback: AwardCard[]): (AwardCard & { image?: unknown })[] => {
|
const APPLICATION_PHASE_OPEN = '0';
|
||||||
const cards = Array.isArray(value) && value.length > 0 ? (value as AwardCardCms[]) : fallback;
|
|
||||||
return cards.map((card, index) => ({ ...(fallback[index] || fallback[0]), ...card }));
|
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 }) {
|
function Lines({ text }: { text: string }) {
|
||||||
@@ -54,6 +97,7 @@ const Participation: React.FC = () => {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const qualSectionRef = useRef<HTMLElement>(null);
|
const qualSectionRef = useRef<HTMLElement>(null);
|
||||||
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
|
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
|
||||||
|
const applicationPhase = useApplicationPhase();
|
||||||
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
|
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
|
||||||
const process = { ...participationContent.process, ...(cms.process || {}) };
|
const process = { ...participationContent.process, ...(cms.process || {}) };
|
||||||
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
|
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
|
||||||
@@ -62,11 +106,15 @@ const Participation: React.FC = () => {
|
|||||||
const awardsGrid = { ...participationContent.awardsGrid, ...(cms.awardsGrid || {}) };
|
const awardsGrid = { ...participationContent.awardsGrid, ...(cms.awardsGrid || {}) };
|
||||||
const applicationForm = { ...participationContent.applicationForm, ...(cms.applicationForm || {}) };
|
const applicationForm = { ...participationContent.applicationForm, ...(cms.applicationForm || {}) };
|
||||||
const form = { ...participationContent.form, ...(cms.form || {}) };
|
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 eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
|
||||||
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
|
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
|
||||||
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
|
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
|
||||||
const desktopDates = fallbackArray(datesBanner.desktopItems, participationContent.datesBanner.desktopItems);
|
const desktopDates = fallbackArray(datesBanner.desktopItems, participationContent.datesBanner.desktopItems);
|
||||||
const applicationFacts = fallbackArray(applicationForm.facts, participationContent.applicationForm.facts);
|
const applicationFacts = fallbackArray(applicationForm.facts, participationContent.applicationForm.facts);
|
||||||
|
const finalSectionFacts = showApplicationForm ? applicationFacts : newsletterCopy.benefits;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
@@ -224,21 +272,21 @@ const Participation: React.FC = () => {
|
|||||||
<AwardsGridSection content={awardsGrid} />
|
<AwardsGridSection content={awardsGrid} />
|
||||||
|
|
||||||
{/* ── FORM SECTION ─────────────────────────────────────────────────────── */}
|
{/* ── 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' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr', flex: 1, minHeight: 0, overflow: isMobile ? 'visible' : 'hidden' }}>
|
||||||
{/* Left – copy */}
|
{/* Left – copy */}
|
||||||
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: isMobile ? 'visible' : 'hidden' }}>
|
<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' }}>
|
<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>
|
</h2>
|
||||||
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
|
<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 }}>
|
<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>
|
</p>
|
||||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||||||
{applicationFacts.map((item, i) => (
|
{finalSectionFacts.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' }}>
|
<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>
|
<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' }}>
|
<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>
|
<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>
|
</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={{ padding: isMobile ? '28px 20px' : '24px 48px 32px 40px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: isMobile && showApplicationForm ? 560 : 0, position: 'relative', zIndex: 1 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
|
{showApplicationForm ? (
|
||||||
<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
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 10 }}>
|
||||||
to={fallbackText(applicationForm.uploadCta?.url, participationContent.applicationForm.uploadCta.url)}
|
<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>
|
||||||
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 }}
|
<Link
|
||||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = '#111D55'; (e.currentTarget as HTMLElement).style.borderColor = '#111D55'; }}
|
to={fallbackText(applicationForm.uploadCta?.url, participationContent.applicationForm.uploadCta.url)}
|
||||||
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)'; }}
|
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'; }}
|
||||||
<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>
|
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)'; }}
|
||||||
{fallbackText(applicationForm.uploadCta?.label, participationContent.applicationForm.uploadCta.label)}
|
>
|
||||||
</Link>
|
<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>
|
||||||
</div>
|
{fallbackText(applicationForm.uploadCta?.label, participationContent.applicationForm.uploadCta.label)}
|
||||||
<BewerbungsForm theme="gold" content={form} />
|
</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>
|
||||||
</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 }}>
|
<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 }}>
|
{showApplicationForm ? (
|
||||||
{fallbackText(applicationForm.uploadPrompt, participationContent.applicationForm.uploadPrompt)}
|
<>
|
||||||
</span>
|
<span style={{ fontFamily: FB, fontSize: 14, color: 'rgba(255,255,255,0.55)', lineHeight: 1.6, maxWidth: 520 }}>
|
||||||
<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.uploadPrompt, participationContent.applicationForm.uploadPrompt)}
|
||||||
{fallbackText(applicationForm.uploadFooterCta?.label, participationContent.applicationForm.uploadFooterCta.label)}
|
</span>
|
||||||
</Link>
|
<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>
|
||||||
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
||||||
</section>
|
</section>
|
||||||
@@ -349,7 +417,9 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const section = { ...participationContent.applicationWays, ...(content || {}) };
|
const section = { ...participationContent.applicationWays, ...(content || {}) };
|
||||||
const institutions = fallbackArray(section.institutions, participationContent.applicationWays.institutions);
|
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 || {}) };
|
const fallbackCta = { ...participationContent.applicationWays.fallbackCta, ...(section.fallbackCta || {}) };
|
||||||
return (
|
return (
|
||||||
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
|
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
|
||||||
@@ -368,24 +438,21 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3 Wege – premium swipe cards on mobile, 3-col grid on desktop */}
|
{/* 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(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 }}>
|
<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 ── */}
|
{/* ── MOBILE: unified premium cards ── */}
|
||||||
{isMobile && ways.map((w) => {
|
{isMobile && ways.map((w) => {
|
||||||
const Icon = ICONS[w.icon as keyof typeof ICONS] || Send;
|
const Icon = ICONS[w.icon as keyof typeof ICONS] || Send;
|
||||||
const facts = fallbackArray<{ label?: string | null; desc?: string | null }>(w.facts, []);
|
const facts = fallbackArray<{ label?: string | null; desc?: string | null }>(w.facts, []);
|
||||||
return (
|
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 */}
|
{/* Header */}
|
||||||
<div style={{ padding: '22px 22px 0', position: 'relative' }}>
|
<div style={{ padding: '22px 22px 0', position: 'relative' }}>
|
||||||
{w.featured && (
|
<div style={{ width: 46, height: 46, borderRadius: 12, background: NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 18 }}>
|
||||||
<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>
|
<Icon size={20} style={{ color: GOLD }} strokeWidth={1.6} />
|
||||||
)}
|
|
||||||
<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>
|
</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>
|
<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>
|
<p style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.55)', lineHeight: 1.6, margin: '0 0 20px' }}>{fallbackText(w.desc, '')}</p>
|
||||||
<div style={{ width: 36, height: 2, background: GOLD }} />
|
<div style={{ width: 36, height: 2, background: GOLD }} />
|
||||||
@@ -420,23 +487,22 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
|
|||||||
|
|
||||||
{ways.map((w, index) => {
|
{ways.map((w, index) => {
|
||||||
const Icon = ICONS[w.icon as keyof typeof ICONS] || Send;
|
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 facts = fallbackArray<{ label?: string | null; desc?: string | null }>(w.facts, []);
|
||||||
const isLast = index === ways.length - 1;
|
const isLast = index === ways.length - 1;
|
||||||
return (
|
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 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: isFeatured ? GOLD : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
|
<div style={{ width: 44, height: 44, background: NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 28 }}>
|
||||||
<Icon size={18} style={{ color: isFeatured ? '#101828' : GOLD }} strokeWidth={1.5} />
|
<Icon size={18} style={{ color: GOLD }} strokeWidth={1.5} />
|
||||||
</div>
|
</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>
|
<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: isFeatured ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: '0 0 8px' }}>
|
<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, '')} />
|
<Lines text={fallbackText(w.title, '')} />
|
||||||
</h3>
|
</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, '')}
|
{fallbackText(w.desc, '')}
|
||||||
</p>
|
</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) => (
|
{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 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 }}>
|
<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>
|
<span style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.65)', lineHeight: 1.55 }}>{fallbackText(inst.text, '')}</span>
|
||||||
</div>
|
</div>
|
||||||
)) : facts.map((item, i) => (
|
)) : 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' }}>
|
<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: isFeatured ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '0.07em' }}>{fallbackText(item.label, '')}</span>
|
<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: isFeatured ? 'rgba(255,255,255,0.50)' : 'rgba(16,24,40,0.45)' }}>{fallbackText(item.desc, '')}</span>
|
<span style={{ fontFamily: FB, fontSize: 16, color: 'rgba(16,24,40,0.45)' }}>{fallbackText(item.desc, '')}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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;
|
export default Participation;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Link } from '@/spa/router';
|
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 { WINNERS, type Winner } from '@/spa/data/winners';
|
||||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
@@ -14,6 +14,7 @@ const GOLD = '#EFBF04';
|
|||||||
const BORDER = '#D0D5DD';
|
const BORDER = '#D0D5DD';
|
||||||
const GRAY = '#666666';
|
const GRAY = '#666666';
|
||||||
const BG_ALT = '#E4E2E3';
|
const BG_ALT = '#E4E2E3';
|
||||||
|
const SIMPLE_LIST_YEAR = 2023;
|
||||||
|
|
||||||
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
|
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
|
||||||
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
|
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
|
||||||
@@ -69,6 +70,7 @@ export default function Preistraeger() {
|
|||||||
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
|
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
|
||||||
|
|
||||||
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]);
|
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]);
|
||||||
|
const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: '#fff', minHeight: '100vh' }}>
|
<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 }}>
|
<div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}>
|
||||||
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
|
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
|
||||||
</div>
|
</div>
|
||||||
|
) : usesSimpleWinnerGrid ? (
|
||||||
|
<SimpleWinnerGrid winners={filtered} />
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -165,38 +169,6 @@ export default function Preistraeger() {
|
|||||||
>
|
>
|
||||||
{fallbackText(cta.primaryCta?.label, preistraegerIndexContent.cta.primaryCta.label)} <ArrowRight size={14} />
|
{fallbackText(cta.primaryCta?.label, preistraegerIndexContent.cta.primaryCta.label)} <ArrowRight size={14} />
|
||||||
</Link>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -257,3 +229,91 @@ function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string
|
|||||||
</Link>
|
</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',
|
title: 'Initiativbewerbung',
|
||||||
desc: 'Für Unternehmer, die selbst die Initiative ergreifen.',
|
desc: 'Für Unternehmer, die selbst die Initiative ergreifen.',
|
||||||
icon: 'userCheck',
|
icon: 'userCheck',
|
||||||
featured: true,
|
featured: false,
|
||||||
cta: { label: 'Jetzt bewerben', url: '#bewerben' },
|
cta: { label: 'Jetzt bewerben', url: '#bewerben' },
|
||||||
listType: 'facts',
|
listType: 'facts',
|
||||||
facts: [
|
facts: [
|
||||||
@@ -117,21 +117,6 @@ export const participationContent = {
|
|||||||
{ label: 'Deadline', desc: '30. Juni 2026' },
|
{ 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',
|
recommendedLabel: 'Empfohlen',
|
||||||
},
|
},
|
||||||
@@ -164,9 +149,60 @@ export const participationContent = {
|
|||||||
{ num: '03', label: 'Mehrstufig', desc: 'Transparenter Prozess' },
|
{ num: '03', label: 'Mehrstufig', desc: 'Transparenter Prozess' },
|
||||||
],
|
],
|
||||||
formEyebrow: 'Bewerbung 2026',
|
formEyebrow: 'Bewerbung 2026',
|
||||||
|
formTitle: 'Kostenlos bewerben',
|
||||||
uploadCta: { label: 'PDF hochladen', url: '/formular-hochladen' },
|
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.',
|
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' },
|
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: {
|
form: {
|
||||||
stepCompleteLabel: '✓',
|
stepCompleteLabel: '✓',
|
||||||
|
|||||||
Reference in New Issue
Block a user