chore: migrate to payload / nextjs

This commit is contained in:
syntaxbullet
2026-06-16 13:02:47 +02:00
parent aff090b693
commit 6b395e5735
257 changed files with 33616 additions and 1451 deletions

View File

@@ -0,0 +1,265 @@
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';
const FF = '"IBM Plex Sans", sans-serif';
const GOLD = '#EFBF04';
const NAVY = '#111D55';
const DARK = '#101828';
const GRAU = '#E4E2E3';
const BORDER = '#D0D5DD';
const HELLBLAU = '#4A8FC9';
type UploadFile = { name: string; size: number; status: 'ready' | 'uploading' | 'done' | 'error' };
function formatBytes(b: number) {
if (b < 1024) return b + ' B';
if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB';
return (b / (1024 * 1024)).toFixed(1) + ' MB';
}
export default function FormularUpload() {
const [files, setFiles] = useState<UploadFile[]>([]);
const [dragging, setDragging] = useState(false);
const [submitted, setSubmitted] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const isMobile = useIsMobile();
const addFiles = (fileList: FileList) => {
const newFiles: UploadFile[] = Array.from(fileList).map(f => ({
name: f.name, size: f.size, status: 'ready',
}));
setFiles(prev => [...prev, ...newFiles]);
};
const remove = (i: number) => setFiles(prev => prev.filter((_, idx) => idx !== i));
const handleSubmit = () => {
if (!files.length) return;
setFiles(prev => prev.map(f => ({ ...f, status: 'uploading' })));
setTimeout(() => {
setFiles(prev => prev.map(f => ({ ...f, status: 'done' })));
setTimeout(() => setSubmitted(true), 600);
}, 1800);
};
return (
<div style={{ background: '#fff', minHeight: '100vh' }}>
{/* ── HERO ────────────────────────────────────────── */}
<div style={{ background: DARK, position: 'relative', overflow: 'hidden', padding: isMobile ? '64px 24px 48px' : '100px 80px 72px' }}>
<div style={{ height: 3, background: `linear-gradient(to right, ${GOLD}, transparent)`, position: 'absolute', top: 0, left: 0, right: 0 }} />
<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
</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
</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.
</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) => (
<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>
))}
</div>
</div>
{/* 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>
</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>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2.2vw, 2rem)', fontWeight: 900, color: DARK, textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 40 }}>
ANFORDERUNGEN & FRISTEN
</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' }}>
<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>
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
{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}
</li>
))}
</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>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2.2vw, 2rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', marginBottom: 40 }}>
DATEIEN HOCHLADEN
</h2>
{submitted ? (
<div style={{ textAlign: 'center', padding: '64px 0' }}>
<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>
<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.
</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>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 360px', gap: 32, alignItems: 'start' }}>
{/* Drop zone */}
<div>
<div
onDragOver={e => { e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
onDrop={e => { e.preventDefault(); setDragging(false); if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files); }}
onClick={() => inputRef.current?.click()}
style={{
border: `2px dashed ${dragging ? GOLD : 'rgba(255,255,255,0.2)'}`,
padding: '52px 32px',
textAlign: 'center',
cursor: 'pointer',
background: dragging ? 'rgba(239,191,4,0.05)' : 'rgba(255,255,255,0.03)',
transition: 'all 0.2s',
marginBottom: 20,
}}
>
<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
</p>
<p style={{ fontFamily: FF, fontSize: 12, color: 'rgba(255,255,255,0.35)', marginBottom: 20 }}>
oder klicken zum Auswählen
</p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}>
{['PDF', 'DOCX', 'XLSX', 'PPT'].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>
<input ref={inputRef} type="file" multiple accept=".pdf,.docx,.xlsx,.pptx" style={{ display: 'none' }} onChange={e => { if (e.target.files) addFiles(e.target.files); }} />
</div>
{/* File list */}
{files.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{files.map((f, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', background: 'rgba(255,255,255,0.05)', borderLeft: `3px solid ${f.status === 'done' ? GOLD : f.status === 'error' ? '#f87171' : 'rgba(255,255,255,0.15)'}` }}>
{f.status === 'done' ? <CheckCircle size={14} color={GOLD} /> : f.status === 'error' ? <AlertCircle size={14} color="#f87171" /> : <FileText size={14} color="rgba(255,255,255,0.4)" />}
<span style={{ fontFamily: FF, fontSize: 12, color: '#fff', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</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' && 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 }}>
<X size={isMobile ? 18 : 14} />
</button>
)}
</div>
))}
</div>
)}
</div>
{/* Sidebar info + submit */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<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>
</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.
</p>
</div>
<button
onClick={handleSubmit}
disabled={files.length === 0}
style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase',
letterSpacing: '0.12em', color: DARK, background: files.length ? GOLD : 'rgba(239,191,4,0.3)',
border: 'none', cursor: files.length ? 'pointer' : 'not-allowed',
padding: '16px 28px', display: 'flex', alignItems: 'center', justifyContent: 'center',
gap: 8, transition: 'background 0.15s, box-shadow 0.2s', width: '100%',
}}
onMouseEnter={e => { if (files.length) { (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 = 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'}
</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.
</p>
</div>
</div>
)}
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</div>
{/* ── 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} />
</Link>
</div>
<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: 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} />
</Link>
</div>
</div>
);
}