feat: manage formular upload page via payload

This commit is contained in:
syntaxbullet
2026-06-22 11:16:14 +02:00
parent 27b29a8a7a
commit a9283b2e46
7 changed files with 484 additions and 55 deletions

View File

@@ -20,6 +20,7 @@
"preload:about-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-about-page-cms.ts", "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: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: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: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: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", "preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",

View File

@@ -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),
],
},
],
},
]

View File

@@ -11,6 +11,7 @@ import { hero } from '@/heros/config'
import { aboutFields } from './aboutFields' import { aboutFields } from './aboutFields'
import { contactFields } from './contactFields' import { contactFields } from './contactFields'
import { datenschutzFields } from './datenschutzFields' import { datenschutzFields } from './datenschutzFields'
import { formularUploadFields } from './formularUploadFields'
import { homeFields } from './homeFields' import { homeFields } from './homeFields'
import { impressumFields } from './impressumFields' import { impressumFields } from './impressumFields'
import { participationFields } from './participationFields' 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' 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 }) => const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
isHomePage(undefined, siblingData) || isHomePage(undefined, siblingData) ||
isContactPage(undefined, siblingData) || isContactPage(undefined, siblingData) ||
@@ -101,7 +110,8 @@ const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: s
isImpressumPage(undefined, siblingData) || isImpressumPage(undefined, siblingData) ||
isDatenschutzPage(undefined, siblingData) || isDatenschutzPage(undefined, siblingData) ||
isPreistraegerIndexPage(undefined, siblingData) || isPreistraegerIndexPage(undefined, siblingData) ||
isPressIndexPage(undefined, siblingData) isPressIndexPage(undefined, siblingData) ||
isFormularUploadPage(undefined, siblingData)
export const Pages: CollectionConfig<'pages'> = { export const Pages: CollectionConfig<'pages'> = {
slug: 'pages', slug: 'pages',
@@ -225,6 +235,13 @@ export const Pages: CollectionConfig<'pages'> = {
fields: pressIndexFields, fields: pressIndexFields,
label: 'Press Index Page', label: 'Press Index Page',
}, },
{
admin: {
condition: (data) => isFormularUploadPage(undefined, data),
},
fields: formularUploadFields,
label: 'Formular Upload Page',
},
{ {
name: 'meta', name: 'meta',
label: 'SEO', label: 'SEO',

View File

@@ -1331,6 +1331,84 @@ export interface Page {
successMessage?: string | null; 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?: { meta?: {
title?: string | null; title?: string | null;
/** /**
@@ -3291,6 +3369,77 @@ export interface PagesSelect<T extends boolean = true> {
successMessage?: T; 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?: meta?:
| T | T
| { | {

View File

@@ -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)
})

View File

@@ -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',
},
}

View File

@@ -2,6 +2,8 @@ import React, { useState, useRef } from 'react';
import { Link } from '@/spa/router'; import { Link } from '@/spa/router';
import { Upload, FileText, CheckCircle, AlertCircle, X, ArrowRight, Clock, Shield, Info } from 'lucide-react'; import { Upload, FileText, CheckCircle, AlertCircle, X, ArrowRight, Clock, Shield, Info } from 'lucide-react';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { useCmsRoute } from '@/spa/cmsRoute';
import { formularUploadContent } from '@/spa/formularUploadContent';
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
@@ -12,6 +14,23 @@ const BORDER = '#D0D5DD';
const HELLBLAU = '#4A8FC9'; const HELLBLAU = '#4A8FC9';
type UploadFile = { name: string; size: number; status: 'ready' | 'uploading' | 'done' | 'error' }; type UploadFile = { name: string; size: number; status: 'ready' | 'uploading' | 'done' | 'error' };
type FormUploadCms = Partial<typeof formularUploadContent>;
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 = <T,>(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) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
}
function formatBytes(b: number) { function formatBytes(b: number) {
if (b < 1024) return b + ' B'; if (b < 1024) return b + ' B';
@@ -20,6 +39,16 @@ function formatBytes(b: number) {
} }
export default function FormularUpload() { 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<HeroStat>(hero.stats, formularUploadContent.hero.stats);
const requirementCards = fallbackArray<RequirementCard>(requirements.cards, formularUploadContent.requirements.cards);
const acceptedFormats = lines(upload.acceptedFormats, formularUploadContent.upload.acceptedFormats);
const [files, setFiles] = useState<UploadFile[]>([]); const [files, setFiles] = useState<UploadFile[]>([]);
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
@@ -53,28 +82,23 @@ export default function FormularUpload() {
<div style={{ maxWidth: 720, position: 'relative', zIndex: 1 }}> <div style={{ maxWidth: 720, position: 'relative', zIndex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.35em', color: HELLBLAU, display: 'block', marginBottom: 16 }}> <span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.35em', color: HELLBLAU, display: 'block', marginBottom: 16 }}>
BMP 2026 Bewerbungsportal {fallbackText(hero.eyebrow, formularUploadContent.hero.eyebrow)}
</span> </span>
<h1 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.6rem, 4vw, 3.6rem)' : 'clamp(2.2rem, 4vw, 3.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.0, margin: '0 0 20px' }}> <h1 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.6rem, 4vw, 3.6rem)' : 'clamp(2.2rem, 4vw, 3.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.0, margin: '0 0 20px' }}>
UNTERLAGEN<br />EINREICHEN <Lines text={fallbackText(hero.heading, formularUploadContent.hero.heading)} />
</h1> </h1>
<div style={{ width: 48, height: 3, background: GOLD, marginBottom: 24 }} /> <div style={{ width: 48, height: 3, background: GOLD, marginBottom: 24 }} />
<p style={{ fontFamily: FF, fontSize: 16, color: 'rgba(255,255,255,0.55)', lineHeight: 1.75, maxWidth: 520, margin: 0 }}> <p style={{ fontFamily: FF, fontSize: 16, color: 'rgba(255,255,255,0.55)', lineHeight: 1.75, maxWidth: 520, margin: 0 }}>
Laden Sie Ihre Bewerbungsunterlagen sicher hoch. Alle Einreichungen werden vertraulich behandelt und direkt an die Jury weitergeleitet. {fallbackText(hero.description, formularUploadContent.hero.description)}
</p> </p>
</div> </div>
{/* Stats row */} {/* Stats row */}
<div style={{ display: 'flex', flexWrap: isMobile ? 'wrap' : 'nowrap', gap: isMobile ? '24px 0' : 0, marginTop: 56, borderTop: '1px solid rgba(255,255,255,0.07)', paddingTop: 32, position: 'relative', zIndex: 1 }}> <div style={{ display: 'flex', flexWrap: isMobile ? 'wrap' : 'nowrap', gap: isMobile ? '24px 0' : 0, marginTop: 56, borderTop: '1px solid rgba(255,255,255,0.07)', paddingTop: 32, position: 'relative', zIndex: 1 }}>
{[ {heroStats.map((s, i) => (
{ 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) => (
<div key={i} style={{ flex: isMobile ? '0 0 50%' : 1, paddingRight: 32 }}> <div key={i} style={{ flex: isMobile ? '0 0 50%' : 1, paddingRight: 32 }}>
<div style={{ fontFamily: FF, fontSize: 22, fontWeight: 900, color: GOLD, lineHeight: 1 }}>{s.num}</div> <div style={{ fontFamily: FF, fontSize: 22, fontWeight: 900, color: GOLD, lineHeight: 1 }}>{fallbackText(s.num, formularUploadContent.hero.stats[i]?.num || '')}</div>
<div style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 4, textTransform: 'uppercase', letterSpacing: '0.1em' }}>{s.label}</div> <div style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 4, textTransform: 'uppercase', letterSpacing: '0.1em' }}>{fallbackText(s.label, formularUploadContent.hero.stats[i]?.label || '')}</div>
</div> </div>
))} ))}
</div> </div>
@@ -82,41 +106,28 @@ export default function FormularUpload() {
{/* Breadcrumb */} {/* Breadcrumb */}
<div style={{ background: '#fff', borderBottom: `1px solid ${BORDER}`, padding: isMobile ? '16px 24px' : '16px 80px' }}> <div style={{ background: '#fff', borderBottom: `1px solid ${BORDER}`, padding: isMobile ? '16px 24px' : '16px 80px' }}>
<span style={{ fontFamily: FF, fontSize: 12, color: HELLBLAU, fontWeight: 700, letterSpacing: '0.04em' }}>Formular hochladen</span> <span style={{ fontFamily: FF, fontSize: 12, color: HELLBLAU, fontWeight: 700, letterSpacing: '0.04em' }}>{fallbackText(breadcrumb.label, formularUploadContent.breadcrumb.label)}</span>
</div> </div>
{/* ── ERKLÄRUNG ───────────────────────────────────── */} {/* ── ERKLÄRUNG ───────────────────────────────────── */}
<div style={{ background: GRAU, padding: isMobile ? '48px 24px' : '64px 80px' }}> <div style={{ background: GRAU, padding: isMobile ? '48px 24px' : '64px 80px' }}>
<p style={{ fontFamily: FF, fontSize: 11, color: '#888', textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 8 }}>Was wird benötigt?</p> <p style={{ fontFamily: FF, fontSize: 11, color: '#888', textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 8 }}>{fallbackText(requirements.eyebrow, formularUploadContent.requirements.eyebrow)}</p>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2.2vw, 2rem)', fontWeight: 900, color: DARK, textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 40 }}> <h2 style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2.2vw, 2rem)', fontWeight: 900, color: DARK, textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 40 }}>
ANFORDERUNGEN & FRISTEN <Lines text={fallbackText(requirements.heading, formularUploadContent.requirements.heading)} />
</h2> </h2>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)', gap: 1, background: BORDER }}> <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)', gap: 1, background: BORDER }}>
{[ {requirementCards.map((card, i) => {
{ const Icon = requirementIcons[card.icon as keyof typeof requirementIcons] || FileText;
icon: FileText, const fallbackCard = formularUploadContent.requirements.cards[i] || formularUploadContent.requirements.cards[0];
title: 'Pflichtdokumente', return (
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)'], <div key={`${fallbackText(card.title, fallbackCard.title)}-${i}`} style={{ background: '#fff', padding: '36px 32px' }}>
},
{
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) => (
<div key={i} style={{ background: '#fff', padding: '36px 32px' }}>
<div style={{ width: 40, height: 40, background: DARK, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}> <div style={{ width: 40, height: 40, background: DARK, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}>
<Icon size={18} color={GOLD} /> <Icon size={18} color={GOLD} />
</div> </div>
<h3 style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, color: DARK, textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 16 }}>{title}</h3> <h3 style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, color: DARK, textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 16 }}>{fallbackText(card.title, fallbackCard.title)}</h3>
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 10 }}> <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
{items.map((item, j) => ( {lines(card.items, fallbackCard.items).map((item, j) => (
<li key={j} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, fontFamily: FF, fontSize: 14, color: '#555', lineHeight: 1.5 }}> <li key={j} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, fontFamily: FF, fontSize: 14, color: '#555', lineHeight: 1.5 }}>
<span style={{ color: GOLD, marginTop: 2, flexShrink: 0 }}></span> <span style={{ color: GOLD, marginTop: 2, flexShrink: 0 }}></span>
{item} {item}
@@ -124,15 +135,16 @@ export default function FormularUpload() {
))} ))}
</ul> </ul>
</div> </div>
))} );
})}
</div> </div>
</div> </div>
{/* ── UPLOAD-BEREICH ──────────────────────────────── */} {/* ── UPLOAD-BEREICH ──────────────────────────────── */}
<div style={{ background: NAVY, padding: isMobile ? '48px 24px' : '72px 80px' }}> <div style={{ background: NAVY, padding: isMobile ? '48px 24px' : '72px 80px' }}>
<p style={{ fontFamily: FF, fontSize: 11, color: HELLBLAU, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.25em', marginBottom: 8 }}>Einreichung</p> <p style={{ fontFamily: FF, fontSize: 11, color: HELLBLAU, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.25em', marginBottom: 8 }}>{fallbackText(upload.eyebrow, formularUploadContent.upload.eyebrow)}</p>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2.2vw, 2rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 40 }}> <h2 style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2.2vw, 2rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 40 }}>
DATEIEN HOCHLADEN <Lines text={fallbackText(upload.heading, formularUploadContent.upload.heading)} />
</h2> </h2>
{submitted ? ( {submitted ? (
@@ -140,12 +152,12 @@ export default function FormularUpload() {
<div style={{ width: 64, height: 64, background: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px' }}> <div style={{ width: 64, height: 64, background: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px' }}>
<CheckCircle size={28} color={DARK} /> <CheckCircle size={28} color={DARK} />
</div> </div>
<h3 style={{ fontFamily: FF, fontSize: 22, fontWeight: 900, color: '#fff', textTransform: 'uppercase', marginBottom: 12 }}>Einreichung erfolgreich</h3> <h3 style={{ fontFamily: FF, fontSize: 22, fontWeight: 900, color: '#fff', textTransform: 'uppercase', marginBottom: 12 }}>{fallbackText(success.heading, formularUploadContent.success.heading)}</h3>
<p style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)', maxWidth: 440, margin: '0 auto 32px', lineHeight: 1.7 }}> <p style={{ fontFamily: FF, fontSize: 15, color: 'rgba(255,255,255,0.45)', maxWidth: 440, margin: '0 auto 32px', lineHeight: 1.7 }}>
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)}
</p> </p>
<Link to="/teilnahme" style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: DARK, background: GOLD, padding: '14px 32px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8 }}> <Link to={fallbackText(success.linkUrl, formularUploadContent.success.linkUrl)} style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: DARK, background: GOLD, padding: '14px 32px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8 }}>
Zur Teilnahmeseite <ArrowRight size={13} /> {fallbackText(success.linkLabel, formularUploadContent.success.linkLabel)} <ArrowRight size={13} />
</Link> </Link>
</div> </div>
) : ( ) : (
@@ -170,13 +182,13 @@ export default function FormularUpload() {
> >
<Upload size={32} color={dragging ? GOLD : 'rgba(255,255,255,0.3)'} style={{ margin: '0 auto 16px' }} /> <Upload size={32} color={dragging ? GOLD : 'rgba(255,255,255,0.3)'} style={{ margin: '0 auto 16px' }} />
<p style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: '#fff', marginBottom: 8 }}> <p style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: '#fff', marginBottom: 8 }}>
Dateien hierher ziehen {fallbackText(upload.dropTitle, formularUploadContent.upload.dropTitle)}
</p> </p>
<p style={{ fontFamily: FF, fontSize: 12, color: 'rgba(255,255,255,0.35)', marginBottom: 20 }}> <p style={{ fontFamily: FF, fontSize: 12, color: 'rgba(255,255,255,0.35)', marginBottom: 20 }}>
oder klicken zum Auswählen {fallbackText(upload.dropDescription, formularUploadContent.upload.dropDescription)}
</p> </p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}>
{['PDF', 'DOCX', 'XLSX', 'PPT'].map(ext => ( {acceptedFormats.map(ext => (
<span key={ext} style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: GOLD, border: `1px solid rgba(239,191,4,0.4)`, padding: '3px 10px' }}>{ext}</span> <span key={ext} style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: GOLD, border: `1px solid rgba(239,191,4,0.4)`, padding: '3px 10px' }}>{ext}</span>
))} ))}
</div> </div>
@@ -193,7 +205,7 @@ export default function FormularUpload() {
<span style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.3)' }}>{formatBytes(f.size)}</span> <span style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.3)' }}>{formatBytes(f.size)}</span>
{f.status === 'uploading' && <div style={{ width: 14, height: 14, border: '2px solid rgba(255,255,255,0.2)', borderTopColor: GOLD, borderRadius: '50%', animation: 'spin 0.8s linear infinite' }} />} {f.status === 'uploading' && <div style={{ width: 14, height: 14, border: '2px solid rgba(255,255,255,0.2)', borderTopColor: GOLD, borderRadius: '50%', animation: 'spin 0.8s linear infinite' }} />}
{f.status !== 'uploading' && f.status !== 'done' && ( {f.status !== 'uploading' && f.status !== 'done' && (
<button onClick={() => remove(i)} aria-label="Datei entfernen" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'rgba(255,255,255,0.3)', padding: isMobile ? 12 : 0, margin: isMobile ? -12 : 0, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <button onClick={() => remove(i)} aria-label={fallbackText(upload.fileRemoveLabel, formularUploadContent.upload.fileRemoveLabel)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'rgba(255,255,255,0.3)', padding: isMobile ? 12 : 0, margin: isMobile ? -12 : 0, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<X size={isMobile ? 18 : 14} /> <X size={isMobile ? 18 : 14} />
</button> </button>
)} )}
@@ -208,10 +220,10 @@ export default function FormularUpload() {
<div style={{ padding: '24px', background: 'rgba(255,255,255,0.04)', borderTop: `2px solid ${GOLD}` }}> <div style={{ padding: '24px', background: 'rgba(255,255,255,0.04)', borderTop: `2px solid ${GOLD}` }}>
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 16 }}> <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 16 }}>
<Info size={14} color={GOLD} style={{ marginTop: 2, flexShrink: 0 }} /> <Info size={14} color={GOLD} style={{ marginTop: 2, flexShrink: 0 }} />
<span style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Hinweis</span> <span style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.06em' }}>{fallbackText(upload.noteTitle, formularUploadContent.upload.noteTitle)}</span>
</div> </div>
<p style={{ fontFamily: FF, fontSize: 12, color: 'rgba(255,255,255,0.4)', lineHeight: 1.7, margin: 0 }}> <p style={{ fontFamily: FF, fontSize: 12, color: 'rgba(255,255,255,0.4)', lineHeight: 1.7, margin: 0 }}>
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)}
</p> </p>
</div> </div>
@@ -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'; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = files.length ? GOLD : 'rgba(239,191,4,0.3)'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
> >
<Upload size={14} /> <Upload size={14} />
{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)}
</button> </button>
<p style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.2)', lineHeight: 1.6, margin: 0 }}> <p style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.2)', lineHeight: 1.6, margin: 0 }}>
Mit der Einreichung stimmen Sie der Verarbeitung Ihrer Daten gemäß unserer Datenschutzerklärung zu. {fallbackText(upload.privacyNote, formularUploadContent.upload.privacyNote)}
</p> </p>
</div> </div>
@@ -245,18 +257,18 @@ export default function FormularUpload() {
{/* ── CTA ─────────────────────────────────────────── */} {/* ── CTA ─────────────────────────────────────────── */}
<div style={{ padding: isMobile ? '48px 24px' : '64px 80px', borderTop: `1px solid ${BORDER}`, display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 24 }}> <div style={{ padding: isMobile ? '48px 24px' : '64px 80px', borderTop: `1px solid ${BORDER}`, display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 24 }}>
<div> <div>
<p style={{ fontFamily: FF, fontSize: 12, color: '#888', marginBottom: 6 }}>Noch Fragen zur Einreichung?</p> <p style={{ fontFamily: FF, fontSize: 12, color: '#888', marginBottom: 6 }}>{fallbackText(cta.eyebrow, formularUploadContent.cta.eyebrow)}</p>
<Link to="/kontakt" style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: NAVY, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6 }}> <Link to={fallbackText(cta.contactUrl, formularUploadContent.cta.contactUrl)} style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: NAVY, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
Kontakt aufnehmen <ArrowRight size={14} /> {fallbackText(cta.contactLabel, formularUploadContent.cta.contactLabel)} <ArrowRight size={14} />
</Link> </Link>
</div> </div>
<Link <Link
to="/teilnahme" to={fallbackText(cta.buttonUrl, formularUploadContent.cta.buttonUrl)}
style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: DARK, background: GOLD, padding: '14px 32px', textDecoration: 'none', display: isMobile ? 'flex' : 'inline-flex', width: isMobile ? '100%' : 'auto', justifyContent: isMobile ? 'center' : undefined, alignItems: 'center', gap: 8, transition: 'background 0.15s, box-shadow 0.2s' }} style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: DARK, background: GOLD, padding: '14px 32px', textDecoration: 'none', display: isMobile ? 'flex' : 'inline-flex', width: isMobile ? '100%' : 'auto', justifyContent: isMobile ? 'center' : undefined, alignItems: 'center', gap: 8, transition: 'background 0.15s, box-shadow 0.2s' }}
onMouseEnter={e => { (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)'; }} onMouseEnter={e => { (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'; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
> >
Zur Teilnahmeseite <ArrowRight size={13} /> {fallbackText(cta.buttonLabel, formularUploadContent.cta.buttonLabel)} <ArrowRight size={13} />
</Link> </Link>
</div> </div>