feat: manage press fallback labels via payload

This commit is contained in:
syntaxbullet
2026-06-22 12:27:36 +02:00
parent 9ce55217a1
commit 6676610e5e
5 changed files with 142 additions and 29 deletions

View File

@@ -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',

View File

@@ -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<T extends boolean = true> {
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<T extends boolean = true> {
heading?: T;
description?: T;
readMoreLabel?: T;
missingTitleLabel?: T;
missingCategoryLabel?: T;
collectionFallbackImage?: T;
fallbackItems?:
| T
| {

View File

@@ -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,
})

View File

@@ -16,6 +16,8 @@ const FB = '"Inter", sans-serif';
type PressIndexCms = Partial<typeof pressIndexContent> & {
hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown }
events?: Partial<typeof pressIndexContent.events> & { collectionFallbackImage?: unknown }
news?: Partial<typeof pressIndexContent.news> & { 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 = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
const statusLabels: Record<string, string> = {
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<StatusLabel>(items, pressIndexContent.events.statusLabels).reduce<Record<string, string>>((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<string, string>, 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<string, string>,
eventsSection: NonNullable<PressIndexCms['events']>,
): 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<PressIndexCms['news']>): 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<HTMLInputElement>) {
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<PressEventItem>(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<PressNewsItem>(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
}, [cmsPosts, newsSection.fallbackItems]);
}, [cmsPosts, newsCollectionFallbackImage, newsSection]);
const [hoveredEvent, setHoveredEvent] = useState<number | null>(null);
const [dlHovered, setDlHovered] = useState(false);
const [submitHovered, setSubmitHovered] = useState(false);

View File

@@ -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',