From 818f283e10541a4e48d1396e36716bf30b678dce Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Mon, 13 Jul 2026 12:14:37 +0200 Subject: [PATCH] fix: align partner media with production CMS data --- scripts/deploy-vps.sh | 2 +- scripts/vps-migrate.sh | 2 +- src/collections/Partners.ts | 50 +------- .../20260712_120000_partner_media_only.ts | 56 +++++++++ src/migrations/index.ts | 6 + src/payload-types.ts | 14 +-- src/spa/cmsMediaField.ts | 18 +-- src/spa/components/ui/partner-logo.tsx | 114 +----------------- src/spa/netzwerkContent.ts | 50 -------- src/spa/pages/Home.tsx | 21 +--- src/spa/pages/Netzwerk.tsx | 9 +- tests/int/cms-media-field.int.spec.ts | 13 ++ 12 files changed, 93 insertions(+), 262 deletions(-) create mode 100644 src/migrations/20260712_120000_partner_media_only.ts create mode 100644 tests/int/cms-media-field.int.spec.ts diff --git a/scripts/deploy-vps.sh b/scripts/deploy-vps.sh index e0a6b9a..d757a11 100755 --- a/scripts/deploy-vps.sh +++ b/scripts/deploy-vps.sh @@ -50,7 +50,7 @@ rsync -az --delete \ ssh -i "$SSH_KEY" -p "$VPS_PORT" "$SSH_TARGET" "printf '%s\n' '$REVISION' > '$APP_ROOT/current/.deploy-revision'" -ssh -i "$SSH_KEY" -p "$VPS_PORT" "$SSH_TARGET" " +ssh -t -i "$SSH_KEY" -p "$VPS_PORT" "$SSH_TARGET" " set -e cd '$APP_ROOT' APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' build app diff --git a/scripts/vps-migrate.sh b/scripts/vps-migrate.sh index a051ad5..c944f9f 100755 --- a/scripts/vps-migrate.sh +++ b/scripts/vps-migrate.sh @@ -11,7 +11,7 @@ APP_DOMAIN="${APP_DOMAIN:-srv1.bayerischer-mittelstandspreis.de}" SSH_TARGET="$SSH_USER@$VPS_HOST" COMPOSE_FILE="$APP_ROOT/current/deploy/vps/docker-compose.yml" -ssh -i "$SSH_KEY" -p "$VPS_PORT" "$SSH_TARGET" " +ssh -t -i "$SSH_KEY" -p "$VPS_PORT" "$SSH_TARGET" " set -e cd '$APP_ROOT' diff --git a/src/collections/Partners.ts b/src/collections/Partners.ts index ef60227..609bb84 100644 --- a/src/collections/Partners.ts +++ b/src/collections/Partners.ts @@ -4,17 +4,6 @@ import { authenticated } from '@/access/authenticated' import { authenticatedOrPublished } from '@/access/authenticatedOrPublished' import { populatePublishedAt } from '@/hooks/populatePublishedAt' -const logoKindOptions = [ - { label: 'Text', value: 'text' }, - { label: 'Serif text', value: 'serif' }, - { label: 'Badge', value: 'badge' }, - { label: 'Monogram', value: 'monogram' }, - { label: 'Dot mark', value: 'dot' }, - { label: 'Leaf mark', value: 'leaf' }, - { label: 'RSM blocks', value: 'rsm' }, - { label: 'Deutsche Bank mark', value: 'deutschebank' }, -] - export const Partners: CollectionConfig = { slug: 'partners', labels: { @@ -28,7 +17,7 @@ export const Partners: CollectionConfig = { update: authenticated, }, admin: { - defaultColumns: ['name', 'role', 'tier', 'sortOrder', 'active', 'showOnHomepage', 'updatedAt'], + defaultColumns: ['name', 'tier', 'sortOrder', 'active', 'showOnHomepage', 'updatedAt'], group: 'Inhalte', useAsTitle: 'name', }, @@ -47,18 +36,6 @@ export const Partners: CollectionConfig = { description: 'Stable machine-readable key used when migrating or matching partner records.', }, }, - { - name: 'role', - type: 'select', - required: true, - options: [ - { label: 'Hauptsponsor', value: 'Hauptsponsor' }, - { label: 'Sponsor', value: 'Sponsor' }, - { label: 'Finanzpartner', value: 'Finanzpartner' }, - { label: 'Partner', value: 'Partner' }, - { label: 'Medienpartner', value: 'Medienpartner' }, - ], - }, { name: 'tier', type: 'number', @@ -89,8 +66,9 @@ export const Partners: CollectionConfig = { name: 'logo', type: 'upload', relationTo: 'media', + required: true, admin: { - description: 'Preferred real partner logo. Styled logo fallback fields below are used when this is empty.', + description: 'Partner logo displayed on partner cards, the detail modal, and the homepage ticker.', }, }, { @@ -98,28 +76,6 @@ export const Partners: CollectionConfig = { type: 'text', label: 'Logo alt text', }, - { - name: 'logoKind', - type: 'select', - defaultValue: 'text', - label: 'Fallback logo style', - options: logoKindOptions, - }, - { - name: 'logoText', - type: 'text', - label: 'Fallback logo text', - }, - { - name: 'logoSubtext', - type: 'text', - label: 'Fallback logo subtext', - }, - { - name: 'logoColor', - type: 'text', - label: 'Fallback logo color', - }, { name: 'desc', type: 'textarea', diff --git a/src/migrations/20260712_120000_partner_media_only.ts b/src/migrations/20260712_120000_partner_media_only.ts new file mode 100644 index 0000000..d17121b --- /dev/null +++ b/src/migrations/20260712_120000_partner_media_only.ts @@ -0,0 +1,56 @@ +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, "''")}'` + +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)};`)) +} + +const partnerColumns = ['role', 'logo_kind', 'logo_text', 'logo_subtext', 'logo_color'] as const +const versionColumns = partnerColumns.map((column) => `version_${column}`) + +export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise { + for (const column of partnerColumns) await dropColumn(db, 'partners', column) + for (const column of versionColumns) await dropColumn(db, '_partners_v', column) +} + +export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise { + await addColumn(db, 'partners', 'role', 'text') + await addColumn(db, 'partners', 'logo_kind', `text DEFAULT ${literal('text')}`) + await addColumn(db, 'partners', 'logo_text', 'text') + await addColumn(db, 'partners', 'logo_subtext', 'text') + await addColumn(db, 'partners', 'logo_color', 'text') + + await addColumn(db, '_partners_v', 'version_role', 'text') + await addColumn(db, '_partners_v', 'version_logo_kind', `text DEFAULT ${literal('text')}`) + await addColumn(db, '_partners_v', 'version_logo_text', 'text') + await addColumn(db, '_partners_v', 'version_logo_subtext', 'text') + await addColumn(db, '_partners_v', 'version_logo_color', 'text') +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 2fe8c44..946a465 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -8,6 +8,7 @@ import * as migration_20260707_122300_preistraeger_zip_download from './20260707 import * as migration_20260707_123500_preistraeger_gallery from './20260707_123500_preistraeger_gallery'; import * as migration_20260707_124500_site_settings_preistraeger_placeholder from './20260707_124500_site_settings_preistraeger_placeholder'; import * as migration_20260707_130500_preistraeger_awards_jury_members from './20260707_130500_preistraeger_awards_jury_members'; +import * as migration_20260712_120000_partner_media_only from './20260712_120000_partner_media_only'; export const migrations = [ { @@ -60,4 +61,9 @@ export const migrations = [ down: migration_20260707_130500_preistraeger_awards_jury_members.down, name: '20260707_130500_preistraeger_awards_jury_members', }, + { + up: migration_20260712_120000_partner_media_only.up, + down: migration_20260712_120000_partner_media_only.down, + name: '20260712_120000_partner_media_only', + }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index 657955e..73a8b4d 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -2763,7 +2763,6 @@ export interface Partner { * Stable machine-readable key used when migrating or matching partner records. */ stableId?: string | null; - role: 'Hauptsponsor' | 'Sponsor' | 'Finanzpartner' | 'Partner' | 'Medienpartner'; tier: number; /** * Lower numbers appear earlier on partner surfaces. @@ -2772,14 +2771,10 @@ export interface Partner { active?: boolean | null; showOnHomepage?: boolean | null; /** - * Preferred real partner logo. Styled logo fallback fields below are used when this is empty. + * Partner logo displayed on partner cards, the detail modal, and the homepage ticker. */ - logo?: (number | null) | Media; + logo: number | Media; logoAlt?: string | null; - logoKind?: ('text' | 'serif' | 'badge' | 'monogram' | 'dot' | 'leaf' | 'rsm' | 'deutschebank') | null; - logoText?: string | null; - logoSubtext?: string | null; - logoColor?: string | null; desc?: string | null; fullDesc?: string | null; industry?: string | null; @@ -5089,17 +5084,12 @@ export interface JuryMitgliederSelect { export interface PartnersSelect { name?: T; stableId?: T; - role?: T; tier?: T; sortOrder?: T; active?: T; showOnHomepage?: T; logo?: T; logoAlt?: T; - logoKind?: T; - logoText?: T; - logoSubtext?: T; - logoColor?: T; desc?: T; fullDesc?: T; industry?: T; diff --git a/src/spa/cmsMediaField.ts b/src/spa/cmsMediaField.ts index f7ab375..cc7f44e 100644 --- a/src/spa/cmsMediaField.ts +++ b/src/spa/cmsMediaField.ts @@ -6,28 +6,14 @@ type MediaLike = { alt?: string | null } -const payloadFilePrefix = '/api/media/file/' - export function normalizeMediaUrl(url: string | null | undefined) { - if (!url) return '' - if (url.startsWith(payloadFilePrefix)) return `/media/${url.slice(payloadFilePrefix.length)}` - - try { - const parsed = new URL(url) - if (parsed.pathname.startsWith(payloadFilePrefix)) { - return `/media/${parsed.pathname.slice(payloadFilePrefix.length)}` - } - } catch { - // Relative URLs are handled above. - } - - return url + return url || '' } export function mediaUrl(media: unknown, fallback: string) { if (!media || typeof media !== 'object') return fallback const doc = media as MediaLike - return normalizeMediaUrl(doc.url) || (doc.filename ? `/media/${doc.filename}` : fallback) + return normalizeMediaUrl(doc.url) || (doc.filename ? `/api/media/file/${encodeURIComponent(doc.filename)}` : fallback) } export function mediaAlt(media: unknown, fallback: string) { diff --git a/src/spa/components/ui/partner-logo.tsx b/src/spa/components/ui/partner-logo.tsx index 375b22d..37d5ce2 100644 --- a/src/spa/components/ui/partner-logo.tsx +++ b/src/spa/components/ui/partner-logo.tsx @@ -3,19 +3,12 @@ import React from 'react' import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField' import Image from '@/spa/components/ui/UnoptimizedImage' -const FF = '"IBM Plex Sans", sans-serif' -const FB = '"Inter", sans-serif' - export type PartnerLogoData = { id?: string | number | null stableId?: string | null name?: string | null logo?: unknown logoAlt?: string | null - logoKind?: 'text' | 'serif' | 'badge' | 'monogram' | 'dot' | 'leaf' | 'rsm' | 'deutschebank' | null - logoText?: string | null - logoSubtext?: string | null - logoColor?: string | null sortOrder?: number | null active?: boolean | null showOnHomepage?: boolean | null @@ -37,31 +30,7 @@ const logoShellStyle: React.CSSProperties = { width: '100%', } -const containedTextStyle: React.CSSProperties = { - display: 'block', - maxWidth: '100%', - minWidth: 0, - textAlign: 'center', - whiteSpace: 'nowrap', -} - -const fittedTextSize = (text: string, preferred: number, compact = 12) => { - if (text.length > 24) return Math.max(9, compact - 3) - if (text.length > 18) return Math.max(10, compact - 2) - if (text.length > 13) return compact - 1 - if (text.length > 9) return Math.max(compact + 1, preferred - 3) - return preferred -} - -export function PartnerLogoMark({ - partner, - defaultLogoColor = '#111D55', - scale = 1, -}: { - partner: PartnerLogoData - defaultLogoColor?: string - scale?: number -}) { +export function PartnerLogoMark({ partner }: { partner: PartnerLogoData }) { const realLogo = mediaUrl(partner.logo, '') const alt = mediaAlt(partner.logo, fallbackText(partner.logoAlt, fallbackText(partner.name, 'Partner logo'))) @@ -78,87 +47,10 @@ export function PartnerLogoMark({ ) } - const color = fallbackText(partner.logoColor, defaultLogoColor) - const text = fallbackText(partner.logoText, fallbackText(partner.name, 'Partner')) - const subtext = fallbackText(partner.logoSubtext, '') - const size = (value: number) => Math.round(value * scale) - const textSize = size(fittedTextSize(text, partner.logoKind === 'serif' ? 21 : 16)) - - if (partner.logoKind === 'badge') { - return ( - - - {text.split('\n').map((line, index) => ( - {index > 0 &&
}{line}
- ))} -
-
- ) - } - - if (partner.logoKind === 'monogram') { - return ( - - - {subtext || text.slice(0, 2)} - {text} - - - ) - } - - if (partner.logoKind === 'dot') { - return ( - - - - {text} - - - ) - } - - if (partner.logoKind === 'leaf') { - return ( - - - - {text} - - - ) - } - - if (partner.logoKind === 'rsm') { - return ( - - - - - - - - {text} {subtext && {subtext}} - - - ) - } - - if (partner.logoKind === 'deutschebank') { - return ( - - - - {text} - - - ) - } - return ( - - {text} + + {fallbackText(partner.name, 'Logo fehlt')} ) diff --git a/src/spa/netzwerkContent.ts b/src/spa/netzwerkContent.ts index 211b78e..3b76c94 100644 --- a/src/spa/netzwerkContent.ts +++ b/src/spa/netzwerkContent.ts @@ -144,15 +144,10 @@ export const netzwerkContent = { { id: 'rsm', name: 'RSM Ebner Stolz', - role: 'Hauptsponsor', tier: 1, desc: 'Führende mittelständische Wirtschaftsprüfungs- und Steuerberatungsgesellschaft.', fullDesc: 'RSM Ebner Stolz ist eine der größten unabhängigen Wirtschaftsprüfungs- und Steuerberatungsgesellschaften Deutschlands mit besonderer Expertise im Mittelstand. Als Hauptsponsor des BMP unterstützen sie die Identifikation herausragender Unternehmen und bringen ihre Netzwerke aktiv in die Nominierungsphase ein.', - logoKind: 'rsm', - logoText: 'RSM', - logoSubtext: 'EBNER STOLZ', - logoColor: '#374151', industry: 'Wirtschaftsprüfung & Beratung', location: 'München, Bayern', website: 'rsm-ebner-stolz.de', @@ -161,14 +156,10 @@ export const netzwerkContent = { { id: 'wwk', name: 'WWK', - role: 'Hauptsponsor', tier: 1, desc: 'Eine starke Gemeinschaft – Lebensversicherungsgruppe mit bayerischen Wurzeln.', fullDesc: 'Die WWK Lebensversicherung a. G. ist eine der leistungsstärksten deutschen Lebensversicherungsgruppen. Als Münchner Traditionsunternehmen verbindet die WWK tiefe bayerische Verwurzelung mit finanzieller Stärke – Werte, die sie mit dem Bayerischen Mittelstandspreis teilt.', - logoKind: 'text', - logoText: 'WWK', - logoColor: '#16a34a', industry: 'Lebensversicherung', location: 'München, Bayern', website: 'wwk.de', @@ -177,14 +168,10 @@ export const netzwerkContent = { { id: 'radiogong', name: 'Radio Gong 96.3', - role: 'Medienpartner', tier: 2, desc: 'Der reichweitenstärkste Radiosender Münchens – Stimme des Mittelstands.', fullDesc: 'Radio Gong 96.3 ist der reichweitenstärkste Radiosender Münchens. Als Medienpartner des BMP sorgt er für Aufmerksamkeit für die Nominierten in Sendemitschnitten und redaktionellen Beiträgen.', - logoKind: 'badge', - logoText: 'Radio\nGong 96.3', - logoColor: '#e11d48', industry: 'Medien', location: 'München', website: 'radiogong.com', @@ -192,14 +179,10 @@ export const netzwerkContent = { { id: 'muenchentv', name: 'münchen.tv', - role: 'Medienpartner', tier: 2, desc: 'Der lokale Fernsehsender der Landeshauptstadt München.', fullDesc: 'münchen.tv ist der lokale Fernsehsender der Landeshauptstadt München. Als Medienpartner überträgt münchen.tv Highlights der BMP-Gala und produziert Porträts der Nominierten.', - logoKind: 'text', - logoText: 'münchen.tv', - logoColor: '#0ea5e9', industry: 'Medien', location: 'München', website: 'muenchen.tv', @@ -207,14 +190,10 @@ export const netzwerkContent = { { id: 'metzler', name: 'METZLER', - role: 'Sponsor', tier: 3, desc: 'Älteste Privatbank Deutschlands – Unabhängigkeit seit 1674.', fullDesc: 'B. Metzler seel. Sohn & Co. KGaA ist die älteste deutsche Privatbank in Familienbesitz – seit über 350 Jahren unabhängig. Ihr Engagement für den BMP unterstreicht die tiefe Verbundenheit mit dem Mittelstand, dessen Werte wie Verlässlichkeit, Substanz und Langfristigkeit Metzler selbst verkörpert.', - logoKind: 'serif', - logoText: 'METZLER', - logoColor: '#1a1a1a', industry: 'Privatbankwesen', location: 'Frankfurt a. M.', website: 'metzler.com', @@ -223,14 +202,10 @@ export const netzwerkContent = { { id: 'wieselhuber', name: 'Dr. Wieselhuber & Partner', - role: 'Sponsor', tier: 3, desc: 'Unabhängige Top-Management-Beratung für Familienunternehmen und Mittelstand.', fullDesc: 'Dr. Wieselhuber & Partner ist eine unabhängige, branchenübergreifende Top-Management-Beratung mit besonderem Fokus auf Familienunternehmen und Mittelstand. Als Sponsor bringt das Haus seine Strategie- und Transformationsexpertise in das Netzwerk des BMP ein.', - logoKind: 'text', - logoText: 'W&P WIESELHUBER', - logoColor: '#1a2b4a', industry: 'Unternehmensberatung', location: 'München, Bayern', website: 'wieselhuber.de', @@ -238,14 +213,10 @@ export const netzwerkContent = { { id: 'fristads', name: 'Fristads', - role: 'Sponsor', tier: 3, desc: 'Premium-Arbeitsbekleidung – Funktion, Qualität und Design.', fullDesc: 'Fristads steht für hochwertige, funktionale Arbeitsbekleidung, die Schutz, Komfort und Design vereint. Als Sponsor des Bayerischen Mittelstandspreises unterstützt das Unternehmen die Auszeichnung herausragender mittelständischer Betriebe.', - logoKind: 'text', - logoText: 'FRISTADS', - logoColor: '#1a1a1a', industry: 'Workwear & Textil', location: 'Deutschland', website: 'fristads.com', @@ -253,15 +224,10 @@ export const netzwerkContent = { { id: 'newedge', name: 'New Edge', - role: 'Sponsor', tier: 3, desc: 'Digitale Markenführung, Web und Design für den Mittelstand.', fullDesc: 'New Edge gestaltet Marken, Web-Erlebnisse und digitale Auftritte für mittelständische Unternehmen. Als Sponsor des BMP unterstützt die Agentur die Sichtbarkeit und das digitale Profil ausgezeichneter Unternehmen.', - logoKind: 'monogram', - logoText: 'New Edge', - logoSubtext: 'NE', - logoColor: '#005E67', industry: 'Branding & Digital', location: 'Bayern', website: 'newedgebrand.com', @@ -269,14 +235,10 @@ export const netzwerkContent = { { id: 'moodtalk', name: 'Moodtalk', - role: 'Sponsor', tier: 3, desc: 'Plattform für Teamkultur und Mitarbeiter-Feedback.', fullDesc: 'Moodtalk ist eine Plattform für Teamkultur, Stimmung und kontinuierliches Mitarbeiter-Feedback. Als Sponsor des BMP steht das Unternehmen für moderne, wertebasierte Unternehmensführung.', - logoKind: 'dot', - logoText: 'moodtalk', - logoColor: '#7c3aed', industry: 'HR & Software', location: 'Bayern', website: 'moodtalk.io', @@ -284,14 +246,10 @@ export const netzwerkContent = { { id: 'deutschebank', name: 'Deutsche Bank', - role: 'Sponsor', tier: 3, desc: 'Partnerbank für die Finanzierung und Skalierung von KMU-Wachstum.', fullDesc: 'Die Deutsche Bank begleitet mittelständische Unternehmen als verlässlicher Finanzpartner durch alle Wachstumsphasen. Im Rahmen des BMP stellt sie ihr deutschlandweites Netzwerk, spezifische Mittelstandsprodukte und direkte Ansprechpartner für die Nominierten bereit.', - logoKind: 'deutschebank', - logoText: 'Deutsche Bank', - logoColor: '#1d1d1b', industry: 'Bankwesen & Finanzen', location: 'Frankfurt a. M.', website: 'db.com', @@ -300,14 +258,10 @@ export const netzwerkContent = { { id: 'bionorica', name: 'Bionorica', - role: 'Sponsor', tier: 3, desc: 'Weltmarktführer für pflanzliche Arzneimittel aus dem bayerischen Neumarkt.', fullDesc: 'Bionorica SE ist ein international agierendes Pharmaunternehmen mit Sitz in Neumarkt i. d. OPf. und Weltmarktführer bei pflanzlichen Arzneimitteln. Das Unternehmen selbst ist ein Paradebeispiel eines bayerischen Mittelständlers mit globalem Anspruch und verkörpert die Innovationskraft, die der BMP würdigt.', - logoKind: 'leaf', - logoText: 'Bionorica®', - logoColor: '#16a34a', industry: 'Pharmaindustrie', location: 'Neumarkt i.d.OPf., Bayern', website: 'bionorica.de', @@ -316,13 +270,9 @@ export const netzwerkContent = { { id: 'primus', name: 'Primus', - role: 'Sponsor', tier: 3, desc: 'Partner des Bayerischen Mittelstandspreises.', fullDesc: 'Primus unterstützt als Sponsor den Bayerischen Mittelstandspreis und sein Engagement für herausragende Unternehmen des bayerischen Mittelstands.', - logoKind: 'serif', - logoText: 'PRIMUS', - logoColor: '#b8860b', industry: 'Partner', location: 'Bayern', website: '', diff --git a/src/spa/pages/Home.tsx b/src/spa/pages/Home.tsx index 9b8caf2..3386b6f 100644 --- a/src/spa/pages/Home.tsx +++ b/src/spa/pages/Home.tsx @@ -90,9 +90,6 @@ type HomeCms = { videoModal?: HomeVideoModalCms; winners?: HomeSectionCms; }; -type NetzwerkPartnersCms = { - defaultLogoColor?: string | null; -}; type RenderedStatusPhase = Omit & { accent: string; bg: string; @@ -165,10 +162,7 @@ function Lines({ text }: { text: string }) { return <>{text.split('\n').map((line, i) => {i > 0 &&
}{line}
)}; } -const isNetzwerkPage = (page: CmsRecord) => - page.spaPath === '/netzwerk' || page.slug === 'netzwerk' || page.slug === 'network' - -function partnerLogos(partners: PartnerLogoData[], defaultLogoColor: string) { +function partnerLogos(partners: PartnerLogoData[]) { return partners.map((partner, index) => ({ id: String(partner.stableId || partner.id || partner.name || index), label: ( @@ -187,7 +181,7 @@ function partnerLogos(partners: PartnerLogoData[], defaultLogoColor: string) { justifyContent: 'center', }} > - +
), })); @@ -255,19 +249,12 @@ const Home: React.FC = () => { const isMobile = useIsMobile(); const placeholderImage = usePreistraegerPlaceholderImage(); const home = (useCmsRoute()?.doc?.home || {}) as HomeCms; - const netzwerkPage = useCmsCollection('pages').find((page) => isNetzwerkPage(page as CmsRecord)) as - | ({ netzwerk?: { partners?: NetzwerkPartnersCms } } & CmsRecord) - | undefined; - const partnerSection = netzwerkPage?.netzwerk?.partners; - const defaultLogoColor = fallbackText(partnerSection?.defaultLogoColor, netzwerkContent.partners.defaultLogoColor); const cmsPartners = useCmsCollection('partners') as PartnerLogoData[]; - const partnerSource = cmsPartners.length ? cmsPartners : (netzwerkContent.partners.items as PartnerLogoData[]); - const networkPartners = sortedPartners(partnerSource) + const networkPartners = sortedPartners(cmsPartners) .filter((partner) => partner.active !== false && partner.showOnHomepage !== false) .map((partner) => ({ ...partner, id: partner.stableId || partner.id, - logoColor: fallbackText(partner.logoColor, defaultLogoColor), })); const hero = home.hero || {}; const quickCheck = home.quickCheck || {}; @@ -391,7 +378,7 @@ const Home: React.FC = () => {
- +
diff --git a/src/spa/pages/Netzwerk.tsx b/src/spa/pages/Netzwerk.tsx index 4d116df..a70ac34 100644 --- a/src/spa/pages/Netzwerk.tsx +++ b/src/spa/pages/Netzwerk.tsx @@ -48,7 +48,6 @@ type Partner = (typeof netzwerkContent.partners.items)[number] & active?: boolean | null heroImg?: string linkedin?: string - logoSubtext?: string showOnHomepage?: boolean | null sortOrder?: number | null stableId?: string | null @@ -241,7 +240,6 @@ const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx const sm = variant === 'sm'; const lastCol = (idx % cols) === cols - 1; const logoHeight = isMobileCell ? (lg ? 76 : sm ? 54 : 62) : lg ? 92 : sm ? 62 : 76; - const logoScale = lg ? 1.55 : sm ? 1.22 : 1.38; return (
- +
{partner.name}
@@ -488,14 +486,11 @@ const Netzwerk: React.FC = () => { ? juryCollectionMembers : fallbackArray(jury.members, netzwerkContent.jury.members); const expertiseRows = fallbackArray(expertise.rows, netzwerkContent.expertise.rows); - const defaultLogoColor = fallbackText(partnerSection.defaultLogoColor, netzwerkContent.partners.defaultLogoColor); - const partnerSource = cmsPartners.length ? cmsPartners : (netzwerkContent.partners.items as Partner[]); - const partners = sortedPartners(partnerSource) + const partners = sortedPartners(cmsPartners) .filter((partner) => partner.active !== false) .map((partner) => ({ ...partner, id: partner.stableId || partner.id, - logoColor: fallbackText(partner.logoColor, defaultLogoColor), })); const partnerTiers = fallbackArray(partnerSection.tiers, netzwerkContent.partners.tiers); const mobileStats = fallbackArray(juryIntro.stats, netzwerkContent.juryIntro.stats); diff --git a/tests/int/cms-media-field.int.spec.ts b/tests/int/cms-media-field.int.spec.ts new file mode 100644 index 0000000..f1af343 --- /dev/null +++ b/tests/int/cms-media-field.int.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' + +import { mediaUrl, normalizeMediaUrl } from '@/spa/cmsMediaField' + +describe('CMS media URLs', () => { + it('preserves Payload file endpoints used by production', () => { + expect(normalizeMediaUrl('/api/media/file/image%20(26).jpg')).toBe('/api/media/file/image%20(26).jpg') + }) + + it('builds a Payload file endpoint when only a filename is populated', () => { + expect(mediaUrl({ filename: 'image (26).jpg' }, '')).toBe('/api/media/file/image%20(26).jpg') + }) +})