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

@@ -3,8 +3,9 @@ import { Link, useParams, Navigate } from '@/spa/router';
import { Trophy, ArrowLeft, MapPin, Building2, Globe, ChevronRight } from 'lucide-react';
import { WINNERS } from '@/spa/data/winners';
import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField';
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif';
@@ -15,6 +16,12 @@ const BORDER = '#D0D5DD';
const GRAY = '#666666';
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() {
const { slug } = useParams<{ slug: string }>();
const route = useCmsRoute();
@@ -36,10 +43,22 @@ export default function PreistraegerDetail() {
industry: String(cmsWinner.industry || 'Mittelstand'),
website: String(cmsWinner.website || '#'),
hasMedia: Boolean(cmsWinner.hasMedia),
detailPage: (cmsWinner.detailPage || {}) as PreistraegerDetailCms,
} : WINNERS.find(w => w.slug === slug);
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 (
<div style={{ background: '#fff', minHeight: '100vh' }}>
@@ -55,8 +74,8 @@ export default function PreistraegerDetail() {
{/* Breadcrumb */}
<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 }}>
Preisträger:innen
<Link to={fallbackText(breadcrumb.url, preistraegerDetailContent.breadcrumb.url)} style={{ fontFamily: FF, fontSize: 16, color: GOLD, textDecoration: 'none', fontWeight: 600 }}>
{fallbackText(breadcrumb.label, preistraegerDetailContent.breadcrumb.label)}
</Link>
<ChevronRight size={14} style={{ color: GRAY }} />
<span style={{ fontFamily: FF, fontSize: 16, color: GRAY }}>{winner.name}</span>
@@ -92,17 +111,17 @@ export default function PreistraegerDetail() {
{/* Left: Text */}
<div>
<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>
<p style={{ fontFamily: FB, fontSize: 18, color: '#333', lineHeight: 1.8, marginBottom: 40 }}>
{winner.longDesc}
</p>
<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>
<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>
{/* Quote */}
@@ -122,21 +141,21 @@ export default function PreistraegerDetail() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{/* Photo */}
<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>
{/* Kenndaten */}
<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 }}>
Auf einen Blick
{fallbackText(facts.heading, preistraegerDetailContent.facts.heading)}
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{[
{ label: 'Kategorie', value: winner.category, icon: <Trophy size={14} style={{ color: GOLD }} /> },
{ label: 'Auszeichnung', value: winner.type, icon: <Trophy size={14} style={{ color: GOLD }} /> },
{ label: 'Jahr', value: String(winner.year), icon: null },
{ label: 'Standort', 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.categoryLabel, preistraegerDetailContent.facts.categoryLabel), value: winner.category, 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: fallbackText(facts.yearLabel, preistraegerDetailContent.facts.yearLabel), value: String(winner.year), icon: null },
{ label: fallbackText(facts.locationLabel, preistraegerDetailContent.facts.locationLabel), value: winner.location, icon: <MapPin size={14} style={{ color: GOLD }} /> },
{ label: fallbackText(facts.industryLabel, preistraegerDetailContent.facts.industryLabel), value: winner.industry, icon: <Building2 size={14} style={{ color: GOLD }} /> },
].map(row => (
<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>
@@ -162,7 +181,7 @@ export default function PreistraegerDetail() {
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; }}
>
<Globe size={16} /> Website besuchen ↗
<Globe size={16} /> {fallbackText(websiteCta.label, preistraegerDetailContent.websiteCta.label)}
</a>
</div>
</div>
@@ -170,12 +189,12 @@ export default function PreistraegerDetail() {
{/* Back link */}
<div style={{ padding: isMobile ? '32px 24px 48px' : '32px 80px 64px', borderTop: `1px solid ${BORDER}` }}>
<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' }}
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = GOLD)}
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>
</div>
</div>