feat: manage about highlight fallback media via payload

This commit is contained in:
syntaxbullet
2026-06-22 12:32:56 +02:00
parent 53fc63b486
commit 9d2994ecdf
5 changed files with 26 additions and 12 deletions

View File

@@ -129,6 +129,7 @@ export const aboutFields: Field[] = [
type: 'group', type: 'group',
admin: sectionAdmin('Header copy and editor-selected winners for the photo grid.'), admin: sectionAdmin('Header copy and editor-selected winners for the photo grid.'),
fields: [ fields: [
uploadField('fallbackImage', 'Fallback winner image', `Current frontend fallback image: /images/${aboutContent.highlights.fallbackImageFilename}`),
text('eyebrow', 'Eyebrow', aboutContent.highlights.eyebrow), text('eyebrow', 'Eyebrow', aboutContent.highlights.eyebrow),
textarea('heading', 'Heading', aboutContent.highlights.heading), textarea('heading', 'Heading', aboutContent.highlights.heading),
ctaGroup('cta', 'CTA', aboutContent.highlights.cta.label, aboutContent.highlights.cta.url), ctaGroup('cta', 'CTA', aboutContent.highlights.cta.label, aboutContent.highlights.cta.url),

View File

@@ -1219,6 +1219,10 @@ export interface Page {
* Header copy and editor-selected winners for the photo grid. * Header copy and editor-selected winners for the photo grid.
*/ */
highlights?: { highlights?: {
/**
* Current frontend fallback image: /images/gala-saal-overview.jpg
*/
fallbackImage?: (number | null) | Media;
eyebrow?: string | null; eyebrow?: string | null;
heading?: string | null; heading?: string | null;
cta?: { cta?: {
@@ -4101,6 +4105,7 @@ export interface PagesSelect<T extends boolean = true> {
highlights?: highlights?:
| T | T
| { | {
fallbackImage?: T;
eyebrow?: T; eyebrow?: T;
heading?: T; heading?: T;
cta?: cta?:

View File

@@ -32,10 +32,11 @@ async function main() {
const page = pageResult.docs[0] const page = pageResult.docs[0]
if (!page) throw new Error('About page not found. Expected spaPath=/der-bmp or slug=der-bmp/about.') if (!page) throw new Error('About page not found. Expected spaPath=/der-bmp or slug=der-bmp/about.')
const [heroImage, prizeImage, mittelstandImage, winnerResult] = await Promise.all([ const [heroImage, prizeImage, mittelstandImage, fallbackWinnerImage, winnerResult] = await Promise.all([
mediaByFilename(payload, aboutContent.hero.backgroundImageFilename), mediaByFilename(payload, aboutContent.hero.backgroundImageFilename),
mediaByFilename(payload, aboutContent.prize.imageFilename), mediaByFilename(payload, aboutContent.prize.imageFilename),
mediaByFilename(payload, aboutContent.mittelstand.imageFilename), mediaByFilename(payload, aboutContent.mittelstand.imageFilename),
mediaByFilename(payload, aboutContent.highlights.fallbackImageFilename),
payload.find({ payload.find({
collection: 'preistraeger', collection: 'preistraeger',
limit: 8, limit: 8,
@@ -49,6 +50,7 @@ async function main() {
const { backgroundImageFilename: _heroFilename, ...heroContent } = aboutContent.hero const { backgroundImageFilename: _heroFilename, ...heroContent } = aboutContent.hero
const { imageFilename: _prizeFilename, ...prizeContent } = aboutContent.prize const { imageFilename: _prizeFilename, ...prizeContent } = aboutContent.prize
const { imageFilename: _mittelstandFilename, ...mittelstandContent } = aboutContent.mittelstand const { imageFilename: _mittelstandFilename, ...mittelstandContent } = aboutContent.mittelstand
const { fallbackImageFilename: _highlightFallbackFilename, ...highlightsContent } = aboutContent.highlights
await payload.update({ await payload.update({
collection: 'pages', collection: 'pages',
@@ -73,7 +75,8 @@ async function main() {
body: aboutContent.mittelstand.body.map((text) => ({ text })), body: aboutContent.mittelstand.body.map((text) => ({ text })),
}, },
highlights: { highlights: {
...aboutContent.highlights, ...highlightsContent,
fallbackImage: fallbackWinnerImage,
selectedWinners: winnerResult.docs.map((winner) => winner.id), selectedWinners: winnerResult.docs.map((winner) => winner.id),
}, },
}, },

View File

@@ -44,6 +44,7 @@ export const aboutContent = {
], ],
}, },
highlights: { highlights: {
fallbackImageFilename: 'gala-saal-overview.jpg',
eyebrow: 'Preisträger-Highlights', eyebrow: 'Preisträger-Highlights',
heading: 'AUSGEZEICHNETE 2025', heading: 'AUSGEZEICHNETE 2025',
cta: { label: 'Alle Preisträger', url: '/preistraeger' }, cta: { label: 'Alle Preisträger', url: '/preistraeger' },

View File

@@ -21,7 +21,7 @@ type AboutCms = Partial<typeof aboutContent> & {
hero?: Partial<typeof aboutContent.hero> & { backgroundImage?: unknown } hero?: Partial<typeof aboutContent.hero> & { backgroundImage?: unknown }
prize?: Partial<typeof aboutContent.prize> & { image?: unknown } prize?: Partial<typeof aboutContent.prize> & { image?: unknown }
mittelstand?: Partial<typeof aboutContent.mittelstand> & { image?: unknown } mittelstand?: Partial<typeof aboutContent.mittelstand> & { image?: unknown }
highlights?: Partial<typeof aboutContent.highlights> & { selectedWinners?: unknown[] | null } highlights?: Partial<typeof aboutContent.highlights> & { fallbackImage?: unknown; selectedWinners?: unknown[] | null }
} }
type TextRow = { text?: string | null } type TextRow = { text?: string | null }
@@ -63,7 +63,7 @@ function HighlightedText({ text, highlight }: { text: string; highlight: string
) )
} }
const normalizeWinner = (doc: CmsRouteDoc | undefined): Winner | undefined => { const normalizeWinner = (doc: CmsRouteDoc | undefined, fallbackImage: string): Winner | undefined => {
if (!doc) return undefined if (!doc) return undefined
return { return {
id: String(doc.id), id: String(doc.id),
@@ -72,7 +72,7 @@ const normalizeWinner = (doc: CmsRouteDoc | undefined): Winner | undefined => {
category: String(doc.category || 'Preisträger'), category: String(doc.category || 'Preisträger'),
year: Number(doc.year || aboutContent.highlights.year), year: Number(doc.year || aboutContent.highlights.year),
type: String(doc.awardType || 'Auszeichnung'), type: String(doc.awardType || 'Auszeichnung'),
img: mediaUrl(doc.image, '/images/gala-saal-overview.jpg'), img: mediaUrl(doc.image, fallbackImage),
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''), shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''), longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
quote: String(doc.quote || ''), quote: String(doc.quote || ''),
@@ -85,10 +85,10 @@ const normalizeWinner = (doc: CmsRouteDoc | undefined): Winner | undefined => {
} }
} }
const winnerFromRelationship = (entry: unknown, allDocs: CmsRouteDoc[]) => { const winnerFromRelationship = (entry: unknown, allDocs: CmsRouteDoc[], fallbackImage: string) => {
if (entry && typeof entry === 'object') return normalizeWinner(entry as CmsRouteDoc) if (entry && typeof entry === 'object') return normalizeWinner(entry as CmsRouteDoc, fallbackImage)
const doc = allDocs.find((item) => String(item.id) === String(entry)) const doc = allDocs.find((item) => String(item.id) === String(entry))
return normalizeWinner(doc) return normalizeWinner(doc, fallbackImage)
} }
const About: React.FC = () => { const About: React.FC = () => {
@@ -98,16 +98,20 @@ const About: React.FC = () => {
const hero = { ...aboutContent.hero, ...(cms.hero || {}) } const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
const prize = { ...aboutContent.prize, ...(cms.prize || {}) } const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) } const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
const highlights = { ...aboutContent.highlights, ...(cms.highlights || {}) } const highlights = useMemo(() => ({ ...aboutContent.highlights, ...(cms.highlights || {}) }), [cms.highlights])
const testimonials = { ...aboutContent.testimonials, ...(cms.testimonials || {}) } const testimonials = { ...aboutContent.testimonials, ...(cms.testimonials || {}) }
const history = { ...aboutContent.history, ...(cms.history || {}) } const history = { ...aboutContent.history, ...(cms.history || {}) }
const goals = { ...aboutContent.goals, ...(cms.goals || {}) } const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
const values = { ...aboutContent.values, ...(cms.values || {}) } const values = { ...aboutContent.values, ...(cms.values || {}) }
const cta = { ...aboutContent.cta, ...(cms.cta || {}) } const cta = { ...aboutContent.cta, ...(cms.cta || {}) }
const highlightFallbackImage = mediaUrl(
highlights.fallbackImage,
`/images/${aboutContent.highlights.fallbackImageFilename}`,
)
const highlightWinners = useMemo(() => { const highlightWinners = useMemo(() => {
const selected = fallbackArray<unknown>(highlights.selectedWinners, []) const selected = fallbackArray<unknown>(highlights.selectedWinners, [])
.map((entry) => winnerFromRelationship(entry, cmsWinners)) .map((entry) => winnerFromRelationship(entry, cmsWinners, highlightFallbackImage))
.filter((winner): winner is Winner => Boolean(winner)) .filter((winner): winner is Winner => Boolean(winner))
if (selected.length) return selected.slice(0, 8) if (selected.length) return selected.slice(0, 8)
@@ -116,12 +120,12 @@ const About: React.FC = () => {
Boolean(winner) && winner?.year === Number(highlights.year || aboutContent.highlights.year) Boolean(winner) && winner?.year === Number(highlights.year || aboutContent.highlights.year)
const cmsByYear = cmsWinners const cmsByYear = cmsWinners
.map(normalizeWinner) .map((winner) => normalizeWinner(winner, highlightFallbackImage))
.filter(isWinnerFromYear) .filter(isWinnerFromYear)
.slice(0, 8) .slice(0, 8)
return cmsByYear.length ? cmsByYear : FALLBACK_HIGHLIGHTS return cmsByYear.length ? cmsByYear : FALLBACK_HIGHLIGHTS
}, [cmsWinners, highlights.selectedWinners, highlights.year]) }, [cmsWinners, highlightFallbackImage, highlights])
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">