324 lines
18 KiB
TypeScript
324 lines
18 KiB
TypeScript
import React, { useState } from 'react'
|
||
import { useParams, Link } from '@/spa/router'
|
||
import { motion } from 'framer-motion'
|
||
import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight, Users, Tag } from 'lucide-react'
|
||
import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent } from '@/spa/data/events'
|
||
import { useCmsRoute } from '@/spa/cmsRoute'
|
||
import { docImageUrl } from '@/spa/cmsMediaField'
|
||
import { useIsMobile } from '@/spa/hooks/useIsMobile'
|
||
|
||
const NAVY = '#111D55'
|
||
const GOLD = '#EFBF04'
|
||
const CREAM = '#EFE5E3'
|
||
const INK = '#3A3A3A'
|
||
|
||
// ── Timestamp formatter ──────────────────────────────────────────────────────
|
||
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']
|
||
function formatTs(iso: string): string {
|
||
const d = new Date(iso)
|
||
return `${d.getDate()}. ${MONATE[d.getMonth()]} ${d.getFullYear()}, ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')} Uhr`
|
||
}
|
||
|
||
// ── Update type styling ──────────────────────────────────────────────────────
|
||
const UPDATE_STYLE: Record<UpdateType, { bg: string; color: string; label: string }> = {
|
||
ankündigung: { bg: '#EBF4FF', color: '#1D4ED8', label: 'Ankündigung' },
|
||
erinnerung: { bg: '#FFFBEB', color: '#B45309', label: 'Erinnerung' },
|
||
update: { bg: '#F3F4F6', color: '#374151', label: 'Update' },
|
||
highlight: { bg: `${NAVY}12`, color: NAVY, label: 'Highlight' },
|
||
ergebnis: { bg: '#ECFDF5', color: '#065F46', label: 'Ergebnis' },
|
||
wichtig: { bg: '#FEF2F2', color: '#991B1B', label: 'Wichtig' },
|
||
}
|
||
|
||
// ── Status badge ─────────────────────────────────────────────────────────────
|
||
const STATUS_LABEL: Record<string, { label: string; bg: string; color: string }> = {
|
||
'geplant': { label: 'In Planung', bg: `${NAVY}18`, color: NAVY },
|
||
'anmeldung-offen': { label: 'Anmeldung offen', bg: '#ECFDF5', color: '#065F46' },
|
||
'intern': { label: 'Intern', bg: '#FEF9C3', color: '#713F12' },
|
||
'abgeschlossen': { label: 'Abgeschlossen', bg: '#F3F4F6', color: '#6B7280' },
|
||
}
|
||
|
||
// ── Update Card ───────────────────────────────────────────────────────────────
|
||
function UpdateCard({ update, index }: { update: EventUpdate; index: number }) {
|
||
const style = UPDATE_STYLE[update.type] ?? UPDATE_STYLE.update
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0, x: 24 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{ duration: 0.35, delay: index * 0.07, ease: 'easeOut' }}
|
||
style={{ display: 'flex', gap: 16, position: 'relative' }}
|
||
>
|
||
{/* Timeline dot */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flexShrink: 0 }}>
|
||
<div style={{ width: 10, height: 10, borderRadius: '50%', background: GOLD, border: `2px solid ${GOLD}`, marginTop: 6, flexShrink: 0 }} />
|
||
<div style={{ flex: 1, width: 1, background: `rgba(3,9,58,0.1)`, marginTop: 6 }} />
|
||
</div>
|
||
|
||
{/* Card */}
|
||
<div style={{
|
||
flex: 1, background: '#fff', borderRadius: 12, padding: '16px 20px',
|
||
marginBottom: 12, border: '1px solid rgba(3,9,58,0.08)',
|
||
boxShadow: '0 1px 8px rgba(3,9,58,0.04)',
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, flexWrap: 'wrap' }}>
|
||
<span style={{
|
||
fontSize: 10, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||
padding: '3px 8px', borderRadius: 20,
|
||
background: style.bg, color: style.color,
|
||
}}>{style.label}</span>
|
||
<span style={{ fontSize: 11, color: 'rgba(58,58,58,0.45)' }}>{formatTs(update.timestamp)}</span>
|
||
</div>
|
||
<div style={{ fontSize: 15, fontWeight: 700, color: NAVY, marginBottom: 4, lineHeight: 1.35 }}>{update.title}</div>
|
||
<div style={{ fontSize: 14, color: 'rgba(58,58,58,0.65)', lineHeight: 1.65 }}>{update.body}</div>
|
||
</div>
|
||
</motion.div>
|
||
)
|
||
}
|
||
|
||
// ── Main Component ────────────────────────────────────────────────────────────
|
||
export default function EventDetail() {
|
||
const { slug } = useParams<{ slug: string }>()
|
||
const route = useCmsRoute()
|
||
const cmsEvent = route?.collection === 'events' ? route.doc : undefined
|
||
const event = getEventBySlug(slug ?? '') || (cmsEvent ? {
|
||
slug: String(cmsEvent.slug || slug),
|
||
title: String(cmsEvent.title || 'Event'),
|
||
subtitle: String(cmsEvent.subtitle || cmsEvent.description || cmsEvent.meta?.description || ''),
|
||
date: String(cmsEvent.date || 'Termin folgt'),
|
||
time: String(cmsEvent.time || 'Uhrzeit folgt'),
|
||
location: String(cmsEvent.location || 'Bayern'),
|
||
venue: String(cmsEvent.venue || 'Ort folgt'),
|
||
category: String(cmsEvent.category || 'Event'),
|
||
status: (cmsEvent.status as any) || 'geplant',
|
||
img: docImageUrl(cmsEvent, '/images/buehne-moderatoren.jpg'),
|
||
description: String(cmsEvent.description || cmsEvent.meta?.description || ''),
|
||
longDescription: String(cmsEvent.longDescription || cmsEvent.description || cmsEvent.meta?.description || 'Dieses Event wird aus Payload CMS geladen.'),
|
||
eckdaten: Array.isArray(cmsEvent.eckdaten) ? cmsEvent.eckdaten as any : [],
|
||
updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as any : [],
|
||
} as BmpEvent : undefined)
|
||
const [subscribed, setSubscribed] = useState(false)
|
||
const isMobile = useIsMobile()
|
||
|
||
if (!event) {
|
||
return (
|
||
<div style={{ paddingTop: 60, minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: CREAM, flexDirection: 'column', gap: 24 }}>
|
||
<div style={{ fontSize: 64, fontWeight: 900, color: `${NAVY}20`, letterSpacing: '-0.04em' }}>404</div>
|
||
<div style={{ fontSize: 20, fontWeight: 700, color: NAVY }}>Event nicht gefunden</div>
|
||
<Link to="/presse" style={{ fontSize: 15, color: NAVY, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, fontWeight: 600, borderBottom: `2px solid ${GOLD}`, paddingBottom: 2 }}>
|
||
<ArrowLeft size={14} /> Zurück zur Presse-Seite
|
||
</Link>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const statusStyle = STATUS_LABEL[event.status] ?? STATUS_LABEL['geplant']
|
||
|
||
return (
|
||
<div style={{ paddingTop: 60, background: CREAM, minHeight: '100vh', fontFamily: '"Inter", sans-serif' }}>
|
||
|
||
{/* ── Hero ─────────────────────────────────────────────────────────── */}
|
||
<section style={{ position: 'relative', height: isMobile ? 'auto' : '65vh', minHeight: isMobile ? 520 : 480, overflow: 'hidden' }}>
|
||
<img
|
||
src={event.img}
|
||
alt={event.title}
|
||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center' }}
|
||
/>
|
||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(3,9,58,0.25) 0%, rgba(3,9,58,0.92) 100%)' }} />
|
||
|
||
{/* Back link */}
|
||
<div style={{ position: 'absolute', top: 28, left: 0, right: 0, padding: isMobile ? '0 24px' : '0 80px', zIndex: 10 }}>
|
||
<Link to="/presse" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, color: 'rgba(255,255,255,0.7)', textDecoration: 'none', fontSize: 14, fontWeight: 600, transition: 'color 0.15s' }}
|
||
onMouseEnter={e => (e.currentTarget.style.color = '#fff')}
|
||
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(255,255,255,0.7)')}
|
||
>
|
||
<ArrowLeft size={14} /> Alle Events
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Hero content */}
|
||
<div style={{ position: isMobile ? 'relative' : 'absolute', bottom: 0, left: 0, right: 0, padding: isMobile ? '88px 24px 48px' : '40px 80px', zIndex: 10, display: 'flex', flexDirection: isMobile ? 'column' : 'row', justifyContent: isMobile ? 'flex-end' : 'space-between', alignItems: isMobile ? 'flex-start' : 'flex-end', gap: isMobile ? 20 : 40, minHeight: isMobile ? 520 : undefined, boxSizing: 'border-box' }}>
|
||
{/* Left */}
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14, flexWrap: 'wrap' }}>
|
||
<span style={{ fontSize: 10, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.2em', background: statusStyle.bg, color: statusStyle.color, padding: '4px 10px', borderRadius: 20 }}>
|
||
{statusStyle.label}
|
||
</span>
|
||
<span style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.15em', color: GOLD }}>
|
||
{event.category}
|
||
</span>
|
||
</div>
|
||
<h1 style={{ fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.8rem)' : 'clamp(2rem, 5vw, 4rem)', fontWeight: 900, color: '#fff', letterSpacing: '-0.03em', lineHeight: 1.05, marginBottom: 12, fontFamily: '"IBM Plex Sans", sans-serif' }}>
|
||
{event.title}
|
||
</h1>
|
||
<p style={{ fontSize: isMobile ? 14 : 16, color: 'rgba(255,255,255,0.65)', maxWidth: 520, lineHeight: 1.6 }}>
|
||
{event.subtitle}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Right – key facts */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, flexShrink: 0, background: 'rgba(3,9,58,0.55)', backdropFilter: 'blur(12px)', borderRadius: 16, padding: isMobile ? '16px 20px' : '24px 28px', border: '1px solid rgba(255,255,255,0.1)', width: isMobile ? '100%' : undefined, boxSizing: 'border-box' }}>
|
||
{[
|
||
{ icon: Calendar, label: event.date },
|
||
{ icon: Clock, label: event.time },
|
||
{ icon: MapPin, label: `${event.venue}, ${event.location}` },
|
||
].map(({ icon: Icon, label }, i) => (
|
||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
|
||
<Icon size={15} color={GOLD} style={{ flexShrink: 0 }} />
|
||
<span style={{ fontSize: 14, color: '#fff', fontWeight: 500, overflowWrap: 'anywhere' }}>{label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Gold bottom line */}
|
||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.3), transparent)` }} />
|
||
</section>
|
||
|
||
{/* ── Main Content ─────────────────────────────────────────────────── */}
|
||
<section style={{ padding: isMobile ? '48px 24px' : '64px 80px', maxWidth: 1400, margin: '0 auto' }}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 400px', gap: isMobile ? 48 : 64, alignItems: 'start' }}>
|
||
|
||
{/* Left – Description + Eckdaten */}
|
||
<div>
|
||
{/* Description */}
|
||
<div style={{ marginBottom: 56 }}>
|
||
<p style={{ fontSize: 11, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.25em', color: GOLD, marginBottom: 16 }}>
|
||
Über die Veranstaltung
|
||
</p>
|
||
{event.longDescription.split('\n\n').map((para, i) => (
|
||
<p key={i} style={{ fontSize: 17, color: 'rgba(58,58,58,0.75)', lineHeight: 1.8, marginBottom: 20 }}>{para}</p>
|
||
))}
|
||
</div>
|
||
|
||
{/* Eckdaten */}
|
||
<div>
|
||
<p style={{ fontSize: 11, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.25em', color: GOLD, marginBottom: 24 }}>
|
||
Eckdaten
|
||
</p>
|
||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 2 }}>
|
||
{event.eckdaten.map((item, i) => (
|
||
<div key={i} style={{
|
||
padding: '20px 24px',
|
||
background: i % 2 === 0 ? '#fff' : 'rgba(255,255,255,0.6)',
|
||
borderRadius: isMobile
|
||
? (i === 0
|
||
? '12px 12px 0 0'
|
||
: i === event.eckdaten.length - 1
|
||
? '0 0 12px 12px'
|
||
: '0')
|
||
: (i === 0 ? '12px 0 0 0' : i === 1 ? '0 12px 0 0' : i === event.eckdaten.length - 2 ? '0 0 0 12px' : i === event.eckdaten.length - 1 ? '0 0 12px 0' : '0'),
|
||
border: '1px solid rgba(3,9,58,0.06)',
|
||
}}>
|
||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: 'rgba(58,58,58,0.4)', marginBottom: 6 }}>
|
||
{item.label}
|
||
</div>
|
||
<div style={{ fontSize: 16, fontWeight: 700, color: NAVY, lineHeight: 1.35 }}>
|
||
{item.value}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* CTA */}
|
||
<div style={{ marginTop: 40, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||
{event.status === 'anmeldung-offen' && (
|
||
<Link to="/teilnahme" style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||
background: NAVY, color: GOLD, textDecoration: 'none',
|
||
padding: '14px 28px', borderRadius: 10, fontWeight: 700, fontSize: 15,
|
||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||
}}>
|
||
Jetzt anmelden <ChevronRight size={16} />
|
||
</Link>
|
||
)}
|
||
<Link to="/kontakt" style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||
background: 'transparent', color: NAVY, textDecoration: 'none',
|
||
padding: '14px 28px', borderRadius: 10, fontWeight: 700, fontSize: 15,
|
||
border: `2px solid ${NAVY}`,
|
||
}}>
|
||
Frage stellen
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right – Live Updates Feed (sticky on desktop, static on mobile) */}
|
||
<div style={isMobile ? undefined : { position: 'sticky', top: 80 }}>
|
||
{/* Header */}
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<Bell size={18} color={NAVY} />
|
||
<span style={{ fontSize: 17, fontWeight: 800, color: NAVY, fontFamily: '"IBM Plex Sans", sans-serif' }}>
|
||
Live Updates
|
||
</span>
|
||
<span style={{
|
||
fontSize: 11, fontWeight: 800, background: NAVY, color: GOLD,
|
||
borderRadius: 20, padding: '2px 8px',
|
||
}}>{event.updates.length}</span>
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => setSubscribed(s => !s)}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer',
|
||
background: subscribed ? NAVY : 'transparent',
|
||
color: subscribed ? GOLD : NAVY,
|
||
border: `1.5px solid ${NAVY}`, borderRadius: 8,
|
||
padding: '8px 14px', fontSize: 12, fontWeight: 700,
|
||
transition: 'all 0.2s', fontFamily: 'inherit',
|
||
}}
|
||
>
|
||
{subscribed ? <><BellOff size={13} /> Abgemeldet</> : <><Bell size={13} /> Abonnieren</>}
|
||
</button>
|
||
</div>
|
||
|
||
{subscribed && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
style={{ background: '#ECFDF5', border: '1px solid #A7F3D0', borderRadius: 10, padding: '10px 14px', marginBottom: 16, fontSize: 14, color: '#065F46', fontWeight: 500 }}
|
||
>
|
||
✓ Sie erhalten Benachrichtigungen für dieses Event.
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* Timeline */}
|
||
<div style={{ maxHeight: isMobile ? 'none' : 560, overflowY: isMobile ? 'visible' : 'auto', paddingRight: 4 }}>
|
||
{event.updates.map((update, i) => (
|
||
<UpdateCard key={update.id} update={update} index={i} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* ── Bottom CTA ───────────────────────────────────────────────────── */}
|
||
<section style={{ background: NAVY, padding: isMobile ? '48px 24px' : '64px 80px', marginTop: 40 }}>
|
||
<div style={{ maxWidth: 1400, margin: '0 auto', display: 'flex', flexDirection: isMobile ? 'column' : 'row', justifyContent: 'space-between', alignItems: isMobile ? 'flex-start' : 'center', gap: isMobile ? 24 : 0 }}>
|
||
<div>
|
||
<p style={{ fontSize: 11, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.25em', color: GOLD, marginBottom: 10 }}>
|
||
Mehr entdecken
|
||
</p>
|
||
<h3 style={{ fontSize: 28, fontWeight: 900, color: '#fff', letterSpacing: '-0.03em', fontFamily: '"IBM Plex Sans", sans-serif' }}>
|
||
Weitere Veranstaltungen
|
||
</h3>
|
||
</div>
|
||
<Link
|
||
to="/presse"
|
||
style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 8, textDecoration: 'none',
|
||
background: GOLD, color: NAVY, padding: '14px 28px', borderRadius: 10,
|
||
fontWeight: 800, fontSize: 15, fontFamily: '"IBM Plex Sans", sans-serif',
|
||
transition: 'background 0.15s, box-shadow 0.2s',
|
||
}}
|
||
onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.background = '#FFD130'; el.style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
|
||
onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = GOLD; el.style.boxShadow = 'none'; }}
|
||
>
|
||
Alle Events <ChevronRight size={16} />
|
||
</Link>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)
|
||
}
|