Initial commit — BMP Website 2026

Vollständiges Redesign mit Gold-Formularen, BMP-Logo, multi-step Forms (Bewerbung, Kontakt, Sponsoring), Netlify-Deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Pachon Araque
2026-05-29 09:01:20 +02:00
commit fea7998835
8482 changed files with 1259493 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import React, { useState } from 'react';
const SESSION_KEY = 'bmp_auth';
const VALID_USER = 'newedgebrand';
const VALID_PASS = 'n3wedg3br4nd2o26';
export function isAuthenticated(): boolean {
return sessionStorage.getItem(SESSION_KEY) === '1';
}
const FF = '"IBM Plex Sans", sans-serif';
export default function LoginGate({ children }: { children: React.ReactNode }) {
const [authed, setAuthed] = useState(isAuthenticated);
const [user, setUser] = useState('');
const [pass, setPass] = useState('');
const [error, setError] = useState('');
const [shake, setShake] = useState(false);
if (authed) return <>{children}</>;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (user === VALID_USER && pass === VALID_PASS) {
sessionStorage.setItem(SESSION_KEY, '1');
setAuthed(true);
} else {
setError('Ungültige Zugangsdaten.');
setShake(true);
setTimeout(() => setShake(false), 600);
}
};
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 9999,
background: '#111D55',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
fontFamily: FF,
}}>
{/* Subtle grid overlay */}
<div style={{
position: 'absolute', inset: 0, opacity: 0.04,
backgroundImage: 'linear-gradient(#EFBF04 1px, transparent 1px), linear-gradient(90deg, #EFBF04 1px, transparent 1px)',
backgroundSize: '60px 60px',
pointerEvents: 'none',
}} />
{/* Logo mark */}
<div style={{ position: 'relative', zIndex: 1, textAlign: 'center', marginBottom: 48 }}>
<div style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 64, height: 64,
border: '2px solid #EFBF04',
marginBottom: 20,
}}>
<span style={{ fontFamily: FF, fontWeight: 700, fontSize: 22, color: '#EFBF04', letterSpacing: '0.05em' }}>BMP</span>
</div>
<div style={{ color: 'rgba(255,255,255,0.35)', fontSize: 10, letterSpacing: '0.35em', textTransform: 'uppercase', fontWeight: 700 }}>
Bayerischer Mittelstandspreis
</div>
</div>
{/* Form card */}
<form
onSubmit={handleSubmit}
style={{
position: 'relative', zIndex: 1,
width: '100%', maxWidth: 380,
padding: '40px 40px 36px',
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.1)',
animation: shake ? 'bmp-shake 0.5s ease' : undefined,
}}
>
<div style={{ marginBottom: 28 }}>
<div style={{ color: 'white', fontSize: 18, fontWeight: 300, marginBottom: 6, letterSpacing: '0.01em' }}>
Zugang
</div>
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: 12 }}>
Bitte melden Sie sich an, um fortzufahren.
</div>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: 'rgba(255,255,255,0.5)', letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 8 }}>
Benutzername
</label>
<input
type="text"
value={user}
onChange={e => { setUser(e.target.value); setError(''); }}
autoComplete="username"
style={{
width: '100%', padding: '12px 14px',
background: 'rgba(255,255,255,0.06)',
border: `1px solid ${error ? '#e53e3e' : 'rgba(255,255,255,0.15)'}`,
color: 'white', fontSize: 14, outline: 'none',
fontFamily: FF, boxSizing: 'border-box',
}}
/>
</div>
<div style={{ marginBottom: 28 }}>
<label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: 'rgba(255,255,255,0.5)', letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 8 }}>
Passwort
</label>
<input
type="password"
value={pass}
onChange={e => { setPass(e.target.value); setError(''); }}
autoComplete="current-password"
style={{
width: '100%', padding: '12px 14px',
background: 'rgba(255,255,255,0.06)',
border: `1px solid ${error ? '#e53e3e' : 'rgba(255,255,255,0.15)'}`,
color: 'white', fontSize: 14, outline: 'none',
fontFamily: FF, boxSizing: 'border-box',
}}
/>
</div>
{error && (
<div style={{ marginBottom: 16, color: '#fc8181', fontSize: 12 }}>{error}</div>
)}
<button
type="submit"
style={{
width: '100%', padding: '14px',
background: '#EFBF04', border: 'none', cursor: 'pointer',
color: '#111D55', fontFamily: FF, fontSize: 12, fontWeight: 700,
letterSpacing: '0.2em', textTransform: 'uppercase',
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 = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
>
Anmelden
</button>
</form>
<div style={{ position: 'relative', zIndex: 1, marginTop: 32, color: 'rgba(255,255,255,0.2)', fontSize: 10, letterSpacing: '0.2em' }}>
© {new Date().getFullYear()} New Edge Brand
</div>
<style>{`
@keyframes bmp-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-6px); }
80% { transform: translateX(6px); }
}
`}</style>
</div>
);
}

View File

@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { ChevronRight } from 'lucide-react';
import MunichSkylineBg from '@/components/ui/munich-skyline-bg';
const FF = '"IBM Plex Sans", sans-serif';
export const TESTIMONIALS = [
{ img: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=600", name: "Dr. Maria Huber", role: "Geschäftsführerin", company: "TechVision GmbH", year: "2025", quote: "Der BMP hat uns neue Türen geöffnet. Die Auszeichnung ist ein wichtiges Qualitätssiegel für unser gesamtes Team." },
{ img: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=600", name: "Hannes Weber", role: "Inhaber & CEO", company: "Weber Präzision GmbH", year: "2023", quote: "Der Gewinn war ein Moment des Stolzes für die gesamte Belegschaft. Die höchste Anerkennung für unsere tägliche Arbeit." },
{ img: "https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&q=80&w=600", name: "Lisa Berger", role: "Gründerin & CEO", company: "EcoTech Bavaria", year: "2024", quote: "Das Netzwerk, das wir durch den BMP gewonnen haben, ist unbezahlbar. Wir fanden Partner, die unsere Werte teilen." },
{ img: "https://images.unsplash.com/photo-1560250097-0b93528c311a?auto=format&fit=crop&q=80&w=600", name: "Dr. Michael Schuh", role: "Vorstand", company: "Bayerischer Maschinenbau", year: "2024", quote: "Der Preis hat unsere Sichtbarkeit weit über die bayerischen Grenzen hinaus gestärkt. Ein echtes Premium-Siegel." },
{ img: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=600", name: "Anna Kellner", role: "Mitgründerin", company: "AlpinTech Solutions", year: "2024", quote: "Der BMP hat unsere Employer Brand transformiert. Wir gewinnen heute die besten Talente, weil wir Preisträger sind." },
];
export default function TestimonialsSection() {
const [active, setActive] = useState(0);
const t = TESTIMONIALS[active];
const prev = () => setActive(i => (i - 1 + TESTIMONIALS.length) % TESTIMONIALS.length);
const next = () => setActive(i => (i + 1) % TESTIMONIALS.length);
return (
<section style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
<MunichSkylineBg />
<div style={{ display: 'grid', gridTemplateColumns: '42% 58%', height: 560, position: 'relative', zIndex: 1 }}>
{/* Left — portrait photo */}
<div style={{ position: 'relative', overflow: 'hidden' }}>
<img
key={t.img}
src={t.img}
alt={t.name}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top', display: 'block', transition: 'opacity 0.4s' }}
/>
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to right, transparent 60%, #fff 100%)' }} />
</div>
{/* Right — quote */}
<div style={{ padding: '80px 72px 80px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.3em', fontWeight: 700, display: 'block', marginBottom: 32 }}>Stimmen der Preisträger</span>
<p style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2.2vw, 2rem)', fontWeight: 300, color: '#101828', lineHeight: 1.55, margin: '0 0 40px' }}>
{t.quote}
</p>
<div style={{ width: 40, height: 2, background: '#EFBF04', marginBottom: 20 }} />
<div>
<div style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: '#101828', marginBottom: 4 }}>{t.name}</div>
<div style={{ fontFamily: FF, fontSize: 12, color: 'rgba(16,24,40,0.5)' }}>{t.role} · {t.company}</div>
<div style={{ fontFamily: FF, fontSize: 11, fontWeight: 700, color: '#4A8FC9', letterSpacing: '0.1em', textTransform: 'uppercase', marginTop: 4 }}>Preisträger {t.year}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 40 }}>
<button onClick={prev} style={{ width: 40, height: 40, border: '1.5px solid #D0D5DD', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.15s' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#111D55'; (e.currentTarget as HTMLElement).style.background = '#111D55'; (e.currentTarget as HTMLElement).style.color = '#fff'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = '#D0D5DD'; (e.currentTarget as HTMLElement).style.background = 'transparent'; (e.currentTarget as HTMLElement).style.color = 'inherit'; }}
>
<ChevronRight size={16} style={{ transform: 'rotate(180deg)' }} />
</button>
<button onClick={next} style={{ width: 40, height: 40, border: '1.5px solid #111D55', background: '#111D55', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.15s', color: '#fff' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#111D55'; (e.currentTarget as HTMLElement).style.borderColor = '#111D55'; }}
>
<ChevronRight size={16} />
</button>
<div style={{ display: 'flex', gap: 6, marginLeft: 8 }}>
{TESTIMONIALS.map((_, i) => (
<button key={i} onClick={() => setActive(i)} style={{ width: i === active ? 20 : 6, height: 6, background: i === active ? '#EFBF04' : '#D0D5DD', border: 'none', cursor: 'pointer', padding: 0, transition: 'all 0.2s' }} />
))}
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,126 @@
import React from 'react';
import { FileText, Search, UserPlus, Trophy } from 'lucide-react';
import {
ContainerScroll,
ContainerSticky,
ProcessCard,
ProcessCardBody,
ProcessCardTitle,
} from '@/components/ui/process-timeline';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
const STEPS = [
{ step: '01', title: 'Online-Einreichung', desc: 'Bewerbungsformular vollständig ausfüllen. Die Teilnahme ist kostenfrei und ohne bürokratischen Aufwand möglich.', Icon: FileText, date: 'Bis 30. Juni 2026' },
{ step: '02', title: 'Formale Vorprüfung', desc: 'Das Gremium sichtet alle Unterlagen auf Vollständigkeit, KMU-Konformität und regionale Zugehörigkeit im Freistaat.', Icon: Search, date: 'Juli 2026' },
{ step: '03', title: 'Audit & Jury-Sitzung',desc: 'Die besten Unternehmen werden durch unabhängige Experten-Audits vor Ort evaluiert und in der Jury-Sitzung bewertet.', Icon: UserPlus, date: 'Aug – Sep 2026' },
{ step: '04', title: 'Gala-Preisverleihung',desc: 'Die Gewinner werden im Rahmen der feierlichen Gala in München vor geladenen Gästen aus Wirtschaft und Politik geehrt.', Icon: Trophy, date: 'Oktober 2026' },
];
export default function WegZurAuszeichnungSection() {
return (
<ContainerScroll id="schritte" className="h-[380vh]" style={{ background: NAVY }}>
<ContainerSticky className="top-0 h-screen flex flex-col">
{/* Section header */}
<div style={{ padding: '56px 80px 40px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40, alignItems: 'flex-end', borderBottom: '1px solid rgba(255,255,255,0.07)', flexShrink: 0 }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 14 }}>Schritt für Schritt</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.8rem, 3vw, 2.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0 }}>
IHR WEG ZUR<br />AUSZEICHNUNG.
</h2>
</div>
<p style={{ fontFamily: FB, fontSize: 14, color: 'rgba(255,255,255,0.65)', lineHeight: 1.8, margin: 0 }}>
Von der Einreichung bis zur Gala — vier klar definierte Schritte auf dem Weg zur höchsten Auszeichnung des bayerischen Mittelstands.
</p>
</div>
{/* Scroll hint — prominent */}
<div style={{ padding: '20px 80px', display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0 }}>
{/* Animated chevron stack */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{[0, 1, 2].map(i => (
<svg
key={i}
width="12" height="7" viewBox="0 0 12 7" fill="none"
style={{ opacity: 1 - i * 0.3, animation: `scrollBounce 1.6s ease-in-out ${i * 0.18}s infinite` }}
>
<polyline points="1,1 6,6 11,1" stroke={GOLD} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
))}
</div>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)' }}>
Scrollen zum Erkunden
</span>
{/* Progress bar */}
<div style={{ flex: 1, height: 1, background: 'rgba(255,255,255,0.07)', position: 'relative', overflow: 'hidden', maxWidth: 200 }}>
<div style={{ position: 'absolute', left: 0, top: 0, height: '100%', width: '25%', background: GOLD, opacity: 0.6 }} />
</div>
<span style={{ fontFamily: FF, fontSize: 9, color: 'rgba(255,255,255,0.50)', letterSpacing: '0.1em' }}>01 / 04</span>
</div>
{/* Cards strip */}
<div className="flex flex-nowrap flex-1 items-stretch px-20 pb-16 gap-0">
{STEPS.map((item, index) => {
const Icon = item.Icon;
return (
<ProcessCard
key={item.step}
itemsLength={4}
index={index}
variant="bmp"
className="min-w-[70%] max-w-[70%] flex-shrink-0"
>
{/* Left accent strip with step number */}
<ProcessCardTitle className="border-r border-[rgba(239,191,4,0.2)] flex flex-col items-center justify-between py-8 px-5 min-w-[80px]">
<div style={{ width: 40, height: 40, border: '1px solid rgba(239,191,4,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon size={16} style={{ color: GOLD }} strokeWidth={1.5} />
</div>
<span style={{ fontFamily: FF, fontSize: 11, fontWeight: 900, color: 'rgba(239,191,4,0.4)', letterSpacing: '0.1em', writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>
{item.step}
</span>
</ProcessCardTitle>
{/* Card body */}
<ProcessCardBody className="flex flex-col justify-between py-10 px-10 gap-6">
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.22em', textTransform: 'uppercase' }}>
Schritt {item.step}
</span>
<span style={{ width: 1, height: 12, background: 'rgba(255,255,255,0.2)' }} />
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(255,255,255,0.62)', letterSpacing: '0.08em' }}>{item.date}</span>
</div>
<div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20, opacity: 0.5 }} />
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.5rem, 2.5vw, 2.2rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: 0 }}>
{item.title}
</h3>
</div>
<p style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.68)', lineHeight: 1.75, margin: 0 }}>
{item.desc}
</p>
{/* Step progress indicator */}
<div style={{ display: 'flex', gap: 4 }}>
{[0,1,2,3].map(i => (
<div key={i} style={{ height: 2, flex: i === index ? 2 : 1, background: i === index ? GOLD : 'rgba(255,255,255,0.1)', transition: 'flex 0.3s' }} />
))}
</div>
</ProcessCardBody>
</ProcessCard>
);
})}
</div>
</ContainerSticky>
<style>{`
@keyframes scrollBounce {
0%, 100% { transform: translateY(0); opacity: inherit; }
50% { transform: translateY(3px); }
}
`}</style>
</ContainerScroll>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,601 @@
import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { ArrowRight, ArrowLeft, Check, Trophy, User, Send, Building2, AlertCircle } from 'lucide-react';
const FF = '"IBM Plex Sans", sans-serif';
/* ── Theme definitions ─────────────────────────────────────────────────── */
const DARK = {
outerBg: 'transparent',
outerBgImage: 'none',
border: 'rgba(255,255,255,0.12)',
borderActive: 'rgba(239,191,4,0.8)',
progressBg: 'rgba(255,255,255,0.1)',
progressFill: 'linear-gradient(to right,#EFBF04,rgba(239,191,4,0.75))',
progressGlow: '0 0 8px rgba(239,191,4,0.6)',
stepActive: '#fff',
stepDone: '#EFBF04',
stepInactive: 'rgba(255,255,255,0.25)',
stepActiveBg: 'rgba(239,191,4,0.07)',
stepUnderline: '#EFBF04',
accentLabel: 'rgba(239,191,4,0.8)',
heading: '#fff',
inputBg: 'rgba(255,255,255,0.05)',
inputBgFocus: 'rgba(255,255,255,0.08)',
inputBorder: 'rgba(255,255,255,0.18)',
inputText: '#fff',
inputPlaceholder: 'rgba(255,255,255,0.35)',
fieldLabel: 'rgba(255,255,255,0.45)',
fieldError: '#f87171',
cardBg: 'rgba(255,255,255,0.06)',
cardBgSelected: 'rgba(239,191,4,0.1)',
cardBorder: 'rgba(255,255,255,0.22)',
cardBorderSel: '#EFBF04',
cardText: '#fff',
cardTextSel: '#EFBF04',
cardDesc: 'rgba(255,255,255,0.65)',
optBg: 'rgba(255,255,255,0.05)',
optBgSel: 'rgba(239,191,4,0.12)',
optBorder: 'rgba(255,255,255,0.22)',
optBorderSel: '#EFBF04',
optText: 'rgba(255,255,255,0.7)',
optTextSel: '#EFBF04',
bodyText: 'rgba(255,255,255,0.6)',
mutedText: 'rgba(255,255,255,0.35)',
rowLabel: 'rgba(255,255,255,0.35)',
rowValue: '#fff',
alertIcon: '#EFBF04',
alertTitle: '#EFBF04',
alertEmail: '#EFBF04',
btnBg: '#EFBF04',
btnBgHover: '#FFD130',
btnText: '#101828',
backText: 'rgba(255,255,255,0.35)',
backTextHover: '#fff',
successIcon: '#EFBF04',
successIconInner: '#111D55',
successHeading: '#fff',
successBody: 'rgba(255,255,255,0.55)',
successName: '#fff',
successAccent: '#EFBF04',
};
const GOLD = {
outerBg: 'transparent',
outerBgImage: 'none',
border: 'rgba(17,29,85,0.15)',
borderActive: '#111D55',
progressBg: 'rgba(17,29,85,0.12)',
progressFill: 'linear-gradient(to right,#111D55,#051A87)',
progressGlow: 'none',
stepActive: '#111D55',
stepDone: 'rgba(17,29,85,0.55)',
stepInactive: 'rgba(17,29,85,0.28)',
stepActiveBg: 'rgba(17,29,85,0.07)',
stepUnderline: '#111D55',
accentLabel: 'rgba(17,29,85,0.5)',
heading: '#111D55',
inputBg: 'rgba(255,255,255,0.5)',
inputBgFocus: 'rgba(255,255,255,0.9)',
inputBorder: 'rgba(17,29,85,0.18)',
inputText: '#111D55',
inputPlaceholder: 'rgba(17,29,85,0.35)',
fieldLabel: 'rgba(17,29,85,0.5)',
fieldError: '#b91c1c',
cardBg: 'rgba(255,255,255,0.38)',
cardBgSelected: 'rgba(17,29,85,0.1)',
cardBorder: 'rgba(17,29,85,0.2)',
cardBorderSel: '#111D55',
cardText: '#111D55',
cardTextSel: '#111D55',
cardDesc: 'rgba(17,29,85,0.6)',
optBg: 'rgba(255,255,255,0.4)',
optBgSel: 'rgba(17,29,85,0.1)',
optBorder: 'rgba(17,29,85,0.2)',
optBorderSel: '#111D55',
optText: 'rgba(17,29,85,0.65)',
optTextSel: '#111D55',
bodyText: 'rgba(17,29,85,0.65)',
mutedText: 'rgba(17,29,85,0.4)',
rowLabel: 'rgba(17,29,85,0.4)',
rowValue: '#111D55',
alertIcon: '#111D55',
alertTitle: '#111D55',
alertEmail: '#111D55',
btnBg: '#111D55',
btnBgHover: '#051A87',
btnText: '#fff',
backText: 'rgba(17,29,85,0.4)',
backTextHover: '#111D55',
successIcon: '#111D55',
successIconInner: '#EFBF04',
successHeading: '#111D55',
successBody: 'rgba(17,29,85,0.6)',
successName: '#111D55',
successAccent: '#111D55',
};
type C = typeof DARK;
/* ── Data / Steps ─────────────────────────────────────────────────────── */
type FormData = {
type: 'selbst' | 'vorschlag' | '';
mitarbeiter: string;
standortBayern: 'ja' | 'nein' | '';
familiengeführt: 'ja' | 'nein' | '';
firmenname: string;
kontaktName: string;
email: string;
telefon: string;
nomFirma: string;
nomBranche: string;
nomStandort: string;
nomName: string;
nomEmail: string;
nomBeziehung: string;
};
const STEPS_SELBST = [
{ label: 'Einreichung', icon: Trophy },
{ label: 'Schnell-Check',icon: Check },
{ label: 'Kontakt', icon: User },
{ label: 'Absenden', icon: Send },
];
const STEPS_VORSCHLAG = [
{ label: 'Einreichung', icon: Trophy },
{ label: 'Unternehmen', icon: Building2},
{ label: 'Nominierender',icon: User },
{ label: 'Absenden', icon: Send },
];
const MITARBEITER_OPTIONS = [
{ value: 'unter10', label: 'Unter 10' },
{ value: '10-50', label: '10 – 50' },
{ value: '51-200', label: '51 – 200' },
{ value: '201-500', label: '201 – 500'},
{ value: 'über500', label: 'Über 500' },
];
const variants = {
enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (dir: number) => ({ x: dir > 0 ? -40 : 40, opacity: 0 }),
};
/* ── Sub-components ───────────────────────────────────────────────────── */
function Field({ label, error, children, optional, c }: {
label: string; error?: string; children: React.ReactNode; optional?: boolean; c: C;
}) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>
{label}{!optional && ' *'}
</label>
{children}
{error && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{error}</span>}
</div>
);
}
function StyledInput({ c, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { c: C }) {
const [focused, setFocused] = useState(false);
return (
<input
{...props}
onFocus={e => { setFocused(true); props.onFocus?.(e); }}
onBlur ={e => { setFocused(false); props.onBlur?.(e); }}
style={{
width: '100%', height: 38, padding: '0 12px',
fontFamily: FF, fontSize: 13, color: c.inputText,
border: `1px solid ${focused ? c.borderActive : c.inputBorder}`,
background: focused ? c.inputBgFocus : c.inputBg,
outline: 'none', boxSizing: 'border-box',
transition: 'border-color 0.15s,background 0.15s',
...props.style,
}}
/>
);
}
function TypeCard({ selected, onClick, title, desc, c }: {
selected: boolean; onClick: () => void; title: string; desc: string; c: C;
}) {
return (
<button type="button" onClick={onClick} style={{
flex: 1, padding: '13px 12px', textAlign: 'left', cursor: 'pointer',
border: `1.5px solid ${selected ? c.cardBorderSel : c.cardBorder}`,
background: selected ? c.cardBgSelected : c.cardBg,
fontFamily: FF, transition: 'all 0.15s',
display: 'flex', flexDirection: 'column', gap: 6,
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: 13, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', color: selected ? c.cardTextSel : c.cardText }}>{title}</span>
<div style={{ width: 14, height: 14, borderRadius: '50%', border: `2px solid ${selected ? c.cardBorderSel : c.border}`, background: selected ? c.cardBorderSel : 'transparent', transition: 'all 0.15s' }} />
</div>
<span style={{ fontSize: 13, color: c.cardDesc, lineHeight: 1.5 }}>{desc}</span>
</button>
);
}
function OptionButton({ selected, onClick, label, c }: {
selected: boolean; onClick: () => void; label: string; c: C;
}) {
return (
<button type="button" onClick={onClick} style={{
padding: '8px 16px', fontFamily: FF, fontSize: 12, fontWeight: 600, cursor: 'pointer',
border: `1.5px solid ${selected ? c.optBorderSel : c.optBorder}`,
background: selected ? c.optBgSel : c.optBg,
color: selected ? c.optTextSel : c.optText,
transition: 'all 0.15s',
}}>
{label}
</button>
);
}
function YesNo({ value, onChange, c }: {
value: 'ja' | 'nein' | ''; onChange: (v: 'ja' | 'nein') => void; c: C;
}) {
return (
<div style={{ display: 'flex', gap: 8 }}>
<OptionButton selected={value === 'ja'} onClick={() => onChange('ja')} label="Ja" c={c} />
<OptionButton selected={value === 'nein'} onClick={() => onChange('nein')} label="Nein" c={c} />
</div>
);
}
function isEligible(data: FormData) {
return ['10-50', '51-200', '201-500'].includes(data.mitarbeiter) && data.standortBayern === 'ja';
}
/* ── Main component ───────────────────────────────────────────────────── */
export default function BewerbungsForm({ theme = 'dark' }: { theme?: 'dark' | 'gold' }) {
const c = theme === 'gold' ? GOLD : DARK;
const [step, setStep] = useState(0);
const [dir, setDir] = useState(1);
const [submitted, setSubmitted] = useState(false);
const [data, setData] = useState<FormData>({
type: '', mitarbeiter: '', standortBayern: '', familiengeführt: '',
firmenname: '', kontaktName: '', email: '', telefon: '',
nomFirma: '', nomBranche: '', nomStandort: '',
nomName: '', nomEmail: '', nomBeziehung: '',
});
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
const STEPS = data.type === 'vorschlag' ? STEPS_VORSCHLAG : STEPS_SELBST;
const progress = step === 0 ? 0 : (step / (STEPS.length - 1)) * 100;
const set = (k: keyof FormData, v: string) => {
setData(d => ({ ...d, [k]: v }));
setErrors(e => { const n = { ...e }; delete n[k]; return n; });
};
const validate = (): boolean => {
const e: typeof errors = {};
if (data.type === 'selbst') {
if (step === 1) {
if (!data.mitarbeiter) e.mitarbeiter = 'Bitte wählen';
if (!data.standortBayern) e.standortBayern = 'Bitte wählen';
if (!data.familiengeführt)e.familiengeführt= 'Bitte wählen';
}
if (step === 2) {
if (!data.firmenname) e.firmenname = 'Pflichtfeld';
if (!data.kontaktName) e.kontaktName = 'Pflichtfeld';
if (!data.email) e.email = 'Pflichtfeld';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = 'Ungültige E-Mail';
}
} else {
if (step === 1) {
if (!data.nomFirma) e.nomFirma = 'Pflichtfeld';
if (!data.nomBranche) e.nomBranche = 'Pflichtfeld';
}
if (step === 2) {
if (!data.nomName) e.nomName = 'Pflichtfeld';
if (!data.nomEmail) e.nomEmail = 'Pflichtfeld';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.nomEmail)) e.nomEmail = 'Ungültige E-Mail';
}
}
setErrors(e);
return Object.keys(e).length === 0;
};
const next = () => { if (validate()) { setDir(1); setStep(s => s + 1); } };
const prev = () => { setDir(-1); setStep(s => s - 1); };
const submit = () => { if (validate()) setSubmitted(true); };
if (submitted) {
return (
<div style={{ fontFamily: FF, display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, borderTop: `1px solid ${c.border}`, background: c.outerBg, backgroundImage: c.outerBgImage }}>
<div style={{ padding: '48px 28px', textAlign: 'center' }}>
<div style={{ width: 52, height: 52, background: c.successIcon, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 20px' }}>
<Check size={22} color={c.successIconInner} strokeWidth={3} />
</div>
<h3 style={{ fontSize: 18, fontWeight: 900, color: c.successHeading, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 10 }}>
{data.type === 'selbst' ? 'Anfrage eingegangen' : 'Nominierung eingegangen'}
</h3>
<p style={{ fontSize: 13, color: c.successBody, maxWidth: 340, margin: '0 auto', lineHeight: 1.7 }}>
{data.type === 'selbst'
? <><span>Vielen Dank, </span><strong style={{ color: c.successName }}>{data.firmenname}</strong><span>. Wir senden Ihnen den vollständigen Fragebogen an </span><strong style={{ color: c.successAccent }}>{data.email}</strong>.</>
: <><span>Vielen Dank, </span><strong style={{ color: c.successName }}>{data.nomName}</strong><span>. Wir nehmen Kontakt mit </span><strong style={{ color: c.successAccent }}>{data.nomFirma}</strong><span> auf und informieren sie über Ihre Nominierung.</span></>
}
</p>
</div>
</div>
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, borderTop: `1px solid ${c.border}`, background: c.outerBg, backgroundImage: c.outerBgImage }}>
{/* Progress bar */}
<div style={{ height: 3, background: c.progressBg, flexShrink: 0 }}>
<div style={{ height: '100%', backgroundImage: c.progressFill, width: `${progress}%`, transition: 'width 0.4s ease', boxShadow: c.progressGlow }} />
</div>
{/* Step indicators */}
<div style={{ display: 'flex', borderBottom: `1px solid ${c.border}`, flexShrink: 0 }}>
{STEPS.map((s, i) => {
const Icon = s.icon;
const done = i < step;
const active = i === step;
return (
<div key={i} style={{
flex: 1, padding: '8px 4px', textAlign: 'center', fontFamily: FF, fontSize: 9,
fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em',
color: active ? c.stepActive : done ? c.stepDone : c.stepInactive,
borderBottom: active ? `2px solid ${c.stepUnderline}` : '2px solid transparent',
background: active ? c.stepActiveBg : 'transparent',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
marginBottom: -1, transition: 'color 0.2s',
}}>
<Icon size={11} strokeWidth={active ? 2 : 1.5} />
<span>{done ? '✓' : s.label}</span>
</div>
);
})}
</div>
{/* Content */}
<div style={{ padding: '20px 28px', flex: 1, minHeight: 0, overflowX: 'hidden', overflowY: 'auto' }}>
<AnimatePresence mode="wait" custom={dir}>
<motion.div key={`${step}-${data.type}`} custom={dir} variants={variants}
initial="enter" animate="center" exit="exit"
transition={{ duration: 0.22, ease: 'easeInOut' }}>
{/* ── Step 0: Typ-Wahl ── */}
{step === 0 && (
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 8 }}>Schritt 1</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 14 }}>Art der Einreichung</h3>
<div style={{ display: 'flex', gap: 8 }}>
<TypeCard c={c} selected={data.type === 'selbst'} title="Eigenbewerbung"
desc="Ich bewerbe mein eigenes Unternehmen für den Bayerischen Mittelstandspreis."
onClick={() => { set('type', 'selbst'); setDir(1); setStep(1); }} />
<TypeCard c={c} selected={data.type === 'vorschlag'} title="Vorschlag"
desc="Ich schlage ein anderes Unternehmen vor, das den Preis verdient."
onClick={() => { set('type', 'vorschlag'); setDir(1); setStep(1); }} />
</div>
</div>
)}
{/* ── Eigenbewerbung Step 1: Schnell-Check ── */}
{step === 1 && data.type === 'selbst' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Schnell-Check</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Grundlegende Eignung</h3>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>Wie viele Mitarbeiter hat Ihr Unternehmen? *</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{MITARBEITER_OPTIONS.map(o => (
<OptionButton c={c} key={o.value} selected={data.mitarbeiter === o.value} onClick={() => set('mitarbeiter', o.value)} label={o.label} />
))}
</div>
{errors.mitarbeiter && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{errors.mitarbeiter}</span>}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>Hat Ihr Unternehmen seinen Sitz in Bayern? *</label>
<YesNo c={c} value={data.standortBayern} onChange={v => set('standortBayern', v)} />
{errors.standortBayern && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{errors.standortBayern}</span>}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.fieldLabel }}>Ist Ihr Unternehmen inhabergeführt oder familiengeführt? *</label>
<YesNo c={c} value={data.familiengeführt} onChange={v => set('familiengeführt', v)} />
{errors.familiengeführt && <span style={{ fontFamily: FF, fontSize: 11, color: c.fieldError }}>{errors.familiengeführt}</span>}
</div>
</div>
</div>
)}
{/* ── Eigenbewerbung Step 2: Kontakt / Ineligible ── */}
{step === 2 && data.type === 'selbst' && (() => {
if (!isEligible(data)) return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<AlertCircle size={18} color={c.alertIcon} />
<h3 style={{ fontFamily: FF, fontSize: 15, fontWeight: 800, color: c.alertTitle, textTransform: 'uppercase' }}>Leider nicht förderfähig</h3>
</div>
<p style={{ fontFamily: FF, fontSize: 13, color: c.bodyText, lineHeight: 1.7 }}>
Der Bayerische Mittelstandspreis richtet sich an inhabergeführte Unternehmen mit 10–500 Mitarbeitern und Sitz in Bayern. Ihr Unternehmen erfüllt diese Voraussetzungen aktuell nicht.
</p>
<p style={{ fontFamily: FF, fontSize: 13, color: c.mutedText, lineHeight: 1.7 }}>
Fragen? Schreiben Sie uns: <span style={{ color: c.alertEmail }}>info@bmp-bayern.de</span>
</p>
</div>
);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Kontakt</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Kontaktdaten</h3>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<Field c={c} label="Firmenname" error={errors.firmenname}>
<StyledInput c={c} value={data.firmenname} onChange={e => set('firmenname', e.target.value)} placeholder="Muster GmbH" />
</Field>
<Field c={c} label="Ihr Name" error={errors.kontaktName}>
<StyledInput c={c} value={data.kontaktName} onChange={e => set('kontaktName', e.target.value)} placeholder="Vor- und Nachname" />
</Field>
<Field c={c} label="E-Mail" error={errors.email}>
<StyledInput c={c} type="email" value={data.email} onChange={e => set('email', e.target.value)} placeholder="name@unternehmen.de" />
</Field>
<Field c={c} label="Telefon" optional>
<StyledInput c={c} type="tel" value={data.telefon} onChange={e => set('telefon', e.target.value)} placeholder="+49 89 ..." />
</Field>
</div>
<p style={{ fontFamily: FF, fontSize: 12, color: c.mutedText, lineHeight: 1.6 }}>
Wir senden Ihnen den vollständigen Fragebogen automatisch per E-Mail zu.
</p>
</div>
);
})()}
{/* ── Eigenbewerbung Step 3: Zusammenfassung ── */}
{step === 3 && data.type === 'selbst' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Zusammenfassung</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Bewerbung</h3>
</div>
<div style={{ borderTop: `1px solid ${c.border}` }}>
{[
{ label: 'Unternehmen', value: data.firmenname },
{ label: 'Mitarbeiter', value: MITARBEITER_OPTIONS.find(o => o.value === data.mitarbeiter)?.label || '' },
{ label: 'Standort', value: `Bayern: ${data.standortBayern === 'ja' ? 'Ja' : 'Nein'}` },
{ label: 'Kontakt', value: `${data.kontaktName} · ${data.email}` },
].map(row => (
<div key={row.label} style={{ display: 'grid', gridTemplateColumns: '90px 1fr', gap: 12, padding: '10px 0', borderBottom: `1px solid ${c.border}`, alignItems: 'baseline' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: c.rowLabel, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{row.label}</span>
<span style={{ fontFamily: FF, fontSize: 13, color: c.rowValue }}>{row.value}</span>
</div>
))}
</div>
</div>
)}
{/* ── Vorschlag Step 1: Nominiertes Unternehmen ── */}
{step === 1 && data.type === 'vorschlag' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Nominierung</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Nominiertes Unternehmen</h3>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<Field c={c} label="Firmenname" error={errors.nomFirma}>
<StyledInput c={c} value={data.nomFirma} onChange={e => set('nomFirma', e.target.value)} placeholder="Muster GmbH" />
</Field>
<Field c={c} label="Branche" error={errors.nomBranche}>
<StyledInput c={c} value={data.nomBranche} onChange={e => set('nomBranche', e.target.value)} placeholder="z. B. Maschinenbau" />
</Field>
<Field c={c} label="Standort Bayern" optional>
<StyledInput c={c} value={data.nomStandort} onChange={e => set('nomStandort', e.target.value)} placeholder="z. B. München" />
</Field>
</div>
</div>
)}
{/* ── Vorschlag Step 2: Nominierender ── */}
{step === 2 && data.type === 'vorschlag' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Nominierender</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Angaben</h3>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<Field c={c} label="Ihr Name" error={errors.nomName}>
<StyledInput c={c} value={data.nomName} onChange={e => set('nomName', e.target.value)} placeholder="Vor- und Nachname" />
</Field>
<Field c={c} label="Ihre E-Mail" error={errors.nomEmail}>
<StyledInput c={c} type="email" value={data.nomEmail} onChange={e => set('nomEmail', e.target.value)} placeholder="ihre@email.de" />
</Field>
<Field c={c} label="Ihre Beziehung zum Unternehmen" optional>
<StyledInput c={c} value={data.nomBeziehung} onChange={e => set('nomBeziehung', e.target.value)} placeholder="z. B. Kunde, Partner, Bekannter" />
</Field>
</div>
<p style={{ fontFamily: FF, fontSize: 12, color: c.mutedText, lineHeight: 1.6 }}>
Wir nehmen Kontakt mit dem nominierten Unternehmen auf und informieren es über Ihre Nominierung.
</p>
</div>
)}
{/* ── Vorschlag Step 3: Zusammenfassung ── */}
{step === 3 && data.type === 'vorschlag' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Zusammenfassung</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihre Nominierung</h3>
</div>
<div style={{ borderTop: `1px solid ${c.border}` }}>
{[
{ label: 'Unternehmen', value: data.nomFirma },
{ label: 'Branche', value: data.nomBranche },
{ label: 'Standort', value: data.nomStandort || '—' },
{ label: 'Nominiert von',value: `${data.nomName} · ${data.nomEmail}` },
].map(row => (
<div key={row.label} style={{ display: 'grid', gridTemplateColumns: '100px 1fr', gap: 12, padding: '10px 0', borderBottom: `1px solid ${c.border}`, alignItems: 'baseline' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: c.rowLabel, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{row.label}</span>
<span style={{ fontFamily: FF, fontSize: 13, color: c.rowValue }}>{row.value}</span>
</div>
))}
</div>
</div>
)}
</motion.div>
</AnimatePresence>
</div>
{/* Navigation */}
{step > 0 && (
<div style={{ padding: '12px 28px', borderTop: `1px solid ${c.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
<button type="button" onClick={prev} style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase',
letterSpacing: '0.1em', color: c.backText, background: 'none',
border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 5,
padding: 0, transition: 'color 0.15s',
}}
onMouseEnter={e => (e.currentTarget.style.color = c.backTextHover)}
onMouseLeave={e => (e.currentTarget.style.color = c.backText)}
>
<ArrowLeft size={12} /> Zurück
</button>
{step < STEPS.length - 1 ? (
step === 2 && data.type === 'selbst' && !isEligible(data) ? <div /> :
<button type="button" onClick={next} style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase',
letterSpacing: '0.1em', color: c.btnText, background: c.btnBg,
border: 'none', cursor: 'pointer', padding: '10px 22px',
display: 'flex', alignItems: 'center', gap: 6, transition: 'background 0.15s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = c.btnBgHover; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = c.btnBg; }}
>
Weiter <ArrowRight size={12} />
</button>
) : (
<button type="button" onClick={submit} style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase',
letterSpacing: '0.1em', color: c.btnText, background: c.btnBg,
border: 'none', cursor: 'pointer', padding: '10px 22px',
display: 'flex', alignItems: 'center', gap: 6, transition: 'background 0.15s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = c.btnBgHover; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = c.btnBg; }}
>
Absenden <Send size={12} />
</button>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,353 @@
import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { ArrowRight, ArrowLeft, Check, MessageSquare, User, Send } from 'lucide-react';
import { useIsMobile } from '@/hooks/useIsMobile';
const FF = '"IBM Plex Sans", sans-serif';
const DARK = {
border: 'rgba(255,255,255,0.12)',
borderActive: 'rgba(239,191,4,0.8)',
progressBg: 'rgba(255,255,255,0.1)',
progressFill: '#EFBF04',
stepActive: '#fff',
stepDone: '#EFBF04',
stepInactive: 'rgba(255,255,255,0.25)',
stepActiveBg: 'rgba(239,191,4,0.07)',
stepUnderline: '#EFBF04',
subtext: 'rgba(255,255,255,0.5)',
inputBg: 'rgba(255,255,255,0.05)',
inputBgFocus: 'rgba(255,255,255,0.09)',
inputBorder: 'rgba(255,255,255,0.18)',
inputBorderFoc: 'rgba(239,191,4,0.8)',
inputText: '#fff',
labelText: 'rgba(255,255,255,0.45)',
errorText: '#f87171',
cardBg: 'rgba(255,255,255,0.06)',
cardBgSel: 'rgba(239,191,4,0.1)',
cardBorder: 'rgba(255,255,255,0.22)',
cardBorderSel: '#EFBF04',
cardTitle: '#fff',
cardTitleSel: '#EFBF04',
cardDesc: 'rgba(255,255,255,0.5)',
radioBorder: 'rgba(255,255,255,0.25)',
radioBorderSel: '#EFBF04',
radioFill: '#EFBF04',
radioInner: '#111D55',
btnBg: '#EFBF04',
btnBgHover: '#FFD130',
btnText: '#101828',
backText: 'rgba(255,255,255,0.35)',
backTextHover: '#fff',
successIcon: '#EFBF04',
successIconIn: '#111D55',
successHeading: '#fff',
successBody: 'rgba(255,255,255,0.6)',
};
const GOLD = {
border: 'rgba(17,29,85,0.15)',
borderActive: '#111D55',
progressBg: 'rgba(17,29,85,0.12)',
progressFill: '#111D55',
stepActive: '#111D55',
stepDone: 'rgba(17,29,85,0.55)',
stepInactive: 'rgba(17,29,85,0.28)',
stepActiveBg: 'rgba(17,29,85,0.07)',
stepUnderline: '#111D55',
subtext: 'rgba(17,29,85,0.6)',
inputBg: 'rgba(255,255,255,0.5)',
inputBgFocus: 'rgba(255,255,255,0.9)',
inputBorder: 'rgba(17,29,85,0.18)',
inputBorderFoc: '#111D55',
inputText: '#111D55',
labelText: 'rgba(17,29,85,0.5)',
errorText: '#b91c1c',
cardBg: 'rgba(255,255,255,0.38)',
cardBgSel: 'rgba(17,29,85,0.1)',
cardBorder: 'rgba(17,29,85,0.2)',
cardBorderSel: '#111D55',
cardTitle: '#111D55',
cardTitleSel: '#111D55',
cardDesc: 'rgba(17,29,85,0.55)',
radioBorder: 'rgba(17,29,85,0.25)',
radioBorderSel: '#111D55',
radioFill: '#111D55',
radioInner: '#fff',
btnBg: '#111D55',
btnBgHover: '#051A87',
btnText: '#fff',
backText: 'rgba(17,29,85,0.4)',
backTextHover: '#111D55',
successIcon: '#111D55',
successIconIn: '#EFBF04',
successHeading: '#111D55',
successBody: 'rgba(17,29,85,0.65)',
};
type C = typeof DARK;
type FormData = {
betreff: string;
name: string;
email: string;
nachricht: string;
};
const STEPS = [
{ label: 'Betreff', icon: MessageSquare },
{ label: 'Kontakt', icon: User },
{ label: 'Nachricht', icon: Send },
];
const BETREFFS = [
{ value: 'allgemein', label: 'Allgemeine Anfrage', desc: 'Ich habe eine allgemeine Frage.' },
{ value: 'bewerbung', label: 'Frage zur Bewerbung', desc: 'Ich möchte mich bewerben oder habe Rückfragen.' },
{ value: 'presse', label: 'Presseanfrage', desc: 'Ich bin Journalist:in und recherchiere zum BMP.' },
{ value: 'sponsoring', label: 'Sponsoring', desc: 'Ich interessiere mich für eine Partnerschaft.' },
];
const variants = {
enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (dir: number) => ({ x: dir > 0 ? -40 : 40, opacity: 0 }),
};
function StyledInput({ c, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { c: C }) {
const [focused, setFocused] = useState(false);
return (
<input
{...props}
onFocus={e => { setFocused(true); props.onFocus?.(e); }}
onBlur ={e => { setFocused(false); props.onBlur?.(e); }}
style={{
width: '100%', height: 42, padding: '0 12px',
fontSize: 14, color: c.inputText, fontFamily: FF,
border: `1.5px solid ${focused ? c.inputBorderFoc : c.inputBorder}`,
background: focused ? c.inputBgFocus : c.inputBg,
outline: 'none', boxSizing: 'border-box',
transition: 'border-color 0.15s, background 0.15s',
...props.style,
}}
/>
);
}
function StyledTextarea({ c, ...props }: React.TextareaHTMLAttributes<HTMLTextAreaElement> & { c: C }) {
const [focused, setFocused] = useState(false);
return (
<textarea
{...props}
onFocus={e => { setFocused(true); props.onFocus?.(e); }}
onBlur ={e => { setFocused(false); props.onBlur?.(e); }}
style={{
width: '100%', padding: '10px 12px',
fontSize: 14, color: c.inputText, fontFamily: FF,
border: `1.5px solid ${focused ? c.inputBorderFoc : c.inputBorder}`,
background: focused ? c.inputBgFocus : c.inputBg,
outline: 'none', boxSizing: 'border-box',
resize: 'vertical', minHeight: 100,
transition: 'border-color 0.15s, background 0.15s',
...props.style,
}}
/>
);
}
function BetreffCard({ c, item, selected, onClick }: { c: C; item: typeof BETREFFS[0]; selected: boolean; onClick: () => void }) {
return (
<button
type="button" onClick={onClick}
style={{
width: '100%', padding: '11px 14px', textAlign: 'left', cursor: 'pointer',
border: `1.5px solid ${selected ? c.cardBorderSel : c.cardBorder}`,
background: selected ? c.cardBgSel : c.cardBg,
fontFamily: FF, transition: 'all 0.15s',
display: 'flex', alignItems: 'center', gap: 12,
}}
>
<div style={{
width: 16, height: 16, borderRadius: '50%', flexShrink: 0,
border: `2px solid ${selected ? c.radioBorderSel : c.radioBorder}`,
background: selected ? c.radioFill : 'transparent',
transition: 'all 0.15s', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{selected && <div style={{ width: 5, height: 5, borderRadius: '50%', background: c.radioInner }} />}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: selected ? c.cardTitleSel : c.cardTitle, marginBottom: 1 }}>{item.label}</div>
<div style={{ fontSize: 11, color: c.cardDesc }}>{item.desc}</div>
</div>
</button>
);
}
export default function KontaktForm({ theme = 'dark' }: { theme?: 'dark' | 'gold' }) {
const c = theme === 'gold' ? GOLD : DARK;
const isMobile = useIsMobile();
const [step, setStep] = useState(0);
const [dir, setDir] = useState(1);
const [submitted, setSubmitted] = useState(false);
const [data, setData] = useState<FormData>({ betreff: 'allgemein', name: '', email: '', nachricht: '' });
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
const set = (k: keyof FormData, v: string) => {
setData(d => ({ ...d, [k]: v }));
setErrors(e => { const n = { ...e }; delete n[k]; return n; });
};
const validate = () => {
const e: typeof errors = {};
if (step === 1) {
if (!data.name) e.name = 'Bitte Namen angeben';
if (!data.email) e.email = 'Bitte E-Mail angeben';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = 'Ungültige E-Mail';
}
if (step === 2 && !data.nachricht) e.nachricht = 'Bitte Nachricht verfassen.';
setErrors(e);
return Object.keys(e).length === 0;
};
const next = () => { if (validate()) { setDir(1); setStep(s => s + 1); } };
const prev = () => { setDir(-1); setStep(s => s - 1); };
const submit = () => { if (validate()) setSubmitted(true); };
if (submitted) {
return (
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }}
style={{ padding: '48px 24px', textAlign: 'center' }}
>
<div style={{ width: 56, height: 56, background: c.successIcon, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 20px' }}>
<Check size={24} color={c.successIconIn} strokeWidth={2.5} />
</div>
<h3 style={{ fontSize: 20, fontWeight: 800, color: c.successHeading, marginBottom: 10, letterSpacing: '-0.02em', fontFamily: FF }}>
Nachricht gesendet
</h3>
<p style={{ fontSize: 15, color: c.successBody, lineHeight: 1.7, maxWidth: 280, margin: '0 auto', fontFamily: FF }}>
Danke, <strong style={{ color: c.successHeading }}>{data.name}</strong>. Wir melden uns zeitnah bei{' '}
<strong style={{ color: c.successIcon }}>{data.email}</strong>.
</p>
</motion.div>
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
{/* Progress */}
<div style={{ height: 3, background: c.progressBg, flexShrink: 0 }}>
<div style={{ height: '100%', background: c.progressFill, width: step === 0 ? '0%' : `${(step / (STEPS.length - 1)) * 100}%`, transition: 'width 0.4s ease' }} />
</div>
{/* Step tabs */}
<div style={{ display: 'flex', borderBottom: `1px solid ${c.border}`, flexShrink: 0 }}>
{STEPS.map((s, i) => {
const Icon = s.icon;
const done = i < step, active = i === step;
return (
<div key={i} style={{
flex: 1, padding: '8px 4px', textAlign: 'center', fontFamily: FF, fontSize: 9,
fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em',
color: active ? c.stepActive : done ? c.stepDone : c.stepInactive,
borderBottom: active ? `2px solid ${c.stepUnderline}` : '2px solid transparent',
background: active ? c.stepActiveBg : 'transparent',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
marginBottom: -1, transition: 'color 0.2s',
}}>
<Icon size={11} strokeWidth={active ? 2 : 1.5} />
<span>{done ? '✓' : s.label}</span>
</div>
);
})}
</div>
{/* Content */}
<div style={{ padding: '20px 0', flex: 1, minHeight: 0, overflowX: 'hidden', overflowY: 'auto' }}>
<AnimatePresence mode="wait" custom={dir}>
<motion.div key={step} custom={dir} variants={variants}
initial="enter" animate="center" exit="exit"
transition={{ duration: 0.2, ease: 'easeOut' }}>
{step === 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<p style={{ fontSize: 14, color: c.subtext, marginBottom: 12, fontFamily: FF }}>Womit können wir Ihnen helfen?</p>
{BETREFFS.map(item => (
<BetreffCard key={item.value} c={c} item={item} selected={data.betreff === item.value}
onClick={() => { set('betreff', item.value); setDir(1); setStep(1); }} />
))}
</div>
)}
{step === 1 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<p style={{ fontSize: 14, color: c.subtext, marginBottom: 4, fontFamily: FF }}>Wie dürfen wir Sie kontaktieren?</p>
{([
{ label: 'Name', key: 'name' as const, type: 'text', placeholder: 'Vor- und Nachname', error: errors.name },
{ label: 'E-Mail', key: 'email' as const, type: 'email', placeholder: 'ihre@email.de', error: errors.email },
] as const).map(f => (
<div key={f.key} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: c.labelText, fontFamily: FF }}>{f.label}</label>
<StyledInput c={c} type={f.type} value={data[f.key]} onChange={e => set(f.key, e.target.value)} placeholder={f.placeholder} />
{f.error && <span style={{ fontSize: 11, color: c.errorText, fontFamily: FF }}>{f.error}</span>}
</div>
))}
</div>
)}
{step === 2 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<p style={{ fontSize: 14, color: c.subtext, marginBottom: 4, fontFamily: FF }}>Was möchten Sie uns mitteilen?</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: c.labelText, fontFamily: FF }}>Nachricht</label>
<StyledTextarea c={c} value={data.nachricht} onChange={e => set('nachricht', e.target.value)} rows={5} placeholder="Schreiben Sie Ihre Nachricht…" />
{errors.nachricht && <span style={{ fontSize: 11, color: c.errorText, fontFamily: FF }}>{errors.nachricht}</span>}
</div>
</div>
)}
</motion.div>
</AnimatePresence>
</div>
{/* Navigation */}
{step > 0 && (
<div style={{ paddingTop: 16, borderTop: `1px solid ${c.border}`, display: 'flex', flexDirection: isMobile ? 'column-reverse' : 'row', justifyContent: 'space-between', alignItems: isMobile ? 'stretch' : 'center', gap: 12, flexShrink: 0 }}>
<button type="button" onClick={prev} style={{
fontSize: 12, fontWeight: 600, color: c.backText, background: 'none', border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : 'flex-start', gap: 5,
padding: isMobile ? '8px 0' : 0, fontFamily: FF, transition: 'color 0.15s',
}}
onMouseEnter={e => (e.currentTarget.style.color = c.backTextHover)}
onMouseLeave={e => (e.currentTarget.style.color = c.backText)}
>
<ArrowLeft size={13} /> Zurück
</button>
{step < STEPS.length - 1 ? (
<button type="button" onClick={next} style={{
fontSize: 14, fontWeight: 700, color: c.btnText, background: c.btnBg, border: 'none', cursor: 'pointer',
padding: '12px 28px', fontFamily: FF, display: 'flex', alignItems: 'center', gap: 8,
transition: 'background 0.15s',
...(isMobile ? { width: '100%', justifyContent: 'center' } : {}),
}}
onMouseEnter={e => (e.currentTarget.style.background = c.btnBgHover)}
onMouseLeave={e => (e.currentTarget.style.background = c.btnBg)}
>
Weiter <ArrowRight size={14} />
</button>
) : (
<button type="button" onClick={submit} style={{
fontSize: 14, fontWeight: 700, color: c.btnText, background: c.btnBg, border: 'none', cursor: 'pointer',
padding: '12px 28px', fontFamily: FF, display: 'flex', alignItems: 'center', gap: 8,
transition: 'background 0.15s',
...(isMobile ? { width: '100%', justifyContent: 'center' } : {}),
}}
onMouseEnter={e => (e.currentTarget.style.background = c.btnBgHover)}
onMouseLeave={e => (e.currentTarget.style.background = c.btnBg)}
>
Anfrage senden <Send size={14} />
</button>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,343 @@
import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { ArrowRight, ArrowLeft, Check, Layers, Building2, Mail } from 'lucide-react';
const FF = '"IBM Plex Sans", sans-serif';
const DARK = {
border: 'rgba(255,255,255,0.12)',
borderActive: 'rgba(239,191,4,0.8)',
progressBg: 'rgba(255,255,255,0.06)',
progressFill: '#EFBF04',
stepActive: '#fff',
stepDone: '#EFBF04',
stepInactive: 'rgba(255,255,255,0.2)',
stepActiveBg: 'rgba(239,191,4,0.07)',
stepUnderline: '#EFBF04',
accentLabel: 'rgba(239,191,4,0.8)',
heading: '#fff',
inputBg: 'rgba(255,255,255,0.05)',
inputBgFocus: 'rgba(255,255,255,0.08)',
inputBorder: 'rgba(255,255,255,0.18)',
inputBorderFoc: 'rgba(239,191,4,0.8)',
inputText: '#fff',
labelText: 'rgba(255,255,255,0.5)',
errorText: '#f87171',
cardBg: 'rgba(255,255,255,0.05)',
cardBgSel: 'rgba(239,191,4,0.08)',
cardBorder: 'rgba(255,255,255,0.12)',
cardBorderSel: '#EFBF04',
cardTitle: '#fff',
cardTitleSel: '#EFBF04',
cardDesc: 'rgba(255,255,255,0.65)',
radioFill: '#EFBF04',
btnBg: '#EFBF04',
btnBgHover: '#FFD130',
btnText: '#101828',
backText: 'rgba(255,255,255,0.35)',
backTextHover: '#fff',
successIcon: '#EFBF04',
successIconIn: '#111D55',
successHeading: '#fff',
successBody: 'rgba(255,255,255,0.45)',
footerText: 'rgba(255,255,255,0.3)',
};
const GOLD = {
border: 'rgba(17,29,85,0.15)',
borderActive: '#111D55',
progressBg: 'rgba(17,29,85,0.1)',
progressFill: '#111D55',
stepActive: '#111D55',
stepDone: 'rgba(17,29,85,0.55)',
stepInactive: 'rgba(17,29,85,0.28)',
stepActiveBg: 'rgba(17,29,85,0.07)',
stepUnderline: '#111D55',
accentLabel: 'rgba(17,29,85,0.5)',
heading: '#111D55',
inputBg: 'rgba(255,255,255,0.5)',
inputBgFocus: 'rgba(255,255,255,0.9)',
inputBorder: 'rgba(17,29,85,0.18)',
inputBorderFoc: '#111D55',
inputText: '#111D55',
labelText: 'rgba(17,29,85,0.5)',
errorText: '#b91c1c',
cardBg: 'rgba(255,255,255,0.38)',
cardBgSel: 'rgba(17,29,85,0.1)',
cardBorder: 'rgba(17,29,85,0.2)',
cardBorderSel: '#111D55',
cardTitle: '#111D55',
cardTitleSel: '#111D55',
cardDesc: 'rgba(17,29,85,0.6)',
radioFill: '#111D55',
btnBg: '#111D55',
btnBgHover: '#051A87',
btnText: '#fff',
backText: 'rgba(17,29,85,0.4)',
backTextHover: '#111D55',
successIcon: '#111D55',
successIconIn: '#EFBF04',
successHeading: '#111D55',
successBody: 'rgba(17,29,85,0.6)',
footerText: 'rgba(17,29,85,0.4)',
};
type C = typeof DARK;
type FormData = {
paket: string;
unternehmen: string;
branche: string;
kontakt: string;
email: string;
};
const STEPS = [
{ label: 'Paket', icon: Layers },
{ label: 'Unternehmen', icon: Building2 },
{ label: 'Kontakt', icon: Mail },
];
const PAKETE = [
{ value: 'main', title: 'Haupt-Sponsoring', desc: 'Maximale Sichtbarkeit auf allen Kanälen — Bühne, Digital, Print.' },
{ value: 'category', title: 'Kategorie-Partner', desc: 'Namensgebung einer Auszeichnungskategorie — fokussiertes Branding.' },
{ value: 'event', title: 'Event-Partner', desc: 'Präsenz beim exklusiven Gala-Abend in München.' },
];
const variants = {
enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (dir: number) => ({ x: dir > 0 ? -40 : 40, opacity: 0 }),
};
function Field({ c, label, error, children }: { c: C; label: string; error?: string; children: React.ReactNode }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontFamily: FF, fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: c.labelText }}>{label}</label>
{children}
{error && <span style={{ fontFamily: FF, fontSize: 11, color: c.errorText }}>{error}</span>}
</div>
);
}
function StyledInput({ c, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { c: C }) {
const [focused, setFocused] = useState(false);
return (
<input
{...props}
onFocus={e => { setFocused(true); props.onFocus?.(e); }}
onBlur ={e => { setFocused(false); props.onBlur?.(e); }}
style={{
width: '100%', height: 44, padding: '0 14px',
fontFamily: FF, fontSize: 14, color: c.inputText,
border: `1px solid ${focused ? c.inputBorderFoc : c.inputBorder}`,
background: focused ? c.inputBgFocus : c.inputBg,
outline: 'none', boxSizing: 'border-box',
transition: 'border-color 0.15s, background 0.15s',
...props.style,
}}
/>
);
}
function PaketCard({ c, pkg, selected, onClick }: { c: C; pkg: typeof PAKETE[0]; selected: boolean; onClick: () => void }) {
return (
<button type="button" onClick={onClick} style={{
width: '100%', padding: '13px 18px', textAlign: 'left', cursor: 'pointer',
border: `2px solid ${selected ? c.cardBorderSel : c.cardBorder}`,
background: selected ? c.cardBgSel : c.cardBg,
fontFamily: FF, transition: 'all 0.15s',
display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12,
}}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: selected ? c.cardTitleSel : c.cardTitle, marginBottom: 5 }}>{pkg.title}</div>
<div style={{ fontSize: 13, color: c.cardDesc, lineHeight: 1.45 }}>{pkg.desc}</div>
</div>
<div style={{
width: 18, height: 18, borderRadius: '50%', flexShrink: 0, marginTop: 2,
border: `2px solid ${selected ? c.cardBorderSel : c.cardBorder}`,
background: selected ? c.radioFill : 'transparent',
transition: 'all 0.15s',
}} />
</button>
);
}
export default function SponsoringForm({ theme = 'dark' }: { theme?: 'dark' | 'gold' }) {
const c = theme === 'gold' ? GOLD : DARK;
const [step, setStep] = useState(0);
const [dir, setDir] = useState(1);
const [submitted, setSubmitted] = useState(false);
const [data, setData] = useState<FormData>({ paket: '', unternehmen: '', branche: '', kontakt: '', email: '' });
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
const set = (k: keyof FormData, v: string) => {
setData(d => ({ ...d, [k]: v }));
setErrors(e => { const n = { ...e }; delete n[k]; return n; });
};
const validate = () => {
const e: typeof errors = {};
if (step === 0 && !data.paket) e.paket = 'Bitte wählen Sie ein Paket.';
if (step === 1 && !data.unternehmen) e.unternehmen = 'Pflichtfeld';
if (step === 2) {
if (!data.kontakt) e.kontakt = 'Pflichtfeld';
if (!data.email) e.email = 'Pflichtfeld';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = 'Ungültige E-Mail';
}
setErrors(e);
return Object.keys(e).length === 0;
};
const next = () => { if (validate()) { setDir(1); setStep(s => s + 1); } };
const prev = () => { setDir(-1); setStep(s => s - 1); };
const submit = () => { if (validate()) setSubmitted(true); };
const progress = (step / (STEPS.length - 1)) * 100;
if (submitted) {
return (
<div style={{ padding: '64px 28px', textAlign: 'center', fontFamily: FF }}>
<div style={{ width: 52, height: 52, background: c.successIcon, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 20px' }}>
<Check size={22} color={c.successIconIn} strokeWidth={3} />
</div>
<h3 style={{ fontSize: 18, fontWeight: 900, color: c.successHeading, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 10 }}>
Anfrage eingegangen
</h3>
<p style={{ fontSize: 12, color: c.successBody, maxWidth: 320, margin: '0 auto', lineHeight: 1.6 }}>
Vielen Dank, <strong style={{ color: c.successHeading }}>{data.kontakt}</strong>. Wir senden Ihnen unser Sponsoring-Exposé innerhalb von 48 Stunden zu.
</p>
</div>
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
{/* Progress bar */}
<div style={{ height: 2, background: c.progressBg, flexShrink: 0 }}>
<div style={{ height: '100%', background: c.progressFill, width: `${progress}%`, transition: 'width 0.4s ease' }} />
</div>
{/* Step tabs */}
<div style={{ display: 'flex', borderBottom: `1px solid ${c.border}`, flexShrink: 0 }}>
{STEPS.map((s, i) => {
const Icon = s.icon;
const done = i < step, active = i === step;
return (
<div key={i} style={{
flex: 1, padding: '10px 4px', textAlign: 'center', fontFamily: FF, fontSize: 9,
fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em',
color: active ? c.stepActive : done ? c.stepDone : c.stepInactive,
borderBottom: active ? `2px solid ${c.stepUnderline}` : '2px solid transparent',
background: active ? c.stepActiveBg : 'transparent',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 5,
marginBottom: -1, transition: 'color 0.2s',
}}>
<Icon size={11} strokeWidth={active ? 2 : 1.5} />
<span>{done ? '✓' : s.label}</span>
</div>
);
})}
</div>
{/* Content */}
<div style={{ padding: '20px 0', flex: 1, minHeight: 0, overflowX: 'hidden', overflowY: 'auto' }}>
<AnimatePresence mode="wait" custom={dir}>
<motion.div key={step} custom={dir} variants={variants}
initial="enter" animate="center" exit="exit"
transition={{ duration: 0.22, ease: 'easeInOut' }}>
{step === 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 6 }}>Schritt 1 — Paket wählen</p>
<h3 style={{ fontFamily: FF, fontSize: 18, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 12 }}>Interesse an</h3>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{PAKETE.map(pkg => (
<PaketCard key={pkg.value} c={c} pkg={pkg} selected={data.paket === pkg.value} onClick={() => set('paket', pkg.value)} />
))}
</div>
{errors.paket && <p style={{ fontFamily: FF, fontSize: 11, color: c.errorText, marginTop: 4 }}>{errors.paket}</p>}
</div>
)}
{step === 1 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 8 }}>Schritt 2 — Unternehmen</p>
<h3 style={{ fontFamily: FF, fontSize: 18, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ihr Unternehmen</h3>
</div>
<Field c={c} label="Unternehmen *" error={errors.unternehmen}>
<StyledInput c={c} value={data.unternehmen} onChange={e => set('unternehmen', e.target.value)} placeholder="Firmenname" />
</Field>
<Field c={c} label="Branche">
<StyledInput c={c} value={data.branche} onChange={e => set('branche', e.target.value)} placeholder="z. B. Finanzdienstleistungen" />
</Field>
</div>
)}
{step === 2 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<p style={{ fontFamily: FF, fontSize: 10, color: c.accentLabel, textTransform: 'uppercase', letterSpacing: '0.25em', fontWeight: 700, marginBottom: 8 }}>Schritt 3 — Kontakt</p>
<h3 style={{ fontFamily: FF, fontSize: 18, fontWeight: 900, color: c.heading, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>Ansprechpartner</h3>
</div>
<Field c={c} label="Ansprechpartner *" error={errors.kontakt}>
<StyledInput c={c} value={data.kontakt} onChange={e => set('kontakt', e.target.value)} placeholder="Vor- und Nachname" />
</Field>
<Field c={c} label="E-Mail *" error={errors.email}>
<StyledInput c={c} type="email" value={data.email} onChange={e => set('email', e.target.value)} placeholder="name@unternehmen.de" />
</Field>
</div>
)}
</motion.div>
</AnimatePresence>
</div>
{/* Nav */}
<div style={{ paddingTop: 14, borderTop: `1px solid ${c.border}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
{step > 0 ? (
<button type="button" onClick={prev} style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em',
color: c.backText, background: 'none', border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 6, padding: 0, transition: 'color 0.15s',
}}
onMouseEnter={e => (e.currentTarget.style.color = c.backTextHover)}
onMouseLeave={e => (e.currentTarget.style.color = c.backText)}
>
<ArrowLeft size={13} /> Zurück
</button>
) : <div />}
{step < STEPS.length - 1 ? (
<button type="button" onClick={next} style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em',
color: c.btnText, background: c.btnBg, border: 'none', cursor: 'pointer',
padding: '12px 24px', display: 'flex', alignItems: 'center', gap: 7, transition: 'background 0.15s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = c.btnBgHover; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = c.btnBg; }}
>
Weiter <ArrowRight size={13} />
</button>
) : (
<button type="button" onClick={submit} style={{
fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em',
color: c.btnText, background: c.btnBg, border: 'none', cursor: 'pointer',
padding: '12px 24px', display: 'flex', alignItems: 'center', gap: 7, transition: 'background 0.15s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = c.btnBgHover; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = c.btnBg; }}
>
Exposé anfordern <ArrowRight size={13} />
</button>
)}
</div>
<p style={{ fontFamily: FF, textAlign: 'center', fontSize: 12, color: c.footerText, paddingTop: 12 }}>
Wir melden uns innerhalb von 48 Stunden.
</p>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,155 @@
import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
const GOLD = '#EFBF04';
const DARK = '#101828';
const SHARDS = [
{ clip: 'polygon(0 0, 50% 0, 50% 50%, 0 50%)', tx: -60, ty: -60 },
{ clip: 'polygon(50% 0, 100% 0, 100% 50%, 50% 50%)', tx: 60, ty: -60 },
{ clip: 'polygon(0 50%, 50% 50%, 50% 100%, 0 100%)', tx: -60, ty: 60 },
{ clip: 'polygon(50% 50%, 100% 50%, 100% 100%, 50% 100%)', tx: 60, ty: 60 },
];
export default function LoadingScreen({ onDone }: { onDone: () => void }) {
const [phase, setPhase] = useState<'logo' | 'progress' | 'shatter' | 'gone'>('logo');
const called = useRef(false);
useEffect(() => {
const t1 = setTimeout(() => setPhase('progress'), 300);
const t2 = setTimeout(() => setPhase('shatter'), 1700);
const t3 = setTimeout(() => {
setPhase('gone');
if (!called.current) { called.current = true; onDone(); }
}, 2500);
return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); };
}, [onDone]);
if (phase === 'gone') return null;
const isShatter = phase === 'shatter';
return (
<div
style={{
position: 'fixed', inset: 0, zIndex: 9999,
pointerEvents: isShatter ? 'none' : 'all',
overflow: 'hidden',
}}
>
{/* ── SOLID PHASE (logo + progress bar) ── */}
<AnimatePresence>
{!isShatter && (
<motion.div
key="splash"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
style={{ position: 'absolute', inset: 0, background: DARK }}
>
{/* Logo + bar centred */}
<div style={{
position: 'absolute', inset: 0,
display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 36,
}}>
<motion.img
src="/bmp-logo.png"
alt="BMP"
initial={{ opacity: 0, scale: 0.86, y: 16 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
style={{
width: 'clamp(260px, 32vw, 480px)',
height: 'auto',
display: 'block',
}}
/>
{/* Gold progress track */}
<div style={{
width: 'clamp(200px, 26vw, 320px)',
height: 2,
background: 'rgba(239,191,4,0.15)',
overflow: 'hidden',
}}>
<motion.div
initial={{ scaleX: 0 }}
animate={{ scaleX: phase === 'progress' ? 1 : 0 }}
transition={{ duration: 1.1, ease: [0.4, 0, 0.2, 1] }}
style={{
height: '100%',
background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.55))`,
boxShadow: '0 0 10px rgba(239,191,4,0.7)',
transformOrigin: 'left',
}}
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* ── LIQUID GLASS SHATTER ── */}
<AnimatePresence>
{isShatter && SHARDS.map((s, i) => (
<motion.div
key={`shard-${i}`}
initial={{ x: 0, y: 0, opacity: 1, filter: 'blur(0px) brightness(1)' }}
animate={{
x: `${s.tx}%`,
y: `${s.ty}%`,
opacity: 0,
filter: 'blur(22px) brightness(2.2)',
}}
exit={{ opacity: 0 }}
transition={{
duration: 0.65,
ease: [0.4, 0, 1, 1],
delay: i * 0.055,
}}
style={{
position: 'absolute', inset: 0,
background: DARK,
clipPath: s.clip,
}}
>
{/* Ghost logo inside each shard */}
<div style={{
position: 'absolute', inset: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<img
src="/bmp-logo.png"
alt=""
aria-hidden
style={{ width: 'clamp(260px, 32vw, 480px)', height: 'auto', opacity: 0.9 }}
/>
</div>
</motion.div>
))}
</AnimatePresence>
{/* Central gold burst on shatter */}
<AnimatePresence>
{isShatter && (
<motion.div
key="burst"
initial={{ opacity: 0.7, scale: 0 }}
animate={{ opacity: 0, scale: 4 }}
transition={{ duration: 0.55, ease: 'easeOut' }}
style={{
position: 'absolute',
top: '50%', left: '50%',
transform: 'translate(-50%, -50%)',
width: 160, height: 160, borderRadius: '50%',
background: 'radial-gradient(circle, rgba(239,191,4,0.55) 0%, transparent 68%)',
pointerEvents: 'none',
}}
/>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,61 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-[#EFBF04] text-[#101828] shadow-xs hover:bg-[#03093A] hover:text-white",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
accent:
"bg-accent text-accent-foreground shadow-xs hover:bg-accent/90",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,209 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{ orientation = "horizontal", opts, setApi, plugins, className, children, ...props },
ref,
) => {
const [carouselRef, api] = useEmblaCarousel(
{ ...opts, axis: orientation === "horizontal" ? "x" : "y" },
plugins,
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => { api?.scrollPrev() }, [api])
const scrollNext = React.useCallback(() => { api?.scrollNext() }, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") { event.preventDefault(); scrollPrev() }
else if (event.key === "ArrowRight") { event.preventDefault(); scrollNext() }
},
[scrollPrev, scrollNext],
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => { api?.off("select", onSelect) }
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef, api, opts,
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev, scrollNext, canScrollPrev, canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
},
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref} variant={variant} size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal" ? "-left-12 top-1/2 -translate-y-1/2" : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref} variant={variant} size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal" ? "-right-12 top-1/2 -translate-y-1/2" : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@@ -0,0 +1,23 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-11 w-full border border-stone-200 bg-white px-4 py-2 text-sm text-[#101828] placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#EFBF04] focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-all",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,20 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
"text-[10px] font-bold uppercase tracking-[0.2em] text-[#101828]/50 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,51 @@
import React, { useEffect, useRef } from 'react';
/**
* MunichSkylineBg
* Drop inside any light section (position: relative required on parent).
* Uses backgroundPositionY for parallax — no transform, no GPU compositing layer.
* No mixBlendMode to avoid Safari compositing bleed into sibling elements.
*/
const MunichSkylineBg: React.FC = () => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const onScroll = () => {
const rect = el.parentElement?.getBoundingClientRect();
if (!rect) return;
const progress = (window.innerHeight - rect.top) / (window.innerHeight + rect.height);
const offset = progress * 30;
el.style.backgroundPositionY = `calc(100% - ${offset}px)`;
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div
ref={ref}
aria-hidden="true"
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '52%',
pointerEvents: 'none',
zIndex: 0,
backgroundImage: 'url(/munich-skyline.jpg)',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'bottom center',
backgroundSize: 'cover',
opacity: 0.08,
}}
/>
);
};
export default MunichSkylineBg;

View File

@@ -0,0 +1,53 @@
import React from "react";
interface PartnerLogo {
id: string;
label: React.ReactNode;
}
interface PartnerTickerProps {
logos: PartnerLogo[];
}
export function PartnerTicker({ logos }: PartnerTickerProps) {
// Duplicate logos for seamless infinite loop
const track = [...logos, ...logos];
return (
<div style={{ overflow: 'hidden', flex: 1, minWidth: 0, position: 'relative' }}>
{/* fade edges */}
<div style={{ position: 'absolute', inset: 0, left: 0, width: 32, background: 'linear-gradient(to right, rgba(5,26,135,0.6), transparent)', zIndex: 1, pointerEvents: 'none' }} />
<div style={{ position: 'absolute', inset: 0, right: 0, left: 'auto', width: 32, background: 'linear-gradient(to left, rgba(5,26,135,0.6), transparent)', zIndex: 1, pointerEvents: 'none' }} />
<div style={{ display: 'flex', animation: 'marquee 22s linear infinite', width: 'max-content' }}>
{track.map((logo, i) => (
<div
key={`${logo.id}-${i}`}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 24px',
color: '#fff',
fontFamily: '"IBM Plex Sans", sans-serif',
textTransform: 'uppercase',
whiteSpace: 'nowrap',
width: 140,
flexShrink: 0,
textAlign: 'center',
}}
>
{logo.label}
</div>
))}
</div>
<style>{`
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
`}</style>
</div>
);
}

View File

@@ -0,0 +1,149 @@
"use client"
import * as React from "react"
import { useMeasure } from "@uidotdev/usehooks"
import { VariantProps, cva } from "class-variance-authority"
import {
HTMLMotionProps,
MotionValue,
motion,
useScroll,
useTransform,
} from "motion/react"
import { cn } from "@/lib/utils"
const processCardVariants = cva("flex border backdrop-blur-lg", {
variants: {
variant: {
indigo:
"flex border text-slate-50 border-slate-700 backdrop-blur-lg bg-gradient-to-br from-[rgba(15,23,42,0.7)_40%] to-[#3730a3_120%]",
light: "shadow",
bmp: "flex border text-white border-[rgba(239,191,4,0.25)] bg-[#111D55]",
},
size: {
sm: "min-w-[25%] max-w-[25%]",
md: "min-w-[50%] max-w-[50%]",
lg: "min-w-[75%] max-w-[75%]",
xl: "min-w-full max-w-full",
},
},
defaultVariants: {
variant: "bmp",
size: "md",
},
})
interface ContainerScrollContextValue {
scrollYProgress: MotionValue<number>
}
interface ProcessCardProps
extends HTMLMotionProps<"div">,
VariantProps<typeof processCardVariants> {
itemsLength: number
index: number
}
const ContainerScrollContext = React.createContext<
ContainerScrollContextValue | undefined
>(undefined)
function useContainerScrollContext() {
const context = React.useContext(ContainerScrollContext)
if (!context) {
throw new Error(
"useContainerScrollContext must be used within a ContainerScroll Component"
)
}
return context
}
export const ContainerScroll = ({
children,
className,
...props
}: React.HtmlHTMLAttributes<HTMLDivElement>) => {
const scrollRef = React.useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: scrollRef,
})
return (
<ContainerScrollContext.Provider value={{ scrollYProgress }}>
<div
ref={scrollRef}
className={cn("relative min-h-[120vh]", className)}
{...props}
>
{children}
</div>
</ContainerScrollContext.Provider>
)
}
export const ContainerSticky = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("sticky left-0 top-0 w-full overflow-hidden", className)}
{...props}
/>
))
ContainerSticky.displayName = "ContainerSticky"
export const ProcessCardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6", className)} {...props} />
))
ProcessCardTitle.displayName = "ProcessCardTitle"
export const ProcessCardBody = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col gap-8 p-6", className)}
{...props}
/>
))
ProcessCardBody.displayName = "ProcessCardBody"
export const ProcessCard: React.FC<ProcessCardProps> = ({
className,
style,
variant,
size,
itemsLength,
index,
...props
}) => {
const { scrollYProgress } = useContainerScrollContext()
const start = index / itemsLength
const end = start + 1 / itemsLength
const { innerWidth } = window
const [ref, { width }] = useMeasure()
const x = useTransform(
scrollYProgress,
[start, end],
[innerWidth, -((width ?? 0) * index) + 64 * index]
)
return (
<motion.div
ref={ref}
style={{
x: index > 0 ? x : 0,
...style,
}}
className={cn(processCardVariants({ variant, size }), className)}
{...props}
/>
)
}
ProcessCard.displayName = "ProcessCard"

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[100px] w-full border border-stone-200 bg-white px-4 py-3 text-sm text-[#101828] placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#EFBF04] focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-all resize-none",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }