From 8dbe129ff2e4c838b13465dcf892daaa1b86bc0d Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Mon, 22 Jun 2026 11:57:59 +0200 Subject: [PATCH] feat: manage contact form copy via payload --- src/collections/Pages/contactFields.ts | 78 +++++++++++++++++ src/payload-types.ts | 105 +++++++++++++++++++++++ src/scripts/preload-contact-page-cms.ts | 3 + src/spa/components/forms/KontaktForm.tsx | 81 +++++++++-------- src/spa/contactFormContent.ts | 42 +++++++++ src/spa/pages/Contact.tsx | 2 +- 6 files changed, 275 insertions(+), 36 deletions(-) create mode 100644 src/spa/contactFormContent.ts diff --git a/src/collections/Pages/contactFields.ts b/src/collections/Pages/contactFields.ts index 52d9c9e..3529c7b 100644 --- a/src/collections/Pages/contactFields.ts +++ b/src/collections/Pages/contactFields.ts @@ -1,5 +1,7 @@ import type { Field } from 'payload' +import { contactFormContent } from '@/spa/contactFormContent' + const uploadField = (name: string, label: string, description?: string): Field => ({ name, type: 'upload', @@ -96,6 +98,82 @@ export const contactFields: Field[] = [ text('formImageLabel', 'Form image label', 'Gala 2025 München'), text('formEyebrow', 'Form eyebrow', 'Kontaktformular'), 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), + ], + }, + ], + }, ], }, { diff --git a/src/payload-types.ts b/src/payload-types.ts index 89de03b..0830ad9 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -456,6 +456,54 @@ export interface Page { formImageLabel?: string | null; formEyebrow?: 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. @@ -3211,6 +3259,63 @@ export interface PagesSelect { formImageLabel?: T; formEyebrow?: 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?: | T diff --git a/src/scripts/preload-contact-page-cms.ts b/src/scripts/preload-contact-page-cms.ts index dcc0c34..3eab57c 100644 --- a/src/scripts/preload-contact-page-cms.ts +++ b/src/scripts/preload-contact-page-cms.ts @@ -3,6 +3,8 @@ import 'dotenv/config' import config from '@payload-config' import { getPayload, type Payload } from 'payload' +import { contactFormContent } from '@/spa/contactFormContent' + const mediaByFilename = async (payload: Payload, filename: string) => { const result = await payload.find({ collection: 'media', @@ -65,6 +67,7 @@ async function main() { formImageLabel: 'Gala 2025 München', formEyebrow: 'Kontaktformular', formTitle: 'Schreiben Sie uns', + formCopy: contactFormContent, }, deadlines: { watermark: '2026', diff --git a/src/spa/components/forms/KontaktForm.tsx b/src/spa/components/forms/KontaktForm.tsx index b0cf106..d812495 100644 --- a/src/spa/components/forms/KontaktForm.tsx +++ b/src/spa/components/forms/KontaktForm.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { ArrowRight, ArrowLeft, Check, MessageSquare, User, Send } from 'lucide-react'; import { useIsMobile } from '@/spa/hooks/useIsMobile'; +import { contactFormContent } from '@/spa/contactFormContent'; const FF = '"IBM Plex Sans", sans-serif'; @@ -94,18 +95,27 @@ type FormData = { nachricht: string; }; -const STEPS = [ - { label: 'Betreff', icon: MessageSquare }, - { label: 'Kontakt', icon: User }, - { label: 'Nachricht', icon: Send }, -]; +type ContactFormCopy = typeof contactFormContent; +type SubjectChoice = (typeof contactFormContent.subjects)[number]; -const BETREFFS = [ - { 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.' }, -]; +const STEP_ICONS = [MessageSquare, User, Send]; +const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; +const fallbackArray = (value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback; + +function mergeCopy(content?: unknown): ContactFormCopy { + const data = (content && typeof content === 'object' ? content : {}) as Partial; + 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 = { enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }), @@ -156,7 +166,7 @@ function StyledTextarea({ c, ...props }: React.TextareaHTMLAttributes void }) { +function BetreffCard({ c, item, selected, onClick }: { c: C; item: SubjectChoice; selected: boolean; onClick: () => void }) { return ( - {step < STEPS.length - 1 ? ( + {step < copy.steps.length - 1 ? ( ) : ( )} diff --git a/src/spa/contactFormContent.ts b/src/spa/contactFormContent.ts new file mode 100644 index 0000000..d265bc4 --- /dev/null +++ b/src/spa/contactFormContent.ts @@ -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', + }, +} diff --git a/src/spa/pages/Contact.tsx b/src/spa/pages/Contact.tsx index 1553b2a..7bcec24 100644 --- a/src/spa/pages/Contact.tsx +++ b/src/spa/pages/Contact.tsx @@ -192,7 +192,7 @@ const Contact: React.FC = () => {

{fallbackText(info.formTitle, 'Schreiben Sie uns')}

- +