feat: manage event detail chrome via payload
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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?:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
src/scripts/preload-event-detail-cms.ts
Normal file
42
src/scripts/preload-event-detail-cms.ts
Normal 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)
|
||||
})
|
||||
49
src/spa/eventDetailContent.ts
Normal file
49
src/spa/eventDetailContent.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user