diff --git a/src/collections/Pages/pressIndexFields.ts b/src/collections/Pages/pressIndexFields.ts index da469d5..cc6b630 100644 --- a/src/collections/Pages/pressIndexFields.ts +++ b/src/collections/Pages/pressIndexFields.ts @@ -40,6 +40,11 @@ const eventFields: Field[] = [ text('slug', 'Route'), ] +const statusLabelFields: Field[] = [ + text('value', 'Raw status value'), + text('label', 'Display label'), +] + const newsFields: Field[] = [ text('title', 'Title'), textarea('excerpt', 'Excerpt'), @@ -81,7 +86,20 @@ export const pressIndexFields: Field[] = [ textarea('heading', 'Heading', pressIndexContent.events.heading), textarea('description', 'Description', pressIndexContent.events.description), text('detailLabel', 'Detail link label', pressIndexContent.events.detailLabel), + text('defaultStatusLabel', 'Missing status fallback label', pressIndexContent.events.defaultStatusLabel), text('openStatusLabel', 'Open status label', pressIndexContent.events.openStatusLabel), + text('missingTitleLabel', 'Missing event title label', pressIndexContent.events.missingTitleLabel), + text('missingDateLabel', 'Missing event date label', pressIndexContent.events.missingDateLabel), + text('missingLocationLabel', 'Missing event location label', pressIndexContent.events.missingLocationLabel), + text('missingCategoryLabel', 'Missing event category label', pressIndexContent.events.missingCategoryLabel), + uploadField('collectionFallbackImage', 'Missing event image fallback', `Current frontend image: /images/${pressIndexContent.events.collectionFallbackImageFilename}`), + { + name: 'statusLabels', + label: 'Status display labels', + type: 'array', + defaultValue: pressIndexContent.events.statusLabels, + fields: statusLabelFields, + }, { name: 'fallbackItems', label: 'Fallback event rows', @@ -102,6 +120,9 @@ export const pressIndexFields: Field[] = [ textarea('heading', 'Heading', pressIndexContent.news.heading), textarea('description', 'Description', pressIndexContent.news.description), text('readMoreLabel', 'Read more label', pressIndexContent.news.readMoreLabel), + text('missingTitleLabel', 'Missing news title label', pressIndexContent.news.missingTitleLabel), + text('missingCategoryLabel', 'Missing news category label', pressIndexContent.news.missingCategoryLabel), + uploadField('collectionFallbackImage', 'Missing news image fallback', `Current frontend image: /images/${pressIndexContent.news.collectionFallbackImageFilename}`), { name: 'fallbackItems', label: 'Fallback news cards', diff --git a/src/payload-types.ts b/src/payload-types.ts index 3925a97..3407a4f 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1492,7 +1492,23 @@ export interface Page { heading?: string | null; description?: string | null; detailLabel?: string | null; + defaultStatusLabel?: string | null; openStatusLabel?: string | null; + missingTitleLabel?: string | null; + missingDateLabel?: string | null; + missingLocationLabel?: string | null; + missingCategoryLabel?: string | null; + /** + * Current frontend image: /images/buehne-moderatoren.jpg + */ + collectionFallbackImage?: (number | null) | Media; + statusLabels?: + | { + value?: string | null; + label?: string | null; + id?: string | null; + }[] + | null; fallbackItems?: | { title?: string | null; @@ -1515,6 +1531,12 @@ export interface Page { heading?: string | null; description?: string | null; readMoreLabel?: string | null; + missingTitleLabel?: string | null; + missingCategoryLabel?: string | null; + /** + * Current frontend image: /images/gala-saal-overview.jpg + */ + collectionFallbackImage?: (number | null) | Media; fallbackItems?: | { title?: string | null; @@ -4299,7 +4321,20 @@ export interface PagesSelect { heading?: T; description?: T; detailLabel?: T; + defaultStatusLabel?: T; openStatusLabel?: T; + missingTitleLabel?: T; + missingDateLabel?: T; + missingLocationLabel?: T; + missingCategoryLabel?: T; + collectionFallbackImage?: T; + statusLabels?: + | T + | { + value?: T; + label?: T; + id?: T; + }; fallbackItems?: | T | { @@ -4321,6 +4356,9 @@ export interface PagesSelect { heading?: T; description?: T; readMoreLabel?: T; + missingTitleLabel?: T; + missingCategoryLabel?: T; + collectionFallbackImage?: T; fallbackItems?: | T | { diff --git a/src/scripts/preload-press-index-page-cms.ts b/src/scripts/preload-press-index-page-cms.ts index 82b6966..3c66cba 100644 --- a/src/scripts/preload-press-index-page-cms.ts +++ b/src/scripts/preload-press-index-page-cms.ts @@ -32,8 +32,14 @@ async function main() { const page = pageResult.docs[0] if (!page) throw new Error('Press index page not found. Expected spaPath=/presse or slug=presse/press.') - const heroImage = await mediaByFilename(payload, pressIndexContent.hero.imageFilename) + const [heroImage, eventFallbackImage, newsFallbackImage] = await Promise.all([ + mediaByFilename(payload, pressIndexContent.hero.imageFilename), + mediaByFilename(payload, pressIndexContent.events.collectionFallbackImageFilename), + mediaByFilename(payload, pressIndexContent.news.collectionFallbackImageFilename), + ]) const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero + const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events + const { collectionFallbackImageFilename: _newsFallbackFilename, ...newsContent } = pressIndexContent.news await payload.update({ collection: 'pages', @@ -47,6 +53,14 @@ async function main() { ...heroContent, image: heroImage, }, + events: { + ...eventsContent, + collectionFallbackImage: eventFallbackImage, + }, + news: { + ...newsContent, + collectionFallbackImage: newsFallbackImage, + }, }, } as never, }) diff --git a/src/spa/pages/Press.tsx b/src/spa/pages/Press.tsx index 23ace13..bdb9da8 100644 --- a/src/spa/pages/Press.tsx +++ b/src/spa/pages/Press.tsx @@ -16,6 +16,8 @@ const FB = '"Inter", sans-serif'; type PressIndexCms = Partial & { hero?: Partial & { image?: unknown } + events?: Partial & { collectionFallbackImage?: unknown } + news?: Partial & { collectionFallbackImage?: unknown } } type PressEventItem = (typeof pressIndexContent.events.fallbackItems)[number] @@ -24,34 +26,45 @@ type PressNewsItem = (typeof pressIndexContent.news.fallbackItems)[number] const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const fallbackArray = (value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback; -const statusLabels: Record = { - geplant: 'In Planung', - 'anmeldung-offen': 'Anmeldung offen', - intern: 'Intern', - abgeschlossen: 'Abgeschlossen', +type StatusLabel = { + value?: string | null + label?: string | null } -const normalizeStatus = (status: unknown) => { - const value = fallbackText(status, 'Geplant') - return statusLabels[value] || value +const statusLabelMap = (items: unknown) => + fallbackArray(items, pressIndexContent.events.statusLabels).reduce>((labels, item) => { + const value = fallbackText(item.value, '') + const label = fallbackText(item.label, '') + if (value && label) labels[value] = label + return labels + }, {}) + +const normalizeStatus = (status: unknown, labels: Record, defaultStatusLabel: string) => { + const value = fallbackText(status, defaultStatusLabel) + return labels[value] || value } -const eventFromCms = (event: CmsRouteDoc): PressEventItem => ({ - title: String(event.title || 'Event'), - date: String(event.date || 'Termin folgt'), - location: String(event.location || event.venue || 'Bayern'), - cat: String(event.category || 'Event'), - status: normalizeStatus(event.status), - img: docImageUrl(event, '/images/buehne-moderatoren.jpg'), +const eventFromCms = ( + event: CmsRouteDoc, + fallbackImage: string, + labels: Record, + eventsSection: NonNullable, +): PressEventItem => ({ + title: String(event.title || fallbackText(eventsSection.missingTitleLabel, pressIndexContent.events.missingTitleLabel)), + date: String(event.date || fallbackText(eventsSection.missingDateLabel, pressIndexContent.events.missingDateLabel)), + location: String(event.location || event.venue || fallbackText(eventsSection.missingLocationLabel, pressIndexContent.events.missingLocationLabel)), + cat: String(event.category || fallbackText(eventsSection.missingCategoryLabel, pressIndexContent.events.missingCategoryLabel)), + status: normalizeStatus(event.status, labels, fallbackText(eventsSection.defaultStatusLabel, pressIndexContent.events.defaultStatusLabel)), + img: docImageUrl(event, fallbackImage), desc: String(event.description || event.meta?.description || ''), slug: String(event.spaPath || `/presse/events/${event.slug}`), }) -const postFromCms = (post: CmsRouteDoc): PressNewsItem => ({ - title: String(post.title || 'Presse'), +const postFromCms = (post: CmsRouteDoc, fallbackImage: string, newsSection: NonNullable): PressNewsItem => ({ + title: String(post.title || fallbackText(newsSection.missingTitleLabel, pressIndexContent.news.missingTitleLabel)), excerpt: String(post.excerpt || post.meta?.description || ''), - cat: String(post.cat || 'Presse'), - img: docImageUrl(post, '/images/gala-saal-overview.jpg', 'heroImage'), + cat: String(post.cat || fallbackText(newsSection.missingCategoryLabel, pressIndexContent.news.missingCategoryLabel)), + img: docImageUrl(post, fallbackImage, 'heroImage'), slug: String(post.spaPath || `/presse/blog/${post.slug}`), }) @@ -204,22 +217,34 @@ function DarkInput(props: React.InputHTMLAttributes) { const Press: React.FC = () => { const isMobile = useIsMobile(); const cms = (useCmsRoute()?.doc?.pressIndex || {}) as PressIndexCms; - const hero = { ...pressIndexContent.hero, ...(cms.hero || {}) }; - const eventsSection = { ...pressIndexContent.events, ...(cms.events || {}) }; - const newsSection = { ...pressIndexContent.news, ...(cms.news || {}) }; - const downloads = { ...pressIndexContent.downloads, ...(cms.downloads || {}) }; - const accreditation = { ...pressIndexContent.accreditation, ...(cms.accreditation || {}) }; + const hero = React.useMemo(() => ({ ...pressIndexContent.hero, ...(cms.hero || {}) }), [cms.hero]); + const eventsSection = React.useMemo(() => ({ ...pressIndexContent.events, ...(cms.events || {}) }), [cms.events]); + const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]); + const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]); + const accreditation = React.useMemo(() => ({ ...pressIndexContent.accreditation, ...(cms.accreditation || {}) }), [cms.accreditation]); const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n'); const cmsEvents = useCmsCollection('events'); const cmsPosts = useCmsCollection('posts'); + const eventStatusLabels = React.useMemo(() => statusLabelMap(eventsSection.statusLabels), [eventsSection.statusLabels]); + const eventCollectionFallbackImage = mediaUrl(eventsSection.collectionFallbackImage, `/images/${pressIndexContent.events.collectionFallbackImageFilename}`); + const newsCollectionFallbackImage = mediaUrl(newsSection.collectionFallbackImage, `/images/${pressIndexContent.news.collectionFallbackImageFilename}`); const combinedEvents = React.useMemo(() => { - if (cmsEvents.length) return cmsEvents.map(eventFromCms); + if (cmsEvents.length) { + return cmsEvents.map((event) => + eventFromCms( + event, + eventCollectionFallbackImage, + eventStatusLabels, + eventsSection, + ), + ); + } return fallbackArray(eventsSection.fallbackItems, pressIndexContent.events.fallbackItems); - }, [cmsEvents, eventsSection.fallbackItems]); + }, [cmsEvents, eventCollectionFallbackImage, eventStatusLabels, eventsSection]); const combinedNews = React.useMemo(() => { - if (cmsPosts.length) return cmsPosts.map(postFromCms); + if (cmsPosts.length) return cmsPosts.map((post) => postFromCms(post, newsCollectionFallbackImage, newsSection)); return fallbackArray(newsSection.fallbackItems, pressIndexContent.news.fallbackItems); - }, [cmsPosts, newsSection.fallbackItems]); + }, [cmsPosts, newsCollectionFallbackImage, newsSection]); const [hoveredEvent, setHoveredEvent] = useState(null); const [dlHovered, setDlHovered] = useState(false); const [submitHovered, setSubmitHovered] = useState(false); diff --git a/src/spa/pressIndexContent.ts b/src/spa/pressIndexContent.ts index a06aa02..c6ae70a 100644 --- a/src/spa/pressIndexContent.ts +++ b/src/spa/pressIndexContent.ts @@ -11,7 +11,19 @@ export const pressIndexContent = { heading: 'EVENTS RUND UM DEN PREIS.', description: 'Von der Jurysitzung bis zur festlichen Gala – alle Veranstaltungen auf einen Blick.', detailLabel: 'Details', + defaultStatusLabel: 'Geplant', openStatusLabel: 'Anmeldung offen', + missingTitleLabel: 'Event', + missingDateLabel: 'Termin folgt', + missingLocationLabel: 'Bayern', + missingCategoryLabel: 'Event', + collectionFallbackImageFilename: 'buehne-moderatoren.jpg', + statusLabels: [ + { value: 'geplant', label: 'In Planung' }, + { value: 'anmeldung-offen', label: 'Anmeldung offen' }, + { value: 'intern', label: 'Intern' }, + { value: 'abgeschlossen', label: 'Abgeschlossen' }, + ], fallbackItems: [ { title: 'Preisverleihung 2026', @@ -50,6 +62,9 @@ export const pressIndexContent = { heading: 'NACHRICHTEN & EINBLICKE.', description: 'Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.', readMoreLabel: 'Weiterlesen', + missingTitleLabel: 'Presse', + missingCategoryLabel: 'Presse', + collectionFallbackImageFilename: 'gala-saal-overview.jpg', fallbackItems: [ { title: 'Wie Bayerns KMU die KI nutzen',