feat: clean up Preistraeger detail structure by removing unused fields and updating related migrations

This commit is contained in:
syntaxbullet
2026-07-13 22:46:35 +02:00
parent 50b1cb6e2e
commit e28666fc47
8 changed files with 124 additions and 100 deletions

View File

@@ -784,13 +784,13 @@ const Netzwerk: React.FC = () => {
{/* ── 7. SPONSORING-FORM ───────────────────────────────────────────────── */}
<section id="sponsoring" style={{
background: NAVY, overflow: 'hidden', position: 'relative',
minHeight: isMobile ? 'auto' : 'calc(100vh - 60px)',
background: '#24366A', overflow: 'hidden', position: 'relative',
height: isMobile ? 'auto' : 'calc(100vh - 60px)',
display: 'flex', flexDirection: 'column',
}}>
<div style={{ flex: 1, position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr', minHeight: 0, overflow: isMobile ? 'visible' : 'visible' }}>
<div style={{ flex: 1, position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr', minHeight: 0, overflow: 'hidden' }}>
{/* Left – pitch */}
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: 'visible' }}>
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: 'hidden' }}>
<span style={{ fontFamily: FF, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, color: GOLD, display: 'block', marginBottom: 8 }}>{fallbackText(sponsoring.eyebrow, netzwerkContent.sponsoring.eyebrow)}</span>
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 12px' }}><Lines text={fallbackText(sponsoring.heading, netzwerkContent.sponsoring.heading)} /></h2>
<div style={{ width: 36, height: 2, background: GOLD, margin: '0 0 14px', flexShrink: 0 }} />
@@ -810,7 +810,7 @@ const Netzwerk: React.FC = () => {
{/* Center divider */}
{!isMobile && <div style={{ background: 'rgba(255,255,255,0.07)' }} />}
{/* Right – gold form panel */}
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'visible', minHeight: 0, position: 'relative', background: 'linear-gradient(160deg,#DDB84A 0%,#C9A227 52%,#A87800 100%)' }}>
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0, position: 'relative', background: 'linear-gradient(160deg,#DDB84A 0%,#C9A227 52%,#A87800 100%)' }}>
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 0 }} aria-hidden="true">
<defs>
<pattern id="sponsoringCross" width="18" height="18" patternUnits="userSpaceOnUse">
@@ -836,7 +836,7 @@ const Netzwerk: React.FC = () => {
</div>
</div>
)}
<div id="sponsoring-form" style={{ padding: isMobile ? '28px 20px' : '24px 48px 32px 40px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: isMobile ? 560 : 0, position: 'relative', zIndex: 1, scrollMarginTop: 80 }}>
<div id="sponsoring-form" style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1, scrollMarginTop: 80 }}>
<SponsoringForm theme="gold" content={cms.sponsoringForm} />
</div>
</div>

View File

@@ -18,7 +18,8 @@ const BG_ALT = '#E4E2E3';
const ARCHIVE_START_YEAR = 2007;
const ARCHIVE_END_YEAR = 2023;
const ARCHIVE_PERIOD = 'archive-2007-2023' as const;
const ARCHIVE_LABEL = `${ARCHIVE_START_YEAR}–${ARCHIVE_END_YEAR}`;
const ARCHIVE_RANGE_LABEL = `${ARCHIVE_START_YEAR}–${ARCHIVE_END_YEAR}`;
const ARCHIVE_TAB_LABEL = 'Bis 2023';
const GOLDENE_BAVARIA_GROUP = 'goldene-bavaria';
const PREISTRAEGER_GROUP = 'preistraeger';
@@ -165,7 +166,7 @@ export default function Preistraeger() {
const [activePeriod, setActivePeriod] = useState<WinnerPeriod | undefined>(undefined);
const selectedPeriod = activePeriod ?? periods[0] ?? ARCHIVE_PERIOD;
const isArchivePeriod = selectedPeriod === ARCHIVE_PERIOD;
const selectedPeriodLabel = isArchivePeriod ? ARCHIVE_LABEL : String(selectedPeriod);
const selectedPeriodLabel = isArchivePeriod ? ARCHIVE_RANGE_LABEL : String(selectedPeriod);
const filtered = useMemo(
() => winners.filter((winner) => isArchivePeriod ? isArchiveWinner(winner) : winner.year === selectedPeriod),
@@ -229,7 +230,7 @@ export default function Preistraeger() {
transition: 'color 0.15s, border-color 0.15s',
}}
>
{period === ARCHIVE_PERIOD ? ARCHIVE_LABEL : period}
{period === ARCHIVE_PERIOD ? ARCHIVE_TAB_LABEL : period}
</button>
))}
</div>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Link, useParams, Navigate } from '@/spa/router';
import { Trophy, ArrowLeft, MapPin, Building2, Globe, ChevronRight, Download, ChevronLeft, X } from 'lucide-react';
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';
@@ -37,6 +37,7 @@ type PreistraegerGalleryCms = {
}
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
const cmsText = (value: unknown) => typeof value === 'string' ? value.trim() : '';
const optionalNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return undefined;
const parsed = Number(value);
@@ -70,9 +71,9 @@ export default function PreistraegerDetail() {
img: docImageUrl(cmsWinner, placeholderImage),
shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''),
longDesc: String(cmsWinner.longDesc || cmsWinner.description || cmsWinner.meta?.description || fallbackWinner.longDesc || preistraegerDetailContent.fallbackWinner.longDesc),
quote: String(cmsWinner.quote || fallbackWinner.quote || preistraegerDetailContent.fallbackWinner.quote),
quotePerson: String(cmsWinner.quotePerson || cmsWinner.title || fallbackWinner.quotePerson || preistraegerDetailContent.fallbackWinner.quotePerson),
quoteRole: String(cmsWinner.quoteRole || fallbackWinner.quoteRole || preistraegerDetailContent.fallbackWinner.quoteRole),
quote: cmsText(cmsWinner.quote),
quotePerson: cmsText(cmsWinner.quotePerson),
quoteRole: cmsText(cmsWinner.quoteRole),
location: String(cmsWinner.location || fallbackWinner.location || preistraegerDetailContent.fallbackWinner.location),
industry: String(cmsWinner.industry || fallbackWinner.industry || preistraegerDetailContent.fallbackWinner.industry),
website: String(cmsWinner.website || '#'),
@@ -85,9 +86,12 @@ export default function PreistraegerDetail() {
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 || {}) };
const juryTextBeforeName = cmsText(sections.juryTextBeforeName);
const juryTextAfterNameBeforeCategory = cmsText(sections.juryTextAfterNameBeforeCategory);
const juryTextAfterCategory = cmsText(sections.juryTextAfterCategory);
const showJuryText = Boolean(juryTextBeforeName || juryTextAfterNameBeforeCategory || juryTextAfterCategory);
const downloadHref = mediaUrl(cmsWinner?.downloadZip, '');
const downloadFilename = mediaFilename(cmsWinner?.downloadZip);
const downloadLabel = fallbackText(cmsWinner?.downloadLabel, 'Pressepaket herunterladen');
@@ -170,51 +174,42 @@ export default function PreistraegerDetail() {
<h2 className="type-subtitle text-role-heading" style={{ textTransform: 'uppercase', marginBottom: 20 }}>
{fallbackText(sections.juryHeading, preistraegerDetailContent.sections.juryHeading)}
</h2>
<p className="type-body text-role-body" style={{ marginBottom: 40 }}>
{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>
{showJuryText ? (
<p className="type-body text-role-body" style={{ marginBottom: 40 }}>
{juryTextBeforeName}
{juryTextAfterNameBeforeCategory ? (
<>{juryTextBeforeName ? ' ' : null}{winner.name} {juryTextAfterNameBeforeCategory} <strong>{winner.category}</strong>.</>
) : null}
{juryTextAfterCategory ? <>{juryTextBeforeName || juryTextAfterNameBeforeCategory ? ' ' : null}{juryTextAfterCategory}</> : null}
</p>
) : null}
{/* Quote */}
<div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}>
<p className="type-body text-role-heading" style={{ fontStyle: 'italic', marginBottom: 12 }}>
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginRight: 4, fontFamily: '"IBM Plex Sans"' }}>&ldquo;</span>
{winner.quote}
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>&rdquo;</span>
</p>
<p className="type-small text-role-secondary" style={{ fontWeight: 700, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
– {winner.quotePerson}, {winner.quoteRole}
</p>
</div>
{winner.quote ? (
<div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}>
<p className="type-body text-role-heading" style={{ fontStyle: 'italic', marginBottom: winner.quotePerson || winner.quoteRole ? 12 : 0 }}>
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginRight: 4, fontFamily: '"IBM Plex Sans"' }}>&ldquo;</span>
{winner.quote}
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>&rdquo;</span>
</p>
{winner.quotePerson || winner.quoteRole ? (
<p className="type-small text-role-secondary" style={{ fontWeight: 700, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
{winner.quotePerson ? <>– {winner.quotePerson}</> : null}
{winner.quotePerson && winner.quoteRole ? ', ' : null}
{winner.quoteRole}
</p>
) : null}
</div>
) : null}
</div>
{/* Right: Photo + Info box */}
{/* Right: Photo + actions */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{/* Photo */}
<div style={{ borderRadius: 0, overflow: 'hidden', aspectRatio: '4/3' }}>
<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 className="type-small text-role-secondary" style={{ fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 20 }}>
{fallbackText(facts.heading, preistraegerDetailContent.facts.heading)}
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{[
{ 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 }} /> },
...(typeof winner.year === 'number' ? [{ 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>
<span style={{ fontFamily: FF, fontSize: 17, fontWeight: 700, color: '#101828', overflowWrap: 'anywhere' }}>{row.value}</span>
</div>
))}
</div>
</div>
{/* Website button */}
<a
href={`https://${winner.website}`}