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

@@ -275,9 +275,6 @@ export const Preistraeger: CollectionConfig = {
{ name: 'category', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.category }, { name: 'category', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.category },
{ name: 'awardType', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.awardType }, { name: 'awardType', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.awardType },
{ name: 'longDesc', type: 'textarea', defaultValue: preistraegerDetailContent.fallbackWinner.longDesc }, { name: 'longDesc', type: 'textarea', defaultValue: preistraegerDetailContent.fallbackWinner.longDesc },
{ name: 'quote', type: 'textarea', defaultValue: preistraegerDetailContent.fallbackWinner.quote },
{ name: 'quotePerson', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.quotePerson },
{ name: 'quoteRole', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.quoteRole },
{ name: 'location', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.location }, { name: 'location', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.location },
{ name: 'industry', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.industry }, { name: 'industry', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.industry },
], ],
@@ -322,19 +319,6 @@ export const Preistraeger: CollectionConfig = {
{ name: 'imageAlt', type: 'text', defaultValue: preistraegerDetailContent.sideImage.imageAlt }, { 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', name: 'websiteCta',
label: 'Website CTA', label: 'Website CTA',

View File

@@ -0,0 +1,73 @@
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 factsColumns = [
['heading', 'Auf einen Blick'],
['category_label', 'Kategorie'],
['award_label', 'Auszeichnung'],
['year_label', 'Jahr'],
['location_label', 'Standort'],
['industry_label', 'Branche'],
] as const
const fallbackQuoteColumns = [
['quote', 'Diese Erfolgsgeschichte wird aktuell in Payload CMS gepflegt.'],
['quote_person', 'BMP'],
['quote_role', 'Preisträger'],
] 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 columnExists(db: MigrationDB, tableName: string, columnName: string) {
if (!(await tableExists(db, tableName))) return false
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
return columns.some((column) => column.name === columnName)
}
async function addColumn(db: MigrationDB, tableName: string, columnName: string, definition: string) {
if (await columnExists(db, tableName, columnName)) return
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} ADD ${ident(columnName)} ${definition};`))
}
async function dropColumn(db: MigrationDB, tableName: string, columnName: string) {
if (!(await columnExists(db, tableName, columnName))) return
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} DROP COLUMN ${ident(columnName)};`))
}
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
for (const [suffix] of factsColumns) {
await dropColumn(db, 'preistraeger', `detail_page_facts_${suffix}`)
await dropColumn(db, '_preistraeger_v', `version_detail_page_facts_${suffix}`)
}
for (const [suffix] of fallbackQuoteColumns) {
await dropColumn(db, 'preistraeger', `detail_page_fallback_winner_${suffix}`)
await dropColumn(db, '_preistraeger_v', `version_detail_page_fallback_winner_${suffix}`)
}
}
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
for (const [suffix, defaultValue] of factsColumns) {
await addColumn(db, 'preistraeger', `detail_page_facts_${suffix}`, `text DEFAULT ${literal(defaultValue)}`)
await addColumn(db, '_preistraeger_v', `version_detail_page_facts_${suffix}`, `text DEFAULT ${literal(defaultValue)}`)
}
for (const [suffix, defaultValue] of fallbackQuoteColumns) {
await addColumn(db, 'preistraeger', `detail_page_fallback_winner_${suffix}`, `text DEFAULT ${literal(defaultValue)}`)
await addColumn(db, '_preistraeger_v', `version_detail_page_fallback_winner_${suffix}`, `text DEFAULT ${literal(defaultValue)}`)
}
}

View File

@@ -14,6 +14,7 @@ import * as migration_20260713_155500_shared_application_form from './20260713_1
import * as migration_20260713_163000_participation_supporting_awards_copy from './20260713_163000_participation_supporting_awards_copy'; import * as migration_20260713_163000_participation_supporting_awards_copy from './20260713_163000_participation_supporting_awards_copy';
import * as migration_20260713_194500_contact_submissions from './20260713_194500_contact_submissions'; 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_213000_netzwerk_jury_selection from './20260713_213000_netzwerk_jury_selection';
import * as migration_20260713_220000_preistraeger_detail_cleanup from './20260713_220000_preistraeger_detail_cleanup';
export const migrations = [ export const migrations = [
{ {
@@ -96,4 +97,9 @@ export const migrations = [
down: migration_20260713_213000_netzwerk_jury_selection.down, down: migration_20260713_213000_netzwerk_jury_selection.down,
name: '20260713_213000_netzwerk_jury_selection', name: '20260713_213000_netzwerk_jury_selection',
}, },
{
up: migration_20260713_220000_preistraeger_detail_cleanup.up,
down: migration_20260713_220000_preistraeger_detail_cleanup.down,
name: '20260713_220000_preistraeger_detail_cleanup',
},
]; ];

View File

@@ -2564,9 +2564,6 @@ export interface Preistraeger {
category?: string | null; category?: string | null;
awardType?: string | null; awardType?: string | null;
longDesc?: string | null; longDesc?: string | null;
quote?: string | null;
quotePerson?: string | null;
quoteRole?: string | null;
location?: string | null; location?: string | null;
industry?: string | null; industry?: string | null;
}; };
@@ -2584,14 +2581,6 @@ export interface Preistraeger {
image?: (number | null) | Media; image?: (number | null) | Media;
imageAlt?: string | null; imageAlt?: string | null;
}; };
facts?: {
heading?: string | null;
categoryLabel?: string | null;
awardLabel?: string | null;
yearLabel?: string | null;
locationLabel?: string | null;
industryLabel?: string | null;
};
websiteCta?: { websiteCta?: {
label?: string | null; label?: string | null;
}; };
@@ -4806,9 +4795,6 @@ export interface PreistraegerSelect<T extends boolean = true> {
category?: T; category?: T;
awardType?: T; awardType?: T;
longDesc?: T; longDesc?: T;
quote?: T;
quotePerson?: T;
quoteRole?: T;
location?: T; location?: T;
industry?: T; industry?: T;
}; };
@@ -4827,16 +4813,6 @@ export interface PreistraegerSelect<T extends boolean = true> {
image?: T; image?: T;
imageAlt?: T; imageAlt?: T;
}; };
facts?:
| T
| {
heading?: T;
categoryLabel?: T;
awardLabel?: T;
yearLabel?: T;
locationLabel?: T;
industryLabel?: T;
};
websiteCta?: websiteCta?:
| T | T
| { | {

View File

@@ -784,13 +784,13 @@ const Netzwerk: React.FC = () => {
{/* ── 7. SPONSORING-FORM ───────────────────────────────────────────────── */} {/* ── 7. SPONSORING-FORM ───────────────────────────────────────────────── */}
<section id="sponsoring" style={{ <section id="sponsoring" style={{
background: NAVY, overflow: 'hidden', position: 'relative', background: '#24366A', overflow: 'hidden', position: 'relative',
minHeight: isMobile ? 'auto' : 'calc(100vh - 60px)', height: isMobile ? 'auto' : 'calc(100vh - 60px)',
display: 'flex', flexDirection: 'column', 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 */} {/* 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> <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> <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 }} /> <div style={{ width: 36, height: 2, background: GOLD, margin: '0 0 14px', flexShrink: 0 }} />
@@ -810,7 +810,7 @@ const Netzwerk: React.FC = () => {
{/* Center divider */} {/* Center divider */}
{!isMobile && <div style={{ background: 'rgba(255,255,255,0.07)' }} />} {!isMobile && <div style={{ background: 'rgba(255,255,255,0.07)' }} />}
{/* Right – gold form panel */} {/* 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"> <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 0 }} aria-hidden="true">
<defs> <defs>
<pattern id="sponsoringCross" width="18" height="18" patternUnits="userSpaceOnUse"> <pattern id="sponsoringCross" width="18" height="18" patternUnits="userSpaceOnUse">
@@ -836,7 +836,7 @@ const Netzwerk: React.FC = () => {
</div> </div>
</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} /> <SponsoringForm theme="gold" content={cms.sponsoringForm} />
</div> </div>
</div> </div>

View File

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

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Link, useParams, Navigate } from '@/spa/router'; 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 { WINNERS } from '@/spa/data/winners';
import { useCmsRoute } from '@/spa/cmsRoute'; import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField'; 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 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) => { const optionalNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return undefined; if (value === null || value === undefined || value === '') return undefined;
const parsed = Number(value); const parsed = Number(value);
@@ -70,9 +71,9 @@ export default function PreistraegerDetail() {
img: docImageUrl(cmsWinner, placeholderImage), img: docImageUrl(cmsWinner, placeholderImage),
shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''), shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''),
longDesc: String(cmsWinner.longDesc || cmsWinner.description || cmsWinner.meta?.description || fallbackWinner.longDesc || preistraegerDetailContent.fallbackWinner.longDesc), longDesc: String(cmsWinner.longDesc || cmsWinner.description || cmsWinner.meta?.description || fallbackWinner.longDesc || preistraegerDetailContent.fallbackWinner.longDesc),
quote: String(cmsWinner.quote || fallbackWinner.quote || preistraegerDetailContent.fallbackWinner.quote), quote: cmsText(cmsWinner.quote),
quotePerson: String(cmsWinner.quotePerson || cmsWinner.title || fallbackWinner.quotePerson || preistraegerDetailContent.fallbackWinner.quotePerson), quotePerson: cmsText(cmsWinner.quotePerson),
quoteRole: String(cmsWinner.quoteRole || fallbackWinner.quoteRole || preistraegerDetailContent.fallbackWinner.quoteRole), quoteRole: cmsText(cmsWinner.quoteRole),
location: String(cmsWinner.location || fallbackWinner.location || preistraegerDetailContent.fallbackWinner.location), location: String(cmsWinner.location || fallbackWinner.location || preistraegerDetailContent.fallbackWinner.location),
industry: String(cmsWinner.industry || fallbackWinner.industry || preistraegerDetailContent.fallbackWinner.industry), industry: String(cmsWinner.industry || fallbackWinner.industry || preistraegerDetailContent.fallbackWinner.industry),
website: String(cmsWinner.website || '#'), website: String(cmsWinner.website || '#'),
@@ -85,9 +86,12 @@ export default function PreistraegerDetail() {
const breadcrumb = { ...preistraegerDetailContent.breadcrumb, ...(detail.breadcrumb || {}) }; const breadcrumb = { ...preistraegerDetailContent.breadcrumb, ...(detail.breadcrumb || {}) };
const sections = { ...preistraegerDetailContent.sections, ...(detail.sections || {}) }; const sections = { ...preistraegerDetailContent.sections, ...(detail.sections || {}) };
const sideImage = { ...preistraegerDetailContent.sideImage, ...(detail.sideImage || {}) }; const sideImage = { ...preistraegerDetailContent.sideImage, ...(detail.sideImage || {}) };
const facts = { ...preistraegerDetailContent.facts, ...(detail.facts || {}) };
const websiteCta = { ...preistraegerDetailContent.websiteCta, ...(detail.websiteCta || {}) }; const websiteCta = { ...preistraegerDetailContent.websiteCta, ...(detail.websiteCta || {}) };
const backLink = { ...preistraegerDetailContent.backLink, ...(detail.backLink || {}) }; 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 downloadHref = mediaUrl(cmsWinner?.downloadZip, '');
const downloadFilename = mediaFilename(cmsWinner?.downloadZip); const downloadFilename = mediaFilename(cmsWinner?.downloadZip);
const downloadLabel = fallbackText(cmsWinner?.downloadLabel, 'Pressepaket herunterladen'); 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 }}> <h2 className="type-subtitle text-role-heading" style={{ textTransform: 'uppercase', marginBottom: 20 }}>
{fallbackText(sections.juryHeading, preistraegerDetailContent.sections.juryHeading)} {fallbackText(sections.juryHeading, preistraegerDetailContent.sections.juryHeading)}
</h2> </h2>
{showJuryText ? (
<p className="type-body text-role-body" style={{ marginBottom: 40 }}> <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)} {juryTextBeforeName}
{juryTextAfterNameBeforeCategory ? (
<>{juryTextBeforeName ? ' ' : null}{winner.name} {juryTextAfterNameBeforeCategory} <strong>{winner.category}</strong>.</>
) : null}
{juryTextAfterCategory ? <>{juryTextBeforeName || juryTextAfterNameBeforeCategory ? ' ' : null}{juryTextAfterCategory}</> : null}
</p> </p>
) : null}
{/* Quote */} {/* Quote */}
{winner.quote ? (
<div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}> <div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}>
<p className="type-body text-role-heading" style={{ fontStyle: 'italic', marginBottom: 12 }}> <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> <span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginRight: 4, fontFamily: '"IBM Plex Sans"' }}>&ldquo;</span>
{winner.quote} {winner.quote}
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>&rdquo;</span> <span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>&rdquo;</span>
</p> </p>
{winner.quotePerson || winner.quoteRole ? (
<p className="type-small text-role-secondary" style={{ fontWeight: 700, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}> <p className="type-small text-role-secondary" style={{ fontWeight: 700, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
– {winner.quotePerson}, {winner.quoteRole} {winner.quotePerson ? <>– {winner.quotePerson}</> : null}
{winner.quotePerson && winner.quoteRole ? ', ' : null}
{winner.quoteRole}
</p> </p>
) : null}
</div> </div>
) : null}
</div> </div>
{/* Right: Photo + Info box */} {/* Right: Photo + actions */}
<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={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)' }} /> <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 */}
<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 */} {/* Website button */}
<a <a
href={`https://${winner.website}`} href={`https://${winner.website}`}

View File

@@ -10,9 +10,6 @@ export const preistraegerDetailContent = {
category: 'Preisträger', category: 'Preisträger',
awardType: 'Auszeichnung', awardType: 'Auszeichnung',
longDesc: 'Dieser Preisträger wird aus Payload CMS geladen.', longDesc: 'Dieser Preisträger wird aus Payload CMS geladen.',
quote: 'Diese Erfolgsgeschichte wird aktuell in Payload CMS gepflegt.',
quotePerson: 'BMP',
quoteRole: 'Preisträger',
location: 'Bayern', location: 'Bayern',
industry: 'Mittelstand', industry: 'Mittelstand',
}, },
@@ -28,14 +25,6 @@ export const preistraegerDetailContent = {
imageFilename: 'gala-preistraeger.jpg', imageFilename: 'gala-preistraeger.jpg',
imageAlt: 'Gala Preisverleihung', imageAlt: 'Gala Preisverleihung',
}, },
facts: {
heading: 'Auf einen Blick',
categoryLabel: 'Kategorie',
awardLabel: 'Auszeichnung',
yearLabel: 'Jahr',
locationLabel: 'Standort',
industryLabel: 'Branche',
},
websiteCta: { websiteCta: {
label: 'Website besuchen ↗', label: 'Website besuchen ↗',
}, },