feat: manage formular upload page via payload
This commit is contained in:
@@ -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