chore: migrate pages and media to be cms controlled.
This commit is contained in:
@@ -2,14 +2,14 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import CmsMediaRewriter from '@/spa/components/CmsMediaRewriter'
|
||||
import Layout from '@/spa/components/layout/Layout'
|
||||
import ScrollToTop from '@/spa/components/layout/ScrollToTop'
|
||||
import type { SpaFooterData, SpaHeaderData } from '@/spa/cmsNavigation'
|
||||
import type { SpaMediaMap } from '@/spa/cmsMedia'
|
||||
import { CmsRouteProvider, type CmsRouteData } from '@/spa/cmsRoute'
|
||||
import About from '@/spa/pages/About'
|
||||
import BlogDetail from '@/spa/pages/BlogDetail'
|
||||
import Contact from '@/spa/pages/Contact'
|
||||
import CmsGenericPage from '@/spa/pages/CmsGenericPage'
|
||||
import Datenschutz from '@/spa/pages/Datenschutz'
|
||||
import EventDetail from '@/spa/pages/EventDetail'
|
||||
import FormularUpload from '@/spa/pages/FormularUpload'
|
||||
@@ -22,7 +22,7 @@ import Preistraeger from '@/spa/pages/Preistraeger'
|
||||
import PreistraegerDetail from '@/spa/pages/PreistraegerDetail'
|
||||
import Press from '@/spa/pages/Press'
|
||||
|
||||
const routes: Record<string, React.ComponentType> = {
|
||||
export const routes: Record<string, React.ComponentType> = {
|
||||
'/': Home,
|
||||
'/teilnahme': Participation,
|
||||
'/preistraeger': Preistraeger,
|
||||
@@ -36,24 +36,33 @@ const routes: Record<string, React.ComponentType> = {
|
||||
'/impressum': Impressum,
|
||||
}
|
||||
|
||||
export function isSpaRoute(pathname: string) {
|
||||
return Boolean(
|
||||
routes[pathname] ||
|
||||
pathname.startsWith('/preistraeger/') ||
|
||||
pathname.startsWith('/presse/events/') ||
|
||||
pathname.startsWith('/presse/blog/'),
|
||||
)
|
||||
}
|
||||
|
||||
function resolvePage(pathname: string) {
|
||||
if (routes[pathname]) return routes[pathname]
|
||||
if (pathname.startsWith('/preistraeger/')) return PreistraegerDetail
|
||||
if (pathname.startsWith('/presse/events/')) return EventDetail
|
||||
if (pathname.startsWith('/presse/blog/')) return BlogDetail
|
||||
return Home
|
||||
return CmsGenericPage
|
||||
}
|
||||
|
||||
export default function NextApp({
|
||||
pathname,
|
||||
header,
|
||||
footer,
|
||||
media,
|
||||
cmsRoute,
|
||||
}: {
|
||||
pathname: string
|
||||
header?: SpaHeaderData
|
||||
footer?: SpaFooterData
|
||||
media?: SpaMediaMap
|
||||
cmsRoute?: CmsRouteData
|
||||
}) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
@@ -82,11 +91,12 @@ export default function NextApp({
|
||||
|
||||
return (
|
||||
<>
|
||||
<CmsMediaRewriter media={media} />
|
||||
<ScrollToTop />
|
||||
<Layout header={header} footer={footer}>
|
||||
<Page />
|
||||
</Layout>
|
||||
<CmsRouteProvider value={cmsRoute}>
|
||||
<ScrollToTop />
|
||||
<Layout header={header} footer={footer}>
|
||||
<Page />
|
||||
</Layout>
|
||||
</CmsRouteProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { Media } from '@/payload-types'
|
||||
|
||||
export type SpaMediaEntry = {
|
||||
source: string
|
||||
media?: { url?: string } | string | number | Media
|
||||
}
|
||||
|
||||
export type SpaMediaMap = Record<string, string>
|
||||
|
||||
export const defaultSpaMediaSources = [
|
||||
'/bmp-logo.png',
|
||||
'/logo.png',
|
||||
'/gala-preistraeger.jpg',
|
||||
'/munich-skyline.jpg',
|
||||
'/bavarian-flag.jpg',
|
||||
'/rauten.webp',
|
||||
'/website-template-OG.webp',
|
||||
'/images/bmp-banner.jpg',
|
||||
'/images/buehne-gewinner.jpg',
|
||||
'/images/buehne-moderatoren.jpg',
|
||||
'/images/ewif-logo.png',
|
||||
'/images/gala-dinner.jpg',
|
||||
'/images/gala-saal-overview.jpg',
|
||||
'/images/gewinner-gruppenfoto.jpg',
|
||||
'/images/mitglied-hero.jpg',
|
||||
'/images/netzwerk-hero.jpg',
|
||||
'/images/networking-innenhof.jpg',
|
||||
'/images/partner-logos.png',
|
||||
'/images/preisuebergabe.jpg',
|
||||
'/images/preistraeger-award.jpg',
|
||||
'/images/preistraeger-gruppe.jpg',
|
||||
'/images/preistraeger-jubel.jpg',
|
||||
'/images/saal-gedeckt.jpg',
|
||||
'/images/saal-leer-atmosphaere.jpg',
|
||||
'/images/tisch-detail.jpg',
|
||||
]
|
||||
|
||||
const isMedia = (value: unknown): value is Media => Boolean(value && typeof value === 'object' && 'url' in value)
|
||||
|
||||
export function normalizeSpaMediaMap(global: any): SpaMediaMap {
|
||||
const entries: SpaMediaEntry[] = Array.isArray(global?.assets) ? global.assets : []
|
||||
|
||||
return entries.reduce((acc: SpaMediaMap, entry: SpaMediaEntry) => {
|
||||
if (!entry?.source) return acc
|
||||
if (isMedia(entry.media) && entry.media.url) acc[entry.source] = entry.media.url
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
23
src/spa/cmsMediaField.ts
Normal file
23
src/spa/cmsMediaField.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { CmsRouteDoc } from './cmsRoute'
|
||||
|
||||
type MediaLike = {
|
||||
url?: string | null
|
||||
filename?: string | null
|
||||
alt?: string | null
|
||||
}
|
||||
|
||||
export function mediaUrl(media: unknown, fallback: string) {
|
||||
if (!media || typeof media !== 'object') return fallback
|
||||
const doc = media as MediaLike
|
||||
return doc.url || (doc.filename ? `/media/${doc.filename}` : fallback)
|
||||
}
|
||||
|
||||
export function mediaAlt(media: unknown, fallback: string) {
|
||||
if (!media || typeof media !== 'object') return fallback
|
||||
const doc = media as MediaLike
|
||||
return doc.alt || fallback
|
||||
}
|
||||
|
||||
export function docImageUrl(doc: CmsRouteDoc | undefined, fallback: string, field = 'image') {
|
||||
return mediaUrl(doc?.[field], fallback)
|
||||
}
|
||||
38
src/spa/cmsRoute.tsx
Normal file
38
src/spa/cmsRoute.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
export type CmsRouteCollection = 'pages' | 'posts' | 'events' | 'preistraeger'
|
||||
|
||||
export type CmsRouteDoc = {
|
||||
id: number | string
|
||||
title?: string | null
|
||||
slug?: string | null
|
||||
spaPath?: string | null
|
||||
description?: string | null
|
||||
meta?: {
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
} | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type CmsRouteData = {
|
||||
collection: CmsRouteCollection
|
||||
doc: CmsRouteDoc
|
||||
lists?: Partial<Record<CmsRouteCollection, CmsRouteDoc[]>>
|
||||
}
|
||||
|
||||
const CmsRouteContext = createContext<CmsRouteData | undefined>(undefined)
|
||||
|
||||
export function CmsRouteProvider({ children, value }: { children: React.ReactNode; value?: CmsRouteData }) {
|
||||
return <CmsRouteContext.Provider value={value}>{children}</CmsRouteContext.Provider>
|
||||
}
|
||||
|
||||
export function useCmsRoute() {
|
||||
return useContext(CmsRouteContext)
|
||||
}
|
||||
|
||||
export function useCmsCollection(collection: CmsRouteCollection) {
|
||||
return useCmsRoute()?.lists?.[collection] || []
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { SpaMediaMap } from '@/spa/cmsMedia'
|
||||
|
||||
const CSS_URL_RE = /url\((['"]?)(.*?)\1\)/g
|
||||
|
||||
function canonical(value: string) {
|
||||
if (!value) return value
|
||||
try {
|
||||
const url = new URL(value, window.location.origin)
|
||||
return url.origin === window.location.origin ? `${url.pathname}${url.search}` : value
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteCssUrls(value: string, media: SpaMediaMap) {
|
||||
return value.replace(CSS_URL_RE, (_match, quote: string, url: string) => {
|
||||
const replacement = media[url] || media[canonical(url)]
|
||||
return replacement ? `url(${quote}${replacement}${quote})` : _match
|
||||
})
|
||||
}
|
||||
|
||||
function rewrite(root: ParentNode, media: SpaMediaMap) {
|
||||
root.querySelectorAll('img, source, video').forEach((node) => {
|
||||
const el = node as HTMLImageElement | HTMLSourceElement | HTMLVideoElement
|
||||
const current = el.getAttribute('src')
|
||||
if (current) {
|
||||
const replacement = media[current] || media[canonical(current)]
|
||||
if (replacement && current !== replacement) el.setAttribute('src', replacement)
|
||||
}
|
||||
|
||||
const poster = el.getAttribute('poster')
|
||||
if (poster) {
|
||||
const replacement = media[poster] || media[canonical(poster)]
|
||||
if (replacement && poster !== replacement) el.setAttribute('poster', replacement)
|
||||
}
|
||||
})
|
||||
|
||||
root.querySelectorAll<HTMLElement>('[style*="url("]').forEach((el) => {
|
||||
const backgroundImage = el.style.backgroundImage
|
||||
if (backgroundImage) {
|
||||
const rewritten = rewriteCssUrls(backgroundImage, media)
|
||||
if (rewritten !== backgroundImage) el.style.backgroundImage = rewritten
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default function CmsMediaRewriter({ media }: { media?: SpaMediaMap }) {
|
||||
useEffect(() => {
|
||||
if (!media || Object.keys(media).length === 0) return
|
||||
|
||||
rewrite(document, media)
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) rewrite(node as Element, media)
|
||||
})
|
||||
if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
|
||||
rewrite((mutation.target as Element).parentNode || document, media)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['src', 'poster', 'style'],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [media])
|
||||
|
||||
return null
|
||||
}
|
||||
60
src/spa/pageRegistry.ts
Normal file
60
src/spa/pageRegistry.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { articles } from './data/blog'
|
||||
import { events } from './data/events'
|
||||
import { WINNERS } from './data/winners'
|
||||
|
||||
export type SpaPageRegistryEntry = {
|
||||
path: string
|
||||
slug: string
|
||||
title: string
|
||||
description?: string
|
||||
source: 'static' | 'winner' | 'event' | 'blog'
|
||||
}
|
||||
|
||||
const staticPages: SpaPageRegistryEntry[] = [
|
||||
{ path: '/', slug: 'startseite', title: 'Startseite', source: 'static' },
|
||||
{ path: '/teilnahme', slug: 'teilnahme', title: 'Teilnahme', source: 'static' },
|
||||
{ path: '/preistraeger', slug: 'preistraeger', title: 'Preisträger', source: 'static' },
|
||||
{ path: '/der-bmp', slug: 'der-bmp', title: 'Der BMP', source: 'static' },
|
||||
{ path: '/netzwerk', slug: 'netzwerk', title: 'Netzwerk', source: 'static' },
|
||||
{ path: '/presse', slug: 'presse', title: 'Presse', source: 'static' },
|
||||
{ path: '/kontakt', slug: 'kontakt', title: 'Kontakt', source: 'static' },
|
||||
{
|
||||
path: '/formular-hochladen',
|
||||
slug: 'formular-hochladen',
|
||||
title: 'Formular hochladen',
|
||||
source: 'static',
|
||||
},
|
||||
{
|
||||
path: '/mitglied-werden',
|
||||
slug: 'mitglied-werden',
|
||||
title: 'Mitglied werden',
|
||||
source: 'static',
|
||||
},
|
||||
{ path: '/datenschutz', slug: 'datenschutz', title: 'Datenschutz', source: 'static' },
|
||||
{ path: '/impressum', slug: 'impressum', title: 'Impressum', source: 'static' },
|
||||
]
|
||||
|
||||
export const spaPages: SpaPageRegistryEntry[] = [
|
||||
...staticPages,
|
||||
...WINNERS.map((winner) => ({
|
||||
path: `/preistraeger/${winner.slug}`,
|
||||
slug: `preistraeger-${winner.slug}`,
|
||||
title: winner.name,
|
||||
description: winner.shortDesc,
|
||||
source: 'winner' as const,
|
||||
})),
|
||||
...events.map((event) => ({
|
||||
path: `/presse/events/${event.slug}`,
|
||||
slug: `presse-events-${event.slug}`,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
source: 'event' as const,
|
||||
})),
|
||||
...articles.map((article) => ({
|
||||
path: `/presse/blog/${article.slug}`,
|
||||
slug: `presse-blog-${article.slug}`,
|
||||
title: article.title,
|
||||
description: article.excerpt,
|
||||
source: 'blog' as const,
|
||||
})),
|
||||
]
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import { useParams, Link } from '@/spa/router';
|
||||
import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getArticleBySlug, articles } from '@/spa/data/blog';
|
||||
import { getArticleBySlug, articles, type BlogArticle } from '@/spa/data/blog';
|
||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
|
||||
const NAVY = '#111D55';
|
||||
@@ -21,7 +23,22 @@ const CAT_COLORS: Record<string, string> = {
|
||||
export default function BlogDetail() {
|
||||
const isMobile = useIsMobile();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const article = slug ? getArticleBySlug(slug) : undefined;
|
||||
const route = useCmsRoute();
|
||||
const cmsArticle = route?.collection === 'posts' ? route.doc : undefined;
|
||||
const article = (slug ? getArticleBySlug(slug) : undefined) || (cmsArticle ? {
|
||||
slug: String(cmsArticle.slug || slug),
|
||||
title: String(cmsArticle.title || 'Presse'),
|
||||
cat: String(cmsArticle.cat || 'Presse'),
|
||||
date: String(cmsArticle.date || 'Aktuell'),
|
||||
author: String(cmsArticle.authorName || 'BMP Redaktion'),
|
||||
authorRole: String(cmsArticle.authorRole || 'Presse'),
|
||||
readTime: String(cmsArticle.readTime || '1 min'),
|
||||
img: docImageUrl(cmsArticle, '/images/gala-saal-overview.jpg', 'heroImage'),
|
||||
excerpt: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.'),
|
||||
sections: Array.isArray(cmsArticle.sections) && cmsArticle.sections.length > 0 ? cmsArticle.sections as any : [{ body: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.') }],
|
||||
tags: Array.isArray(cmsArticle.tags) ? cmsArticle.tags.map((item: any) => item.tag).filter(Boolean) : ['Presse'],
|
||||
relatedSlugs: Array.isArray(cmsArticle.relatedSlugs) ? cmsArticle.relatedSlugs.map((item: any) => item.slug).filter(Boolean) : [],
|
||||
} as BlogArticle : undefined);
|
||||
|
||||
if (!article) {
|
||||
return (
|
||||
|
||||
40
src/spa/pages/CmsGenericPage.tsx
Normal file
40
src/spa/pages/CmsGenericPage.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { useCmsRoute } from '@/spa/cmsRoute'
|
||||
|
||||
const NAVY = '#111D55'
|
||||
const GOLD = '#EFBF04'
|
||||
const CREAM = '#EFE5E3'
|
||||
const FF = '"IBM Plex Sans", sans-serif'
|
||||
const FB = '"Inter", sans-serif'
|
||||
|
||||
export default function CmsGenericPage() {
|
||||
const route = useCmsRoute()
|
||||
const doc = route?.doc
|
||||
const title = doc?.title || doc?.meta?.title || 'Seite'
|
||||
const description = doc?.description || doc?.meta?.description
|
||||
|
||||
return (
|
||||
<main style={{ minHeight: '70vh', background: CREAM, padding: '140px 24px 80px' }}>
|
||||
<div style={{ maxWidth: 960, margin: '0 auto' }}>
|
||||
<div style={{ color: GOLD, fontFamily: FF, fontWeight: 800, fontSize: 12, letterSpacing: '0.18em', textTransform: 'uppercase', marginBottom: 20 }}>
|
||||
{route?.collection || 'CMS'}
|
||||
</div>
|
||||
<h1 style={{ color: NAVY, fontFamily: FF, fontSize: 'clamp(2.25rem, 7vw, 5rem)', lineHeight: 1, fontWeight: 900, textTransform: 'uppercase', margin: 0 }}>
|
||||
{title}
|
||||
</h1>
|
||||
{description ? (
|
||||
<p style={{ color: 'rgba(58,58,58,0.78)', fontFamily: FB, fontSize: 22, lineHeight: 1.6, marginTop: 28, maxWidth: 760 }}>
|
||||
{description}
|
||||
</p>
|
||||
) : (
|
||||
<p style={{ color: 'rgba(58,58,58,0.65)', fontFamily: FB, fontSize: 18, lineHeight: 1.7, marginTop: 28, maxWidth: 760 }}>
|
||||
Diese Route wird aus Payload CMS geladen. Ergänze Inhalte im entsprechenden Collection-Dokument.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,9 @@ 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 } from '@/spa/data/events'
|
||||
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'
|
||||
@@ -75,7 +77,24 @@ function UpdateCard({ update, index }: { update: EventUpdate; index: number }) {
|
||||
// ── Main Component ────────────────────────────────────────────────────────────
|
||||
export default function EventDetail() {
|
||||
const { slug } = useParams<{ slug: string }>()
|
||||
const event = getEventBySlug(slug ?? '')
|
||||
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()
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { useState, useMemo } from 'react';
|
||||
import { Link } from '@/spa/router';
|
||||
import { Trophy, Search, ChevronDown, ChevronRight, X, ArrowRight } from 'lucide-react';
|
||||
import { WINNERS, CATEGORIES, YEARS } from '@/spa/data/winners';
|
||||
import { useCmsCollection } from '@/spa/cmsRoute';
|
||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
|
||||
const FF = '"IBM Plex Sans", sans-serif';
|
||||
@@ -17,14 +19,41 @@ export default function Preistraeger() {
|
||||
const [filterCategory, setFilterCategory] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [mediaOnly, setMediaOnly] = useState(false);
|
||||
const cmsPreistraeger = useCmsCollection('preistraeger');
|
||||
const winners = useMemo(() => {
|
||||
const existingSlugs = new Set(WINNERS.map((winner) => winner.slug));
|
||||
return [
|
||||
...WINNERS,
|
||||
...cmsPreistraeger
|
||||
.filter((doc) => doc.slug && !existingSlugs.has(String(doc.slug)))
|
||||
.map((doc) => ({
|
||||
id: String(doc.id),
|
||||
slug: String(doc.slug),
|
||||
name: String(doc.title || 'Preisträger'),
|
||||
category: String(doc.category || 'Preisträger'),
|
||||
year: Number(doc.year || new Date().getFullYear()),
|
||||
type: String(doc.awardType || 'Auszeichnung'),
|
||||
img: docImageUrl(doc, '/images/gala-saal-overview.jpg'),
|
||||
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
|
||||
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
|
||||
quote: String(doc.quote || ''),
|
||||
quotePerson: String(doc.quotePerson || ''),
|
||||
quoteRole: String(doc.quoteRole || ''),
|
||||
location: String(doc.location || 'Bayern'),
|
||||
industry: String(doc.industry || 'Mittelstand'),
|
||||
website: String(doc.website || '#'),
|
||||
hasMedia: Boolean(doc.hasMedia),
|
||||
})),
|
||||
];
|
||||
}, [cmsPreistraeger]);
|
||||
|
||||
const filtered = useMemo(() => WINNERS.filter(w => {
|
||||
const filtered = useMemo(() => winners.filter(w => {
|
||||
if (w.year !== activeYear) return false;
|
||||
if (filterCategory && w.category !== filterCategory) return false;
|
||||
if (search && !w.name.toLowerCase().includes(search.toLowerCase())) return false;
|
||||
if (mediaOnly && !w.hasMedia) return false;
|
||||
return true;
|
||||
}), [activeYear, filterCategory, search, mediaOnly]);
|
||||
}), [activeYear, filterCategory, search, mediaOnly, winners]);
|
||||
|
||||
const reset = () => { setFilterCategory(''); setSearch(''); setMediaOnly(false); };
|
||||
const hasFilter = !!(filterCategory || search || mediaOnly);
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { Link, useParams, Navigate } from '@/spa/router';
|
||||
import { Trophy, ArrowLeft, MapPin, Building2, Globe, ChevronRight } from 'lucide-react';
|
||||
import { WINNERS } from '@/spa/data/winners';
|
||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
|
||||
const FF = '"IBM Plex Sans", sans-serif';
|
||||
@@ -14,7 +16,26 @@ const BG_ALT = '#E4E2E3';
|
||||
|
||||
export default function PreistraegerDetail() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const winner = WINNERS.find(w => w.slug === slug);
|
||||
const route = useCmsRoute();
|
||||
const cmsWinner = route?.collection === 'preistraeger' ? route.doc : undefined;
|
||||
const winner = WINNERS.find(w => w.slug === slug) || (cmsWinner ? {
|
||||
id: String(cmsWinner.id),
|
||||
slug: String(cmsWinner.slug || slug),
|
||||
name: String(cmsWinner.title || 'Preisträger'),
|
||||
category: String(cmsWinner.category || 'Preisträger'),
|
||||
year: Number(cmsWinner.year || new Date().getFullYear()),
|
||||
type: String(cmsWinner.awardType || 'Auszeichnung'),
|
||||
img: docImageUrl(cmsWinner, '/images/gala-saal-overview.jpg'),
|
||||
shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''),
|
||||
longDesc: String(cmsWinner.longDesc || cmsWinner.description || cmsWinner.meta?.description || 'Dieser Preisträger wird aus Payload CMS geladen.'),
|
||||
quote: String(cmsWinner.quote || 'Diese Erfolgsgeschichte wird aktuell in Payload CMS gepflegt.'),
|
||||
quotePerson: String(cmsWinner.quotePerson || cmsWinner.title || 'BMP'),
|
||||
quoteRole: String(cmsWinner.quoteRole || 'Preisträger'),
|
||||
location: String(cmsWinner.location || 'Bayern'),
|
||||
industry: String(cmsWinner.industry || 'Mittelstand'),
|
||||
website: String(cmsWinner.website || '#'),
|
||||
hasMedia: Boolean(cmsWinner.hasMedia),
|
||||
} : undefined);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (!winner) return <Navigate to="/preistraeger" replace />;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Link } from '@/spa/router';
|
||||
import { Calendar, MapPin, Download, ArrowRight, FileText } from 'lucide-react';
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import { useCmsCollection } from '@/spa/cmsRoute';
|
||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||
|
||||
const NAVY = '#111D55';
|
||||
const GOLD = '#EFBF04';
|
||||
@@ -183,6 +185,41 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
|
||||
|
||||
const Press: React.FC = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const cmsEvents = useCmsCollection('events');
|
||||
const cmsPosts = useCmsCollection('posts');
|
||||
const combinedEvents = React.useMemo(() => {
|
||||
const existing = new Set(events.map((event) => event.slug.split('/').pop()));
|
||||
return [
|
||||
...events,
|
||||
...cmsEvents
|
||||
.filter((event) => event.slug && !existing.has(String(event.slug)))
|
||||
.map((event) => ({
|
||||
title: String(event.title || 'Event'),
|
||||
date: String(event.date || 'Termin folgt'),
|
||||
location: String(event.location || event.venue || 'Bayern'),
|
||||
cat: String(event.category || 'Event'),
|
||||
status: String(event.status || 'Geplant'),
|
||||
img: docImageUrl(event, '/images/buehne-moderatoren.jpg'),
|
||||
desc: String(event.description || event.meta?.description || ''),
|
||||
slug: String(event.spaPath || `/presse/events/${event.slug}`),
|
||||
})),
|
||||
];
|
||||
}, [cmsEvents]);
|
||||
const combinedNews = React.useMemo(() => {
|
||||
const existing = new Set(news.map((item) => item.slug.split('/').pop()));
|
||||
return [
|
||||
...news,
|
||||
...cmsPosts
|
||||
.filter((post) => post.slug && !existing.has(String(post.slug)))
|
||||
.map((post) => ({
|
||||
title: String(post.title || 'Presse'),
|
||||
excerpt: String(post.excerpt || post.meta?.description || ''),
|
||||
cat: String(post.cat || 'Presse'),
|
||||
img: docImageUrl(post, '/images/gala-saal-overview.jpg', 'heroImage'),
|
||||
slug: String(post.spaPath || `/presse/blog/${post.slug}`),
|
||||
})),
|
||||
];
|
||||
}, [cmsPosts]);
|
||||
const [hoveredEvent, setHoveredEvent] = useState<number | null>(null);
|
||||
const [dlHovered, setDlHovered] = useState(false);
|
||||
const [submitHovered, setSubmitHovered] = useState(false);
|
||||
@@ -375,7 +412,7 @@ const Press: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Event rows */}
|
||||
{events.map((event, idx) => (
|
||||
{combinedEvents.map((event, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
onMouseEnter={() => setHoveredEvent(idx)}
|
||||
@@ -477,7 +514,7 @@ const Press: React.FC = () => {
|
||||
|
||||
{/* 3-column news grid */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)' }}>
|
||||
{news.map((item, idx) => (
|
||||
{combinedNews.map((item, idx) => (
|
||||
<NewsCard key={idx} item={item} idx={idx} isMobile={isMobile} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user