feat: manage contact form copy via payload

This commit is contained in:
syntaxbullet
2026-06-22 11:57:59 +02:00
parent aa839fcebc
commit 8dbe129ff2
6 changed files with 275 additions and 36 deletions

View File

@@ -1,5 +1,7 @@
import type { Field } from 'payload' import type { Field } from 'payload'
import { contactFormContent } from '@/spa/contactFormContent'
const uploadField = (name: string, label: string, description?: string): Field => ({ const uploadField = (name: string, label: string, description?: string): Field => ({
name, name,
type: 'upload', type: 'upload',
@@ -96,6 +98,82 @@ export const contactFields: Field[] = [
text('formImageLabel', 'Form image label', 'Gala 2025 München'), text('formImageLabel', 'Form image label', 'Gala 2025 München'),
text('formEyebrow', 'Form eyebrow', 'Kontaktformular'), text('formEyebrow', 'Form eyebrow', 'Kontaktformular'),
text('formTitle', 'Form title', 'Schreiben Sie uns'), text('formTitle', 'Form title', 'Schreiben Sie uns'),
{
name: 'formCopy',
label: 'Contact form copy',
type: 'group',
admin: sectionAdmin('Step labels, subject choices, field labels, validation messages, and success copy.'),
fields: [
{
name: 'steps',
label: 'Step labels',
type: 'array',
defaultValue: contactFormContent.steps,
fields: [text('label', 'Label')],
},
{
name: 'subjects',
label: 'Subject choices',
type: 'array',
defaultValue: contactFormContent.subjects,
fields: [text('value', 'Value'), text('label', 'Label'), textarea('desc', 'Description')],
},
{
name: 'prompts',
label: 'Prompts',
type: 'group',
fields: [
text('subject', 'Subject step prompt', contactFormContent.prompts.subject),
text('contact', 'Contact step prompt', contactFormContent.prompts.contact),
text('message', 'Message step prompt', contactFormContent.prompts.message),
],
},
{
name: 'fields',
label: 'Fields',
type: 'group',
fields: [
text('nameLabel', 'Name label', contactFormContent.fields.nameLabel),
text('namePlaceholder', 'Name placeholder', contactFormContent.fields.namePlaceholder),
text('emailLabel', 'Email label', contactFormContent.fields.emailLabel),
text('emailPlaceholder', 'Email placeholder', contactFormContent.fields.emailPlaceholder),
text('messageLabel', 'Message label', contactFormContent.fields.messageLabel),
text('messagePlaceholder', 'Message placeholder', contactFormContent.fields.messagePlaceholder),
],
},
{
name: 'validation',
label: 'Validation messages',
type: 'group',
fields: [
text('nameRequired', 'Name required', contactFormContent.validation.nameRequired),
text('emailRequired', 'Email required', contactFormContent.validation.emailRequired),
text('emailInvalid', 'Email invalid', contactFormContent.validation.emailInvalid),
text('messageRequired', 'Message required', contactFormContent.validation.messageRequired),
],
},
{
name: 'navigation',
label: 'Navigation labels',
type: 'group',
fields: [
text('back', 'Back', contactFormContent.navigation.back),
text('next', 'Next', contactFormContent.navigation.next),
text('submit', 'Submit', contactFormContent.navigation.submit),
],
},
{
name: 'success',
label: 'Success copy',
type: 'group',
fields: [
text('heading', 'Heading', contactFormContent.success.heading),
text('prefix', 'Prefix', contactFormContent.success.prefix),
text('suffixBeforeEmail', 'Suffix before email', contactFormContent.success.suffixBeforeEmail),
],
},
],
},
], ],
}, },
{ {

View File

@@ -456,6 +456,54 @@ export interface Page {
formImageLabel?: string | null; formImageLabel?: string | null;
formEyebrow?: string | null; formEyebrow?: string | null;
formTitle?: string | null; formTitle?: string | null;
/**
* Step labels, subject choices, field labels, validation messages, and success copy.
*/
formCopy?: {
steps?:
| {
label?: string | null;
id?: string | null;
}[]
| null;
subjects?:
| {
value?: string | null;
label?: string | null;
desc?: string | null;
id?: string | null;
}[]
| null;
prompts?: {
subject?: string | null;
contact?: string | null;
message?: string | null;
};
fields?: {
nameLabel?: string | null;
namePlaceholder?: string | null;
emailLabel?: string | null;
emailPlaceholder?: string | null;
messageLabel?: string | null;
messagePlaceholder?: string | null;
};
validation?: {
nameRequired?: string | null;
emailRequired?: string | null;
emailInvalid?: string | null;
messageRequired?: string | null;
};
navigation?: {
back?: string | null;
next?: string | null;
submit?: string | null;
};
success?: {
heading?: string | null;
prefix?: string | null;
suffixBeforeEmail?: string | null;
};
};
}; };
/** /**
* Dark 2026 timeline with three deadline columns. * Dark 2026 timeline with three deadline columns.
@@ -3211,6 +3259,63 @@ export interface PagesSelect<T extends boolean = true> {
formImageLabel?: T; formImageLabel?: T;
formEyebrow?: T; formEyebrow?: T;
formTitle?: T; formTitle?: T;
formCopy?:
| T
| {
steps?:
| T
| {
label?: T;
id?: T;
};
subjects?:
| T
| {
value?: T;
label?: T;
desc?: T;
id?: T;
};
prompts?:
| T
| {
subject?: T;
contact?: T;
message?: T;
};
fields?:
| T
| {
nameLabel?: T;
namePlaceholder?: T;
emailLabel?: T;
emailPlaceholder?: T;
messageLabel?: T;
messagePlaceholder?: T;
};
validation?:
| T
| {
nameRequired?: T;
emailRequired?: T;
emailInvalid?: T;
messageRequired?: T;
};
navigation?:
| T
| {
back?: T;
next?: T;
submit?: T;
};
success?:
| T
| {
heading?: T;
prefix?: T;
suffixBeforeEmail?: T;
};
};
}; };
deadlines?: deadlines?:
| T | T

View File

@@ -3,6 +3,8 @@ 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'
const mediaByFilename = async (payload: Payload, filename: string) => { const mediaByFilename = async (payload: Payload, filename: string) => {
const result = await payload.find({ const result = await payload.find({
collection: 'media', collection: 'media',
@@ -65,6 +67,7 @@ async function main() {
formImageLabel: 'Gala 2025 München', formImageLabel: 'Gala 2025 München',
formEyebrow: 'Kontaktformular', formEyebrow: 'Kontaktformular',
formTitle: 'Schreiben Sie uns', formTitle: 'Schreiben Sie uns',
formCopy: contactFormContent,
}, },
deadlines: { deadlines: {
watermark: '2026', watermark: '2026',

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { ArrowRight, ArrowLeft, Check, MessageSquare, User, Send } from 'lucide-react'; import { ArrowRight, ArrowLeft, Check, MessageSquare, User, Send } from 'lucide-react';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { contactFormContent } from '@/spa/contactFormContent';
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
@@ -94,18 +95,27 @@ type FormData = {
nachricht: string; nachricht: string;
}; };
const STEPS = [ type ContactFormCopy = typeof contactFormContent;
{ label: 'Betreff', icon: MessageSquare }, type SubjectChoice = (typeof contactFormContent.subjects)[number];
{ label: 'Kontakt', icon: User },
{ label: 'Nachricht', icon: Send },
];
const BETREFFS = [ const STEP_ICONS = [MessageSquare, User, Send];
{ value: 'allgemein', label: 'Allgemeine Anfrage', desc: 'Ich habe eine allgemeine Frage.' }, const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
{ value: 'bewerbung', label: 'Frage zur Bewerbung', desc: 'Ich möchte mich bewerben oder habe Rückfragen.' }, const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
{ value: 'presse', label: 'Presseanfrage', desc: 'Ich bin Journalist:in und recherchiere zum BMP.' },
{ value: 'sponsoring', label: 'Sponsoring', desc: 'Ich interessiere mich für eine Partnerschaft.' }, function mergeCopy(content?: unknown): ContactFormCopy {
]; const data = (content && typeof content === 'object' ? content : {}) as Partial<ContactFormCopy>;
return {
...contactFormContent,
...data,
steps: fallbackArray(data.steps, contactFormContent.steps),
subjects: fallbackArray(data.subjects, contactFormContent.subjects),
prompts: { ...contactFormContent.prompts, ...data.prompts },
fields: { ...contactFormContent.fields, ...data.fields },
validation: { ...contactFormContent.validation, ...data.validation },
navigation: { ...contactFormContent.navigation, ...data.navigation },
success: { ...contactFormContent.success, ...data.success },
};
}
const variants = { const variants = {
enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }), enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }),
@@ -156,7 +166,7 @@ function StyledTextarea({ c, ...props }: React.TextareaHTMLAttributes<HTMLTextAr
); );
} }
function BetreffCard({ c, item, selected, onClick }: { c: C; item: typeof BETREFFS[0]; selected: boolean; onClick: () => void }) { function BetreffCard({ c, item, selected, onClick }: { c: C; item: SubjectChoice; selected: boolean; onClick: () => void }) {
return ( return (
<button <button
type="button" onClick={onClick} type="button" onClick={onClick}
@@ -184,7 +194,8 @@ function BetreffCard({ c, item, selected, onClick }: { c: C; item: typeof BETREF
); );
} }
export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold' }) { export default function KontaktForm({ theme = 'dark', content }: { theme?: 'dark' | 'gold'; content?: unknown }) {
const copy = mergeCopy(content);
const c = theme === 'gold' ? GOLD : DARK; const c = theme === 'gold' ? GOLD : DARK;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
@@ -201,11 +212,11 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
const validate = () => { const validate = () => {
const e: typeof errors = {}; const e: typeof errors = {};
if (step === 1) { if (step === 1) {
if (!data.name) e.name = 'Bitte Namen angeben'; if (!data.name) e.name = copy.validation.nameRequired;
if (!data.email) e.email = 'Bitte E-Mail angeben'; if (!data.email) e.email = copy.validation.emailRequired;
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = 'Ungültige E-Mail'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = copy.validation.emailInvalid;
} }
if (step === 2 && !data.nachricht) e.nachricht = 'Bitte Nachricht verfassen.'; if (step === 2 && !data.nachricht) e.nachricht = copy.validation.messageRequired;
setErrors(e); setErrors(e);
return Object.keys(e).length === 0; return Object.keys(e).length === 0;
}; };
@@ -223,10 +234,10 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
<Check size={24} color={c.successIconIn} strokeWidth={2.5} /> <Check size={24} color={c.successIconIn} strokeWidth={2.5} />
</div> </div>
<h3 style={{ fontSize: 20, fontWeight: 800, color: c.successHeading, marginBottom: 10, letterSpacing: '-0.02em', fontFamily: FF }}> <h3 style={{ fontSize: 20, fontWeight: 800, color: c.successHeading, marginBottom: 10, letterSpacing: '-0.02em', fontFamily: FF }}>
Nachricht gesendet {fallbackText(copy.success.heading, contactFormContent.success.heading)}
</h3> </h3>
<p style={{ fontSize: 15, color: c.successBody, lineHeight: 1.7, maxWidth: 280, margin: '0 auto', fontFamily: FF, overflowWrap: 'anywhere' }}> <p style={{ fontSize: 15, color: c.successBody, lineHeight: 1.7, maxWidth: 280, margin: '0 auto', fontFamily: FF, overflowWrap: 'anywhere' }}>
Danke, <strong style={{ color: c.successHeading }}>{data.name}</strong>. Wir melden uns zeitnah bei{' '} {fallbackText(copy.success.prefix, contactFormContent.success.prefix)} <strong style={{ color: c.successHeading }}>{data.name}</strong>. {fallbackText(copy.success.suffixBeforeEmail, contactFormContent.success.suffixBeforeEmail)}{' '}
<strong style={{ color: c.successIcon }}>{data.email}</strong>. <strong style={{ color: c.successIcon }}>{data.email}</strong>.
</p> </p>
</motion.div> </motion.div>
@@ -237,13 +248,13 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}> <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
{/* Progress */} {/* Progress */}
<div style={{ height: 3, background: c.progressBg, flexShrink: 0 }}> <div style={{ height: 3, background: c.progressBg, flexShrink: 0 }}>
<div style={{ height: '100%', background: c.progressFill, width: step === 0 ? '0%' : `${(step / (STEPS.length - 1)) * 100}%`, transition: 'width 0.4s ease' }} /> <div style={{ height: '100%', background: c.progressFill, width: step === 0 ? '0%' : `${(step / (copy.steps.length - 1)) * 100}%`, transition: 'width 0.4s ease' }} />
</div> </div>
{/* Step tabs */} {/* Step tabs */}
<div style={{ display: 'flex', borderBottom: `1px solid ${c.border}`, flexShrink: 0 }}> <div style={{ display: 'flex', borderBottom: `1px solid ${c.border}`, flexShrink: 0 }}>
{STEPS.map((s, i) => { {copy.steps.map((s, i) => {
const Icon = s.icon; const Icon = STEP_ICONS[i] || Send;
const done = i < step, active = i === step; const done = i < step, active = i === step;
return ( return (
<div key={i} style={{ <div key={i} style={{
@@ -256,7 +267,7 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
marginBottom: -1, transition: 'color 0.2s', marginBottom: -1, transition: 'color 0.2s',
}}> }}>
<Icon size={11} strokeWidth={active ? 2 : 1.5} /> <Icon size={11} strokeWidth={active ? 2 : 1.5} />
<span>{done ? '✓' : s.label}</span> <span>{done ? '✓' : fallbackText(s.label, contactFormContent.steps[i]?.label || '')}</span>
</div> </div>
); );
})} })}
@@ -271,8 +282,8 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
{step === 0 && ( {step === 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<p style={{ fontSize: 14, color: c.subtext, marginBottom: 12, fontFamily: FF }}>Womit können wir Ihnen helfen?</p> <p style={{ fontSize: 14, color: c.subtext, marginBottom: 12, fontFamily: FF }}>{fallbackText(copy.prompts.subject, contactFormContent.prompts.subject)}</p>
{BETREFFS.map(item => ( {copy.subjects.map(item => (
<BetreffCard key={item.value} c={c} item={item} selected={data.betreff === item.value} <BetreffCard key={item.value} c={c} item={item} selected={data.betreff === item.value}
onClick={() => { set('betreff', item.value); setDir(1); setStep(1); }} /> onClick={() => { set('betreff', item.value); setDir(1); setStep(1); }} />
))} ))}
@@ -281,10 +292,10 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
{step === 1 && ( {step === 1 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<p style={{ fontSize: 14, color: c.subtext, marginBottom: 4, fontFamily: FF }}>Wie dürfen wir Sie kontaktieren?</p> <p style={{ fontSize: 14, color: c.subtext, marginBottom: 4, fontFamily: FF }}>{fallbackText(copy.prompts.contact, contactFormContent.prompts.contact)}</p>
{([ {([
{ label: 'Name', key: 'name' as const, type: 'text', placeholder: 'Vor- und Nachname', error: errors.name }, { label: copy.fields.nameLabel, key: 'name' as const, type: 'text', placeholder: copy.fields.namePlaceholder, error: errors.name },
{ label: 'E-Mail', key: 'email' as const, type: 'email', placeholder: 'ihre@email.de', error: errors.email }, { label: copy.fields.emailLabel, key: 'email' as const, type: 'email', placeholder: copy.fields.emailPlaceholder, error: errors.email },
] as const).map(f => ( ] as const).map(f => (
<div key={f.key} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div key={f.key} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: c.labelText, fontFamily: FF }}>{f.label}</label> <label style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: c.labelText, fontFamily: FF }}>{f.label}</label>
@@ -297,10 +308,10 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
{step === 2 && ( {step === 2 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<p style={{ fontSize: 14, color: c.subtext, marginBottom: 4, fontFamily: FF }}>Was möchten Sie uns mitteilen?</p> <p style={{ fontSize: 14, color: c.subtext, marginBottom: 4, fontFamily: FF }}>{fallbackText(copy.prompts.message, contactFormContent.prompts.message)}</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: c.labelText, fontFamily: FF }}>Nachricht</label> <label style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: c.labelText, fontFamily: FF }}>{fallbackText(copy.fields.messageLabel, contactFormContent.fields.messageLabel)}</label>
<StyledTextarea c={c} value={data.nachricht} onChange={e => set('nachricht', e.target.value)} rows={5} placeholder="Schreiben Sie Ihre Nachricht…" /> <StyledTextarea c={c} value={data.nachricht} onChange={e => set('nachricht', e.target.value)} rows={5} placeholder={fallbackText(copy.fields.messagePlaceholder, contactFormContent.fields.messagePlaceholder)} />
{errors.nachricht && <span style={{ fontSize: 11, color: c.errorText, fontFamily: FF }}>{errors.nachricht}</span>} {errors.nachricht && <span style={{ fontSize: 11, color: c.errorText, fontFamily: FF }}>{errors.nachricht}</span>}
</div> </div>
</div> </div>
@@ -320,10 +331,10 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
onMouseEnter={e => (e.currentTarget.style.color = c.backTextHover)} onMouseEnter={e => (e.currentTarget.style.color = c.backTextHover)}
onMouseLeave={e => (e.currentTarget.style.color = c.backText)} onMouseLeave={e => (e.currentTarget.style.color = c.backText)}
> >
<ArrowLeft size={13} /> Zurück <ArrowLeft size={13} /> {fallbackText(copy.navigation.back, contactFormContent.navigation.back)}
</button> </button>
{step < STEPS.length - 1 ? ( {step < copy.steps.length - 1 ? (
<button type="button" onClick={next} style={{ <button type="button" onClick={next} style={{
fontSize: 14, fontWeight: 700, color: c.btnText, background: c.btnBg, border: 'none', cursor: 'pointer', fontSize: 14, fontWeight: 700, color: c.btnText, background: c.btnBg, border: 'none', cursor: 'pointer',
padding: '12px 28px', fontFamily: FF, display: 'flex', alignItems: 'center', gap: 8, padding: '12px 28px', fontFamily: FF, display: 'flex', alignItems: 'center', gap: 8,
@@ -333,7 +344,7 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
onMouseEnter={e => (e.currentTarget.style.background = c.btnBgHover)} onMouseEnter={e => (e.currentTarget.style.background = c.btnBgHover)}
onMouseLeave={e => (e.currentTarget.style.background = c.btnBg)} onMouseLeave={e => (e.currentTarget.style.background = c.btnBg)}
> >
Weiter <ArrowRight size={14} /> {fallbackText(copy.navigation.next, contactFormContent.navigation.next)} <ArrowRight size={14} />
</button> </button>
) : ( ) : (
<button type="button" onClick={submit} style={{ <button type="button" onClick={submit} style={{
@@ -345,7 +356,7 @@ export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold
onMouseEnter={e => (e.currentTarget.style.background = c.btnBgHover)} onMouseEnter={e => (e.currentTarget.style.background = c.btnBgHover)}
onMouseLeave={e => (e.currentTarget.style.background = c.btnBg)} onMouseLeave={e => (e.currentTarget.style.background = c.btnBg)}
> >
Anfrage senden <Send size={14} /> {fallbackText(copy.navigation.submit, contactFormContent.navigation.submit)} <Send size={14} />
</button> </button>
)} )}
</div> </div>

View File

@@ -0,0 +1,42 @@
export const contactFormContent = {
steps: [
{ label: 'Betreff' },
{ label: 'Kontakt' },
{ label: 'Nachricht' },
],
subjects: [
{ value: 'allgemein', label: 'Allgemeine Anfrage', desc: 'Ich habe eine allgemeine Frage.' },
{ value: 'bewerbung', label: 'Frage zur Bewerbung', desc: 'Ich möchte mich bewerben oder habe Rückfragen.' },
{ value: 'presse', label: 'Presseanfrage', desc: 'Ich bin Journalist:in und recherchiere zum BMP.' },
{ value: 'sponsoring', label: 'Sponsoring', desc: 'Ich interessiere mich für eine Partnerschaft.' },
],
prompts: {
subject: 'Womit können wir Ihnen helfen?',
contact: 'Wie dürfen wir Sie kontaktieren?',
message: 'Was möchten Sie uns mitteilen?',
},
fields: {
nameLabel: 'Name',
namePlaceholder: 'Vor- und Nachname',
emailLabel: 'E-Mail',
emailPlaceholder: 'ihre@email.de',
messageLabel: 'Nachricht',
messagePlaceholder: 'Schreiben Sie Ihre Nachricht…',
},
validation: {
nameRequired: 'Bitte Namen angeben',
emailRequired: 'Bitte E-Mail angeben',
emailInvalid: 'Ungültige E-Mail',
messageRequired: 'Bitte Nachricht verfassen.',
},
navigation: {
back: 'Zurück',
next: 'Weiter',
submit: 'Anfrage senden',
},
success: {
heading: 'Nachricht gesendet',
prefix: 'Danke,',
suffixBeforeEmail: 'Wir melden uns zeitnah bei',
},
}

View File

@@ -192,7 +192,7 @@ const Contact: React.FC = () => {
<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, 'Schreiben Sie uns')}
</h3> </h3>
<KontaktForm theme="gold" /> <KontaktForm theme="gold" content={info.formCopy || undefined} />
</div> </div>
</div> </div>
</div> </div>