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'), text('slug', 'Route'),
] ]
const statusLabelFields: Field[] = [
text('value', 'Raw status value'),
text('label', 'Display label'),
]
const newsFields: Field[] = [ const newsFields: Field[] = [
text('title', 'Title'), text('title', 'Title'),
textarea('excerpt', 'Excerpt'), textarea('excerpt', 'Excerpt'),
@@ -81,7 +86,20 @@ export const pressIndexFields: Field[] = [
textarea('heading', 'Heading', pressIndexContent.events.heading), textarea('heading', 'Heading', pressIndexContent.events.heading),
textarea('description', 'Description', pressIndexContent.events.description), textarea('description', 'Description', pressIndexContent.events.description),
text('detailLabel', 'Detail link label', pressIndexContent.events.detailLabel), 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('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', name: 'fallbackItems',
label: 'Fallback event rows', label: 'Fallback event rows',
@@ -102,6 +120,9 @@ export const pressIndexFields: Field[] = [
textarea('heading', 'Heading', pressIndexContent.news.heading), textarea('heading', 'Heading', pressIndexContent.news.heading),
textarea('description', 'Description', pressIndexContent.news.description), textarea('description', 'Description', pressIndexContent.news.description),
text('readMoreLabel', 'Read more label', pressIndexContent.news.readMoreLabel), 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', name: 'fallbackItems',
label: 'Fallback news cards', label: 'Fallback news cards',

View File

@@ -1492,7 +1492,23 @@ export interface Page {
heading?: string | null; heading?: string | null;
description?: string | null; description?: string | null;
detailLabel?: string | null; detailLabel?: string | null;
defaultStatusLabel?: string | null;
openStatusLabel?: 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?: fallbackItems?:
| { | {
title?: string | null; title?: string | null;
@@ -1515,6 +1531,12 @@ export interface Page {
heading?: string | null; heading?: string | null;
description?: string | null; description?: string | null;
readMoreLabel?: string | null; readMoreLabel?: string | null;
missingTitleLabel?: string | null;
missingCategoryLabel?: string | null;
/**
* Current frontend image: /images/gala-saal-overview.jpg
*/
collectionFallbackImage?: (number | null) | Media;
fallbackItems?: fallbackItems?:
| { | {
title?: string | null; title?: string | null;
@@ -4299,7 +4321,20 @@ export interface PagesSelect<T extends boolean = true> {
heading?: T; heading?: T;
description?: T; description?: T;
detailLabel?: T; detailLabel?: T;
defaultStatusLabel?: T;
openStatusLabel?: T; openStatusLabel?: T;
missingTitleLabel?: T;
missingDateLabel?: T;
missingLocationLabel?: T;
missingCategoryLabel?: T;
collectionFallbackImage?: T;
statusLabels?:
| T
| {
value?: T;
label?: T;
id?: T;
};
fallbackItems?: fallbackItems?:
| T | T
| { | {
@@ -4321,6 +4356,9 @@ export interface PagesSelect<T extends boolean = true> {
heading?: T; heading?: T;
description?: T; description?: T;
readMoreLabel?: T; readMoreLabel?: T;
missingTitleLabel?: T;
missingCategoryLabel?: T;
collectionFallbackImage?: T;
fallbackItems?: fallbackItems?:
| T | T
| { | {

View File

@@ -32,8 +32,14 @@ async function main() {
const page = pageResult.docs[0] const page = pageResult.docs[0]
if (!page) throw new Error('Press index page not found. Expected spaPath=/presse or slug=presse/press.') 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 { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero
const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events
const { collectionFallbackImageFilename: _newsFallbackFilename, ...newsContent } = pressIndexContent.news
await payload.update({ await payload.update({
collection: 'pages', collection: 'pages',
@@ -47,6 +53,14 @@ async function main() {
...heroContent, ...heroContent,
image: heroImage, image: heroImage,
}, },
events: {
...eventsContent,
collectionFallbackImage: eventFallbackImage,
},
news: {
...newsContent,
collectionFallbackImage: newsFallbackImage,
},
}, },
} as never, } as never,
}) })

View File

@@ -16,6 +16,8 @@ const FB = '"Inter", sans-serif';
type PressIndexCms = Partial<typeof pressIndexContent> & { type PressIndexCms = Partial<typeof pressIndexContent> & {
hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown } 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] 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 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 fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
const statusLabels: Record<string, string> = { type StatusLabel = {
geplant: 'In Planung', value?: string | null
'anmeldung-offen': 'Anmeldung offen', label?: string | null
intern: 'Intern',
abgeschlossen: 'Abgeschlossen',
} }
const normalizeStatus = (status: unknown) => { const statusLabelMap = (items: unknown) =>
const value = fallbackText(status, 'Geplant') fallbackArray<StatusLabel>(items, pressIndexContent.events.statusLabels).reduce<Record<string, string>>((labels, item) => {
return statusLabels[value] || value 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 => ({ const eventFromCms = (
title: String(event.title || 'Event'), event: CmsRouteDoc,
date: String(event.date || 'Termin folgt'), fallbackImage: string,
location: String(event.location || event.venue || 'Bayern'), labels: Record<string, string>,
cat: String(event.category || 'Event'), eventsSection: NonNullable<PressIndexCms['events']>,
status: normalizeStatus(event.status), ): PressEventItem => ({
img: docImageUrl(event, '/images/buehne-moderatoren.jpg'), 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 || ''), desc: String(event.description || event.meta?.description || ''),
slug: String(event.spaPath || `/presse/events/${event.slug}`), slug: String(event.spaPath || `/presse/events/${event.slug}`),
}) })
const postFromCms = (post: CmsRouteDoc): PressNewsItem => ({ const postFromCms = (post: CmsRouteDoc, fallbackImage: string, newsSection: NonNullable<PressIndexCms['news']>): PressNewsItem => ({
title: String(post.title || 'Presse'), title: String(post.title || fallbackText(newsSection.missingTitleLabel, pressIndexContent.news.missingTitleLabel)),
excerpt: String(post.excerpt || post.meta?.description || ''), excerpt: String(post.excerpt || post.meta?.description || ''),
cat: String(post.cat || 'Presse'), cat: String(post.cat || fallbackText(newsSection.missingCategoryLabel, pressIndexContent.news.missingCategoryLabel)),
img: docImageUrl(post, '/images/gala-saal-overview.jpg', 'heroImage'), img: docImageUrl(post, fallbackImage, 'heroImage'),
slug: String(post.spaPath || `/presse/blog/${post.slug}`), slug: String(post.spaPath || `/presse/blog/${post.slug}`),
}) })
@@ -204,22 +217,34 @@ function DarkInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
const Press: React.FC = () => { const Press: React.FC = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const cms = (useCmsRoute()?.doc?.pressIndex || {}) as PressIndexCms; const cms = (useCmsRoute()?.doc?.pressIndex || {}) as PressIndexCms;
const hero = { ...pressIndexContent.hero, ...(cms.hero || {}) }; const hero = React.useMemo(() => ({ ...pressIndexContent.hero, ...(cms.hero || {}) }), [cms.hero]);
const eventsSection = { ...pressIndexContent.events, ...(cms.events || {}) }; const eventsSection = React.useMemo(() => ({ ...pressIndexContent.events, ...(cms.events || {}) }), [cms.events]);
const newsSection = { ...pressIndexContent.news, ...(cms.news || {}) }; const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]);
const downloads = { ...pressIndexContent.downloads, ...(cms.downloads || {}) }; const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]);
const accreditation = { ...pressIndexContent.accreditation, ...(cms.accreditation || {}) }; const accreditation = React.useMemo(() => ({ ...pressIndexContent.accreditation, ...(cms.accreditation || {}) }), [cms.accreditation]);
const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n'); const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n');
const cmsEvents = useCmsCollection('events'); const cmsEvents = useCmsCollection('events');
const cmsPosts = useCmsCollection('posts'); 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(() => { 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); return fallbackArray<PressEventItem>(eventsSection.fallbackItems, pressIndexContent.events.fallbackItems);
}, [cmsEvents, eventsSection.fallbackItems]); }, [cmsEvents, eventCollectionFallbackImage, eventStatusLabels, eventsSection]);
const combinedNews = React.useMemo(() => { 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); return fallbackArray<PressNewsItem>(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
}, [cmsPosts, newsSection.fallbackItems]); }, [cmsPosts, newsCollectionFallbackImage, newsSection]);
const [hoveredEvent, setHoveredEvent] = useState<number | null>(null); const [hoveredEvent, setHoveredEvent] = useState<number | null>(null);
const [dlHovered, setDlHovered] = useState(false); const [dlHovered, setDlHovered] = useState(false);
const [submitHovered, setSubmitHovered] = useState(false); const [submitHovered, setSubmitHovered] = useState(false);

View File

@@ -11,7 +11,19 @@ export const pressIndexContent = {
heading: 'EVENTS RUND UM DEN PREIS.', heading: 'EVENTS RUND UM DEN PREIS.',
description: 'Von der Jurysitzung bis zur festlichen Gala – alle Veranstaltungen auf einen Blick.', description: 'Von der Jurysitzung bis zur festlichen Gala – alle Veranstaltungen auf einen Blick.',
detailLabel: 'Details', detailLabel: 'Details',
defaultStatusLabel: 'Geplant',
openStatusLabel: 'Anmeldung offen', 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: [ fallbackItems: [
{ {
title: 'Preisverleihung 2026', title: 'Preisverleihung 2026',
@@ -50,6 +62,9 @@ export const pressIndexContent = {
heading: 'NACHRICHTEN & EINBLICKE.', heading: 'NACHRICHTEN & EINBLICKE.',
description: 'Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.', description: 'Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.',
readMoreLabel: 'Weiterlesen', readMoreLabel: 'Weiterlesen',
missingTitleLabel: 'Presse',
missingCategoryLabel: 'Presse',
collectionFallbackImageFilename: 'gala-saal-overview.jpg',
fallbackItems: [ fallbackItems: [
{ {
title: 'Wie Bayerns KMU die KI nutzen', title: 'Wie Bayerns KMU die KI nutzen',