feat: manage winner detail chrome via payload

This commit is contained in:
syntaxbullet
2026-06-22 11:48:19 +02:00
parent 5bd1977c63
commit a19b703416
7 changed files with 311 additions and 17 deletions

View File

@@ -26,6 +26,7 @@
"preload:mitglied-werden-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-mitglied-werden-page-cms.ts", "preload:mitglied-werden-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-mitglied-werden-page-cms.ts",
"preload:netzwerk-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-netzwerk-page-cms.ts", "preload:netzwerk-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-netzwerk-page-cms.ts",
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts", "preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
"preload:preistraeger-detail-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-preistraeger-detail-cms.ts",
"preload:preistraeger-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-preistraeger-index-page-cms.ts", "preload:preistraeger-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-preistraeger-index-page-cms.ts",
"preload:press-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-press-index-page-cms.ts", "preload:press-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-press-index-page-cms.ts",
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install", "reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",

View File

@@ -3,6 +3,7 @@ import type { CollectionConfig } from 'payload'
import { authenticated } from '@/access/authenticated' import { authenticated } from '@/access/authenticated'
import { authenticatedOrPublished } from '@/access/authenticatedOrPublished' import { authenticatedOrPublished } from '@/access/authenticatedOrPublished'
import { populatePublishedAt } from '@/hooks/populatePublishedAt' import { populatePublishedAt } from '@/hooks/populatePublishedAt'
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
import { generatePreviewPath } from '@/utilities/generatePreviewPath' import { generatePreviewPath } from '@/utilities/generatePreviewPath'
import { slugField } from 'payload' import { slugField } from 'payload'
@@ -110,6 +111,94 @@ export const Preistraeger: CollectionConfig = {
name: 'hasMedia', name: 'hasMedia',
type: 'checkbox', type: 'checkbox',
}, },
{
name: 'detailPage',
label: 'Detail page chrome',
type: 'group',
admin: {
description:
'Shared labels, explanatory copy, CTAs, and supporting media for the Preisträger detail route.',
},
fields: [
{
name: 'breadcrumb',
label: 'Breadcrumb',
type: 'group',
fields: [
{ name: 'url', type: 'text', label: 'URL', defaultValue: preistraegerDetailContent.breadcrumb.url },
{ name: 'label', type: 'text', label: 'Label', defaultValue: preistraegerDetailContent.breadcrumb.label },
],
},
{
name: 'sections',
label: 'Section copy',
type: 'group',
fields: [
{ name: 'companyHeading', type: 'text', defaultValue: preistraegerDetailContent.sections.companyHeading },
{ name: 'juryHeading', type: 'text', defaultValue: preistraegerDetailContent.sections.juryHeading },
{
name: 'juryTextBeforeName',
type: 'textarea',
defaultValue: preistraegerDetailContent.sections.juryTextBeforeName,
},
{
name: 'juryTextAfterNameBeforeCategory',
type: 'textarea',
defaultValue: preistraegerDetailContent.sections.juryTextAfterNameBeforeCategory,
},
{
name: 'juryTextAfterCategory',
type: 'textarea',
defaultValue: preistraegerDetailContent.sections.juryTextAfterCategory,
},
],
},
{
name: 'sideImage',
label: 'Side image',
type: 'group',
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
admin: {
description: `Current frontend image: /${preistraegerDetailContent.sideImage.imageFilename}`,
},
},
{ name: 'imageAlt', type: 'text', defaultValue: preistraegerDetailContent.sideImage.imageAlt },
],
},
{
name: 'facts',
label: 'Facts box labels',
type: 'group',
fields: [
{ name: 'heading', type: 'text', defaultValue: preistraegerDetailContent.facts.heading },
{ name: 'categoryLabel', type: 'text', defaultValue: preistraegerDetailContent.facts.categoryLabel },
{ name: 'awardLabel', type: 'text', defaultValue: preistraegerDetailContent.facts.awardLabel },
{ name: 'yearLabel', type: 'text', defaultValue: preistraegerDetailContent.facts.yearLabel },
{ name: 'locationLabel', type: 'text', defaultValue: preistraegerDetailContent.facts.locationLabel },
{ name: 'industryLabel', type: 'text', defaultValue: preistraegerDetailContent.facts.industryLabel },
],
},
{
name: 'websiteCta',
label: 'Website CTA',
type: 'group',
fields: [{ name: 'label', type: 'text', defaultValue: preistraegerDetailContent.websiteCta.label }],
},
{
name: 'backLink',
label: 'Back link',
type: 'group',
fields: [
{ name: 'url', type: 'text', label: 'URL', defaultValue: preistraegerDetailContent.backLink.url },
{ name: 'label', type: 'text', label: 'Label', defaultValue: preistraegerDetailContent.backLink.label },
],
},
],
},
{ {
name: 'spaPath', name: 'spaPath',
type: 'text', type: 'text',

View File

@@ -2460,6 +2460,44 @@ export interface Preistraeger {
industry?: string | null; industry?: string | null;
website?: string | null; website?: string | null;
hasMedia?: boolean | null; hasMedia?: boolean | null;
/**
* Shared labels, explanatory copy, CTAs, and supporting media for the Preisträger detail route.
*/
detailPage?: {
breadcrumb?: {
url?: string | null;
label?: string | null;
};
sections?: {
companyHeading?: string | null;
juryHeading?: string | null;
juryTextBeforeName?: string | null;
juryTextAfterNameBeforeCategory?: string | null;
juryTextAfterCategory?: string | null;
};
sideImage?: {
/**
* Current frontend image: /gala-preistraeger.jpg
*/
image?: (number | null) | Media;
imageAlt?: string | null;
};
facts?: {
heading?: string | null;
categoryLabel?: string | null;
awardLabel?: string | null;
yearLabel?: string | null;
locationLabel?: string | null;
industryLabel?: string | null;
};
websiteCta?: {
label?: string | null;
};
backLink?: {
url?: string | null;
label?: string | null;
};
};
/** /**
* Route in the legacy SPA used for previewing the current migrated Preisträger state. * Route in the legacy SPA used for previewing the current migrated Preisträger state.
*/ */
@@ -4451,6 +4489,52 @@ export interface PreistraegerSelect<T extends boolean = true> {
industry?: T; industry?: T;
website?: T; website?: T;
hasMedia?: T; hasMedia?: T;
detailPage?:
| T
| {
breadcrumb?:
| T
| {
url?: T;
label?: T;
};
sections?:
| T
| {
companyHeading?: T;
juryHeading?: T;
juryTextBeforeName?: T;
juryTextAfterNameBeforeCategory?: T;
juryTextAfterCategory?: T;
};
sideImage?:
| T
| {
image?: T;
imageAlt?: T;
};
facts?:
| T
| {
heading?: T;
categoryLabel?: T;
awardLabel?: T;
yearLabel?: T;
locationLabel?: T;
industryLabel?: T;
};
websiteCta?:
| T
| {
label?: T;
};
backLink?:
| T
| {
url?: T;
label?: T;
};
};
spaPath?: T; spaPath?: T;
publishedAt?: T; publishedAt?: T;
meta?: meta?:

View File

@@ -6,6 +6,7 @@ import { getPayload } from 'payload'
import { articles } from '@/spa/data/blog' import { articles } from '@/spa/data/blog'
import { events } from '@/spa/data/events' import { events } from '@/spa/data/events'
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
import { WINNERS } from '@/spa/data/winners' import { WINNERS } from '@/spa/data/winners'
import { spaPages, type SpaPageRegistryEntry } from '@/spa/pageRegistry' import { spaPages, type SpaPageRegistryEntry } from '@/spa/pageRegistry'
@@ -159,6 +160,7 @@ function eventData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
function preistraegerData(page: SpaPageRegistryEntry, mediaMap: MediaMap) { function preistraegerData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
const winner = WINNERS.find((item) => item.slug === targetSlug(page)) const winner = WINNERS.find((item) => item.slug === targetSlug(page))
const { imageFilename: _sideImageFilename, ...sideImage } = preistraegerDetailContent.sideImage
return { return {
...baseData(page), ...baseData(page),
image: mediaID(mediaMap, winner?.img), image: mediaID(mediaMap, winner?.img),
@@ -175,6 +177,13 @@ function preistraegerData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
industry: winner?.industry, industry: winner?.industry,
website: winner?.website, website: winner?.website,
hasMedia: winner?.hasMedia, hasMedia: winner?.hasMedia,
detailPage: {
...preistraegerDetailContent,
sideImage: {
...sideImage,
image: mediaID(mediaMap, `/${preistraegerDetailContent.sideImage.imageFilename}`),
},
},
} }
} }
@@ -265,6 +274,7 @@ async function upsertPage(payload: Payload, page: SpaPageRegistryEntry, mediaMap
async function loadMediaMap(payload: Payload): Promise<MediaMap> { async function loadMediaMap(payload: Payload): Promise<MediaMap> {
const sources = new Set([ const sources = new Set([
...WINNERS.map((winner) => winner.img), ...WINNERS.map((winner) => winner.img),
`/${preistraegerDetailContent.sideImage.imageFilename}`,
...events.map((event) => event.img), ...events.map((event) => event.img),
...articles.map((article) => article.img), ...articles.map((article) => article.img),
]) ])

View File

@@ -0,0 +1,57 @@
import 'dotenv/config'
import config from '@payload-config'
import { getPayload, type Payload } from 'payload'
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
const mediaByFilename = async (payload: Payload, filename: string) => {
const result = await payload.find({
collection: 'media',
limit: 1,
pagination: false,
where: { filename: { equals: filename } },
})
return result.docs[0]?.id
}
async function main() {
const payload = await getPayload({ config })
const sideImage = await mediaByFilename(payload, preistraegerDetailContent.sideImage.imageFilename)
const { imageFilename: _sideImageFilename, ...sideImageContent } = preistraegerDetailContent.sideImage
const winners = await payload.find({
collection: 'preistraeger',
draft: true,
overrideAccess: true,
pagination: false,
limit: 1000,
})
await Promise.all(
winners.docs.map((winner) =>
payload.update({
collection: 'preistraeger',
id: winner.id,
overrideAccess: true,
context: { disableRevalidate: true },
data: {
detailPage: {
...preistraegerDetailContent,
sideImage: {
...sideImageContent,
image: sideImage,
},
},
} as never,
}),
),
)
payload.logger.info(`Preloaded Preisträger detail CMS fields for ${winners.docs.length} records`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})

View File

@@ -3,8 +3,9 @@ import { Link, useParams, Navigate } from '@/spa/router';
import { Trophy, ArrowLeft, MapPin, Building2, Globe, ChevronRight } from 'lucide-react'; import { Trophy, ArrowLeft, MapPin, Building2, Globe, ChevronRight } from 'lucide-react';
import { WINNERS } from '@/spa/data/winners'; import { WINNERS } from '@/spa/data/winners';
import { useCmsRoute } from '@/spa/cmsRoute'; import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField'; import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent';
import Image from '@/spa/components/ui/UnoptimizedImage' import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
@@ -15,6 +16,12 @@ const BORDER = '#D0D5DD';
const GRAY = '#666666'; const GRAY = '#666666';
const BG_ALT = '#E4E2E3'; const BG_ALT = '#E4E2E3';
type PreistraegerDetailCms = Partial<typeof preistraegerDetailContent> & {
sideImage?: Partial<typeof preistraegerDetailContent.sideImage> & { image?: unknown }
}
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
export default function PreistraegerDetail() { export default function PreistraegerDetail() {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const route = useCmsRoute(); const route = useCmsRoute();
@@ -36,10 +43,22 @@ export default function PreistraegerDetail() {
industry: String(cmsWinner.industry || 'Mittelstand'), industry: String(cmsWinner.industry || 'Mittelstand'),
website: String(cmsWinner.website || '#'), website: String(cmsWinner.website || '#'),
hasMedia: Boolean(cmsWinner.hasMedia), hasMedia: Boolean(cmsWinner.hasMedia),
detailPage: (cmsWinner.detailPage || {}) as PreistraegerDetailCms,
} : WINNERS.find(w => w.slug === slug); } : WINNERS.find(w => w.slug === slug);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (!winner) return <Navigate to="/preistraeger" replace />; const detail = {
...preistraegerDetailContent,
...((winner && 'detailPage' in winner ? winner.detailPage : {}) || {}),
} as PreistraegerDetailCms;
const breadcrumb = { ...preistraegerDetailContent.breadcrumb, ...(detail.breadcrumb || {}) };
const sections = { ...preistraegerDetailContent.sections, ...(detail.sections || {}) };
const sideImage = { ...preistraegerDetailContent.sideImage, ...(detail.sideImage || {}) };
const facts = { ...preistraegerDetailContent.facts, ...(detail.facts || {}) };
const websiteCta = { ...preistraegerDetailContent.websiteCta, ...(detail.websiteCta || {}) };
const backLink = { ...preistraegerDetailContent.backLink, ...(detail.backLink || {}) };
if (!winner) return <Navigate to={preistraegerDetailContent.notFoundRedirect} replace />;
return ( return (
<div style={{ background: '#fff', minHeight: '100vh' }}> <div style={{ background: '#fff', minHeight: '100vh' }}>
@@ -55,8 +74,8 @@ export default function PreistraegerDetail() {
{/* Breadcrumb */} {/* Breadcrumb */}
<div style={{ background: '#fff', borderBottom: `1px solid ${BORDER}`, padding: isMobile ? '16px 24px' : '16px 80px', display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ background: '#fff', borderBottom: `1px solid ${BORDER}`, padding: isMobile ? '16px 24px' : '16px 80px', display: 'flex', alignItems: 'center', gap: 8 }}>
<Link to="/preistraeger" style={{ fontFamily: FF, fontSize: 16, color: GOLD, textDecoration: 'none', fontWeight: 600 }}> <Link to={fallbackText(breadcrumb.url, preistraegerDetailContent.breadcrumb.url)} style={{ fontFamily: FF, fontSize: 16, color: GOLD, textDecoration: 'none', fontWeight: 600 }}>
Preisträger:innen {fallbackText(breadcrumb.label, preistraegerDetailContent.breadcrumb.label)}
</Link> </Link>
<ChevronRight size={14} style={{ color: GRAY }} /> <ChevronRight size={14} style={{ color: GRAY }} />
<span style={{ fontFamily: FF, fontSize: 16, color: GRAY }}>{winner.name}</span> <span style={{ fontFamily: FF, fontSize: 16, color: GRAY }}>{winner.name}</span>
@@ -92,17 +111,17 @@ export default function PreistraegerDetail() {
{/* Left: Text */} {/* Left: Text */}
<div> <div>
<h2 style={{ fontFamily: FF, fontSize: 18, fontWeight: 800, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 20 }}> <h2 style={{ fontFamily: FF, fontSize: 18, fontWeight: 800, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 20 }}>
Über das Unternehmen {fallbackText(sections.companyHeading, preistraegerDetailContent.sections.companyHeading)}
</h2> </h2>
<p style={{ fontFamily: FB, fontSize: 18, color: '#333', lineHeight: 1.8, marginBottom: 40 }}> <p style={{ fontFamily: FB, fontSize: 18, color: '#333', lineHeight: 1.8, marginBottom: 40 }}>
{winner.longDesc} {winner.longDesc}
</p> </p>
<h2 style={{ fontFamily: FF, fontSize: 18, fontWeight: 800, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 20 }}> <h2 style={{ fontFamily: FF, fontSize: 18, fontWeight: 800, color: '#101828', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 20 }}>
Warum ausgezeichnet? {fallbackText(sections.juryHeading, preistraegerDetailContent.sections.juryHeading)}
</h2> </h2>
<p style={{ fontFamily: FB, fontSize: 18, color: '#333', lineHeight: 1.8, marginBottom: 40 }}> <p style={{ fontFamily: FB, fontSize: 18, color: '#333', lineHeight: 1.8, marginBottom: 40 }}>
Die Jury des Bayerischen Mittelstandspreises überzeugte {winner.name} durch außergewöhnliche Leistungen im Bereich <strong>{winner.category}</strong>. Das Unternehmen steht exemplarisch für die Stärken des bayerischen Mittelstands: unternehmerischen Mut, wertebasierte Führung und nachhaltige Wirkung weit über den eigenen Betrieb hinaus. {fallbackText(sections.juryTextBeforeName, preistraegerDetailContent.sections.juryTextBeforeName)} {winner.name} {fallbackText(sections.juryTextAfterNameBeforeCategory, preistraegerDetailContent.sections.juryTextAfterNameBeforeCategory)} <strong>{winner.category}</strong>. {fallbackText(sections.juryTextAfterCategory, preistraegerDetailContent.sections.juryTextAfterCategory)}
</p> </p>
{/* Quote */} {/* Quote */}
@@ -122,21 +141,21 @@ export default function PreistraegerDetail() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{/* Photo */} {/* Photo */}
<div style={{ borderRadius: 0, overflow: 'hidden', aspectRatio: '4/3' }}> <div style={{ borderRadius: 0, overflow: 'hidden', aspectRatio: '4/3' }}>
<Image unoptimized src="/gala-preistraeger.jpg" alt="Gala Preisverleihung" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: 'brightness(0.95)' }} /> <Image unoptimized src={mediaUrl(sideImage.image, `/${preistraegerDetailContent.sideImage.imageFilename}`)} alt={mediaAlt(sideImage.image, fallbackText(sideImage.imageAlt, preistraegerDetailContent.sideImage.imageAlt))} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: 'brightness(0.95)' }} />
</div> </div>
{/* Kenndaten */} {/* Kenndaten */}
<div style={{ background: BG_ALT, borderLeft: `3px solid ${GOLD}`, padding: '20px 24px' }}> <div style={{ background: BG_ALT, borderLeft: `3px solid ${GOLD}`, padding: '20px 24px' }}>
<h3 style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: GRAY, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 20 }}> <h3 style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: GRAY, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 20 }}>
Auf einen Blick {fallbackText(facts.heading, preistraegerDetailContent.facts.heading)}
</h3> </h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{[ {[
{ label: 'Kategorie', value: winner.category, icon: <Trophy size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.categoryLabel, preistraegerDetailContent.facts.categoryLabel), value: winner.category, icon: <Trophy size={14} style={{ color: GOLD }} /> },
{ label: 'Auszeichnung', value: winner.type, icon: <Trophy size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.awardLabel, preistraegerDetailContent.facts.awardLabel), value: winner.type, icon: <Trophy size={14} style={{ color: GOLD }} /> },
{ label: 'Jahr', value: String(winner.year), icon: null }, { label: fallbackText(facts.yearLabel, preistraegerDetailContent.facts.yearLabel), value: String(winner.year), icon: null },
{ label: 'Standort', value: winner.location, icon: <MapPin size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.locationLabel, preistraegerDetailContent.facts.locationLabel), value: winner.location, icon: <MapPin size={14} style={{ color: GOLD }} /> },
{ label: 'Branche', value: winner.industry, icon: <Building2 size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.industryLabel, preistraegerDetailContent.facts.industryLabel), value: winner.industry, icon: <Building2 size={14} style={{ color: GOLD }} /> },
].map(row => ( ].map(row => (
<div key={row.label} style={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <div key={row.label} style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<span style={{ fontFamily: FF, fontSize: 11, color: GRAY, textTransform: 'uppercase', letterSpacing: '0.08em' }}>{row.label}</span> <span style={{ fontFamily: FF, fontSize: 11, color: GRAY, textTransform: 'uppercase', letterSpacing: '0.08em' }}>{row.label}</span>
@@ -162,7 +181,7 @@ export default function PreistraegerDetail() {
onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.background = NAVY; el.style.color = '#fff'; }} onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.background = NAVY; el.style.color = '#fff'; }}
onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = 'transparent'; el.style.color = NAVY; }} onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = 'transparent'; el.style.color = NAVY; }}
> >
<Globe size={16} /> Website besuchen <Globe size={16} /> {fallbackText(websiteCta.label, preistraegerDetailContent.websiteCta.label)}
</a> </a>
</div> </div>
</div> </div>
@@ -170,12 +189,12 @@ export default function PreistraegerDetail() {
{/* Back link */} {/* Back link */}
<div style={{ padding: isMobile ? '32px 24px 48px' : '32px 80px 64px', borderTop: `1px solid ${BORDER}` }}> <div style={{ padding: isMobile ? '32px 24px 48px' : '32px 80px 64px', borderTop: `1px solid ${BORDER}` }}>
<Link <Link
to="/preistraeger" to={fallbackText(backLink.url, preistraegerDetailContent.backLink.url)}
style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: FF, fontSize: 17, fontWeight: 700, color: '#101828', textDecoration: 'none', textTransform: 'uppercase', letterSpacing: '0.06em', transition: 'color 0.15s' }} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: FF, fontSize: 17, fontWeight: 700, color: '#101828', textDecoration: 'none', textTransform: 'uppercase', letterSpacing: '0.06em', transition: 'color 0.15s' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = GOLD)} onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = GOLD)}
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = NAVY)} onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = NAVY)}
> >
<ArrowLeft size={16} /> Alle Preisträger <ArrowLeft size={16} /> {fallbackText(backLink.label, preistraegerDetailContent.backLink.label)}
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,34 @@
export const preistraegerDetailContent = {
notFoundRedirect: '/preistraeger',
breadcrumb: {
url: '/preistraeger',
label: 'Preisträger:innen',
},
sections: {
companyHeading: 'Über das Unternehmen',
juryHeading: 'Warum ausgezeichnet?',
juryTextBeforeName: 'Die Jury des Bayerischen Mittelstandspreises überzeugte',
juryTextAfterNameBeforeCategory: 'durch außergewöhnliche Leistungen im Bereich',
juryTextAfterCategory:
'Das Unternehmen steht exemplarisch für die Stärken des bayerischen Mittelstands: unternehmerischen Mut, wertebasierte Führung und nachhaltige Wirkung weit über den eigenen Betrieb hinaus.',
},
sideImage: {
imageFilename: 'gala-preistraeger.jpg',
imageAlt: 'Gala Preisverleihung',
},
facts: {
heading: 'Auf einen Blick',
categoryLabel: 'Kategorie',
awardLabel: 'Auszeichnung',
yearLabel: 'Jahr',
locationLabel: 'Standort',
industryLabel: 'Branche',
},
websiteCta: {
label: 'Website besuchen ↗',
},
backLink: {
url: '/preistraeger',
label: 'Alle Preisträger',
},
}