feat: centralize contact cms defaults

This commit is contained in:
syntaxbullet
2026-06-22 12:18:17 +02:00
parent c21f0d0c8f
commit 1e164d7624
4 changed files with 135 additions and 175 deletions

View File

@@ -1,6 +1,6 @@
import type { Field } from 'payload' import type { Field } from 'payload'
import { contactFormContent } from '@/spa/contactFormContent' import { contactContent } from '@/spa/contactContent'
const uploadField = (name: string, label: string, description?: string): Field => ({ const uploadField = (name: string, label: string, description?: string): Field => ({
name, name,
@@ -45,14 +45,14 @@ export const contactFields: Field[] = [
type: 'group', type: 'group',
admin: sectionAdmin('Top image, eyebrow, headline, and intro copy.'), admin: sectionAdmin('Top image, eyebrow, headline, and intro copy.'),
fields: [ fields: [
uploadField('backgroundImage', 'Background image', 'Current frontend image: /images/gala-dinner.jpg'), uploadField('backgroundImage', 'Background image', `Current frontend image: /images/${contactContent.hero.backgroundImageFilename}`),
text('imageAlt', 'Image alt text', 'BMP Gala'), text('imageAlt', 'Image alt text', contactContent.hero.imageAlt),
text('eyebrow', 'Eyebrow', 'Dialog & Service'), text('eyebrow', 'Eyebrow', contactContent.hero.eyebrow),
textarea('heading', 'Headline', 'Wir sind\nfür Sie da.'), textarea('heading', 'Headline', contactContent.hero.heading),
textarea( textarea(
'description', 'description',
'Intro paragraph', 'Intro paragraph',
'Fragen zur Bewerbung, zur Gala oder zu Partnermöglichkeiten unser Team begleitet Sie persönlich.', contactContent.hero.description,
), ),
], ],
}, },
@@ -62,18 +62,13 @@ export const contactFields: Field[] = [
type: 'group', type: 'group',
admin: sectionAdmin('Left direct-contact list plus the gold contact form panel.'), admin: sectionAdmin('Left direct-contact list plus the gold contact form panel.'),
fields: [ fields: [
text('eyebrow', 'Direct contact eyebrow', 'Direktkontakt'), text('eyebrow', 'Direct contact eyebrow', contactContent.info.eyebrow),
textarea('heading', 'Direct contact heading', 'Kontakt­möglichkeiten'), textarea('heading', 'Direct contact heading', contactContent.info.heading),
{ {
name: 'items', name: 'items',
label: 'Contact methods', label: 'Contact methods',
type: 'array', type: 'array',
defaultValue: [ defaultValue: contactContent.info.items,
{ icon: 'mapPin', title: 'Geschäftsstelle', lines: 'Maximilianstraße 42\n80538 München' },
{ icon: 'mail', title: 'E-Mail', lines: 'info@bmp-bayern.de' },
{ icon: 'phone', title: 'Telefon', lines: '+49 89 123 456 78' },
{ icon: 'clock', title: 'Bürozeiten', lines: 'Mo Fr: 09:00 17:00 Uhr' },
],
fields: [ fields: [
{ {
name: 'icon', name: 'icon',
@@ -91,13 +86,13 @@ export const contactFields: Field[] = [
textarea('lines', 'Lines'), textarea('lines', 'Lines'),
], ],
}, },
text('nextAwardLabel', 'Next award label', 'Nächste Preisverleihung:'), text('nextAwardLabel', 'Next award label', contactContent.info.nextAwardLabel),
text('nextAwardDate', 'Next award date', '22. Oktober 2026'), text('nextAwardDate', 'Next award date', contactContent.info.nextAwardDate),
uploadField('formImage', 'Desktop form image', 'Current frontend image: /images/preistraeger-gruppe.jpg'), uploadField('formImage', 'Desktop form image', `Current frontend image: /images/${contactContent.info.formImageFilename}`),
text('formImageAlt', 'Form image alt text', 'BMP Gala 2025'), text('formImageAlt', 'Form image alt text', contactContent.info.formImageAlt),
text('formImageLabel', 'Form image label', 'Gala 2025 München'), text('formImageLabel', 'Form image label', contactContent.info.formImageLabel),
text('formEyebrow', 'Form eyebrow', 'Kontaktformular'), text('formEyebrow', 'Form eyebrow', contactContent.info.formEyebrow),
text('formTitle', 'Form title', 'Schreiben Sie uns'), text('formTitle', 'Form title', contactContent.info.formTitle),
{ {
name: 'formCopy', name: 'formCopy',
label: 'Contact form copy', label: 'Contact form copy',
@@ -108,14 +103,14 @@ export const contactFields: Field[] = [
name: 'steps', name: 'steps',
label: 'Step labels', label: 'Step labels',
type: 'array', type: 'array',
defaultValue: contactFormContent.steps, defaultValue: contactContent.info.formCopy.steps,
fields: [text('label', 'Label')], fields: [text('label', 'Label')],
}, },
{ {
name: 'subjects', name: 'subjects',
label: 'Subject choices', label: 'Subject choices',
type: 'array', type: 'array',
defaultValue: contactFormContent.subjects, defaultValue: contactContent.info.formCopy.subjects,
fields: [text('value', 'Value'), text('label', 'Label'), textarea('desc', 'Description')], fields: [text('value', 'Value'), text('label', 'Label'), textarea('desc', 'Description')],
}, },
{ {
@@ -123,9 +118,9 @@ export const contactFields: Field[] = [
label: 'Prompts', label: 'Prompts',
type: 'group', type: 'group',
fields: [ fields: [
text('subject', 'Subject step prompt', contactFormContent.prompts.subject), text('subject', 'Subject step prompt', contactContent.info.formCopy.prompts.subject),
text('contact', 'Contact step prompt', contactFormContent.prompts.contact), text('contact', 'Contact step prompt', contactContent.info.formCopy.prompts.contact),
text('message', 'Message step prompt', contactFormContent.prompts.message), text('message', 'Message step prompt', contactContent.info.formCopy.prompts.message),
], ],
}, },
{ {
@@ -133,12 +128,12 @@ export const contactFields: Field[] = [
label: 'Fields', label: 'Fields',
type: 'group', type: 'group',
fields: [ fields: [
text('nameLabel', 'Name label', contactFormContent.fields.nameLabel), text('nameLabel', 'Name label', contactContent.info.formCopy.fields.nameLabel),
text('namePlaceholder', 'Name placeholder', contactFormContent.fields.namePlaceholder), text('namePlaceholder', 'Name placeholder', contactContent.info.formCopy.fields.namePlaceholder),
text('emailLabel', 'Email label', contactFormContent.fields.emailLabel), text('emailLabel', 'Email label', contactContent.info.formCopy.fields.emailLabel),
text('emailPlaceholder', 'Email placeholder', contactFormContent.fields.emailPlaceholder), text('emailPlaceholder', 'Email placeholder', contactContent.info.formCopy.fields.emailPlaceholder),
text('messageLabel', 'Message label', contactFormContent.fields.messageLabel), text('messageLabel', 'Message label', contactContent.info.formCopy.fields.messageLabel),
text('messagePlaceholder', 'Message placeholder', contactFormContent.fields.messagePlaceholder), text('messagePlaceholder', 'Message placeholder', contactContent.info.formCopy.fields.messagePlaceholder),
], ],
}, },
{ {
@@ -146,10 +141,10 @@ export const contactFields: Field[] = [
label: 'Validation messages', label: 'Validation messages',
type: 'group', type: 'group',
fields: [ fields: [
text('nameRequired', 'Name required', contactFormContent.validation.nameRequired), text('nameRequired', 'Name required', contactContent.info.formCopy.validation.nameRequired),
text('emailRequired', 'Email required', contactFormContent.validation.emailRequired), text('emailRequired', 'Email required', contactContent.info.formCopy.validation.emailRequired),
text('emailInvalid', 'Email invalid', contactFormContent.validation.emailInvalid), text('emailInvalid', 'Email invalid', contactContent.info.formCopy.validation.emailInvalid),
text('messageRequired', 'Message required', contactFormContent.validation.messageRequired), text('messageRequired', 'Message required', contactContent.info.formCopy.validation.messageRequired),
], ],
}, },
{ {
@@ -157,9 +152,9 @@ export const contactFields: Field[] = [
label: 'Navigation labels', label: 'Navigation labels',
type: 'group', type: 'group',
fields: [ fields: [
text('back', 'Back', contactFormContent.navigation.back), text('back', 'Back', contactContent.info.formCopy.navigation.back),
text('next', 'Next', contactFormContent.navigation.next), text('next', 'Next', contactContent.info.formCopy.navigation.next),
text('submit', 'Submit', contactFormContent.navigation.submit), text('submit', 'Submit', contactContent.info.formCopy.navigation.submit),
], ],
}, },
{ {
@@ -167,9 +162,9 @@ export const contactFields: Field[] = [
label: 'Success copy', label: 'Success copy',
type: 'group', type: 'group',
fields: [ fields: [
text('heading', 'Heading', contactFormContent.success.heading), text('heading', 'Heading', contactContent.info.formCopy.success.heading),
text('prefix', 'Prefix', contactFormContent.success.prefix), text('prefix', 'Prefix', contactContent.info.formCopy.success.prefix),
text('suffixBeforeEmail', 'Suffix before email', contactFormContent.success.suffixBeforeEmail), text('suffixBeforeEmail', 'Suffix before email', contactContent.info.formCopy.success.suffixBeforeEmail),
], ],
}, },
], ],
@@ -182,17 +177,13 @@ export const contactFields: Field[] = [
type: 'group', type: 'group',
admin: sectionAdmin('Dark 2026 timeline with three deadline columns.'), admin: sectionAdmin('Dark 2026 timeline with three deadline columns.'),
fields: [ fields: [
text('watermark', 'Large background year', '2026'), text('watermark', 'Large background year', contactContent.deadlines.watermark),
text('eyebrow', 'Eyebrow', 'Fahrplan 2026'), text('eyebrow', 'Eyebrow', contactContent.deadlines.eyebrow),
{ {
name: 'items', name: 'items',
label: 'Deadline items', label: 'Deadline items',
type: 'array', type: 'array',
defaultValue: [ defaultValue: contactContent.deadlines.items,
{ step: '01', label: 'Bewerbungsschluss', date: '30. Juni 2026', desc: 'Letzte Möglichkeit zur Einreichung Ihrer Unterlagen.' },
{ step: '02', label: 'Nominierung', date: '15. August 2026', desc: 'Bekanntgabe der Nominierten nach Jury-Sichtung.' },
{ step: '03', label: 'Preisverleihung', date: '22. Oktober 2026', desc: 'Festliche Gala in München.' },
],
fields: [text('step', 'Step'), text('label', 'Label'), text('date', 'Date'), textarea('desc', 'Description')], fields: [text('step', 'Step'), text('label', 'Label'), text('date', 'Date'), textarea('desc', 'Description')],
}, },
], ],
@@ -203,30 +194,13 @@ export const contactFields: Field[] = [
type: 'group', type: 'group',
admin: sectionAdmin('FAQ heading and accordion entries.'), admin: sectionAdmin('FAQ heading and accordion entries.'),
fields: [ fields: [
text('eyebrow', 'Eyebrow', 'Antworten'), text('eyebrow', 'Eyebrow', contactContent.faq.eyebrow),
textarea('heading', 'Heading', 'Häufige\nFragen'), textarea('heading', 'Heading', contactContent.faq.heading),
{ {
name: 'items', name: 'items',
label: 'FAQ items', label: 'FAQ items',
type: 'array', type: 'array',
defaultValue: [ defaultValue: contactContent.faq.items,
{
q: 'Was kostet die Teilnahme am BMP?',
a: 'Die Teilnahme am Bayerischen Mittelstandspreis ist für alle Unternehmen vollständig kostenfrei. Wir finanzieren uns durch Partner und Sponsoren, um exzellenten Mittelstand ohne finanzielle Hürden zu fördern.',
},
{
q: 'Nach welchen Kriterien wird bewertet?',
a: 'Die Bewertung basiert auf vier Säulen: Zukunftsfähigkeit & Innovation, Nachhaltigkeit & Verantwortung, Unternehmenskultur sowie Regionale Verwurzelung. Jede Säule fließt mit 25 % in das Gesamtergebnis ein.',
},
{
q: 'Wie läuft der Bewerbungsprozess ab?',
a: 'Der Prozess ist mehrstufig: 1. Online-Bewerbung oder Vorschlag, 2. Formale Prüfung & erste Jury-Sichtung, 3. Fach-Audits bei den Finalisten vor Ort, 4. Finale Jury-Sitzung und abschließende Preisverleihung.',
},
{
q: 'Wie lange dauert die Bewerbung?',
a: 'Dank unseres optimierten Bewerbungsformulars benötigen Sie für die erste Einreichung ca. 3045 Minuten. Tiefergehende Unterlagen werden erst in der Audit-Phase angefordert.',
},
],
fields: [textarea('q', 'Question'), textarea('a', 'Answer')], fields: [textarea('q', 'Question'), textarea('a', 'Answer')],
}, },
], ],

View File

@@ -3,7 +3,7 @@ import 'dotenv/config'
import config from '@payload-config' import config from '@payload-config'
import { getPayload, type Payload } from 'payload' import { getPayload, type Payload } from 'payload'
import { contactFormContent } from '@/spa/contactFormContent' import { contactContent } from '@/spa/contactContent'
const mediaByFilename = async (payload: Payload, filename: string) => { const mediaByFilename = async (payload: Payload, filename: string) => {
const result = await payload.find({ const result = await payload.find({
@@ -33,9 +33,11 @@ async function main() {
if (!page) throw new Error('Contact page not found. Expected spaPath=/kontakt or slug=kontakt/contact.') if (!page) throw new Error('Contact page not found. Expected spaPath=/kontakt or slug=kontakt/contact.')
const [heroImage, formImage] = await Promise.all([ const [heroImage, formImage] = await Promise.all([
mediaByFilename(payload, 'gala-dinner.jpg'), mediaByFilename(payload, contactContent.hero.backgroundImageFilename),
mediaByFilename(payload, 'preistraeger-gruppe.jpg'), mediaByFilename(payload, contactContent.info.formImageFilename),
]) ])
const { backgroundImageFilename: _heroFilename, ...heroContent } = contactContent.hero
const { formImageFilename: _formImageFilename, ...infoContent } = contactContent.info
await payload.update({ await payload.update({
collection: 'pages', collection: 'pages',
@@ -45,63 +47,17 @@ async function main() {
data: { data: {
contact: { contact: {
hero: { hero: {
...heroContent,
backgroundImage: heroImage, backgroundImage: heroImage,
imageAlt: 'BMP Gala',
eyebrow: 'Dialog & Service',
heading: 'Wir sind\nfür Sie da.',
description: 'Fragen zur Bewerbung, zur Gala oder zu Partnermöglichkeiten unser Team begleitet Sie persönlich.',
}, },
info: { info: {
eyebrow: 'Direktkontakt', ...infoContent,
heading: 'Kontakt­möglichkeiten',
items: [
{ icon: 'mapPin', title: 'Geschäftsstelle', lines: 'Maximilianstraße 42\n80538 München' },
{ icon: 'mail', title: 'E-Mail', lines: 'info@bmp-bayern.de' },
{ icon: 'phone', title: 'Telefon', lines: '+49 89 123 456 78' },
{ icon: 'clock', title: 'Bürozeiten', lines: 'Mo Fr: 09:00 17:00 Uhr' },
],
nextAwardLabel: 'Nächste Preisverleihung:',
nextAwardDate: '22. Oktober 2026',
formImage, formImage,
formImageAlt: 'BMP Gala 2025',
formImageLabel: 'Gala 2025 München',
formEyebrow: 'Kontaktformular',
formTitle: 'Schreiben Sie uns',
formCopy: contactFormContent,
},
deadlines: {
watermark: '2026',
eyebrow: 'Fahrplan 2026',
items: [
{ step: '01', label: 'Bewerbungsschluss', date: '30. Juni 2026', desc: 'Letzte Möglichkeit zur Einreichung Ihrer Unterlagen.' },
{ step: '02', label: 'Nominierung', date: '15. August 2026', desc: 'Bekanntgabe der Nominierten nach Jury-Sichtung.' },
{ step: '03', label: 'Preisverleihung', date: '22. Oktober 2026', desc: 'Festliche Gala in München.' },
],
},
faq: {
eyebrow: 'Antworten',
heading: 'Häufige\nFragen',
items: [
{
q: 'Was kostet die Teilnahme am BMP?',
a: 'Die Teilnahme am Bayerischen Mittelstandspreis ist für alle Unternehmen vollständig kostenfrei. Wir finanzieren uns durch Partner und Sponsoren, um exzellenten Mittelstand ohne finanzielle Hürden zu fördern.',
},
{
q: 'Nach welchen Kriterien wird bewertet?',
a: 'Die Bewertung basiert auf vier Säulen: Zukunftsfähigkeit & Innovation, Nachhaltigkeit & Verantwortung, Unternehmenskultur sowie Regionale Verwurzelung. Jede Säule fließt mit 25 % in das Gesamtergebnis ein.',
},
{
q: 'Wie läuft der Bewerbungsprozess ab?',
a: 'Der Prozess ist mehrstufig: 1. Online-Bewerbung oder Vorschlag, 2. Formale Prüfung & erste Jury-Sichtung, 3. Fach-Audits bei den Finalisten vor Ort, 4. Finale Jury-Sitzung und abschließende Preisverleihung.',
},
{
q: 'Wie lange dauert die Bewerbung?',
a: 'Dank unseres optimierten Bewerbungsformulars benötigen Sie für die erste Einreichung ca. 3045 Minuten. Tiefergehende Unterlagen werden erst in der Audit-Phase angefordert.',
},
],
},
}, },
deadlines: contactContent.deadlines,
faq: contactContent.faq,
}, },
} as never,
}) })
payload.logger.info(`Preloaded contact CMS fields for page ${page.id}`) payload.logger.info(`Preloaded contact CMS fields for page ${page.id}`)

61
src/spa/contactContent.ts Normal file
View File

@@ -0,0 +1,61 @@
import { contactFormContent } from '@/spa/contactFormContent'
export const contactContent = {
hero: {
backgroundImageFilename: 'gala-dinner.jpg',
imageAlt: 'BMP Gala',
eyebrow: 'Dialog & Service',
heading: 'Wir sind\nfür Sie da.',
description: 'Fragen zur Bewerbung, zur Gala oder zu Partnermöglichkeiten unser Team begleitet Sie persönlich.',
},
info: {
eyebrow: 'Direktkontakt',
heading: 'Kontakt­möglichkeiten',
items: [
{ icon: 'mapPin', title: 'Geschäftsstelle', lines: 'Maximilianstraße 42\n80538 München' },
{ icon: 'mail', title: 'E-Mail', lines: 'info@bmp-bayern.de' },
{ icon: 'phone', title: 'Telefon', lines: '+49 89 123 456 78' },
{ icon: 'clock', title: 'Bürozeiten', lines: 'Mo Fr: 09:00 17:00 Uhr' },
],
nextAwardLabel: 'Nächste Preisverleihung:',
nextAwardDate: '22. Oktober 2026',
formImageFilename: 'preistraeger-gruppe.jpg',
formImageAlt: 'BMP Gala 2025',
formImageLabel: 'Gala 2025 München',
formEyebrow: 'Kontaktformular',
formTitle: 'Schreiben Sie uns',
formCopy: contactFormContent,
},
deadlines: {
watermark: '2026',
eyebrow: 'Fahrplan 2026',
items: [
{ step: '01', label: 'Bewerbungsschluss', date: '30. Juni 2026', desc: 'Letzte Möglichkeit zur Einreichung Ihrer Unterlagen.' },
{ step: '02', label: 'Nominierung', date: '15. August 2026', desc: 'Bekanntgabe der Nominierten nach Jury-Sichtung.' },
{ step: '03', label: 'Preisverleihung', date: '22. Oktober 2026', desc: 'Festliche Gala in München.' },
],
},
faq: {
eyebrow: 'Antworten',
heading: 'Häufige\nFragen',
items: [
{
q: 'Was kostet die Teilnahme am BMP?',
a: 'Die Teilnahme am Bayerischen Mittelstandspreis ist für alle Unternehmen vollständig kostenfrei. Wir finanzieren uns durch Partner und Sponsoren, um exzellenten Mittelstand ohne finanzielle Hürden zu fördern.',
},
{
q: 'Nach welchen Kriterien wird bewertet?',
a: 'Die Bewertung basiert auf vier Säulen: Zukunftsfähigkeit & Innovation, Nachhaltigkeit & Verantwortung, Unternehmenskultur sowie Regionale Verwurzelung. Jede Säule fließt mit 25 % in das Gesamtergebnis ein.',
},
{
q: 'Wie läuft der Bewerbungsprozess ab?',
a: 'Der Prozess ist mehrstufig: 1. Online-Bewerbung oder Vorschlag, 2. Formale Prüfung & erste Jury-Sichtung, 3. Fach-Audits bei den Finalisten vor Ort, 4. Finale Jury-Sitzung und abschließende Preisverleihung.',
},
{
q: 'Wie lange dauert die Bewerbung?',
a: 'Dank unseres optimierten Bewerbungsformulars benötigen Sie für die erste Einreichung ca. 3045 Minuten. Tiefergehende Unterlagen werden erst in der Audit-Phase angefordert.',
},
],
},
}

View File

@@ -7,6 +7,7 @@ import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useCmsRoute } from '@/spa/cmsRoute'; import { useCmsRoute } from '@/spa/cmsRoute';
import Image from '@/spa/components/ui/UnoptimizedImage' import Image from '@/spa/components/ui/UnoptimizedImage'
import type { Page } from '@/payload-types'; import type { Page } from '@/payload-types';
import { contactContent } from '@/spa/contactContent';
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
@@ -17,7 +18,7 @@ type ContactCms = NonNullable<Page['contact']>;
type ContactItem = NonNullable<NonNullable<ContactCms['info']>['items']>[number] | { type ContactItem = NonNullable<NonNullable<ContactCms['info']>['items']>[number] | {
icon: string; icon: string;
title: string; title: string;
lines: string[]; lines: string | string[];
}; };
type DeadlineItem = NonNullable<NonNullable<ContactCms['deadlines']>['items']>[number]; type DeadlineItem = NonNullable<NonNullable<ContactCms['deadlines']>['items']>[number];
type FaqItem = NonNullable<NonNullable<ContactCms['faq']>['items']>[number]; type FaqItem = NonNullable<NonNullable<ContactCms['faq']>['items']>[number];
@@ -39,38 +40,6 @@ const ICONS = {
clock: Clock, clock: Clock,
}; };
const DEFAULT_FAQS = [
{
q: 'Was kostet die Teilnahme am BMP?',
a: 'Die Teilnahme am Bayerischen Mittelstandspreis ist für alle Unternehmen vollständig kostenfrei. Wir finanzieren uns durch Partner und Sponsoren, um exzellenten Mittelstand ohne finanzielle Hürden zu fördern.',
},
{
q: 'Nach welchen Kriterien wird bewertet?',
a: 'Die Bewertung basiert auf vier Säulen: Zukunftsfähigkeit & Innovation, Nachhaltigkeit & Verantwortung, Unternehmenskultur sowie Regionale Verwurzelung. Jede Säule fließt mit 25 % in das Gesamtergebnis ein.',
},
{
q: 'Wie läuft der Bewerbungsprozess ab?',
a: 'Der Prozess ist mehrstufig: 1. Online-Bewerbung oder Vorschlag, 2. Formale Prüfung & erste Jury-Sichtung, 3. Fach-Audits bei den Finalisten vor Ort, 4. Finale Jury-Sitzung und abschließende Preisverleihung.',
},
{
q: 'Wie lange dauert die Bewerbung?',
a: 'Dank unseres optimierten Bewerbungsformulars benötigen Sie für die erste Einreichung ca. 3045 Minuten. Tiefergehende Unterlagen werden erst in der Audit-Phase angefordert.',
},
];
const DEFAULT_DEADLINES = [
{ step: '01', label: 'Bewerbungsschluss', date: '30. Juni 2026', desc: 'Letzte Möglichkeit zur Einreichung Ihrer Unterlagen.' },
{ step: '02', label: 'Nominierung', date: '15. August 2026', desc: 'Bekanntgabe der Nominierten nach Jury-Sichtung.' },
{ step: '03', label: 'Preisverleihung', date: '22. Oktober 2026', desc: 'Festliche Gala in München.' },
];
const DEFAULT_CONTACT_ITEMS = [
{ icon: 'mapPin', title: 'Geschäftsstelle', lines: ['Maximilianstraße 42', '80538 München'] },
{ icon: 'mail', title: 'E-Mail', lines: ['info@bmp-bayern.de'] },
{ icon: 'phone', title: 'Telefon', lines: ['+49 89 123 456 78'] },
{ icon: 'clock', title: 'Bürozeiten', lines: ['Mo Fr: 09:00 17:00 Uhr'] },
];
const Contact: React.FC = () => { const Contact: React.FC = () => {
const [openFaq, setOpenFaq] = useState<number | null>(0); const [openFaq, setOpenFaq] = useState<number | null>(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -79,9 +48,9 @@ const Contact: React.FC = () => {
const info = contact.info || {}; const info = contact.info || {};
const deadlines = contact.deadlines || {}; const deadlines = contact.deadlines || {};
const faq = contact.faq || {}; const faq = contact.faq || {};
const contactItems = fallbackArray<ContactItem>(info.items, DEFAULT_CONTACT_ITEMS); const contactItems = fallbackArray<ContactItem>(info.items, contactContent.info.items);
const deadlineItems = fallbackArray<DeadlineItem>(deadlines.items, DEFAULT_DEADLINES); const deadlineItems = fallbackArray<DeadlineItem>(deadlines.items, contactContent.deadlines.items);
const faqItems = fallbackArray<FaqItem>(faq.items, DEFAULT_FAQS); const faqItems = fallbackArray<FaqItem>(faq.items, contactContent.faq.items);
return ( return (
<div style={{ background: CREAM }}> <div style={{ background: CREAM }}>
@@ -89,8 +58,8 @@ const Contact: React.FC = () => {
{/* ── Hero ───────────────────────────────────────────────────────── */} {/* ── Hero ───────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', height: '68vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}> <section style={{ position: 'relative', height: '68vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
<Image unoptimized <Image unoptimized
src={mediaUrl(hero.backgroundImage, '/images/gala-dinner.jpg')} src={mediaUrl(hero.backgroundImage, `/images/${contactContent.hero.backgroundImageFilename}`)}
alt={mediaAlt(hero.backgroundImage, fallbackText(hero.imageAlt, 'BMP Gala'))} alt={mediaAlt(hero.backgroundImage, fallbackText(hero.imageAlt, contactContent.hero.imageAlt))}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }}
/> />
{/* Diagonal overlay */} {/* Diagonal overlay */}
@@ -100,13 +69,13 @@ const Contact: React.FC = () => {
<div style={{ position: 'relative', zIndex: 2, width: '100%', paddingBottom: isMobile ? '3rem' : '5rem', paddingLeft: isMobile ? 24 : 80, paddingRight: isMobile ? 24 : 80 }}> <div style={{ position: 'relative', zIndex: 2, width: '100%', paddingBottom: isMobile ? '3rem' : '5rem', paddingLeft: isMobile ? 24 : 80, paddingRight: isMobile ? 24 : 80 }}>
<p style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: GOLD, marginBottom: 16 }}> <p style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: GOLD, marginBottom: 16 }}>
{fallbackText(hero.eyebrow, 'Dialog & Service')} {fallbackText(hero.eyebrow, contactContent.hero.eyebrow)}
</p> </p>
<h1 style={{ fontSize: isMobile ? 'clamp(2rem, 10vw, 3rem)' : 'clamp(3rem, 7vw, 6rem)', fontWeight: 900, lineHeight: 1.0, color: '#fff', letterSpacing: '-0.03em', marginBottom: 24, maxWidth: 640 }}> <h1 style={{ fontSize: isMobile ? 'clamp(2rem, 10vw, 3rem)' : 'clamp(3rem, 7vw, 6rem)', fontWeight: 900, lineHeight: 1.0, color: '#fff', letterSpacing: '-0.03em', marginBottom: 24, maxWidth: 640 }}>
<Lines text={fallbackText(hero.heading, 'Wir sind\nfür Sie da.')} /> <Lines text={fallbackText(hero.heading, contactContent.hero.heading)} />
</h1> </h1>
<p style={{ fontSize: 18, color: 'rgba(255,255,255,0.72)', maxWidth: 480, lineHeight: 1.65, fontWeight: 400 }}> <p style={{ fontSize: 18, color: 'rgba(255,255,255,0.72)', maxWidth: 480, lineHeight: 1.65, fontWeight: 400 }}>
{fallbackText(hero.description, 'Fragen zur Bewerbung, zur Gala oder zu Partnermöglichkeiten unser Team begleitet Sie persönlich.')} {fallbackText(hero.description, contactContent.hero.description)}
</p> </p>
</div> </div>
</section> </section>
@@ -121,11 +90,11 @@ const Contact: React.FC = () => {
{/* Left info panel */} {/* Left info panel */}
<div style={{ padding: isMobile ? '48px 24px' : '88px 56px 88px 64px', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative' }}> <div style={{ padding: isMobile ? '48px 24px' : '88px 56px 88px 64px', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative' }}>
<p style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', marginBottom: 20 }}> <p style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', marginBottom: 20 }}>
{fallbackText(info.eyebrow, 'Direktkontakt')} {fallbackText(info.eyebrow, contactContent.info.eyebrow)}
</p> </p>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 20 }} /> <div style={{ width: 36, height: 2, background: GOLD, marginBottom: 20 }} />
<h2 style={{ fontSize: 'clamp(1.6rem, 3vw, 2.4rem)', fontWeight: 900, color: '#fff', lineHeight: 1.15, letterSpacing: '-0.03em', marginBottom: 52 }}> <h2 style={{ fontSize: 'clamp(1.6rem, 3vw, 2.4rem)', fontWeight: 900, color: '#fff', lineHeight: 1.15, letterSpacing: '-0.03em', marginBottom: 52 }}>
<Lines text={fallbackText(info.heading, 'Kontakt­möglichkeiten')} /> <Lines text={fallbackText(info.heading, contactContent.info.heading)} />
</h2> </h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
@@ -152,7 +121,7 @@ const Contact: React.FC = () => {
<div style={{ marginTop: 52, paddingTop: 28, borderTop: '1px solid rgba(255,255,255,0.10)' }}> <div style={{ marginTop: 52, paddingTop: 28, borderTop: '1px solid rgba(255,255,255,0.10)' }}>
<p style={{ fontSize: 15, color: 'rgba(255,255,255,0.40)', lineHeight: 1.65 }}> <p style={{ fontSize: 15, color: 'rgba(255,255,255,0.40)', lineHeight: 1.65 }}>
{fallbackText(info.nextAwardLabel, 'Nächste Preisverleihung:')} <strong style={{ color: '#fff' }}>{fallbackText(info.nextAwardDate, '22. Oktober 2026')}</strong> {fallbackText(info.nextAwardLabel, contactContent.info.nextAwardLabel)} <strong style={{ color: '#fff' }}>{fallbackText(info.nextAwardDate, contactContent.info.nextAwardDate)}</strong>
</p> </p>
</div> </div>
</div> </div>
@@ -178,19 +147,19 @@ const Contact: React.FC = () => {
{!isMobile && ( {!isMobile && (
<div style={{ height: 200, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}> <div style={{ height: 200, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<Image unoptimized src={mediaUrl(info.formImage, '/images/preistraeger-gruppe.jpg')} alt={mediaAlt(info.formImage, fallbackText(info.formImageAlt, 'BMP Gala 2025'))} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} /> <Image unoptimized src={mediaUrl(info.formImage, `/images/${contactContent.info.formImageFilename}`)} alt={mediaAlt(info.formImage, fallbackText(info.formImageAlt, contactContent.info.formImageAlt))} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 5, height: 5, borderRadius: '50%', background: 'rgba(17,29,85,0.7)' }} /> <div style={{ width: 5, height: 5, borderRadius: '50%', background: 'rgba(17,29,85,0.7)' }} />
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, color: 'rgba(17,29,85,0.75)', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{fallbackText(info.formImageLabel, 'Gala 2025 München')}</span> <span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, color: 'rgba(17,29,85,0.75)', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{fallbackText(info.formImageLabel, contactContent.info.formImageLabel)}</span>
</div> </div>
</div> </div>
)} )}
<div style={{ padding: isMobile ? '36px 20px' : '32px 48px 32px 40px', flex: isMobile ? 'none' : 1, display: 'flex', flexDirection: 'column', minHeight: isMobile ? 480 : 0, position: 'relative', zIndex: 1 }}> <div style={{ padding: isMobile ? '36px 20px' : '32px 48px 32px 40px', flex: isMobile ? 'none' : 1, display: 'flex', flexDirection: 'column', minHeight: isMobile ? 480 : 0, position: 'relative', zIndex: 1 }}>
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{fallbackText(info.formEyebrow, 'Kontaktformular')}</span> <span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{fallbackText(info.formEyebrow, contactContent.info.formEyebrow)}</span>
<h3 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', letterSpacing: '-0.025em', textTransform: 'uppercase', lineHeight: 1.1, marginBottom: 24 }}> <h3 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', letterSpacing: '-0.025em', textTransform: 'uppercase', lineHeight: 1.1, marginBottom: 24 }}>
{fallbackText(info.formTitle, 'Schreiben Sie uns')} {fallbackText(info.formTitle, contactContent.info.formTitle)}
</h3> </h3>
<KontaktForm theme="gold" content={info.formCopy || undefined} /> <KontaktForm theme="gold" content={info.formCopy || undefined} />
</div> </div>
@@ -201,12 +170,12 @@ const Contact: React.FC = () => {
{/* ── Deadlines ──────────────────────────────────────────────────── */} {/* ── Deadlines ──────────────────────────────────────────────────── */}
<section style={{ background: '#0D1640', padding: isMobile ? '56px 0' : '88px 0', position: 'relative', overflow: 'hidden' }}> <section style={{ background: '#0D1640', padding: isMobile ? '56px 0' : '88px 0', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', right: -20, top: '50%', transform: 'translateY(-50%)', fontSize: 'clamp(160px, 22vw, 280px)', fontWeight: 900, color: '#fff', opacity: 0.025, lineHeight: 1, pointerEvents: 'none', userSelect: 'none', letterSpacing: '-0.05em' }}> <div style={{ position: 'absolute', right: -20, top: '50%', transform: 'translateY(-50%)', fontSize: 'clamp(160px, 22vw, 280px)', fontWeight: 900, color: '#fff', opacity: 0.025, lineHeight: 1, pointerEvents: 'none', userSelect: 'none', letterSpacing: '-0.05em' }}>
{fallbackText(deadlines.watermark, '2026')} {fallbackText(deadlines.watermark, contactContent.deadlines.watermark)}
</div> </div>
<div className="container mx-auto px-6" style={{ position: 'relative' }}> <div className="container mx-auto px-6" style={{ position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 20, marginBottom: isMobile ? 48 : 72 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 20, marginBottom: isMobile ? 48 : 72 }}>
<span style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: '#EFBF04', whiteSpace: 'nowrap' }}> <span style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: '#EFBF04', whiteSpace: 'nowrap' }}>
{fallbackText(deadlines.eyebrow, 'Fahrplan 2026')} {fallbackText(deadlines.eyebrow, contactContent.deadlines.eyebrow)}
</span> </span>
<div style={{ flex: 1, height: 1, background: 'rgba(255,255,255,0.10)' }} /> <div style={{ flex: 1, height: 1, background: 'rgba(255,255,255,0.10)' }} />
</div> </div>
@@ -248,11 +217,11 @@ const Contact: React.FC = () => {
{/* Left sticky label */} {/* Left sticky label */}
<div style={isMobile ? undefined : { position: 'sticky', top: 96, alignSelf: 'start' }}> <div style={isMobile ? undefined : { position: 'sticky', top: 96, alignSelf: 'start' }}>
<p style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: '#4A8FC9', marginBottom: 20 }}> <p style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.28em', textTransform: 'uppercase', color: '#4A8FC9', marginBottom: 20 }}>
{fallbackText(faq.eyebrow, 'Antworten')} {fallbackText(faq.eyebrow, contactContent.faq.eyebrow)}
</p> </p>
<div style={{ width: 28, height: 2, background: GOLD, marginBottom: 20 }} /> <div style={{ width: 28, height: 2, background: GOLD, marginBottom: 20 }} />
<h2 style={{ fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.4rem)' : 'clamp(2rem, 3.5vw, 2.8rem)', fontWeight: 900, color: NAVY, letterSpacing: '-0.03em', lineHeight: 1.1 }}> <h2 style={{ fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.4rem)' : 'clamp(2rem, 3.5vw, 2.8rem)', fontWeight: 900, color: NAVY, letterSpacing: '-0.03em', lineHeight: 1.1 }}>
<Lines text={fallbackText(faq.heading, 'Häufige\nFragen')} /> <Lines text={fallbackText(faq.heading, contactContent.faq.heading)} />
</h2> </h2>
</div> </div>