feat: manage formular upload page via payload
This commit is contained in:
140
src/collections/Pages/formularUploadFields.ts
Normal file
140
src/collections/Pages/formularUploadFields.ts
Normal 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),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -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',
|
||||
|
||||
@@ -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<T extends boolean = true> {
|
||||
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
|
||||
| {
|
||||
|
||||
41
src/scripts/preload-formular-upload-page-cms.ts
Normal file
41
src/scripts/preload-formular-upload-page-cms.ts
Normal 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)
|
||||
})
|
||||
69
src/spa/formularUploadContent.ts
Normal file
69
src/spa/formularUploadContent.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
@@ -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<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) {
|
||||
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<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 [dragging, setDragging] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
@@ -53,28 +82,23 @@ export default function FormularUpload() {
|
||||
|
||||
<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 }}>
|
||||
BMP 2026 – Bewerbungsportal
|
||||
{fallbackText(hero.eyebrow, formularUploadContent.hero.eyebrow)}
|
||||
</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' }}>
|
||||
UNTERLAGEN<br />EINREICHEN
|
||||
<Lines text={fallbackText(hero.heading, formularUploadContent.hero.heading)} />
|
||||
</h1>
|
||||
<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 }}>
|
||||
Laden Sie Ihre Bewerbungsunterlagen sicher hoch. Alle Einreichungen werden vertraulich behandelt und direkt an die Jury weitergeleitet.
|
||||
{fallbackText(hero.description, formularUploadContent.hero.description)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 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 }}>
|
||||
{[
|
||||
{ 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) => (
|
||||
<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: 11, color: 'rgba(255,255,255,0.35)', marginTop: 4, textTransform: 'uppercase', letterSpacing: '0.1em' }}>{s.label}</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' }}>{fallbackText(s.label, formularUploadContent.hero.stats[i]?.label || '')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -82,41 +106,28 @@ export default function FormularUpload() {
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<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>
|
||||
|
||||
{/* ── ERKLÄRUNG ───────────────────────────────────── */}
|
||||
<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 }}>
|
||||
ANFORDERUNGEN & FRISTEN
|
||||
<Lines text={fallbackText(requirements.heading, formularUploadContent.requirements.heading)} />
|
||||
</h2>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)', gap: 1, background: BORDER }}>
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<div key={i} style={{ background: '#fff', padding: '36px 32px' }}>
|
||||
{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 (
|
||||
<div key={`${fallbackText(card.title, fallbackCard.title)}-${i}`} style={{ background: '#fff', padding: '36px 32px' }}>
|
||||
<div style={{ width: 40, height: 40, background: DARK, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}>
|
||||
<Icon size={18} color={GOLD} />
|
||||
</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 }}>
|
||||
{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 }}>
|
||||
<span style={{ color: GOLD, marginTop: 2, flexShrink: 0 }}>–</span>
|
||||
{item}
|
||||
@@ -124,15 +135,16 @@ export default function FormularUpload() {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── UPLOAD-BEREICH ──────────────────────────────── */}
|
||||
<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 }}>
|
||||
DATEIEN HOCHLADEN
|
||||
<Lines text={fallbackText(upload.heading, formularUploadContent.upload.heading)} />
|
||||
</h2>
|
||||
|
||||
{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' }}>
|
||||
<CheckCircle size={28} color={DARK} />
|
||||
</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 }}>
|
||||
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>
|
||||
<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 }}>
|
||||
Zur Teilnahmeseite <ArrowRight size={13} />
|
||||
<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 }}>
|
||||
{fallbackText(success.linkLabel, formularUploadContent.success.linkLabel)} <ArrowRight size={13} />
|
||||
</Link>
|
||||
</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' }} />
|
||||
<p style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: '#fff', marginBottom: 8 }}>
|
||||
Dateien hierher ziehen
|
||||
{fallbackText(upload.dropTitle, formularUploadContent.upload.dropTitle)}
|
||||
</p>
|
||||
<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>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
{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' && (
|
||||
<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} />
|
||||
</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={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 16 }}>
|
||||
<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>
|
||||
<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>
|
||||
</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'; }}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -245,18 +257,18 @@ export default function FormularUpload() {
|
||||
{/* ── 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>
|
||||
<p style={{ fontFamily: FF, fontSize: 12, color: '#888', marginBottom: 6 }}>Noch Fragen zur Einreichung?</p>
|
||||
<Link to="/kontakt" style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: NAVY, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
Kontakt aufnehmen <ArrowRight size={14} />
|
||||
<p style={{ fontFamily: FF, fontSize: 12, color: '#888', marginBottom: 6 }}>{fallbackText(cta.eyebrow, formularUploadContent.cta.eyebrow)}</p>
|
||||
<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 }}>
|
||||
{fallbackText(cta.contactLabel, formularUploadContent.cta.contactLabel)} <ArrowRight size={14} />
|
||||
</Link>
|
||||
</div>
|
||||
<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' }}
|
||||
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'; }}
|
||||
>
|
||||
Zur Teilnahmeseite <ArrowRight size={13} />
|
||||
{fallbackText(cta.buttonLabel, formularUploadContent.cta.buttonLabel)} <ArrowRight size={13} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user