fix: align partner media with production CMS data

This commit is contained in:
syntaxbullet
2026-07-13 12:14:37 +02:00
parent d6f8f55c09
commit 818f283e10
12 changed files with 93 additions and 262 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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',

View File

@@ -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<void> {
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<void> {
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')
}

View File

@@ -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',
},
];

View File

@@ -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<T extends boolean = true> {
export interface PartnersSelect<T extends boolean = true> {
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;

View File

@@ -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) {

View File

@@ -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 (
<span style={logoShellStyle}>
<span style={{ background: color, color: '#fff', fontWeight: 900, fontSize: size(fittedTextSize(text, 14, 11)), padding: `${size(4)}px ${size(10)}px`, lineHeight: 1.25, display: 'inline-block', maxWidth: '100%', textAlign: 'center', fontFamily: FF }}>
{text.split('\n').map((line, index) => (
<React.Fragment key={`${line}-${index}`}>{index > 0 && <br />}{line}</React.Fragment>
))}
</span>
</span>
)
}
if (partner.logoKind === 'monogram') {
return (
<span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<span style={{ width: size(22), height: size(22), background: '#3a3a3a', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 900, fontSize: size(11), fontFamily: FF, flexShrink: 0 }}>{subtext || text.slice(0, 2)}</span>
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FF }}>{text}</span>
</span>
</span>
)
}
if (partner.logoKind === 'dot') {
return (
<span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<span style={{ width: size(20), height: size(20), borderRadius: '50%', background: color, display: 'block', flexShrink: 0 }} />
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 16, 11)), color, fontFamily: FF }}>{text}</span>
</span>
</span>
)
}
if (partner.logoKind === 'leaf') {
return (
<span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><circle cx="10" cy="10" r="9" fill={color} /><path d="M10 3 Q14 7 14 10 Q14 14 10 17 Q6 14 6 10 Q6 7 10 3Z" fill="white" opacity="0.8" /></svg>
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span>
</span>
</span>
)
}
if (partner.logoKind === 'rsm') {
return (
<span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<span style={{ display: 'flex', gap: Math.max(2, size(2)), flexShrink: 0 }}>
<span style={{ width: size(10), height: size(10), background: '#6b7280', display: 'block' }} />
<span style={{ width: size(10), height: size(10), background: '#22c55e', display: 'block' }} />
<span style={{ width: size(10), height: size(10), background: '#3b82f6', display: 'block' }} />
</span>
<span style={{ ...containedTextStyle, fontWeight: 900, fontSize: size(15), letterSpacing: 0, color, fontFamily: FF }}>{text} {subtext && <span style={{ color: '#6b7280', fontWeight: 600 }}>{subtext}</span>}</span>
</span>
</span>
)
}
if (partner.logoKind === 'deutschebank') {
return (
<span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(8), maxWidth: '100%', minWidth: 0 }}>
<svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><rect x="1" y="1" width="18" height="18" fill="none" stroke={color} strokeWidth="1.5" /><line x1="5" y1="15" x2="15" y2="5" stroke={color} strokeWidth="2" /></svg>
<span style={{ ...containedTextStyle, fontWeight: 600, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span>
</span>
</span>
)
}
return (
<span style={logoShellStyle}>
<span style={{ ...containedTextStyle, fontWeight: 900, fontSize: textSize, letterSpacing: partner.logoKind === 'serif' ? '0.08em' : 0, color, fontFamily: partner.logoKind === 'serif' ? 'Georgia, serif' : FF }}>
{text}
<span style={{ color: 'rgba(16,24,40,0.45)', fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 13, fontWeight: 700, textAlign: 'center' }}>
{fallbackText(partner.name, 'Logo fehlt')}
</span>
</span>
)

View File

@@ -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: '',

View File

@@ -90,9 +90,6 @@ type HomeCms = {
videoModal?: HomeVideoModalCms;
winners?: HomeSectionCms;
};
type NetzwerkPartnersCms = {
defaultLogoColor?: string | null;
};
type RenderedStatusPhase = Omit<SpaApplicationPhaseItem, 'accent' | 'bg' | 'cta' | 'membershipCta'> & {
accent: string;
bg: string;
@@ -165,10 +162,7 @@ function Lines({ text }: { text: string }) {
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
}
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',
}}
>
<PartnerLogoMark partner={partner} defaultLogoColor={defaultLogoColor} />
<PartnerLogoMark partner={partner} />
</span>
),
}));
@@ -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 = () => {
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
<div style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.2)' }} />
<div style={{ width: 'min(820px, calc(100vw - 260px))', overflow: 'hidden', flexShrink: 0 }}>
<PartnerTicker logos={partnerLogos(networkPartners, defaultLogoColor)} />
<PartnerTicker logos={partnerLogos(networkPartners)} />
</div>
</div>
</div>

View File

@@ -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 (
<div
onClick={onClick}
@@ -259,7 +257,7 @@ const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx
}}
>
<div style={{ height: logoHeight, display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', minWidth: 0 }}>
<PartnerLogoMark partner={partner} scale={logoScale} />
<PartnerLogoMark partner={partner} />
</div>
<div style={{ width: hovered ? 56 : 24, height: 1, background: GOLD, transition: 'width 0.3s ease' }} />
<div style={{ fontFamily: FF, fontSize: lg ? 22 : 18, fontWeight: 700, color: '#101828', letterSpacing: '-0.01em', lineHeight: 1.15 }}>{partner.name}</div>
@@ -488,14 +486,11 @@ const Netzwerk: React.FC = () => {
? juryCollectionMembers
: fallbackArray<JuryMember>(jury.members, netzwerkContent.jury.members);
const expertiseRows = fallbackArray<ExpertiseRow>(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<PartnerTier>(partnerSection.tiers, netzwerkContent.partners.tiers);
const mobileStats = fallbackArray<StatItem>(juryIntro.stats, netzwerkContent.juryIntro.stats);

View File

@@ -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')
})
})