Files
bmp-website-2026/src/spa/pages/FormularUpload.tsx
2026-06-16 13:02:47 +02:00

266 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}