diff --git a/package.json b/package.json index 2cfc383..2e4476e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "preload:about-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-about-page-cms.ts", "preload:contact-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-contact-page-cms.ts", "preload:datenschutz-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-datenschutz-page-cms.ts", + "preload:formular-upload-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-formular-upload-page-cms.ts", "preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts", "preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts", "preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts", diff --git a/src/collections/Pages/formularUploadFields.ts b/src/collections/Pages/formularUploadFields.ts new file mode 100644 index 0000000..afca14f --- /dev/null +++ b/src/collections/Pages/formularUploadFields.ts @@ -0,0 +1,140 @@ +import type { Field } from 'payload' + +import { formularUploadContent } from '@/spa/formularUploadContent' + +const text = (name: string, label: string, defaultValue?: string): Field => ({ + name, + type: 'text', + label, + defaultValue, +}) + +const textarea = (name: string, label: string, defaultValue?: string): Field => ({ + name, + type: 'textarea', + label, + defaultValue, +}) + +const sectionAdmin = (description: string) => ({ + description, + initCollapsed: true, +}) + +export const formularUploadFields: Field[] = [ + { + name: 'formUpload', + label: 'Form upload page content', + type: 'group', + admin: { + description: + 'Edit the Formular hochladen page copy in frontend order. Upload interaction remains handled by the SPA component.', + }, + fields: [ + { + name: 'hero', + label: '01 · Hero', + type: 'group', + admin: sectionAdmin('Top eyebrow, heading, intro copy, and stat row.'), + fields: [ + text('eyebrow', 'Eyebrow', formularUploadContent.hero.eyebrow), + textarea('heading', 'Heading', formularUploadContent.hero.heading), + textarea('description', 'Description', formularUploadContent.hero.description), + { + name: 'stats', + label: 'Stats', + type: 'array', + dbName: 'upload_stats', + defaultValue: formularUploadContent.hero.stats, + fields: [text('num', 'Value'), text('label', 'Label')], + }, + ], + }, + { + name: 'breadcrumb', + label: '02 · Breadcrumb', + type: 'group', + admin: sectionAdmin('Small route label below the hero.'), + fields: [text('label', 'Label', formularUploadContent.breadcrumb.label)], + }, + { + name: 'requirements', + label: '03 · Requirements', + type: 'group', + admin: sectionAdmin('Requirement cards above the upload area.'), + fields: [ + text('eyebrow', 'Eyebrow', formularUploadContent.requirements.eyebrow), + textarea('heading', 'Heading', formularUploadContent.requirements.heading), + { + name: 'cards', + label: 'Cards', + type: 'array', + dbName: 'upload_req_cards', + defaultValue: formularUploadContent.requirements.cards, + fields: [ + { + name: 'icon', + label: 'Icon', + type: 'select', + defaultValue: 'fileText', + options: [ + { label: 'File text', value: 'fileText' }, + { label: 'Clock', value: 'clock' }, + { label: 'Shield', value: 'shield' }, + ], + }, + text('title', 'Title'), + textarea('items', 'Items, one per line'), + ], + }, + ], + }, + { + name: 'upload', + label: '04 · Upload area', + type: 'group', + admin: sectionAdmin('Dropzone labels, accepted format chips, note card, submit labels, and privacy note.'), + fields: [ + text('eyebrow', 'Eyebrow', formularUploadContent.upload.eyebrow), + textarea('heading', 'Heading', formularUploadContent.upload.heading), + text('dropTitle', 'Dropzone title', formularUploadContent.upload.dropTitle), + text('dropDescription', 'Dropzone description', formularUploadContent.upload.dropDescription), + textarea('acceptedFormats', 'Accepted format chips, one per line', formularUploadContent.upload.acceptedFormats), + text('fileRemoveLabel', 'Remove-file aria label', formularUploadContent.upload.fileRemoveLabel), + text('noteTitle', 'Note title', formularUploadContent.upload.noteTitle), + textarea('note', 'Note', formularUploadContent.upload.note), + text('selectedSubmitPrefix', 'Selected submit singular word', formularUploadContent.upload.selectedSubmitPrefix), + text('selectedSubmitPluralSuffix', 'Selected submit plural suffix', formularUploadContent.upload.selectedSubmitPluralSuffix), + text('selectedSubmitAction', 'Selected submit action', formularUploadContent.upload.selectedSubmitAction), + text('emptySubmitLabel', 'Empty submit label', formularUploadContent.upload.emptySubmitLabel), + textarea('privacyNote', 'Privacy note', formularUploadContent.upload.privacyNote), + ], + }, + { + name: 'success', + label: '05 · Success state', + type: 'group', + admin: sectionAdmin('Confirmation copy shown after simulated upload completion.'), + fields: [ + text('heading', 'Heading', formularUploadContent.success.heading), + textarea('description', 'Description', formularUploadContent.success.description), + text('linkLabel', 'Link label', formularUploadContent.success.linkLabel), + text('linkUrl', 'Link URL', formularUploadContent.success.linkUrl), + ], + }, + { + name: 'cta', + label: '06 · Bottom CTA', + type: 'group', + admin: sectionAdmin('Bottom question prompt and navigation links.'), + fields: [ + text('eyebrow', 'Prompt', formularUploadContent.cta.eyebrow), + text('contactLabel', 'Contact link label', formularUploadContent.cta.contactLabel), + text('contactUrl', 'Contact link URL', formularUploadContent.cta.contactUrl), + text('buttonLabel', 'Button label', formularUploadContent.cta.buttonLabel), + text('buttonUrl', 'Button URL', formularUploadContent.cta.buttonUrl), + ], + }, + ], + }, +] diff --git a/src/collections/Pages/index.ts b/src/collections/Pages/index.ts index 07582ab..5b94e43 100644 --- a/src/collections/Pages/index.ts +++ b/src/collections/Pages/index.ts @@ -11,6 +11,7 @@ import { hero } from '@/heros/config' import { aboutFields } from './aboutFields' import { contactFields } from './contactFields' import { datenschutzFields } from './datenschutzFields' +import { formularUploadFields } from './formularUploadFields' import { homeFields } from './homeFields' import { impressumFields } from './impressumFields' import { participationFields } from './participationFields' @@ -93,6 +94,14 @@ const isPressIndexPage = (_: unknown, siblingData?: { slug?: string; spaPath?: s return spaPath === '/presse' || slug === 'presse' || slug === 'press' || title === 'presse' || title === 'press' } +const isFormularUploadPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => { + const slug = siblingData?.slug + const spaPath = siblingData?.spaPath + const title = siblingData?.title?.toLowerCase() + + return spaPath === '/formular-hochladen' || slug === 'formular-hochladen' || slug === 'upload' || title === 'formular hochladen' +} + const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => isHomePage(undefined, siblingData) || isContactPage(undefined, siblingData) || @@ -101,7 +110,8 @@ const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: s isImpressumPage(undefined, siblingData) || isDatenschutzPage(undefined, siblingData) || isPreistraegerIndexPage(undefined, siblingData) || - isPressIndexPage(undefined, siblingData) + isPressIndexPage(undefined, siblingData) || + isFormularUploadPage(undefined, siblingData) export const Pages: CollectionConfig<'pages'> = { slug: 'pages', @@ -225,6 +235,13 @@ export const Pages: CollectionConfig<'pages'> = { fields: pressIndexFields, label: 'Press Index Page', }, + { + admin: { + condition: (data) => isFormularUploadPage(undefined, data), + }, + fields: formularUploadFields, + label: 'Formular Upload Page', + }, { name: 'meta', label: 'SEO', diff --git a/src/payload-types.ts b/src/payload-types.ts index b0a6eba..e62eb43 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1331,6 +1331,84 @@ export interface Page { successMessage?: string | null; }; }; + /** + * Edit the Formular hochladen page copy in frontend order. Upload interaction remains handled by the SPA component. + */ + formUpload?: { + /** + * Top eyebrow, heading, intro copy, and stat row. + */ + hero?: { + eyebrow?: string | null; + heading?: string | null; + description?: string | null; + stats?: + | { + num?: string | null; + label?: string | null; + id?: string | null; + }[] + | null; + }; + /** + * Small route label below the hero. + */ + breadcrumb?: { + label?: string | null; + }; + /** + * Requirement cards above the upload area. + */ + requirements?: { + eyebrow?: string | null; + heading?: string | null; + cards?: + | { + icon?: ('fileText' | 'clock' | 'shield') | null; + title?: string | null; + items?: string | null; + id?: string | null; + }[] + | null; + }; + /** + * Dropzone labels, accepted format chips, note card, submit labels, and privacy note. + */ + upload?: { + eyebrow?: string | null; + heading?: string | null; + dropTitle?: string | null; + dropDescription?: string | null; + acceptedFormats?: string | null; + fileRemoveLabel?: string | null; + noteTitle?: string | null; + note?: string | null; + selectedSubmitPrefix?: string | null; + selectedSubmitPluralSuffix?: string | null; + selectedSubmitAction?: string | null; + emptySubmitLabel?: string | null; + privacyNote?: string | null; + }; + /** + * Confirmation copy shown after simulated upload completion. + */ + success?: { + heading?: string | null; + description?: string | null; + linkLabel?: string | null; + linkUrl?: string | null; + }; + /** + * Bottom question prompt and navigation links. + */ + cta?: { + eyebrow?: string | null; + contactLabel?: string | null; + contactUrl?: string | null; + buttonLabel?: string | null; + buttonUrl?: string | null; + }; + }; meta?: { title?: string | null; /** @@ -3291,6 +3369,77 @@ export interface PagesSelect { successMessage?: T; }; }; + formUpload?: + | T + | { + hero?: + | T + | { + eyebrow?: T; + heading?: T; + description?: T; + stats?: + | T + | { + num?: T; + label?: T; + id?: T; + }; + }; + breadcrumb?: + | T + | { + label?: T; + }; + requirements?: + | T + | { + eyebrow?: T; + heading?: T; + cards?: + | T + | { + icon?: T; + title?: T; + items?: T; + id?: T; + }; + }; + upload?: + | T + | { + eyebrow?: T; + heading?: T; + dropTitle?: T; + dropDescription?: T; + acceptedFormats?: T; + fileRemoveLabel?: T; + noteTitle?: T; + note?: T; + selectedSubmitPrefix?: T; + selectedSubmitPluralSuffix?: T; + selectedSubmitAction?: T; + emptySubmitLabel?: T; + privacyNote?: T; + }; + success?: + | T + | { + heading?: T; + description?: T; + linkLabel?: T; + linkUrl?: T; + }; + cta?: + | T + | { + eyebrow?: T; + contactLabel?: T; + contactUrl?: T; + buttonLabel?: T; + buttonUrl?: T; + }; + }; meta?: | T | { diff --git a/src/scripts/preload-formular-upload-page-cms.ts b/src/scripts/preload-formular-upload-page-cms.ts new file mode 100644 index 0000000..db55689 --- /dev/null +++ b/src/scripts/preload-formular-upload-page-cms.ts @@ -0,0 +1,41 @@ +import 'dotenv/config' + +import config from '@payload-config' +import { getPayload } from 'payload' + +import { formularUploadContent } from '@/spa/formularUploadContent' + +async function main() { + const payload = await getPayload({ config }) + + const pageResult = await payload.find({ + collection: 'pages', + limit: 1, + pagination: false, + draft: true, + overrideAccess: true, + where: { + or: [{ spaPath: { equals: '/formular-hochladen' } }, { slug: { equals: 'formular-hochladen' } }, { slug: { equals: 'upload' } }], + }, + }) + + const page = pageResult.docs[0] + if (!page) throw new Error('Formular upload page not found. Expected spaPath=/formular-hochladen or slug=formular-hochladen/upload.') + + await payload.update({ + collection: 'pages', + id: page.id, + overrideAccess: true, + context: { disableRevalidate: true }, + data: { + formUpload: formularUploadContent, + } as never, + }) + + payload.logger.info(`Preloaded Formular upload CMS fields for page ${page.id}`) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/src/spa/formularUploadContent.ts b/src/spa/formularUploadContent.ts new file mode 100644 index 0000000..84db929 --- /dev/null +++ b/src/spa/formularUploadContent.ts @@ -0,0 +1,69 @@ +export const formularUploadContent = { + hero: { + eyebrow: 'BMP 2026 – Bewerbungsportal', + heading: 'UNTERLAGEN\nEINREICHEN', + description: + 'Laden Sie Ihre Bewerbungsunterlagen sicher hoch. Alle Einreichungen werden vertraulich behandelt und direkt an die Jury weitergeleitet.', + stats: [ + { num: '100%', label: 'Vertraulich' }, + { num: 'SSL', label: 'Verschlüsselt' }, + { num: '30 MB', label: 'Max. Dateigröße' }, + { num: 'PDF/DOCX', label: 'Akzeptierte Formate' }, + ], + }, + breadcrumb: { + label: 'Formular hochladen', + }, + requirements: { + eyebrow: 'Was wird benötigt?', + heading: 'ANFORDERUNGEN & FRISTEN', + cards: [ + { + icon: 'fileText', + title: 'Pflichtdokumente', + items: + 'Kurzprofil des Unternehmens (max. 2 Seiten)\nBegründung der Bewerbung (mind. 500 Wörter)\nAnschreiben der Geschäftsführung\nLetzte 2 Jahresabschlüsse (optional, falls vorhanden)', + }, + { + icon: 'clock', + title: 'Fristen & Einreichung', + items: + 'Bewerbungsschluss: 30. Juni 2026\nNachreichungen bis: 15. Juli 2026\nBenachrichtigung: August 2026\nGala & Verleihung: Oktober 2026', + }, + { + icon: 'shield', + title: 'Technische Anforderungen', + items: 'Formate: PDF, DOCX, XLSX\nMax. Dateigröße: 30 MB pro Datei\nMax. Gesamtgröße: 100 MB\nSprache: Deutsch', + }, + ], + }, + upload: { + eyebrow: 'Einreichung', + heading: 'DATEIEN HOCHLADEN', + dropTitle: 'Dateien hierher ziehen', + dropDescription: 'oder klicken zum Auswählen', + acceptedFormats: 'PDF\nDOCX\nXLSX\nPPT', + fileRemoveLabel: 'Datei entfernen', + noteTitle: 'Hinweis', + note: + 'Stellen Sie sicher, dass alle Pflichtdokumente vollständig sind, bevor Sie einreichen. Unvollständige Bewerbungen können nicht berücksichtigt werden.', + selectedSubmitPrefix: 'Datei', + selectedSubmitPluralSuffix: 'en', + selectedSubmitAction: 'einreichen', + emptySubmitLabel: 'Keine Dateien ausgewählt', + privacyNote: 'Mit der Einreichung stimmen Sie der Verarbeitung Ihrer Daten gemäß unserer Datenschutzerklärung zu.', + }, + success: { + heading: 'Einreichung erfolgreich', + description: 'Ihre Unterlagen wurden übermittelt. Sie erhalten eine Bestätigung per E-Mail. Die Jury meldet sich bis August 2026.', + linkLabel: 'Zur Teilnahmeseite', + linkUrl: '/teilnahme', + }, + cta: { + eyebrow: 'Noch Fragen zur Einreichung?', + contactLabel: 'Kontakt aufnehmen', + contactUrl: '/kontakt', + buttonLabel: 'Zur Teilnahmeseite', + buttonUrl: '/teilnahme', + }, +} diff --git a/src/spa/pages/FormularUpload.tsx b/src/spa/pages/FormularUpload.tsx index 58788a0..713f2eb 100644 --- a/src/spa/pages/FormularUpload.tsx +++ b/src/spa/pages/FormularUpload.tsx @@ -2,6 +2,8 @@ import React, { useState, useRef } from 'react'; import { Link } from '@/spa/router'; import { Upload, FileText, CheckCircle, AlertCircle, X, ArrowRight, Clock, Shield, Info } from 'lucide-react'; import { useIsMobile } from '@/spa/hooks/useIsMobile'; +import { useCmsRoute } from '@/spa/cmsRoute'; +import { formularUploadContent } from '@/spa/formularUploadContent'; const FF = '"IBM Plex Sans", sans-serif'; const GOLD = '#EFBF04'; @@ -12,6 +14,23 @@ const BORDER = '#D0D5DD'; const HELLBLAU = '#4A8FC9'; type UploadFile = { name: string; size: number; status: 'ready' | 'uploading' | 'done' | 'error' }; +type FormUploadCms = Partial; +type RequirementCard = (typeof formularUploadContent.requirements.cards)[number]; +type HeroStat = (typeof formularUploadContent.hero.stats)[number]; + +const requirementIcons = { + fileText: FileText, + clock: Clock, + shield: Shield, +}; + +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; +const lines = (value: unknown, fallback: string) => fallbackText(value, fallback).split('\n').filter(Boolean); + +function Lines({ text }: { text: string }) { + return <>{text.split('\n').map((line, i) => {i > 0 &&
}{line}
)}; +} function formatBytes(b: number) { if (b < 1024) return b + ' B'; @@ -20,6 +39,16 @@ function formatBytes(b: number) { } export default function FormularUpload() { + const cms = (useCmsRoute()?.doc?.formUpload || {}) as FormUploadCms; + const hero = { ...formularUploadContent.hero, ...(cms.hero || {}) }; + const breadcrumb = { ...formularUploadContent.breadcrumb, ...(cms.breadcrumb || {}) }; + const requirements = { ...formularUploadContent.requirements, ...(cms.requirements || {}) }; + const upload = { ...formularUploadContent.upload, ...(cms.upload || {}) }; + const success = { ...formularUploadContent.success, ...(cms.success || {}) }; + const cta = { ...formularUploadContent.cta, ...(cms.cta || {}) }; + const heroStats = fallbackArray(hero.stats, formularUploadContent.hero.stats); + const requirementCards = fallbackArray(requirements.cards, formularUploadContent.requirements.cards); + const acceptedFormats = lines(upload.acceptedFormats, formularUploadContent.upload.acceptedFormats); const [files, setFiles] = useState([]); const [dragging, setDragging] = useState(false); const [submitted, setSubmitted] = useState(false); @@ -53,28 +82,23 @@ export default function FormularUpload() {
- BMP 2026 – Bewerbungsportal + {fallbackText(hero.eyebrow, formularUploadContent.hero.eyebrow)}

- UNTERLAGEN
EINREICHEN +

- Laden Sie Ihre Bewerbungsunterlagen sicher hoch. Alle Einreichungen werden vertraulich behandelt und direkt an die Jury weitergeleitet. + {fallbackText(hero.description, formularUploadContent.hero.description)}

{/* Stats row */}
- {[ - { num: '100%', label: 'Vertraulich' }, - { num: 'SSL', label: 'Verschlüsselt' }, - { num: '30 MB', label: 'Max. Dateigröße' }, - { num: 'PDF/DOCX', label: 'Akzeptierte Formate' }, - ].map((s, i) => ( + {heroStats.map((s, i) => (
-
{s.num}
-
{s.label}
+
{fallbackText(s.num, formularUploadContent.hero.stats[i]?.num || '')}
+
{fallbackText(s.label, formularUploadContent.hero.stats[i]?.label || '')}
))}
@@ -82,41 +106,28 @@ export default function FormularUpload() { {/* Breadcrumb */}
- Formular hochladen + {fallbackText(breadcrumb.label, formularUploadContent.breadcrumb.label)}
{/* ── ERKLÄRUNG ───────────────────────────────────── */}
-

Was wird benötigt?

+

{fallbackText(requirements.eyebrow, formularUploadContent.requirements.eyebrow)}

- ANFORDERUNGEN & FRISTEN +

- {[ - { - icon: FileText, - title: 'Pflichtdokumente', - items: ['Kurzprofil des Unternehmens (max. 2 Seiten)', 'Begründung der Bewerbung (mind. 500 Wörter)', 'Anschreiben der Geschäftsführung', 'Letzte 2 Jahresabschlüsse (optional, falls vorhanden)'], - }, - { - icon: Clock, - title: 'Fristen & Einreichung', - items: ['Bewerbungsschluss: 30. Juni 2026', 'Nachreichungen bis: 15. Juli 2026', 'Benachrichtigung: August 2026', 'Gala & Verleihung: Oktober 2026'], - }, - { - icon: Shield, - title: 'Technische Anforderungen', - items: ['Formate: PDF, DOCX, XLSX', 'Max. Dateigröße: 30 MB pro Datei', 'Max. Gesamtgröße: 100 MB', 'Sprache: Deutsch'], - }, - ].map(({ icon: Icon, title, items }, i) => ( -
+ {requirementCards.map((card, i) => { + const Icon = requirementIcons[card.icon as keyof typeof requirementIcons] || FileText; + const fallbackCard = formularUploadContent.requirements.cards[i] || formularUploadContent.requirements.cards[0]; + return ( +
-

{title}

+

{fallbackText(card.title, fallbackCard.title)}

    - {items.map((item, j) => ( + {lines(card.items, fallbackCard.items).map((item, j) => (
  • {item} @@ -124,15 +135,16 @@ export default function FormularUpload() { ))}
- ))} + ); + })}
{/* ── UPLOAD-BEREICH ──────────────────────────────── */}
-

Einreichung

+

{fallbackText(upload.eyebrow, formularUploadContent.upload.eyebrow)}

- DATEIEN HOCHLADEN +

{submitted ? ( @@ -140,12 +152,12 @@ export default function FormularUpload() {
-

Einreichung erfolgreich

+

{fallbackText(success.heading, formularUploadContent.success.heading)}

- Ihre Unterlagen wurden übermittelt. Sie erhalten eine Bestätigung per E-Mail. Die Jury meldet sich bis August 2026. + {fallbackText(success.description, formularUploadContent.success.description)}

- - Zur Teilnahmeseite + + {fallbackText(success.linkLabel, formularUploadContent.success.linkLabel)}
) : ( @@ -170,13 +182,13 @@ export default function FormularUpload() { >

- Dateien hierher ziehen + {fallbackText(upload.dropTitle, formularUploadContent.upload.dropTitle)}

- oder klicken zum Auswählen + {fallbackText(upload.dropDescription, formularUploadContent.upload.dropDescription)}

- {['PDF', 'DOCX', 'XLSX', 'PPT'].map(ext => ( + {acceptedFormats.map(ext => ( {ext} ))}
@@ -193,7 +205,7 @@ export default function FormularUpload() { {formatBytes(f.size)} {f.status === 'uploading' &&
} {f.status !== 'uploading' && f.status !== 'done' && ( - )} @@ -208,10 +220,10 @@ export default function FormularUpload() {
- Hinweis + {fallbackText(upload.noteTitle, formularUploadContent.upload.noteTitle)}

- Stellen Sie sicher, dass alle Pflichtdokumente vollständig sind, bevor Sie einreichen. Unvollständige Bewerbungen können nicht berücksichtigt werden. + {fallbackText(upload.note, formularUploadContent.upload.note)}

@@ -229,11 +241,11 @@ export default function FormularUpload() { onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = files.length ? GOLD : 'rgba(239,191,4,0.3)'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }} > - {files.length ? `${files.length} Datei${files.length > 1 ? 'en' : ''} einreichen` : 'Keine Dateien ausgewählt'} + {files.length ? `${files.length} ${fallbackText(upload.selectedSubmitPrefix, formularUploadContent.upload.selectedSubmitPrefix)}${files.length > 1 ? fallbackText(upload.selectedSubmitPluralSuffix, formularUploadContent.upload.selectedSubmitPluralSuffix) : ''} ${fallbackText(upload.selectedSubmitAction, formularUploadContent.upload.selectedSubmitAction)}` : fallbackText(upload.emptySubmitLabel, formularUploadContent.upload.emptySubmitLabel)}

- Mit der Einreichung stimmen Sie der Verarbeitung Ihrer Daten gemäß unserer Datenschutzerklärung zu. + {fallbackText(upload.privacyNote, formularUploadContent.upload.privacyNote)}

@@ -245,18 +257,18 @@ export default function FormularUpload() { {/* ── CTA ─────────────────────────────────────────── */}
-

Noch Fragen zur Einreichung?

- - Kontakt aufnehmen +

{fallbackText(cta.eyebrow, formularUploadContent.cta.eyebrow)}

+ + {fallbackText(cta.contactLabel, formularUploadContent.cta.contactLabel)}
{ (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }} > - Zur Teilnahmeseite + {fallbackText(cta.buttonLabel, formularUploadContent.cta.buttonLabel)}