fix(cms): remove unpublished content fallbacks
This commit is contained in:
@@ -29,32 +29,11 @@ const sectionAdmin = (description: string) => ({
|
||||
initCollapsed: true,
|
||||
})
|
||||
|
||||
const eventFields: Field[] = [
|
||||
text('title', 'Title'),
|
||||
text('date', 'Date'),
|
||||
text('location', 'Location'),
|
||||
text('cat', 'Category'),
|
||||
text('status', 'Status'),
|
||||
uploadField('image', 'Image'),
|
||||
text('img', 'Image URL or path'),
|
||||
textarea('desc', 'Description'),
|
||||
text('slug', 'Route'),
|
||||
]
|
||||
|
||||
const statusLabelFields: Field[] = [
|
||||
text('value', 'Raw status value'),
|
||||
text('label', 'Display label'),
|
||||
]
|
||||
|
||||
const newsFields: Field[] = [
|
||||
text('title', 'Title'),
|
||||
textarea('excerpt', 'Excerpt'),
|
||||
text('cat', 'Category'),
|
||||
uploadField('image', 'Image'),
|
||||
text('img', 'Image URL or path'),
|
||||
text('slug', 'Route'),
|
||||
]
|
||||
|
||||
export const pressIndexFields: Field[] = [
|
||||
{
|
||||
name: 'pressIndex',
|
||||
@@ -62,7 +41,7 @@ export const pressIndexFields: Field[] = [
|
||||
type: 'group',
|
||||
admin: {
|
||||
description:
|
||||
'Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections; fallback cards remain editable here.',
|
||||
'Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections.',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
@@ -82,7 +61,7 @@ export const pressIndexFields: Field[] = [
|
||||
name: 'events',
|
||||
label: '02 · Events section',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Section header, labels, and fallback event rows.'),
|
||||
admin: sectionAdmin('Section header, labels, and missing-field presentation fallbacks.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', pressIndexContent.events.eyebrow),
|
||||
textarea('heading', 'Heading', pressIndexContent.events.heading),
|
||||
@@ -102,21 +81,13 @@ export const pressIndexFields: Field[] = [
|
||||
defaultValue: pressIndexContent.events.statusLabels,
|
||||
fields: statusLabelFields,
|
||||
},
|
||||
{
|
||||
name: 'fallbackItems',
|
||||
label: 'Fallback event rows',
|
||||
type: 'array',
|
||||
dbName: 'press_events_fb',
|
||||
defaultValue: pressIndexContent.events.fallbackItems,
|
||||
fields: eventFields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'news',
|
||||
label: '03 · Newsroom section',
|
||||
type: 'group',
|
||||
admin: sectionAdmin('Section header, read-more label, and fallback news cards.'),
|
||||
admin: sectionAdmin('Section header, read-more label, and missing-field presentation fallbacks.'),
|
||||
fields: [
|
||||
text('eyebrow', 'Eyebrow', pressIndexContent.news.eyebrow),
|
||||
textarea('heading', 'Heading', pressIndexContent.news.heading),
|
||||
@@ -125,14 +96,6 @@ export const pressIndexFields: Field[] = [
|
||||
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',
|
||||
type: 'array',
|
||||
dbName: 'press_news_fb',
|
||||
defaultValue: pressIndexContent.news.fallbackItems,
|
||||
fields: newsFields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
114
src/migrations/20260731_143000_remove_press_record_fallbacks.ts
Normal file
114
src/migrations/20260731_143000_remove_press_record_fallbacks.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
|
||||
|
||||
type MigrationDB = MigrateUpArgs['db']
|
||||
|
||||
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
|
||||
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
|
||||
|
||||
const fallbackTables = [
|
||||
'press_events_fb',
|
||||
'_press_events_fb_v',
|
||||
'press_news_fb',
|
||||
'_press_news_fb_v',
|
||||
] as const
|
||||
|
||||
async function tableExists(db: MigrationDB, tableName: string) {
|
||||
const rows = (await db.all(
|
||||
sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
|
||||
)) as Array<{ name: string }>
|
||||
|
||||
return rows.length > 0
|
||||
}
|
||||
|
||||
async function createIndex(db: MigrationDB, tableName: string, suffix: string, columnName: string) {
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_${suffix}_idx`)} ON ${ident(tableName)} (${ident(columnName)});`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async function createLiveFallbackTable(
|
||||
db: MigrationDB,
|
||||
tableName: 'press_events_fb' | 'press_news_fb',
|
||||
itemColumns: string,
|
||||
) {
|
||||
if (await tableExists(db, tableName)) return
|
||||
|
||||
await db.run(
|
||||
sql.raw(`
|
||||
CREATE TABLE ${ident(tableName)} (
|
||||
${ident('_order')} integer NOT NULL,
|
||||
${ident('_parent_id')} integer NOT NULL,
|
||||
${ident('id')} text PRIMARY KEY NOT NULL,
|
||||
${itemColumns},
|
||||
FOREIGN KEY (${ident('image_id')}) REFERENCES ${ident('media')}(${ident('id')}) ON UPDATE no action ON DELETE set null,
|
||||
FOREIGN KEY (${ident('_parent_id')}) REFERENCES ${ident('pages')}(${ident('id')}) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
)
|
||||
|
||||
await createIndex(db, tableName, 'order', '_order')
|
||||
await createIndex(db, tableName, 'parent_id', '_parent_id')
|
||||
await createIndex(db, tableName, 'image', 'image_id')
|
||||
}
|
||||
|
||||
async function createVersionFallbackTable(
|
||||
db: MigrationDB,
|
||||
tableName: '_press_events_fb_v' | '_press_news_fb_v',
|
||||
itemColumns: string,
|
||||
) {
|
||||
if (await tableExists(db, tableName)) return
|
||||
|
||||
await db.run(
|
||||
sql.raw(`
|
||||
CREATE TABLE ${ident(tableName)} (
|
||||
${ident('_order')} integer NOT NULL,
|
||||
${ident('_parent_id')} integer NOT NULL,
|
||||
${ident('id')} integer PRIMARY KEY NOT NULL,
|
||||
${itemColumns},
|
||||
${ident('_uuid')} text,
|
||||
FOREIGN KEY (${ident('image_id')}) REFERENCES ${ident('media')}(${ident('id')}) ON UPDATE no action ON DELETE set null,
|
||||
FOREIGN KEY (${ident('_parent_id')}) REFERENCES ${ident('_pages_v')}(${ident('id')}) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
)
|
||||
|
||||
await createIndex(db, tableName, 'order', '_order')
|
||||
await createIndex(db, tableName, 'parent_id', '_parent_id')
|
||||
await createIndex(db, tableName, 'image', 'image_id')
|
||||
}
|
||||
|
||||
const eventColumns = [
|
||||
`${ident('title')} text`,
|
||||
`${ident('date')} text`,
|
||||
`${ident('location')} text`,
|
||||
`${ident('cat')} text`,
|
||||
`${ident('status')} text`,
|
||||
`${ident('image_id')} integer`,
|
||||
`${ident('img')} text`,
|
||||
`${ident('desc')} text`,
|
||||
`${ident('slug')} text`,
|
||||
].join(',\n ')
|
||||
|
||||
const newsColumns = [
|
||||
`${ident('title')} text`,
|
||||
`${ident('excerpt')} text`,
|
||||
`${ident('cat')} text`,
|
||||
`${ident('image_id')} integer`,
|
||||
`${ident('img')} text`,
|
||||
`${ident('slug')} text`,
|
||||
].join(',\n ')
|
||||
|
||||
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
|
||||
for (const tableName of fallbackTables) {
|
||||
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(tableName)};`))
|
||||
}
|
||||
}
|
||||
|
||||
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
|
||||
await createLiveFallbackTable(db, 'press_events_fb', eventColumns)
|
||||
await createVersionFallbackTable(db, '_press_events_fb_v', eventColumns)
|
||||
await createLiveFallbackTable(db, 'press_news_fb', newsColumns)
|
||||
await createVersionFallbackTable(db, '_press_news_fb_v', newsColumns)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import * as migration_20260713_163000_participation_supporting_awards_copy from
|
||||
import * as migration_20260713_194500_contact_submissions from './20260713_194500_contact_submissions';
|
||||
import * as migration_20260713_213000_netzwerk_jury_selection from './20260713_213000_netzwerk_jury_selection';
|
||||
import * as migration_20260713_220000_preistraeger_detail_cleanup from './20260713_220000_preistraeger_detail_cleanup';
|
||||
import * as migration_20260731_143000_remove_press_record_fallbacks from './20260731_143000_remove_press_record_fallbacks';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -102,4 +103,9 @@ export const migrations = [
|
||||
down: migration_20260713_220000_preistraeger_detail_cleanup.down,
|
||||
name: '20260713_220000_preistraeger_detail_cleanup',
|
||||
},
|
||||
{
|
||||
up: migration_20260731_143000_remove_press_record_fallbacks.up,
|
||||
down: migration_20260731_143000_remove_press_record_fallbacks.down,
|
||||
name: '20260731_143000_remove_press_record_fallbacks',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1495,7 +1495,7 @@ export interface Page {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections; fallback cards remain editable here.
|
||||
* Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections.
|
||||
*/
|
||||
pressIndex?: {
|
||||
/**
|
||||
@@ -1512,7 +1512,7 @@ export interface Page {
|
||||
description?: string | null;
|
||||
};
|
||||
/**
|
||||
* Section header, labels, and fallback event rows.
|
||||
* Section header, labels, and missing-field presentation fallbacks.
|
||||
*/
|
||||
events?: {
|
||||
eyebrow?: string | null;
|
||||
@@ -1536,23 +1536,9 @@ export interface Page {
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
fallbackItems?:
|
||||
| {
|
||||
title?: string | null;
|
||||
date?: string | null;
|
||||
location?: string | null;
|
||||
cat?: string | null;
|
||||
status?: string | null;
|
||||
image?: (number | null) | Media;
|
||||
img?: string | null;
|
||||
desc?: string | null;
|
||||
slug?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Section header, read-more label, and fallback news cards.
|
||||
* Section header, read-more label, and missing-field presentation fallbacks.
|
||||
*/
|
||||
news?: {
|
||||
eyebrow?: string | null;
|
||||
@@ -1565,17 +1551,6 @@ export interface Page {
|
||||
* Current frontend image: /images/gala-saal-overview.jpg
|
||||
*/
|
||||
collectionFallbackImage?: (number | null) | Media;
|
||||
fallbackItems?:
|
||||
| {
|
||||
title?: string | null;
|
||||
excerpt?: string | null;
|
||||
cat?: string | null;
|
||||
image?: (number | null) | Media;
|
||||
img?: string | null;
|
||||
slug?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
/**
|
||||
* Download button and press contact block.
|
||||
@@ -4176,20 +4151,6 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
label?: T;
|
||||
id?: T;
|
||||
};
|
||||
fallbackItems?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
date?: T;
|
||||
location?: T;
|
||||
cat?: T;
|
||||
status?: T;
|
||||
image?: T;
|
||||
img?: T;
|
||||
desc?: T;
|
||||
slug?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
news?:
|
||||
| T
|
||||
@@ -4201,17 +4162,6 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
missingTitleLabel?: T;
|
||||
missingCategoryLabel?: T;
|
||||
collectionFallbackImage?: T;
|
||||
fallbackItems?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
excerpt?: T;
|
||||
cat?: T;
|
||||
image?: T;
|
||||
img?: T;
|
||||
slug?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
downloads?:
|
||||
| T
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { getPayload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { pressIndexContent } from '@/spa/pressIndexContent'
|
||||
|
||||
const filenameFromPath = (source: string) => source.split('?')[0]?.split('/').filter(Boolean).pop()
|
||||
|
||||
const fallbackItemsWithImages = async <T extends { img?: string }>(
|
||||
payload: Payload,
|
||||
items: T[],
|
||||
) =>
|
||||
Promise.all(
|
||||
items.map(async (item) => {
|
||||
const filename = item.img ? filenameFromPath(item.img) : undefined
|
||||
return {
|
||||
...item,
|
||||
image: filename ? await mediaByFilename(payload, filename) : undefined,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
async function main() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
@@ -48,11 +32,6 @@ async function main() {
|
||||
const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero
|
||||
const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events
|
||||
const { collectionFallbackImageFilename: _newsFallbackFilename, ...newsContent } = pressIndexContent.news
|
||||
const [eventFallbackItems, newsFallbackItems] = await Promise.all([
|
||||
fallbackItemsWithImages(payload, pressIndexContent.events.fallbackItems),
|
||||
fallbackItemsWithImages(payload, pressIndexContent.news.fallbackItems),
|
||||
])
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: page.id,
|
||||
@@ -68,12 +47,10 @@ async function main() {
|
||||
events: {
|
||||
...eventsContent,
|
||||
collectionFallbackImage: eventFallbackImage,
|
||||
fallbackItems: eventFallbackItems,
|
||||
},
|
||||
news: {
|
||||
...newsContent,
|
||||
collectionFallbackImage: newsFallbackImage,
|
||||
fallbackItems: newsFallbackItems,
|
||||
},
|
||||
downloads: {
|
||||
...pressIndexContent.downloads,
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function HomeWinnersSection({ content }: { content?: HomeWinnersC
|
||||
const placeholderImage = usePreistraegerPlaceholderImage();
|
||||
const cmsPreistraeger = useCmsCollection('preistraeger');
|
||||
const winnersSection = content || {};
|
||||
const homeWinners = resolveHomeWinners(winnersSection.featured, cmsPreistraeger, WINNERS);
|
||||
const homeWinners = resolveHomeWinners(winnersSection.featured, cmsPreistraeger);
|
||||
const scrollDesktopWinners = !isMobile && homeWinners.length > 8;
|
||||
const scrollWinnerGrid = isMobile || scrollDesktopWinners;
|
||||
|
||||
|
||||
@@ -3,21 +3,12 @@ import { resolveCmsRelationships, type CmsRouteDoc } from '@/spa/cmsRoute'
|
||||
const FALLBACK_YEAR = 2025
|
||||
const FALLBACK_LIMIT = 8
|
||||
|
||||
type FallbackWinner = {
|
||||
id: string | number
|
||||
year?: string | number
|
||||
}
|
||||
|
||||
export function resolveHomeWinners(
|
||||
featured: unknown,
|
||||
cmsWinners: CmsRouteDoc[],
|
||||
fallbackWinners: FallbackWinner[],
|
||||
) {
|
||||
export function resolveHomeWinners(featured: unknown, cmsWinners: CmsRouteDoc[]) {
|
||||
const selectedWinners = Array.isArray(featured) ? featured : []
|
||||
|
||||
if (selectedWinners.length) return resolveCmsRelationships(selectedWinners, cmsWinners)
|
||||
|
||||
return (cmsWinners.length ? cmsWinners : fallbackWinners)
|
||||
return cmsWinners
|
||||
.filter((winner) => Number(winner.year) === FALLBACK_YEAR)
|
||||
.slice(0, FALLBACK_LIMIT)
|
||||
}
|
||||
|
||||
@@ -483,15 +483,11 @@ const Netzwerk: React.FC = () => {
|
||||
const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) };
|
||||
const patronPeople = fallbackArray<PatronPerson>(patronage.people, netzwerkContent.patronage.people);
|
||||
const cmsJurySelection = cms.jury?.members;
|
||||
const hasCmsJurySelection = cmsJurySelection !== undefined;
|
||||
const sourcedJuryDocs = selectJuryDocs(cmsJurySelection, cmsJuryMembers);
|
||||
const normalizedJuryMembers = sourcedJuryDocs
|
||||
.map(normalizeJuryMember)
|
||||
.filter((member) => member.active !== false && member.name);
|
||||
const jurySourceMembers: JuryMember[] = normalizedJuryMembers.length || hasCmsJurySelection
|
||||
? normalizedJuryMembers
|
||||
: netzwerkContent.jury.members;
|
||||
const juryMembers = sortJuryMembersQuoteFirst(jurySourceMembers);
|
||||
const juryMembers = sortJuryMembersQuoteFirst(normalizedJuryMembers);
|
||||
const expertiseRows = fallbackArray<ExpertiseRow>(expertise.rows, netzwerkContent.expertise.rows);
|
||||
const partners = sortedPartners(cmsPartners)
|
||||
.filter((partner) => partner.active !== false)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Link } from '@/spa/router';
|
||||
import { Trophy, ArrowRight, Medal } from 'lucide-react';
|
||||
import { WINNERS, type Winner } from '@/spa/data/winners';
|
||||
import type { Winner } from '@/spa/data/winners';
|
||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
@@ -151,8 +151,7 @@ export default function Preistraeger() {
|
||||
);
|
||||
const winners = useMemo(() => {
|
||||
const fallbackWinner = card.fallbackWinner || preistraegerIndexContent.card.fallbackWinner;
|
||||
if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug);
|
||||
return WINNERS;
|
||||
return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug);
|
||||
}, [card, cmsPreistraeger, placeholderImage]);
|
||||
const periods = useMemo<WinnerPeriod[]>(() => {
|
||||
const annualYears = [...new Set(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Link, useParams, Navigate } from '@/spa/router';
|
||||
import { ArrowLeft, Globe, ChevronRight, Download, ChevronLeft, X } from 'lucide-react';
|
||||
import { WINNERS } from '@/spa/data/winners';
|
||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
||||
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
@@ -79,7 +78,7 @@ export default function PreistraegerDetail() {
|
||||
website: String(cmsWinner.website || '#'),
|
||||
hasMedia: Boolean(cmsWinner.hasMedia),
|
||||
detailPage: (cmsWinner.detailPage || {}) as PreistraegerDetailCms,
|
||||
} : WINNERS.find(w => w.slug === slug);
|
||||
} : undefined;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const detail = (winner && 'detailPage' in winner ? { ...preistraegerDetailContent, ...(winner.detailPage || {}) } : cmsDetail) as PreistraegerDetailCms;
|
||||
|
||||
@@ -2,9 +2,15 @@ import React from 'react';
|
||||
import { Calendar, MapPin } from 'lucide-react';
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
import { useCmsCollection, useCmsRoute } from '@/spa/cmsRoute';
|
||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||
import {
|
||||
pressEventsFromCms,
|
||||
pressNewsFromCms,
|
||||
type PressEventItem,
|
||||
type PressNewsItem,
|
||||
} from '@/spa/pressCollectionItems';
|
||||
import { pressIndexContent } from '@/spa/pressIndexContent';
|
||||
import { Link } from '@/spa/router';
|
||||
|
||||
@@ -15,14 +21,11 @@ const FB = '"Inter", sans-serif';
|
||||
|
||||
type PressIndexCms = Partial<typeof pressIndexContent> & {
|
||||
hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown }
|
||||
events?: Partial<typeof pressIndexContent.events> & { collectionFallbackImage?: unknown; fallbackItems?: PressEventItem[] }
|
||||
news?: Partial<typeof pressIndexContent.news> & { collectionFallbackImage?: unknown; fallbackItems?: PressNewsItem[] }
|
||||
events?: Partial<typeof pressIndexContent.events> & { collectionFallbackImage?: unknown }
|
||||
news?: Partial<typeof pressIndexContent.news> & { collectionFallbackImage?: unknown }
|
||||
downloads?: Partial<typeof pressIndexContent.downloads> & { kitFile?: unknown }
|
||||
}
|
||||
|
||||
type PressEventItem = (typeof pressIndexContent.events.fallbackItems)[number] & { image?: unknown }
|
||||
type PressNewsItem = (typeof pressIndexContent.news.fallbackItems)[number] & { image?: unknown }
|
||||
|
||||
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;
|
||||
|
||||
@@ -39,43 +42,6 @@ const statusLabelMap = (items: unknown) =>
|
||||
return labels
|
||||
}, {})
|
||||
|
||||
const normalizeStatus = (status: unknown, labels: Record<string, string>, defaultStatusLabel: string) => {
|
||||
const value = fallbackText(status, defaultStatusLabel)
|
||||
return labels[value] || value
|
||||
}
|
||||
|
||||
const detailPath = (spaPath: unknown, slug: unknown, prefix: '/presse/events' | '/presse/blog') => {
|
||||
const path = fallbackText(spaPath, '')
|
||||
if (path.startsWith(`${prefix}/`)) return path
|
||||
|
||||
const pathSlug = fallbackText(slug, '').replace(/^\/+/, '')
|
||||
return pathSlug ? `${prefix}/${pathSlug}` : prefix
|
||||
}
|
||||
|
||||
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: detailPath(event.spaPath, event.slug, '/presse/events'),
|
||||
})
|
||||
|
||||
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 || fallbackText(newsSection.missingCategoryLabel, pressIndexContent.news.missingCategoryLabel)),
|
||||
img: docImageUrl(post, fallbackImage, 'heroImage'),
|
||||
slug: detailPath(post.spaPath, post.slug, '/presse/blog'),
|
||||
})
|
||||
|
||||
function Lines({ text }: { text: string }) {
|
||||
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>
|
||||
}
|
||||
@@ -185,23 +151,14 @@ const Press: React.FC = () => {
|
||||
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((event) =>
|
||||
eventFromCms(
|
||||
event,
|
||||
eventCollectionFallbackImage,
|
||||
eventStatusLabels,
|
||||
eventsSection,
|
||||
),
|
||||
);
|
||||
}
|
||||
return fallbackArray<PressEventItem>(eventsSection.fallbackItems, pressIndexContent.events.fallbackItems);
|
||||
}, [cmsEvents, eventCollectionFallbackImage, eventStatusLabels, eventsSection]);
|
||||
const combinedNews = React.useMemo(() => {
|
||||
if (cmsPosts.length) return cmsPosts.map((post) => postFromCms(post, newsCollectionFallbackImage, newsSection));
|
||||
return fallbackArray<PressNewsItem>(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
|
||||
}, [cmsPosts, newsCollectionFallbackImage, newsSection]);
|
||||
const combinedEvents = React.useMemo(
|
||||
() => pressEventsFromCms(cmsEvents, eventCollectionFallbackImage, eventStatusLabels, eventsSection),
|
||||
[cmsEvents, eventCollectionFallbackImage, eventStatusLabels, eventsSection],
|
||||
);
|
||||
const combinedNews = React.useMemo(
|
||||
() => pressNewsFromCms(cmsPosts, newsCollectionFallbackImage, newsSection),
|
||||
[cmsPosts, newsCollectionFallbackImage, newsSection],
|
||||
);
|
||||
return (
|
||||
<div className="animate-fade-in">
|
||||
|
||||
|
||||
102
src/spa/pressCollectionItems.ts
Normal file
102
src/spa/pressCollectionItems.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { docImageUrl } from '@/spa/cmsMediaField'
|
||||
import type { CmsRouteDoc } from '@/spa/cmsRoute'
|
||||
import { pressIndexContent } from '@/spa/pressIndexContent'
|
||||
|
||||
export type PressEventItem = {
|
||||
title: string
|
||||
date: string
|
||||
location: string
|
||||
cat: string
|
||||
status: string
|
||||
img: string
|
||||
desc: string
|
||||
slug: string
|
||||
image?: unknown
|
||||
}
|
||||
|
||||
export type PressNewsItem = {
|
||||
title: string
|
||||
excerpt: string
|
||||
cat: string
|
||||
img: string
|
||||
slug: string
|
||||
image?: unknown
|
||||
}
|
||||
|
||||
type PressEventsSection = Partial<typeof pressIndexContent.events>
|
||||
type PressNewsSection = Partial<typeof pressIndexContent.news>
|
||||
|
||||
const fallbackText = (value: unknown, fallback: string) =>
|
||||
typeof value === 'string' && value.length > 0 ? value : fallback
|
||||
|
||||
const normalizeStatus = (
|
||||
status: unknown,
|
||||
labels: Record<string, string>,
|
||||
defaultStatusLabel: string,
|
||||
) => {
|
||||
const value = fallbackText(status, defaultStatusLabel)
|
||||
return labels[value] || value
|
||||
}
|
||||
|
||||
const detailPath = (spaPath: unknown, slug: unknown, prefix: '/presse/events' | '/presse/blog') => {
|
||||
const path = fallbackText(spaPath, '')
|
||||
if (path.startsWith(`${prefix}/`)) return path
|
||||
|
||||
const pathSlug = fallbackText(slug, '').replace(/^\/+/, '')
|
||||
return pathSlug ? `${prefix}/${pathSlug}` : prefix
|
||||
}
|
||||
|
||||
export function pressEventsFromCms(
|
||||
events: CmsRouteDoc[],
|
||||
fallbackImage: string,
|
||||
labels: Record<string, string>,
|
||||
section: PressEventsSection,
|
||||
): PressEventItem[] {
|
||||
return events.map((event) => ({
|
||||
title: String(
|
||||
event.title ||
|
||||
fallbackText(section.missingTitleLabel, pressIndexContent.events.missingTitleLabel),
|
||||
),
|
||||
date: String(
|
||||
event.date ||
|
||||
fallbackText(section.missingDateLabel, pressIndexContent.events.missingDateLabel),
|
||||
),
|
||||
location: String(
|
||||
event.location ||
|
||||
event.venue ||
|
||||
fallbackText(section.missingLocationLabel, pressIndexContent.events.missingLocationLabel),
|
||||
),
|
||||
cat: String(
|
||||
event.category ||
|
||||
fallbackText(section.missingCategoryLabel, pressIndexContent.events.missingCategoryLabel),
|
||||
),
|
||||
status: normalizeStatus(
|
||||
event.status,
|
||||
labels,
|
||||
fallbackText(section.defaultStatusLabel, pressIndexContent.events.defaultStatusLabel),
|
||||
),
|
||||
img: docImageUrl(event, fallbackImage),
|
||||
desc: String(event.description || event.meta?.description || ''),
|
||||
slug: detailPath(event.spaPath, event.slug, '/presse/events'),
|
||||
}))
|
||||
}
|
||||
|
||||
export function pressNewsFromCms(
|
||||
posts: CmsRouteDoc[],
|
||||
fallbackImage: string,
|
||||
section: PressNewsSection,
|
||||
): PressNewsItem[] {
|
||||
return posts.map((post) => ({
|
||||
title: String(
|
||||
post.title ||
|
||||
fallbackText(section.missingTitleLabel, pressIndexContent.news.missingTitleLabel),
|
||||
),
|
||||
excerpt: String(post.excerpt || post.meta?.description || ''),
|
||||
cat: String(
|
||||
post.cat ||
|
||||
fallbackText(section.missingCategoryLabel, pressIndexContent.news.missingCategoryLabel),
|
||||
),
|
||||
img: docImageUrl(post, fallbackImage, 'heroImage'),
|
||||
slug: detailPath(post.spaPath, post.slug, '/presse/blog'),
|
||||
}))
|
||||
}
|
||||
@@ -24,38 +24,6 @@ export const pressIndexContent = {
|
||||
{ value: 'intern', label: 'Intern' },
|
||||
{ value: 'abgeschlossen', label: 'Abgeschlossen' },
|
||||
],
|
||||
fallbackItems: [
|
||||
{
|
||||
title: 'Preisverleihung 2026',
|
||||
date: '22. Okt 2026',
|
||||
location: 'München, Residenz',
|
||||
cat: 'Gala / Event',
|
||||
status: 'In Planung',
|
||||
img: '/images/buehne-moderatoren.jpg',
|
||||
desc: 'Der glanzvolle Höhepunkt des Jahres. Die Verleihung der Preise in der prachtvollen Kulisse der Residenz München.',
|
||||
slug: '/presse/events/preisverleihung-2026',
|
||||
},
|
||||
{
|
||||
title: 'Mittelstands-Gipfel',
|
||||
date: '15. Juni 2026',
|
||||
location: 'Nürnberg',
|
||||
cat: 'Workshop',
|
||||
status: 'Anmeldung offen',
|
||||
img: '/images/networking-innenhof.jpg',
|
||||
desc: 'Regionaler Austausch und Best-Practices für bayerische Unternehmen auf dem Weg zum Preis.',
|
||||
slug: '/presse/events/mittelstands-gipfel-2026',
|
||||
},
|
||||
{
|
||||
title: 'Nominierten-Auswahl',
|
||||
date: '04. Aug 2026',
|
||||
location: 'Regensburg',
|
||||
cat: 'Jury-Sitzung',
|
||||
status: 'Intern',
|
||||
img: '/images/saal-gedeckt.jpg',
|
||||
desc: 'Das Gremium sichtet die Ergebnisse der Audits und legt die Nominierten für die Hauptpreise fest.',
|
||||
slug: '/presse/events/nominierten-auswahl-2026',
|
||||
},
|
||||
],
|
||||
},
|
||||
news: {
|
||||
eyebrow: 'Newsroom',
|
||||
@@ -65,29 +33,6 @@ export const pressIndexContent = {
|
||||
missingTitleLabel: 'Presse',
|
||||
missingCategoryLabel: 'Presse',
|
||||
collectionFallbackImageFilename: 'gala-saal-overview.jpg',
|
||||
fallbackItems: [
|
||||
{
|
||||
title: 'Wie Bayerns KMU die KI nutzen',
|
||||
excerpt: 'Ein Deep-Dive in die Bewerbungsunterlagen 2024 zeigt: Der Mittelstand treibt die Digitalisierung aktiv voran.',
|
||||
cat: 'Innovation',
|
||||
img: '/images/gala-saal-overview.jpg',
|
||||
slug: '/presse/blog/ki-nutzung-bayerischer-kmu',
|
||||
},
|
||||
{
|
||||
title: 'Start der Ehrenamts-Initiative',
|
||||
excerpt: 'Gemeinsam mit unseren Partnern fördern wir soziale Projekte mittelständischer Unternehmen.',
|
||||
cat: 'Engagement',
|
||||
img: '/images/networking-innenhof.jpg',
|
||||
slug: '/presse/blog/ehrenamts-initiative-2026',
|
||||
},
|
||||
{
|
||||
title: 'Bayerische Wirtschaft wächst',
|
||||
excerpt: 'Neue Prognosen zeigen ein stabiles Wachstum für den Mittelstand – gute Aussichten für die Awards.',
|
||||
cat: 'Wirtschaft',
|
||||
img: '/images/gala-dinner.jpg',
|
||||
slug: '/presse/blog/bayerische-wirtschaft-waechst',
|
||||
},
|
||||
],
|
||||
},
|
||||
downloads: {
|
||||
eyebrow: 'Pressekontakt & Material',
|
||||
|
||||
Reference in New Issue
Block a user