feat: manage event detail chrome via payload

This commit is contained in:
syntaxbullet
2026-06-22 11:51:47 +02:00
parent a19b703416
commit 43f85dfea2
7 changed files with 373 additions and 41 deletions

View File

@@ -20,6 +20,7 @@
"preload:about-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-about-page-cms.ts",
"preload:contact-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-contact-page-cms.ts",
"preload:datenschutz-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-datenschutz-page-cms.ts",
"preload:event-detail-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-event-detail-cms.ts",
"preload:formular-upload-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-formular-upload-page-cms.ts",
"preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts",
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",

View File

@@ -3,6 +3,7 @@ import type { CollectionConfig } from 'payload'
import { authenticated } from '@/access/authenticated'
import { authenticatedOrPublished } from '@/access/authenticatedOrPublished'
import { populatePublishedAt } from '@/hooks/populatePublishedAt'
import { eventDetailContent } from '@/spa/eventDetailContent'
import { generatePreviewPath } from '@/utilities/generatePreviewPath'
import { slugField } from 'payload'
@@ -120,6 +121,97 @@ export const Events: CollectionConfig = {
{ name: 'body', type: 'textarea', required: true },
],
},
{
name: 'detailPage',
label: 'Detail page chrome',
type: 'group',
admin: {
description:
'Shared labels, status/update labels, subscription copy, CTAs, and formatting copy for the Event detail route.',
},
fields: [
{
name: 'notFound',
label: 'Not found state',
type: 'group',
fields: [
{ name: 'title', type: 'text', defaultValue: eventDetailContent.notFound.title },
{ name: 'backLabel', type: 'text', defaultValue: eventDetailContent.notFound.backLabel },
{ name: 'backUrl', type: 'text', defaultValue: eventDetailContent.notFound.backUrl },
],
},
{
name: 'dateFormat',
label: 'Date format',
type: 'group',
fields: [
{
name: 'monthNames',
type: 'json',
defaultValue: eventDetailContent.dateFormat.monthNames,
admin: { description: 'Array of 12 month names used when formatting update timestamps.' },
},
{ name: 'timeSuffix', type: 'text', defaultValue: eventDetailContent.dateFormat.timeSuffix },
],
},
{
name: 'updateStyles',
label: 'Update type labels/styles JSON',
type: 'json',
defaultValue: eventDetailContent.updateStyles,
},
{
name: 'statusStyles',
label: 'Status labels/styles JSON',
type: 'json',
defaultValue: eventDetailContent.statusStyles,
},
{
name: 'heroBackLink',
label: 'Hero back link',
type: 'group',
fields: [
{ name: 'label', type: 'text', defaultValue: eventDetailContent.heroBackLink.label },
{ name: 'url', type: 'text', defaultValue: eventDetailContent.heroBackLink.url },
],
},
{
name: 'sections',
label: 'Section labels and CTAs',
type: 'group',
fields: [
{ name: 'aboutEyebrow', type: 'text', defaultValue: eventDetailContent.sections.aboutEyebrow },
{ name: 'factsEyebrow', type: 'text', defaultValue: eventDetailContent.sections.factsEyebrow },
{ name: 'registerLabel', type: 'text', defaultValue: eventDetailContent.sections.registerLabel },
{ name: 'registerUrl', type: 'text', defaultValue: eventDetailContent.sections.registerUrl },
{ name: 'questionLabel', type: 'text', defaultValue: eventDetailContent.sections.questionLabel },
{ name: 'questionUrl', type: 'text', defaultValue: eventDetailContent.sections.questionUrl },
],
},
{
name: 'updatesCopy',
label: 'Updates panel',
type: 'group',
fields: [
{ name: 'heading', type: 'text', defaultValue: eventDetailContent.updates.heading },
{ name: 'subscribeLabel', type: 'text', defaultValue: eventDetailContent.updates.subscribeLabel },
{ name: 'unsubscribeLabel', type: 'text', defaultValue: eventDetailContent.updates.unsubscribeLabel },
{ name: 'subscribedMessage', type: 'text', defaultValue: eventDetailContent.updates.subscribedMessage },
],
},
{
name: 'bottomCta',
label: 'Bottom CTA',
type: 'group',
fields: [
{ name: 'eyebrow', type: 'text', defaultValue: eventDetailContent.bottomCta.eyebrow },
{ name: 'heading', type: 'text', defaultValue: eventDetailContent.bottomCta.heading },
{ name: 'label', type: 'text', defaultValue: eventDetailContent.bottomCta.label },
{ name: 'url', type: 'text', defaultValue: eventDetailContent.bottomCta.url },
],
},
],
},
{
name: 'spaPath',
type: 'text',

View File

@@ -2549,6 +2549,73 @@ export interface Event {
id?: string | null;
}[]
| null;
/**
* Shared labels, status/update labels, subscription copy, CTAs, and formatting copy for the Event detail route.
*/
detailPage?: {
notFound?: {
title?: string | null;
backLabel?: string | null;
backUrl?: string | null;
};
dateFormat?: {
/**
* Array of 12 month names used when formatting update timestamps.
*/
monthNames?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
timeSuffix?: string | null;
};
updateStyles?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
statusStyles?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
heroBackLink?: {
label?: string | null;
url?: string | null;
};
sections?: {
aboutEyebrow?: string | null;
factsEyebrow?: string | null;
registerLabel?: string | null;
registerUrl?: string | null;
questionLabel?: string | null;
questionUrl?: string | null;
};
updatesCopy?: {
heading?: string | null;
subscribeLabel?: string | null;
unsubscribeLabel?: string | null;
subscribedMessage?: string | null;
};
bottomCta?: {
eyebrow?: string | null;
heading?: string | null;
label?: string | null;
url?: string | null;
};
};
/**
* Route in the legacy SPA used for previewing the current migrated event state.
*/
@@ -4455,6 +4522,57 @@ export interface EventsSelect<T extends boolean = true> {
body?: T;
id?: T;
};
detailPage?:
| T
| {
notFound?:
| T
| {
title?: T;
backLabel?: T;
backUrl?: T;
};
dateFormat?:
| T
| {
monthNames?: T;
timeSuffix?: T;
};
updateStyles?: T;
statusStyles?: T;
heroBackLink?:
| T
| {
label?: T;
url?: T;
};
sections?:
| T
| {
aboutEyebrow?: T;
factsEyebrow?: T;
registerLabel?: T;
registerUrl?: T;
questionLabel?: T;
questionUrl?: T;
};
updatesCopy?:
| T
| {
heading?: T;
subscribeLabel?: T;
unsubscribeLabel?: T;
subscribedMessage?: T;
};
bottomCta?:
| T
| {
eyebrow?: T;
heading?: T;
label?: T;
url?: T;
};
};
spaPath?: T;
publishedAt?: T;
meta?:

View File

@@ -5,6 +5,7 @@ import 'dotenv/config'
import { getPayload } from 'payload'
import { articles } from '@/spa/data/blog'
import { eventDetailContent } from '@/spa/eventDetailContent'
import { events } from '@/spa/data/events'
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
import { WINNERS } from '@/spa/data/winners'
@@ -155,6 +156,10 @@ function eventData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
status: event?.status || 'geplant',
eckdaten: event?.eckdaten || [],
updates: event?.updates?.map(({ id: _id, ...update }) => update) || [],
detailPage: {
...eventDetailContent,
updatesCopy: eventDetailContent.updates,
},
}
}

View File

@@ -0,0 +1,42 @@
import 'dotenv/config'
import config from '@payload-config'
import { getPayload } from 'payload'
import { eventDetailContent } from '@/spa/eventDetailContent'
async function main() {
const payload = await getPayload({ config })
const events = await payload.find({
collection: 'events',
draft: true,
overrideAccess: true,
pagination: false,
limit: 1000,
})
await Promise.all(
events.docs.map((event) =>
payload.update({
collection: 'events',
id: event.id,
overrideAccess: true,
context: { disableRevalidate: true },
data: {
detailPage: {
...eventDetailContent,
updatesCopy: eventDetailContent.updates,
},
} as never,
}),
),
)
payload.logger.info(`Preloaded Event detail CMS fields for ${events.docs.length} records`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})

View File

@@ -0,0 +1,49 @@
export const eventDetailContent = {
notFound: {
title: 'Event nicht gefunden',
backLabel: 'Zurück zur Presse-Seite',
backUrl: '/presse',
},
dateFormat: {
monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
timeSuffix: 'Uhr',
},
updateStyles: {
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: '#111D5512', color: '#111D55', label: 'Highlight' },
ergebnis: { bg: '#ECFDF5', color: '#065F46', label: 'Ergebnis' },
wichtig: { bg: '#FEF2F2', color: '#991B1B', label: 'Wichtig' },
},
statusStyles: {
geplant: { label: 'In Planung', bg: '#111D5518', color: '#111D55' },
'anmeldung-offen': { label: 'Anmeldung offen', bg: '#ECFDF5', color: '#065F46' },
intern: { label: 'Intern', bg: '#FEF9C3', color: '#713F12' },
abgeschlossen: { label: 'Abgeschlossen', bg: '#F3F4F6', color: '#6B7280' },
},
heroBackLink: {
label: 'Alle Events',
url: '/presse',
},
sections: {
aboutEyebrow: 'Über die Veranstaltung',
factsEyebrow: 'Eckdaten',
registerLabel: 'Jetzt anmelden',
registerUrl: '/teilnahme',
questionLabel: 'Frage stellen',
questionUrl: '/kontakt',
},
updates: {
heading: 'Live Updates',
subscribeLabel: 'Abonnieren',
unsubscribeLabel: 'Abgemeldet',
subscribedMessage: '✓ Sie erhalten Benachrichtigungen für dieses Event.',
},
bottomCta: {
eyebrow: 'Mehr entdecken',
heading: 'Weitere Veranstaltungen',
label: 'Alle Events',
url: '/presse',
},
}

View File

@@ -2,44 +2,61 @@ import React, { useState } from 'react'
import { useParams, Link } from '@/spa/router'
import { motion } from 'framer-motion'
import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight } from 'lucide-react'
import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent, type EventEckdaten } from '@/spa/data/events'
import { getEventBySlug, EventUpdate, type BmpEvent, type EventEckdaten } from '@/spa/data/events'
import { useCmsRoute } from '@/spa/cmsRoute'
import { docImageUrl } from '@/spa/cmsMediaField'
import { useIsMobile } from '@/spa/hooks/useIsMobile'
import { eventDetailContent } from '@/spa/eventDetailContent'
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55'
const GOLD = '#EFBF04'
const CREAM = '#EFE5E3'
type EventDetailCms = Omit<Partial<typeof eventDetailContent>, 'updateStyles' | 'statusStyles'> & {
updateStyles?: unknown
statusStyles?: unknown
updatesCopy?: Partial<typeof eventDetailContent.updates>
}
type StyleEntry = { bg: string; color: string; label: string }
type StyleMap = Record<string, StyleEntry>
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback
// ── Timestamp formatter ──────────────────────────────────────────────────────
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']
function formatTs(iso: string): string {
function formatTs(iso: string, detail: EventDetailCms): 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`
const monthNames = Array.isArray(detail.dateFormat?.monthNames) && detail.dateFormat.monthNames.length >= 12
? detail.dateFormat.monthNames
: eventDetailContent.dateFormat.monthNames
return `${d.getDate()}. ${monthNames[d.getMonth()]} ${d.getFullYear()}, ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')} ${fallbackText(detail.dateFormat?.timeSuffix, eventDetailContent.dateFormat.timeSuffix)}`
}
// ── 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' },
const asStyleMap = (value: unknown, fallback: StyleMap): StyleMap => {
if (!value || typeof value !== 'object') return fallback
return { ...fallback, ...(value as StyleMap) }
}
// ── 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' },
const getDetailPage = (value: unknown): EventDetailCms => {
const detail = (value && typeof value === 'object' ? value : {}) as EventDetailCms
return {
...eventDetailContent,
...detail,
notFound: { ...eventDetailContent.notFound, ...detail.notFound },
dateFormat: { ...eventDetailContent.dateFormat, ...detail.dateFormat },
updateStyles: asStyleMap(detail.updateStyles, eventDetailContent.updateStyles),
statusStyles: asStyleMap(detail.statusStyles, eventDetailContent.statusStyles),
heroBackLink: { ...eventDetailContent.heroBackLink, ...detail.heroBackLink },
sections: { ...eventDetailContent.sections, ...detail.sections },
updatesCopy: { ...eventDetailContent.updates, ...(detail.updatesCopy || detail.updates) },
bottomCta: { ...eventDetailContent.bottomCta, ...detail.bottomCta },
}
}
// ── Update Card ───────────────────────────────────────────────────────────────
function UpdateCard({ update, index }: { update: EventUpdate; index: number }) {
const style = UPDATE_STYLE[update.type] ?? UPDATE_STYLE.update
function UpdateCard({ update, index, detail }: { update: EventUpdate; index: number; detail: EventDetailCms }) {
const styles = asStyleMap(detail.updateStyles, eventDetailContent.updateStyles)
const style = styles[update.type] ?? styles.update
return (
<motion.div
initial={{ opacity: 0, x: 24 }}
@@ -65,7 +82,7 @@ function UpdateCard({ update, index }: { update: EventUpdate; index: number }) {
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>
<span style={{ fontSize: 11, color: 'rgba(58,58,58,0.45)' }}>{formatTs(update.timestamp, detail)}</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>
@@ -94,7 +111,14 @@ export default function EventDetail() {
longDescription: String(cmsEvent.longDescription || cmsEvent.description || cmsEvent.meta?.description || 'Dieses Event wird aus Payload CMS geladen.'),
eckdaten: Array.isArray(cmsEvent.eckdaten) ? cmsEvent.eckdaten as EventEckdaten[] : [],
updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as EventUpdate[] : [],
detailPage: cmsEvent.detailPage,
} as BmpEvent : undefined)
const detail = getDetailPage(event && 'detailPage' in event ? event.detailPage : undefined)
const notFound = detail.notFound || eventDetailContent.notFound
const heroBackLink = detail.heroBackLink || eventDetailContent.heroBackLink
const sections = detail.sections || eventDetailContent.sections
const updatesCopy = detail.updatesCopy || eventDetailContent.updates
const bottomCta = detail.bottomCta || eventDetailContent.bottomCta
const [subscribed, setSubscribed] = useState(false)
const isMobile = useIsMobile()
@@ -102,15 +126,16 @@ export default function EventDetail() {
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
<div style={{ fontSize: 20, fontWeight: 700, color: NAVY }}>{fallbackText(notFound.title, eventDetailContent.notFound.title)}</div>
<Link to={fallbackText(notFound.backUrl, eventDetailContent.notFound.backUrl)} style={{ fontSize: 15, color: NAVY, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, fontWeight: 600, borderBottom: `2px solid ${GOLD}`, paddingBottom: 2 }}>
<ArrowLeft size={14} /> {fallbackText(notFound.backLabel, eventDetailContent.notFound.backLabel)}
</Link>
</div>
)
}
const statusStyle = STATUS_LABEL[event.status] ?? STATUS_LABEL['geplant']
const statusStyles = asStyleMap(detail.statusStyles, eventDetailContent.statusStyles)
const statusStyle = statusStyles[event.status] ?? statusStyles.geplant
return (
<div style={{ paddingTop: 60, background: CREAM, minHeight: '100vh', fontFamily: '"Inter", sans-serif' }}>
@@ -126,11 +151,11 @@ export default function EventDetail() {
{/* 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' }}
<Link to={fallbackText(heroBackLink.url, eventDetailContent.heroBackLink.url)} 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
<ArrowLeft size={14} /> {fallbackText(heroBackLink.label, eventDetailContent.heroBackLink.label)}
</Link>
</div>
@@ -182,7 +207,7 @@ export default function EventDetail() {
{/* Description */}
<div style={{ marginBottom: 56 }}>
<p style={{ fontSize: 11, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.25em', color: GOLD, marginBottom: 16 }}>
Über die Veranstaltung
{fallbackText(sections.aboutEyebrow, eventDetailContent.sections.aboutEyebrow)}
</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>
@@ -192,7 +217,7 @@ export default function EventDetail() {
{/* Eckdaten */}
<div>
<p style={{ fontSize: 11, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.25em', color: GOLD, marginBottom: 24 }}>
Eckdaten
{fallbackText(sections.factsEyebrow, eventDetailContent.sections.factsEyebrow)}
</p>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 2 }}>
{event.eckdaten.map((item, i) => (
@@ -222,22 +247,22 @@ export default function EventDetail() {
{/* CTA */}
<div style={{ marginTop: 40, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{event.status === 'anmeldung-offen' && (
<Link to="/teilnahme" style={{
<Link to={fallbackText(sections.registerUrl, eventDetailContent.sections.registerUrl)} 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} />
{fallbackText(sections.registerLabel, eventDetailContent.sections.registerLabel)} <ChevronRight size={16} />
</Link>
)}
<Link to="/kontakt" style={{
<Link to={fallbackText(sections.questionUrl, eventDetailContent.sections.questionUrl)} 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
{fallbackText(sections.questionLabel, eventDetailContent.sections.questionLabel)}
</Link>
</div>
</div>
@@ -249,7 +274,7 @@ export default function EventDetail() {
<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
{fallbackText(updatesCopy.heading, eventDetailContent.updates.heading)}
</span>
<span style={{
fontSize: 11, fontWeight: 800, background: NAVY, color: GOLD,
@@ -268,7 +293,7 @@ export default function EventDetail() {
transition: 'all 0.2s', fontFamily: 'inherit',
}}
>
{subscribed ? <><BellOff size={13} /> Abgemeldet</> : <><Bell size={13} /> Abonnieren</>}
{subscribed ? <><BellOff size={13} /> {fallbackText(updatesCopy.unsubscribeLabel, eventDetailContent.updates.unsubscribeLabel)}</> : <><Bell size={13} /> {fallbackText(updatesCopy.subscribeLabel, eventDetailContent.updates.subscribeLabel)}</>}
</button>
</div>
@@ -278,14 +303,14 @@ export default function EventDetail() {
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.
{fallbackText(updatesCopy.subscribedMessage, eventDetailContent.updates.subscribedMessage)}
</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} />
<UpdateCard key={update.id} update={update} index={i} detail={detail} />
))}
</div>
</div>
@@ -297,14 +322,14 @@ export default function EventDetail() {
<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
{fallbackText(bottomCta.eyebrow, eventDetailContent.bottomCta.eyebrow)}
</p>
<h3 style={{ fontSize: 28, fontWeight: 900, color: '#fff', letterSpacing: '-0.03em', fontFamily: '"IBM Plex Sans", sans-serif' }}>
Weitere Veranstaltungen
{fallbackText(bottomCta.heading, eventDetailContent.bottomCta.heading)}
</h3>
</div>
<Link
to="/presse"
to={fallbackText(bottomCta.url, eventDetailContent.bottomCta.url)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8, textDecoration: 'none',
background: GOLD, color: NAVY, padding: '14px 28px', borderRadius: 10,
@@ -314,7 +339,7 @@ export default function EventDetail() {
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} />
{fallbackText(bottomCta.label, eventDetailContent.bottomCta.label)} <ChevronRight size={16} />
</Link>
</div>
</section>