From e28666fc4703d7c16ac0be559ea02d2d68eee2b1 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Mon, 13 Jul 2026 22:46:35 +0200 Subject: [PATCH] feat: clean up Preistraeger detail structure by removing unused fields and updating related migrations --- src/collections/Preistraeger.ts | 16 ---- ...0713_220000_preistraeger_detail_cleanup.ts | 73 ++++++++++++++++++ src/migrations/index.ts | 6 ++ src/payload-types.ts | 24 ------ src/spa/pages/Netzwerk.tsx | 12 +-- src/spa/pages/Preistraeger.tsx | 7 +- src/spa/pages/PreistraegerDetail.tsx | 75 +++++++++---------- src/spa/preistraegerDetailContent.ts | 11 --- 8 files changed, 124 insertions(+), 100 deletions(-) create mode 100644 src/migrations/20260713_220000_preistraeger_detail_cleanup.ts diff --git a/src/collections/Preistraeger.ts b/src/collections/Preistraeger.ts index 9f830e1..70fec09 100644 --- a/src/collections/Preistraeger.ts +++ b/src/collections/Preistraeger.ts @@ -275,9 +275,6 @@ export const Preistraeger: CollectionConfig = { { name: 'category', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.category }, { name: 'awardType', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.awardType }, { 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: 'industry', type: 'text', defaultValue: preistraegerDetailContent.fallbackWinner.industry }, ], @@ -322,19 +319,6 @@ export const Preistraeger: CollectionConfig = { { 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', diff --git a/src/migrations/20260713_220000_preistraeger_detail_cleanup.ts b/src/migrations/20260713_220000_preistraeger_detail_cleanup.ts new file mode 100644 index 0000000..14b9cdc --- /dev/null +++ b/src/migrations/20260713_220000_preistraeger_detail_cleanup.ts @@ -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 { + 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 { + 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)}`) + } +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 0b32304..8b22e51 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -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_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_220000_preistraeger_detail_cleanup from './20260713_220000_preistraeger_detail_cleanup'; export const migrations = [ { @@ -96,4 +97,9 @@ export const migrations = [ down: migration_20260713_213000_netzwerk_jury_selection.down, 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', + }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index f72e082..8f59f2e 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -2564,9 +2564,6 @@ export interface Preistraeger { category?: string | null; awardType?: string | null; longDesc?: string | null; - quote?: string | null; - quotePerson?: string | null; - quoteRole?: string | null; location?: string | null; industry?: string | null; }; @@ -2584,14 +2581,6 @@ export interface Preistraeger { 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; }; @@ -4806,9 +4795,6 @@ export interface PreistraegerSelect { category?: T; awardType?: T; longDesc?: T; - quote?: T; - quotePerson?: T; - quoteRole?: T; location?: T; industry?: T; }; @@ -4827,16 +4813,6 @@ export interface PreistraegerSelect { image?: T; imageAlt?: T; }; - facts?: - | T - | { - heading?: T; - categoryLabel?: T; - awardLabel?: T; - yearLabel?: T; - locationLabel?: T; - industryLabel?: T; - }; websiteCta?: | T | { diff --git a/src/spa/pages/Netzwerk.tsx b/src/spa/pages/Netzwerk.tsx index 3397d24..3fbb093 100644 --- a/src/spa/pages/Netzwerk.tsx +++ b/src/spa/pages/Netzwerk.tsx @@ -784,13 +784,13 @@ const Netzwerk: React.FC = () => { {/* ── 7. SPONSORING-FORM ───────────────────────────────────────────────── */}
-
+
{/* Left – pitch */} -
+
{fallbackText(sponsoring.eyebrow, netzwerkContent.sponsoring.eyebrow)}

@@ -810,7 +810,7 @@ const Netzwerk: React.FC = () => { {/* Center divider */} {!isMobile &&
} {/* Right – gold form panel */} -
+
)} -
+
diff --git a/src/spa/pages/Preistraeger.tsx b/src/spa/pages/Preistraeger.tsx index 019cba0..b729b40 100644 --- a/src/spa/pages/Preistraeger.tsx +++ b/src/spa/pages/Preistraeger.tsx @@ -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(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} ))}
diff --git a/src/spa/pages/PreistraegerDetail.tsx b/src/spa/pages/PreistraegerDetail.tsx index 62bc36e..c167030 100644 --- a/src/spa/pages/PreistraegerDetail.tsx +++ b/src/spa/pages/PreistraegerDetail.tsx @@ -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() {

{fallbackText(sections.juryHeading, preistraegerDetailContent.sections.juryHeading)}

-

- {fallbackText(sections.juryTextBeforeName, preistraegerDetailContent.sections.juryTextBeforeName)} {winner.name} {fallbackText(sections.juryTextAfterNameBeforeCategory, preistraegerDetailContent.sections.juryTextAfterNameBeforeCategory)} {winner.category}. {fallbackText(sections.juryTextAfterCategory, preistraegerDetailContent.sections.juryTextAfterCategory)} -

+ {showJuryText ? ( +

+ {juryTextBeforeName} + {juryTextAfterNameBeforeCategory ? ( + <>{juryTextBeforeName ? ' ' : null}{winner.name} {juryTextAfterNameBeforeCategory} {winner.category}. + ) : null} + {juryTextAfterCategory ? <>{juryTextBeforeName || juryTextAfterNameBeforeCategory ? ' ' : null}{juryTextAfterCategory} : null} +

+ ) : null} {/* Quote */} -
-

- “ - {winner.quote} - ” -

-

- – {winner.quotePerson}, {winner.quoteRole} -

-
+ {winner.quote ? ( +
+

+ “ + {winner.quote} + ” +

+ {winner.quotePerson || winner.quoteRole ? ( +

+ {winner.quotePerson ? <>– {winner.quotePerson} : null} + {winner.quotePerson && winner.quoteRole ? ', ' : null} + {winner.quoteRole} +

+ ) : null} +
+ ) : null}
- {/* Right: Photo + Info box */} + {/* Right: Photo + actions */}
{/* Photo */}
{mediaAlt(sideImage.image,
- {/* Kenndaten */} -
-

- {fallbackText(facts.heading, preistraegerDetailContent.facts.heading)} -

-
- {[ - { label: fallbackText(facts.categoryLabel, preistraegerDetailContent.facts.categoryLabel), value: winner.category, icon: }, - { label: fallbackText(facts.awardLabel, preistraegerDetailContent.facts.awardLabel), value: winner.type, icon: }, - ...(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: }, - { label: fallbackText(facts.industryLabel, preistraegerDetailContent.facts.industryLabel), value: winner.industry, icon: }, - ].map(row => ( -
- {row.label} - {row.value} -
- ))} -
-
- {/* Website button */}