feat: manage about page via payload

This commit is contained in:
syntaxbullet
2026-06-22 10:54:18 +02:00
parent e1713f6d6e
commit 5944ebc62a
7 changed files with 1085 additions and 251 deletions

View File

@@ -17,6 +17,7 @@
"migrate:spa-media": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/migrate-spa-media.ts",
"migrate:spa-pages": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/migrate-spa-pages.ts",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"preload:about-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-about-page-cms.ts",
"preload:contact-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-contact-page-cms.ts",
"preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts",
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",

View File

@@ -0,0 +1,254 @@
import type { Field } from 'payload'
import { aboutContent } from '@/spa/aboutContent'
const uploadField = (name: string, label: string, description?: string): Field => ({
name,
type: 'upload',
relationTo: 'media',
label,
admin: description ? { description } : undefined,
})
const text = (name: string, label: string, defaultValue?: string): Field => ({
name,
type: 'text',
label,
defaultValue,
})
const textarea = (name: string, label: string, defaultValue?: string): Field => ({
name,
type: 'textarea',
label,
defaultValue,
})
const ctaGroup = (name: string, label: string, labelDefault: string, urlDefault: string): Field => ({
name,
label,
type: 'group',
fields: [text('label', 'Label', labelDefault), text('url', 'URL', urlDefault)],
})
const sectionAdmin = (description: string) => ({
description,
initCollapsed: true,
})
const statFields: Field[] = [text('value', 'Value'), text('label', 'Label')]
const bodyFields: Field[] = [textarea('text', 'Paragraph')]
const numberedFields: Field[] = [text('num', 'Number'), text('title', 'Title'), textarea('desc', 'Description')]
export const aboutFields: Field[] = [
{
name: 'about',
label: 'About / Der BMP page layout sections',
type: 'group',
admin: {
description:
'Edit the Der BMP page in the same order it appears on the frontend. The generic Hero and Content tabs are hidden for this SPA-managed route.',
},
fields: [
{
name: 'hero',
label: '01 · Hero',
type: 'group',
admin: sectionAdmin('Top hero image, headline, CTAs, and stats strip.'),
fields: [
uploadField('backgroundImage', 'Background image', `Current frontend image: /images/${aboutContent.hero.backgroundImageFilename}`),
text('imageAlt', 'Image alt text', aboutContent.hero.imageAlt),
text('eyebrow', 'Eyebrow', aboutContent.hero.eyebrow),
textarea('heading', 'Heading', aboutContent.hero.heading),
text('highlight', 'Highlighted heading line', aboutContent.hero.highlight),
textarea('description', 'Intro paragraph', aboutContent.hero.description),
ctaGroup('primaryCta', 'Primary CTA', aboutContent.hero.primaryCta.label, aboutContent.hero.primaryCta.url),
ctaGroup('secondaryCta', 'Secondary CTA', aboutContent.hero.secondaryCta.label, aboutContent.hero.secondaryCta.url),
{
name: 'stats',
label: 'Stats',
type: 'array',
dbName: 'about_hero_stats',
defaultValue: aboutContent.hero.stats,
fields: statFields,
},
],
},
{
name: 'prize',
label: '02 · Was ist der Preis?',
type: 'group',
admin: sectionAdmin('Cream text/image section explaining the prize.'),
fields: [
uploadField('image', 'Image', `Current frontend image: /images/${aboutContent.prize.imageFilename}`),
text('imageAlt', 'Image alt text', aboutContent.prize.imageAlt),
text('eyebrow', 'Eyebrow', aboutContent.prize.eyebrow),
textarea('heading', 'Heading', aboutContent.prize.heading),
{
name: 'body',
label: 'Body paragraphs',
type: 'array',
dbName: 'about_prize_body',
defaultValue: aboutContent.prize.body.map((text) => ({ text })),
fields: bodyFields,
},
{
name: 'facts',
label: 'Key facts',
type: 'array',
dbName: 'about_prize_facts',
defaultValue: aboutContent.prize.facts,
fields: statFields,
},
text('imageLabel', 'Desktop image label', aboutContent.prize.imageLabel),
],
},
{
name: 'mittelstand',
label: '03 · Bedeutung für den Mittelstand',
type: 'group',
admin: sectionAdmin('Navy text/image section about relevance and goals.'),
fields: [
uploadField('image', 'Image', `Current frontend image: /images/${aboutContent.mittelstand.imageFilename}`),
text('imageAlt', 'Image alt text', aboutContent.mittelstand.imageAlt),
text('eyebrow', 'Eyebrow', aboutContent.mittelstand.eyebrow),
textarea('heading', 'Heading', aboutContent.mittelstand.heading),
{
name: 'body',
label: 'Body paragraphs',
type: 'array',
dbName: 'about_mid_body',
defaultValue: aboutContent.mittelstand.body.map((text) => ({ text })),
fields: bodyFields,
},
],
},
{
name: 'highlights',
label: '04 · Preisträger highlights',
type: 'group',
admin: sectionAdmin('Header copy and editor-selected winners for the photo grid.'),
fields: [
text('eyebrow', 'Eyebrow', aboutContent.highlights.eyebrow),
textarea('heading', 'Heading', aboutContent.highlights.heading),
ctaGroup('cta', 'CTA', aboutContent.highlights.cta.label, aboutContent.highlights.cta.url),
text('hoverLabel', 'Card hover label', aboutContent.highlights.hoverLabel),
{
name: 'year',
label: 'Fallback winner year',
type: 'number',
defaultValue: aboutContent.highlights.year,
},
{
name: 'selectedWinners',
label: 'Selected winners',
type: 'relationship',
relationTo: 'preistraeger',
hasMany: true,
maxRows: 8,
admin: {
description: 'Choose up to 8 Preisträger to show. If empty, the frontend falls back to winners from the selected year.',
},
},
],
},
{
name: 'testimonials',
label: '05 · Testimonials',
type: 'group',
admin: sectionAdmin('Testimonial heading and slides. Upload images when available, or keep the migrated URL fallback.'),
fields: [
text('eyebrow', 'Eyebrow', aboutContent.testimonials.eyebrow),
textarea('heading', 'Heading', aboutContent.testimonials.heading),
{
name: 'items',
label: 'Testimonials',
type: 'array',
dbName: 'about_tst_items',
defaultValue: aboutContent.testimonials.items,
fields: [
uploadField('image', 'Image'),
text('imageUrl', 'Fallback image URL'),
text('name', 'Name'),
text('role', 'Role'),
text('company', 'Company'),
text('year', 'Award year'),
textarea('quote', 'Quote'),
],
},
],
},
{
name: 'history',
label: '06 · Geschichte & Meilensteine',
type: 'group',
admin: sectionAdmin('Navy timeline section.'),
fields: [
text('eyebrow', 'Eyebrow', aboutContent.history.eyebrow),
textarea('heading', 'Heading', aboutContent.history.heading),
text('highlight', 'Highlighted heading word', aboutContent.history.highlight),
text('watermark', 'Desktop watermark', aboutContent.history.watermark),
{
name: 'items',
label: 'Timeline items',
type: 'array',
dbName: 'about_hist_items',
defaultValue: aboutContent.history.items,
fields: [text('year', 'Year'), text('title', 'Title'), textarea('desc', 'Description')],
},
],
},
{
name: 'goals',
label: '07 · Zielsetzung',
type: 'group',
admin: sectionAdmin('Cream values list next to the history timeline.'),
fields: [
text('eyebrow', 'Eyebrow', aboutContent.goals.eyebrow),
textarea('heading', 'Heading', aboutContent.goals.heading),
{
name: 'items',
label: 'Goal rows',
type: 'array',
dbName: 'about_goal_items',
defaultValue: aboutContent.goals.items,
fields: numberedFields,
},
],
},
{
name: 'values',
label: '08 · Vorteile & Werte',
type: 'group',
admin: sectionAdmin('Cream editorial rows before the final CTA.'),
fields: [
text('eyebrow', 'Eyebrow', aboutContent.values.eyebrow),
textarea('heading', 'Heading', aboutContent.values.heading),
textarea('description', 'Description', aboutContent.values.description),
{
name: 'items',
label: 'Value rows',
type: 'array',
dbName: 'about_val_items',
defaultValue: aboutContent.values.items,
fields: [text('num', 'Number'), text('title', 'Title')],
},
],
},
{
name: 'cta',
label: '09 · Final CTA',
type: 'group',
admin: sectionAdmin('Bottom navy CTA section.'),
fields: [
text('eyebrow', 'Eyebrow', aboutContent.cta.eyebrow),
textarea('heading', 'Heading', aboutContent.cta.heading),
textarea('description', 'Description', aboutContent.cta.description),
ctaGroup('primaryCta', 'Primary CTA', aboutContent.cta.primaryCta.label, aboutContent.cta.primaryCta.url),
text('secondaryPrefix', 'Secondary prefix', aboutContent.cta.secondaryPrefix),
ctaGroup('secondaryCta', 'Secondary CTA', aboutContent.cta.secondaryCta.label, aboutContent.cta.secondaryCta.url),
],
},
],
},
]

View File

@@ -8,6 +8,7 @@ import { Content } from '../../blocks/Content/config'
import { FormBlock } from '../../blocks/Form/config'
import { MediaBlock } from '../../blocks/MediaBlock/config'
import { hero } from '@/heros/config'
import { aboutFields } from './aboutFields'
import { contactFields } from './contactFields'
import { homeFields } from './homeFields'
import { impressumFields } from './impressumFields'
@@ -49,6 +50,14 @@ const isParticipationPage = (_: unknown, siblingData?: { slug?: string; spaPath?
return spaPath === '/teilnahme' || slug === 'teilnahme' || slug === 'participation' || title === 'teilnahme' || title === 'participation'
}
const isAboutPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
const slug = siblingData?.slug
const spaPath = siblingData?.spaPath
const title = siblingData?.title?.toLowerCase()
return spaPath === '/der-bmp' || slug === 'der-bmp' || slug === 'about' || title === 'der bmp' || title === 'about'
}
const isImpressumPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
const slug = siblingData?.slug
const spaPath = siblingData?.spaPath
@@ -61,6 +70,7 @@ const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: s
isHomePage(undefined, siblingData) ||
isContactPage(undefined, siblingData) ||
isParticipationPage(undefined, siblingData) ||
isAboutPage(undefined, siblingData) ||
isImpressumPage(undefined, siblingData)
export const Pages: CollectionConfig<'pages'> = {
@@ -150,6 +160,13 @@ export const Pages: CollectionConfig<'pages'> = {
fields: participationFields,
label: 'Participation Page',
},
{
admin: {
condition: (data) => isAboutPage(undefined, data),
},
fields: aboutFields,
label: 'About Page',
},
{
admin: {
condition: (data) => isImpressumPage(undefined, data),

View File

@@ -913,6 +913,184 @@ export interface Page {
};
};
};
/**
* Edit the Der BMP page in the same order it appears on the frontend. The generic Hero and Content tabs are hidden for this SPA-managed route.
*/
about?: {
/**
* Top hero image, headline, CTAs, and stats strip.
*/
hero?: {
/**
* Current frontend image: /images/saal-leer-atmosphaere.jpg
*/
backgroundImage?: (number | null) | Media;
imageAlt?: string | null;
eyebrow?: string | null;
heading?: string | null;
highlight?: string | null;
description?: string | null;
primaryCta?: {
label?: string | null;
url?: string | null;
};
secondaryCta?: {
label?: string | null;
url?: string | null;
};
stats?:
| {
value?: string | null;
label?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Cream text/image section explaining the prize.
*/
prize?: {
/**
* Current frontend image: /images/preistraeger-gruppe.jpg
*/
image?: (number | null) | Media;
imageAlt?: string | null;
eyebrow?: string | null;
heading?: string | null;
body?:
| {
text?: string | null;
id?: string | null;
}[]
| null;
facts?:
| {
value?: string | null;
label?: string | null;
id?: string | null;
}[]
| null;
imageLabel?: string | null;
};
/**
* Navy text/image section about relevance and goals.
*/
mittelstand?: {
/**
* Current frontend image: /images/networking-innenhof.jpg
*/
image?: (number | null) | Media;
imageAlt?: string | null;
eyebrow?: string | null;
heading?: string | null;
body?:
| {
text?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Header copy and editor-selected winners for the photo grid.
*/
highlights?: {
eyebrow?: string | null;
heading?: string | null;
cta?: {
label?: string | null;
url?: string | null;
};
hoverLabel?: string | null;
year?: number | null;
/**
* Choose up to 8 Preisträger to show. If empty, the frontend falls back to winners from the selected year.
*/
selectedWinners?: (number | Preistraeger)[] | null;
};
/**
* Testimonial heading and slides. Upload images when available, or keep the migrated URL fallback.
*/
testimonials?: {
eyebrow?: string | null;
heading?: string | null;
items?:
| {
image?: (number | null) | Media;
imageUrl?: string | null;
name?: string | null;
role?: string | null;
company?: string | null;
year?: string | null;
quote?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Navy timeline section.
*/
history?: {
eyebrow?: string | null;
heading?: string | null;
highlight?: string | null;
watermark?: string | null;
items?:
| {
year?: string | null;
title?: string | null;
desc?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Cream values list next to the history timeline.
*/
goals?: {
eyebrow?: string | null;
heading?: string | null;
items?:
| {
num?: string | null;
title?: string | null;
desc?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Cream editorial rows before the final CTA.
*/
values?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
items?:
| {
num?: string | null;
title?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Bottom navy CTA section.
*/
cta?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
primaryCta?: {
label?: string | null;
url?: string | null;
};
secondaryPrefix?: string | null;
secondaryCta?: {
label?: string | null;
url?: string | null;
};
};
};
/**
* Edit the Impressum page content. Legal section body blocks are stored as JSON so nested paragraphs, lists, notices, addresses, and subsections can keep their frontend order.
*/
@@ -2558,6 +2736,172 @@ export interface PagesSelect<T extends boolean = true> {
};
};
};
about?:
| T
| {
hero?:
| T
| {
backgroundImage?: T;
imageAlt?: T;
eyebrow?: T;
heading?: T;
highlight?: T;
description?: T;
primaryCta?:
| T
| {
label?: T;
url?: T;
};
secondaryCta?:
| T
| {
label?: T;
url?: T;
};
stats?:
| T
| {
value?: T;
label?: T;
id?: T;
};
};
prize?:
| T
| {
image?: T;
imageAlt?: T;
eyebrow?: T;
heading?: T;
body?:
| T
| {
text?: T;
id?: T;
};
facts?:
| T
| {
value?: T;
label?: T;
id?: T;
};
imageLabel?: T;
};
mittelstand?:
| T
| {
image?: T;
imageAlt?: T;
eyebrow?: T;
heading?: T;
body?:
| T
| {
text?: T;
id?: T;
};
};
highlights?:
| T
| {
eyebrow?: T;
heading?: T;
cta?:
| T
| {
label?: T;
url?: T;
};
hoverLabel?: T;
year?: T;
selectedWinners?: T;
};
testimonials?:
| T
| {
eyebrow?: T;
heading?: T;
items?:
| T
| {
image?: T;
imageUrl?: T;
name?: T;
role?: T;
company?: T;
year?: T;
quote?: T;
id?: T;
};
};
history?:
| T
| {
eyebrow?: T;
heading?: T;
highlight?: T;
watermark?: T;
items?:
| T
| {
year?: T;
title?: T;
desc?: T;
id?: T;
};
};
goals?:
| T
| {
eyebrow?: T;
heading?: T;
items?:
| T
| {
num?: T;
title?: T;
desc?: T;
id?: T;
};
};
values?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
items?:
| T
| {
num?: T;
title?: T;
id?: T;
};
};
cta?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
primaryCta?:
| T
| {
label?: T;
url?: T;
};
secondaryPrefix?: T;
secondaryCta?:
| T
| {
label?: T;
url?: T;
};
};
};
impressum?:
| T
| {

View File

@@ -0,0 +1,89 @@
import 'dotenv/config'
import config from '@payload-config'
import { getPayload, type Payload } from 'payload'
import { aboutContent } from '@/spa/aboutContent'
const mediaByFilename = async (payload: Payload, filename: string) => {
const result = await payload.find({
collection: 'media',
limit: 1,
pagination: false,
where: { filename: { equals: filename } },
})
return result.docs[0]?.id
}
async function main() {
const payload = await getPayload({ config })
const pageResult = await payload.find({
collection: 'pages',
limit: 1,
pagination: false,
draft: true,
overrideAccess: true,
where: {
or: [{ spaPath: { equals: '/der-bmp' } }, { slug: { equals: 'der-bmp' } }, { slug: { equals: 'about' } }],
},
})
const page = pageResult.docs[0]
if (!page) throw new Error('About page not found. Expected spaPath=/der-bmp or slug=der-bmp/about.')
const [heroImage, prizeImage, mittelstandImage, winnerResult] = await Promise.all([
mediaByFilename(payload, aboutContent.hero.backgroundImageFilename),
mediaByFilename(payload, aboutContent.prize.imageFilename),
mediaByFilename(payload, aboutContent.mittelstand.imageFilename),
payload.find({
collection: 'preistraeger',
limit: 8,
pagination: false,
draft: true,
overrideAccess: true,
where: { year: { equals: aboutContent.highlights.year } },
}),
])
const { backgroundImageFilename: _heroFilename, ...heroContent } = aboutContent.hero
const { imageFilename: _prizeFilename, ...prizeContent } = aboutContent.prize
const { imageFilename: _mittelstandFilename, ...mittelstandContent } = aboutContent.mittelstand
await payload.update({
collection: 'pages',
id: page.id,
overrideAccess: true,
context: { disableRevalidate: true },
data: {
about: {
...aboutContent,
hero: {
...heroContent,
backgroundImage: heroImage,
},
prize: {
...prizeContent,
image: prizeImage,
body: aboutContent.prize.body.map((text) => ({ text })),
},
mittelstand: {
...mittelstandContent,
image: mittelstandImage,
body: aboutContent.mittelstand.body.map((text) => ({ text })),
},
highlights: {
...aboutContent.highlights,
selectedWinners: winnerResult.docs.map((winner) => winner.id),
},
},
} as never,
})
payload.logger.info(`Preloaded About CMS fields for page ${page.id}`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})

142
src/spa/aboutContent.ts Normal file
View File

@@ -0,0 +1,142 @@
export const aboutContent = {
hero: {
backgroundImageFilename: 'saal-leer-atmosphaere.jpg',
imageAlt: 'BMP Gala',
eyebrow: 'Bayerischer Mittelstandspreis',
heading: 'WERTE, DIE\nBAYERN BEWEGEN',
highlight: 'BAYERN BEWEGEN',
description:
'Seit 2005 ehrt der BMP herausragende Leistungen im bayerischen Mittelstand. Entdecken Sie die Geschichte, die Vision und die Menschen dahinter.',
primaryCta: { label: 'Jetzt bewerben', url: '/teilnahme' },
secondaryCta: { label: 'Alle Preisträger', url: '/preistraeger' },
stats: [
{ value: '20+', label: 'Jahre' },
{ value: '0 €', label: 'Teilnahme' },
{ value: '100%', label: 'Unabhängig' },
],
},
prize: {
imageFilename: 'preistraeger-gruppe.jpg',
imageAlt: 'BMP Gala Preisverleihung',
eyebrow: 'Was ist der Preis?',
heading: 'DER BAYERISCHE\nMITTELSTANDSPREIS',
body: [
'Seit 2005 ist der BMP eine der bedeutendsten Auszeichnungen für den bayerischen Mittelstand. Vergeben von einer vollständig unabhängigen Expertenjury, ehrt er Unternehmen, die durch Innovation, Nachhaltigkeit und regionale Verwurzelung herausragen.',
'Entstanden aus einer Initiative bayerischer Wirtschaftsvertreter, ehrt der Preis heute jährlich die Leuchttürme unserer Wirtschaft.',
],
facts: [
{ value: '2005', label: 'Gegründet' },
{ value: '0 €', label: 'Teilnahmegebühr' },
{ value: '5', label: 'Jury-Schritte' },
],
imageLabel: 'BMP Preisverleihung · München',
},
mittelstand: {
imageFilename: 'networking-innenhof.jpg',
imageAlt: 'Bayerischer Mittelstand',
eyebrow: 'Relevanz & Zielsetzung',
heading: 'BEDEUTUNG FÜR\nDEN MITTELSTAND',
body: [
'Der Bayerische Mittelstandspreis ist weit mehr als eine Trophäe. Seit seiner Gründung im Jahr 2005 verfolgt er das Ziel, die Vielfalt und Leistungsstärke bayerischer Familienunternehmen und KMU sichtbar zu machen.',
'In einer globalisierten Welt sind es diese Unternehmen, die für Stabilität, Innovation und soziale Verantwortung stehen. Der BMP macht sie sichtbar.',
],
},
highlights: {
eyebrow: 'Preisträger-Highlights',
heading: 'AUSGEZEICHNETE 2025',
cta: { label: 'Alle Preisträger', url: '/preistraeger' },
hoverLabel: 'Detail →',
year: 2025,
selectedWinners: [],
},
testimonials: {
eyebrow: 'Stimmen der Preisträger',
heading: 'DAS SAGEN UNSERE\nPREISTRÄGER.',
items: [
{
imageUrl: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=600',
name: 'Dr. Maria Huber',
role: 'Geschäftsführerin',
company: 'TechVision GmbH',
year: '2025',
quote: 'Der BMP hat uns neue Türen geöffnet. Die Auszeichnung ist ein wichtiges Qualitätssiegel für unser gesamtes Team.',
},
{
imageUrl: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=600',
name: 'Hannes Weber',
role: 'Inhaber & CEO',
company: 'Weber Präzision GmbH',
year: '2023',
quote: 'Der Gewinn war ein Moment des Stolzes für die gesamte Belegschaft. Die höchste Anerkennung für unsere tägliche Arbeit.',
},
{
imageUrl: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&q=80&w=600',
name: 'Lisa Berger',
role: 'Gründerin & CEO',
company: 'EcoTech Bavaria',
year: '2024',
quote: 'Das Netzwerk, das wir durch den BMP gewonnen haben, ist unbezahlbar. Wir fanden Partner, die unsere Werte teilen.',
},
{
imageUrl: 'https://images.unsplash.com/photo-1560250097-0b93528c311a?auto=format&fit=crop&q=80&w=600',
name: 'Dr. Michael Schuh',
role: 'Vorstand',
company: 'Bayerischer Maschinenbau',
year: '2024',
quote: 'Der Preis hat unsere Sichtbarkeit weit über die bayerischen Grenzen hinaus gestärkt. Ein echtes Premium-Siegel.',
},
{
imageUrl: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=600',
name: 'Anna Kellner',
role: 'Mitgründerin',
company: 'AlpinTech Solutions',
year: '2024',
quote: 'Der BMP hat unsere Employer Brand transformiert. Wir gewinnen heute die besten Talente, weil wir Preisträger sind.',
},
],
},
history: {
eyebrow: 'Geschichte & Meilensteine',
heading: '20 JAHRE\nMITTELSTANDSEXZELLENZ',
highlight: 'MITTELSTANDS',
watermark: '20',
items: [
{ year: '2005', title: 'Gründung', desc: 'Initiative bayerischer Wirtschaftsvertreter zur Stärkung des Mittelstands.' },
{ year: '2010', title: 'Erste mediale Partnerschaft', desc: 'Kooperation mit bayernweiten TV- und Printmedien.' },
{ year: '2015', title: '100 Preisträger', desc: 'Jubiläum: 100 ausgezeichnete Unternehmen seit der ersten Verleihung.' },
{ year: '2020', title: 'Digital-Transformation', desc: 'Erweiterung um digitale Bewerbungsprozesse und Online-Formate.' },
{ year: '2025', title: 'Jubiläumsjahr', desc: '20 Jahre BMP: stärker, sichtbarer und vernetzter denn je.' },
{ year: '2026', title: 'Bavarian Future Award', desc: 'Ergänzung des Portfolios um den Bavarian Future Award mit Fokus auf zukunftsweisende Unternehmen.' },
],
},
goals: {
eyebrow: 'Zielsetzung',
heading: 'WOFÜR WIR\nSTEHEN',
items: [
{ num: '01', title: 'Sichtbarkeit', desc: 'Die Vielfalt und Leistungsstärke des bayerischen Mittelstands in der Öffentlichkeit sichtbar machen.' },
{ num: '02', title: 'Netzwerk', desc: 'Branchenübergreifende Verbindungen zwischen herausragenden Unternehmen und Entscheidern schaffen.' },
{ num: '03', title: 'Werte', desc: 'Wertebasierte Führung, nachhaltige Unternehmenskultur und gesellschaftliche Verantwortung fördern.' },
{ num: '04', title: 'Arbeitgebermarke', desc: 'Den Mittelstand als attraktiven Arbeitgeber stärken und Talente für Bayern gewinnen.' },
{ num: '05', title: 'Resilienz', desc: 'Die Widerstandsfähigkeit bayerischer Unternehmen würdigen, die auch in unsicheren Zeiten Stabilität und Verantwortung beweisen.' },
],
},
values: {
eyebrow: 'Vorteile & Werte',
heading: 'WARUM DER\nBMP ZÄHLT',
description: 'Drei Dimensionen, die den Unterschied machen: für Ihr Unternehmen, Ihre Mitarbeitenden und Ihren Marktauftritt.',
items: [
{ num: '01', title: 'Exzellenz-Siegel' },
{ num: '02', title: 'Partner-Netzwerk' },
{ num: '03', title: 'Reichweite' },
],
},
cta: {
eyebrow: 'Starten Sie Ihre Reise',
heading: 'SCHREIBEN AUCH SIE GESCHICHTE',
description:
'Werden Sie Teil der Wall of Fame des bayerischen Mittelstands. Die Teilnahmegebühr beträgt 0 EUR und die Teilnahme lohnt sich in jeder Phase.',
primaryCta: { label: 'Jetzt kostenlos bewerben', url: '/teilnahme' },
secondaryPrefix: 'Oder:',
secondaryCta: { label: 'Vereinsmitglied werden', url: '/mitglied-werden' },
},
}

View File

@@ -1,146 +1,238 @@
import React, { useState } from 'react';
import { Link } from '@/spa/router';
import { Star, ChevronRight } from 'lucide-react';
import TestimonialsSection from '@/spa/components/TestimonialsSection';
import { WINNERS } from '@/spa/data/winners';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import React, { useMemo, useState } from 'react'
import { ChevronRight, Star } from 'lucide-react'
import TestimonialsSection from '@/spa/components/TestimonialsSection'
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'
import Image from '@/spa/components/ui/UnoptimizedImage'
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute'
import { WINNERS, type Winner } from '@/spa/data/winners'
import { useIsMobile } from '@/spa/hooks/useIsMobile'
import { Link } from '@/spa/router'
import { aboutContent } from '@/spa/aboutContent'
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
const CREAM = '#E4E2E3';
const FF = '"IBM Plex Sans", sans-serif'
const FB = '"Inter", sans-serif'
const NAVY = '#111D55'
const GOLD = '#EFBF04'
const CREAM = '#E4E2E3'
// 8 highlight winners from 2025
const HIGHLIGHTS = WINNERS.filter(w => w.year === 2025).slice(0, 8);
type AboutCms = Partial<typeof aboutContent> & {
hero?: Partial<typeof aboutContent.hero> & { backgroundImage?: unknown }
prize?: Partial<typeof aboutContent.prize> & { image?: unknown }
mittelstand?: Partial<typeof aboutContent.mittelstand> & { image?: unknown }
highlights?: Partial<typeof aboutContent.highlights> & { selectedWinners?: unknown[] | null }
}
type TextRow = { text?: string | null }
const FALLBACK_HIGHLIGHTS = WINNERS.filter((w) => w.year === aboutContent.highlights.year).slice(0, 8)
const fallbackText = (value: unknown, fallback: string) => (typeof value === 'string' && value.length > 0 ? value : fallback)
const fallbackArray = <T,>(value: unknown, fallback: T[]) => (Array.isArray(value) && value.length ? (value as T[]) : fallback)
function Lines({ text }: { text: string }) {
return (
<>
{text.split('\n').map((line, i) => (
<React.Fragment key={`${line}-${i}`}>
{i > 0 && <br />}
{line}
</React.Fragment>
))}
</>
)
}
const paragraphs = (value: unknown, fallback: string[]) =>
Array.isArray(value) && value.length
? (value as Array<string | TextRow>).map((item) => (typeof item === 'string' ? item : fallbackText(item.text, ''))).filter(Boolean)
: fallback
function HighlightedText({ text, highlight }: { text: string; highlight: string }) {
const index = text.indexOf(highlight)
if (!highlight || index < 0) return <>{text}</>
return (
<>
{text.slice(0, index)}
<span style={{ color: GOLD }}>{highlight}</span>
{text.slice(index + highlight.length)}
</>
)
}
const normalizeWinner = (doc: CmsRouteDoc | undefined): Winner | undefined => {
if (!doc) return undefined
return {
id: String(doc.id),
slug: String(doc.slug || ''),
name: String(doc.title || 'Preisträger'),
category: String(doc.category || 'Preisträger'),
year: Number(doc.year || aboutContent.highlights.year),
type: String(doc.awardType || 'Auszeichnung'),
img: mediaUrl(doc.image, '/images/gala-saal-overview.jpg'),
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
quote: String(doc.quote || ''),
quotePerson: String(doc.quotePerson || doc.title || ''),
quoteRole: String(doc.quoteRole || ''),
location: String(doc.location || ''),
industry: String(doc.industry || ''),
website: String(doc.website || ''),
hasMedia: Boolean(doc.hasMedia),
}
}
const winnerFromRelationship = (entry: unknown, allDocs: CmsRouteDoc[]) => {
if (entry && typeof entry === 'object') return normalizeWinner(entry as CmsRouteDoc)
const doc = allDocs.find((item) => String(item.id) === String(entry))
return normalizeWinner(doc)
}
const About: React.FC = () => {
const isMobile = useIsMobile();
const isMobile = useIsMobile()
const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms
const cmsWinners = useCmsCollection('preistraeger')
const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
const highlights = { ...aboutContent.highlights, ...(cms.highlights || {}) }
const testimonials = { ...aboutContent.testimonials, ...(cms.testimonials || {}) }
const history = { ...aboutContent.history, ...(cms.history || {}) }
const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
const values = { ...aboutContent.values, ...(cms.values || {}) }
const cta = { ...aboutContent.cta, ...(cms.cta || {}) }
const highlightWinners = useMemo(() => {
const selected = fallbackArray<unknown>(highlights.selectedWinners, [])
.map((entry) => winnerFromRelationship(entry, cmsWinners))
.filter((winner): winner is Winner => Boolean(winner))
if (selected.length) return selected.slice(0, 8)
const isWinnerFromYear = (winner: Winner | undefined): winner is Winner =>
Boolean(winner) && winner?.year === Number(highlights.year || aboutContent.highlights.year)
const cmsByYear = cmsWinners
.map(normalizeWinner)
.filter(isWinnerFromYear)
.slice(0, 8)
return cmsByYear.length ? cmsByYear : FALLBACK_HIGHLIGHTS
}, [cmsWinners, highlights.selectedWinners, highlights.year])
return (
<div className="animate-fade-in">
{/* ── 1. HERO ─────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
<Image unoptimized
src="/images/saal-leer-atmosphaere.jpg"
alt="BMP Gala"
<Image
unoptimized
src={mediaUrl(hero.backgroundImage, `/images/${aboutContent.hero.backgroundImageFilename}`)}
alt={mediaAlt(hero.backgroundImage, fallbackText(hero.imageAlt, aboutContent.hero.imageAlt))}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
/>
{/* Left-to-right dark overlay */}
<div style={{ position: 'absolute', inset: 0, background: isMobile ? 'linear-gradient(to top, rgba(2,9,48,0.95) 0%, rgba(2,9,48,0.72) 38%, rgba(2,9,48,0.22) 72%, transparent 100%)' : 'linear-gradient(to right, #020930 0%, rgba(2,9,48,0.90) 38%, rgba(2,9,48,0.18) 65%, transparent 100%)' }} />
{/* Gold accent line */}
<div style={{ position: 'absolute', top: 0, left: 80, width: 2, height: '100%', background: `linear-gradient(to bottom, transparent, ${GOLD}, transparent)`, opacity: 0.4 }} />
{/* Gold bottom line */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.3), transparent)', zIndex: 2 }} />
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '0 24px 48px' : '0 80px 80px', maxWidth: 860 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 28 }}>
<div style={{ width: 40, height: 2, background: GOLD }} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04' }}>
Bayerischer Mittelstandspreis
{fallbackText(hero.eyebrow, aboutContent.hero.eyebrow)}
</span>
</div>
<h1 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.6rem, 6vw, 5rem)' : 'clamp(2.8rem, 6vw, 5rem)', fontWeight: 900, color: '#fff', lineHeight: 0.95, letterSpacing: '-0.03em', textTransform: 'uppercase', margin: '0 0 28px' }}>
WERTE, DIE<br />
<span style={{ color: GOLD }}>BAYERN BEWEGEN</span>
{fallbackText(hero.heading, aboutContent.hero.heading).split('\n').map((line, index) => (
<React.Fragment key={`${line}-${index}`}>
{index > 0 && <br />}
<span style={{ color: line === fallbackText(hero.highlight, aboutContent.hero.highlight) ? GOLD : '#fff' }}>{line}</span>
</React.Fragment>
))}
</h1>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.65)', lineHeight: 1.7, maxWidth: 560, margin: '0 0 40px', fontWeight: 300 }}>
Seit 2005 ehrt der BMP herausragende Leistungen im bayerischen Mittelstand. Entdecken Sie die Geschichte, die Vision und die Menschen dahinter.
{fallbackText(hero.description, aboutContent.hero.description)}
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>
<Link
to="/teilnahme"
to={fallbackText(hero.primaryCta?.url, aboutContent.hero.primaryCta.url)}
style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: '#fff', background: '#111D55', padding: '14px 28px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, transition: 'background 0.15s, color 0.15s', border: '2px solid rgba(255,255,255,0.15)' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.color = '#101828'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#111D55'; (e.currentTarget as HTMLElement).style.color = '#fff'; }}
onMouseEnter={(e) => { e.currentTarget.style.background = GOLD; e.currentTarget.style.color = '#101828' }}
onMouseLeave={(e) => { e.currentTarget.style.background = '#111D55'; e.currentTarget.style.color = '#fff' }}
>
Jetzt bewerben <ChevronRight size={16} />
{fallbackText(hero.primaryCta?.label, aboutContent.hero.primaryCta.label)} <ChevronRight size={16} />
</Link>
<Link
to="/preistraeger"
to={fallbackText(hero.secondaryCta?.url, aboutContent.hero.secondaryCta.url)}
style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '14px 28px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, transition: 'background 0.15s, box-shadow 0.2s' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
onMouseEnter={(e) => { e.currentTarget.style.background = '#FFD130'; e.currentTarget.style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)' }}
onMouseLeave={(e) => { e.currentTarget.style.background = GOLD; e.currentTarget.style.boxShadow = 'none' }}
>
Alle Preisträger <ChevronRight size={16} />
{fallbackText(hero.secondaryCta?.label, aboutContent.hero.secondaryCta.label)} <ChevronRight size={16} />
</Link>
</div>
</div>
{/* Stats strip */}
<div style={{ position: 'absolute', bottom: 0, right: 0, display: isMobile ? 'none' : 'flex', borderTop: '1px solid rgba(255,255,255,0.08)', borderLeft: '1px solid rgba(255,255,255,0.08)' }}>
{[['20+', 'Jahre'], ['0 €', 'Teilnahme'], ['100%', 'Unabhängig']].map(([val, lbl], i) => (
<div key={i} style={{ padding: '20px 36px', textAlign: 'center', borderRight: '1px solid rgba(255,255,255,0.08)' }}>
<div style={{ fontFamily: FF, fontSize: 22, fontWeight: 900, color: GOLD }}>{val}</div>
<div style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', marginTop: 2 }}>{lbl}</div>
{fallbackArray(hero.stats, aboutContent.hero.stats).map((item, i) => (
<div key={`${item.value}-${i}`} style={{ padding: '20px 36px', textAlign: 'center', borderRight: '1px solid rgba(255,255,255,0.08)' }}>
<div style={{ fontFamily: FF, fontSize: 22, fontWeight: 900, color: GOLD }}>{fallbackText(item.value, '')}</div>
<div style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', marginTop: 2 }}>{fallbackText(item.label, '')}</div>
</div>
))}
</div>
</section>
{/* ── 2a. WAS IST DER PREIS? (cream, text left / image right) ─────────── */}
<section id="was-ist" style={{ position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 580 }}>
{/* Left text on cream */}
<div style={{ background: CREAM, padding: isMobile ? '32px 24px 44px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}>
{!isMobile && (
<>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>Was ist der Preis?</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2.1rem, 3.4vw, 3.1rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.04, margin: '0 0 20px' }}>
DER BAYERISCHE<br />MITTELSTANDS<wbr />PREIS
<Lines text={fallbackText(prize.heading, aboutContent.prize.heading)} />
</h2>
</>
)}
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
<p style={{ fontFamily: FB, fontSize: 19, color: 'rgba(16,24,40,0.8)', lineHeight: 1.8, maxWidth: 440, marginBottom: 16 }}>
Seit 2005 ist der BMP eine der bedeutendsten Auszeichnungen für den bayerischen Mittelstand. Vergeben von einer vollständig unabhängigen Expertenjury, ehrt er Unternehmen, die durch Innovation, Nachhaltigkeit und regionale Verwurzelung herausragen.
</p>
<p style={{ fontFamily: FB, fontSize: 19, color: 'rgba(16,24,40,0.8)', lineHeight: 1.8, maxWidth: 440, marginBottom: isMobile ? 0 : 44 }}>
Entstanden aus einer Initiative bayerischer Wirtschaftsvertreter, ehrt der Preis heute jährlich die Leuchttürme unserer Wirtschaft.
</p>
{/* Key facts moved above the copy on mobile */}
{paragraphs(prize.body, aboutContent.prize.body).map((text, index, arr) => (
<p key={index} style={{ fontFamily: FB, fontSize: 19, color: 'rgba(16,24,40,0.8)', lineHeight: 1.8, maxWidth: 440, marginBottom: index === arr.length - 1 ? (isMobile ? 0 : 44) : 16 }}>
{text}
</p>
))}
<div style={{ display: 'flex', order: isMobile ? -1 : 0, borderTop: isMobile ? 'none' : '1px solid #D0D5DD', borderBottom: isMobile ? '1px solid #D0D5DD' : 'none', marginBottom: isMobile ? 26 : 0 }}>
{[
{ val: '2005', label: 'Gegründet' },
{ val: '0 €', label: 'Teilnahmegebühr' },
{ val: '5', label: 'Jury-Schritte' },
].map((f, i) => (
<div key={i} style={{ flex: 1, padding: '20px 0', borderRight: i < 2 ? '1px solid #D0D5DD' : 'none', paddingLeft: 0, paddingRight: i < 2 ? 20 : 0 }}>
<div style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2vw, 1.8rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.03em', lineHeight: 1 }}>{f.val}</div>
<div style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.18em', marginTop: 6 }}>{f.label}</div>
{fallbackArray(prize.facts, aboutContent.prize.facts).map((f, i, arr) => (
<div key={`${f.value}-${i}`} style={{ flex: 1, padding: '20px 0', borderRight: i < arr.length - 1 ? '1px solid #D0D5DD' : 'none', paddingLeft: 0, paddingRight: i < arr.length - 1 ? 20 : 0 }}>
<div style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2vw, 1.8rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.03em', lineHeight: 1 }}>{fallbackText(f.value, '')}</div>
<div style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.18em', marginTop: 6 }}>{fallbackText(f.label, '')}</div>
</div>
))}
</div>
</div>
{/* Right full-bleed image (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined, order: isMobile ? 1 : 0 }}>
<Image unoptimized
src="/images/preistraeger-gruppe.jpg"
alt="BMP Gala Preisverleihung"
<Image
unoptimized
src={mediaUrl(prize.image, `/images/${aboutContent.prize.imageFilename}`)}
alt={mediaAlt(prize.image, fallbackText(prize.imageAlt, aboutContent.prize.imageAlt))}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
/>
<div style={{ position: 'absolute', inset: 0, background: isMobile
? 'linear-gradient(to top, rgba(3,9,58,0.94) 0%, rgba(3,9,58,0.55) 38%, rgba(3,9,58,0.12) 72%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)'
: `linear-gradient(to right, ${CREAM} 0%, transparent 25%), linear-gradient(to top, rgba(3,9,58,0.75) 0%, transparent 60%)` }} />
{/* Heading overlay mobile only */}
{isMobile && (
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>Was ist der Preis?</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.9rem, 8vw, 2.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.04, margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.6)' }}>
DER BAYERISCHE<br />MITTELSTANDS<wbr />PREIS
<Lines text={fallbackText(prize.heading, aboutContent.prize.heading)} />
</h2>
</div>
)}
{/* Decorative label desktop only */}
{!isMobile && (
<div style={{ position: 'absolute', bottom: 36, left: 36 }}>
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', color: GOLD, marginBottom: 6 }}>BMP Preisverleihung · München</div>
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', color: GOLD, marginBottom: 6 }}>{fallbackText(prize.imageLabel, aboutContent.prize.imageLabel)}</div>
<div style={{ width: 28, height: 2, background: GOLD }} />
</div>
)}
@@ -148,15 +240,13 @@ const About: React.FC = () => {
</div>
</section>
{/* ── 2b. BEDEUTUNG FÜR DEN MITTELSTAND (navy, image left / text right) ── */}
<section id="mittelstand" style={{ overflow: 'hidden' }}>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '45% 55%', minHeight: isMobile ? 'auto' : 580 }}>
{/* Left full-bleed image (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
<Image unoptimized
src="/images/networking-innenhof.jpg"
alt="Bayerischer Mittelstand"
<Image
unoptimized
src={mediaUrl(mittelstand.image, `/images/${aboutContent.mittelstand.imageFilename}`)}
alt={mediaAlt(mittelstand.image, fallbackText(mittelstand.imageAlt, aboutContent.mittelstand.imageAlt))}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
/>
<div style={{ position: 'absolute', inset: 0, background: isMobile
@@ -167,141 +257,100 @@ const About: React.FC = () => {
<div style={{ width: 2, height: 40, background: GOLD }} />
</div>
)}
{/* Heading overlay mobile only */}
{isMobile && (
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>Relevanz &amp; Zielsetzung</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(mittelstand.eyebrow, aboutContent.mittelstand.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(1.9rem, 8vw, 2.6rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.04, margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.6)' }}>
BEDEUTUNG FÜR<br />DEN MITTELSTAND
<Lines text={fallbackText(mittelstand.heading, aboutContent.mittelstand.heading)} />
</h2>
</div>
)}
</div>
{/* Right text on navy */}
<div style={{ background: NAVY, padding: isMobile ? '32px 24px 40px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
{!isMobile && (
<>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>Relevanz &amp; Zielsetzung</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#EFBF04', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(mittelstand.eyebrow, aboutContent.mittelstand.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.2vw, 2.9rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.04, margin: '0 0 20px' }}>
BEDEUTUNG FÜR<br />DEN MITTELSTAND
<Lines text={fallbackText(mittelstand.heading, aboutContent.mittelstand.heading)} />
</h2>
</>
)}
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.5)', lineHeight: 1.8, maxWidth: 440, marginBottom: 16 }}>
Der Bayerische Mittelstandspreis ist weit mehr als eine Trophäe. Seit seiner Gründung im Jahr 2005 verfolgt er das Ziel, die Vielfalt und Leistungsstärke bayerischer Familienunternehmen und KMU sichtbar zu machen.
</p>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.5)', lineHeight: 1.8, maxWidth: 440, marginBottom: 40 }}>
In einer globalisierten Welt sind es diese Unternehmen, die für Stabilität, Innovation und soziale Verantwortung stehen. Der BMP macht sie sichtbar.
</p>
{paragraphs(mittelstand.body, aboutContent.mittelstand.body).map((text, index, arr) => (
<p key={index} style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.5)', lineHeight: 1.8, maxWidth: 440, marginBottom: index === arr.length - 1 ? 40 : 16 }}>
{text}
</p>
))}
</div>
</div>
</section>
{/* ── 3. PREISTRÄGER-HIGHLIGHTS ─────────────────────────────────────────── */}
<section style={{ background: '#111D55' }}>
{/* Section header */}
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', gap: isMobile ? 16 : 0, justifyContent: 'space-between', maxWidth: 1600, margin: '0 auto' }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>Preisträger-Highlights</span>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>{fallbackText(highlights.eyebrow, aboutContent.highlights.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: 0, lineHeight: 1 }}>
AUSGEZEICHNETE 2025
<Lines text={fallbackText(highlights.heading, aboutContent.highlights.heading)} />
</h2>
</div>
<Link
to="/preistraeger"
to={fallbackText(highlights.cta?.url, aboutContent.highlights.cta.url)}
style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: GOLD, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: `1px solid ${GOLD}`, whiteSpace: 'nowrap' }}
>
Alle Preisträger <ChevronRight size={14} />
{fallbackText(highlights.cta?.label, aboutContent.highlights.cta.label)} <ChevronRight size={14} />
</Link>
</div>
{/* 4-col photo grid */}
<div style={{
display: 'grid',
gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)',
gridAutoFlow: isMobile ? 'column' : undefined,
gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined,
gridAutoColumns: isMobile ? '66vw' : undefined,
overflowX: isMobile ? 'auto' : undefined,
scrollSnapType: isMobile ? 'x mandatory' : undefined,
gap: isMobile ? 8 : 0,
padding: isMobile ? '0 16px 8px' : 0,
WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'none',
}}>
{HIGHLIGHTS.map(w => <HighlightCard key={w.id} winner={w} />)}
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)', gridAutoFlow: isMobile ? 'column' : undefined, gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined, gridAutoColumns: isMobile ? '66vw' : undefined, overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 8 : 0, padding: isMobile ? '0 16px 8px' : 0, WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
{highlightWinners.map((w) => <HighlightCard key={w.id} winner={w} hoverLabel={fallbackText(highlights.hoverLabel, aboutContent.highlights.hoverLabel)} />)}
</div>
</section>
<TestimonialsSection data={testimonials} />
{/* ── 4. TESTIMONIALS ──────────────────────────────────────────────────── */}
<TestimonialsSection />
{/* ── 5. GESCHICHTE & ZIELSETZUNG ──────────────────────────────────────── */}
<section id="geschichte" style={{ overflow: 'hidden' }}>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', minHeight: isMobile ? 'auto' : 700 }}>
{/* LEFT navy, timeline */}
<div style={{ background: NAVY, padding: isMobile ? '56px 24px' : '88px 80px', position: 'relative', overflow: 'hidden' }}>
{/* Ghost "20" watermark desktop only */}
{!isMobile && <div style={{ position: 'absolute', right: -24, bottom: -40, fontFamily: FF, fontSize: 'clamp(160px, 20vw, 240px)', fontWeight: 900, color: 'rgba(239,191,4,0.04)', lineHeight: 1, userSelect: 'none', pointerEvents: 'none', letterSpacing: '-0.04em' }}>20</div>}
{!isMobile && <div style={{ position: 'absolute', right: -24, bottom: -40, fontFamily: FF, fontSize: 'clamp(160px, 20vw, 240px)', fontWeight: 900, color: 'rgba(239,191,4,0.04)', lineHeight: 1, userSelect: 'none', pointerEvents: 'none', letterSpacing: '-0.04em' }}>{fallbackText(history.watermark, aboutContent.history.watermark)}</div>}
<span style={{ fontFamily: FF, fontSize: 10, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>Geschichte &amp; Meilensteine</span>
<span style={{ fontFamily: FF, fontSize: 10, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(history.eyebrow, aboutContent.history.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3vw, 2.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 1.0, margin: '0 0 52px' }}>
20 JAHRE<br /><span style={{ color: GOLD }}>MITTELSTANDS</span><wbr />EXZELLENZ
{fallbackText(history.heading, aboutContent.history.heading).split('\n').map((line, index) => (
<React.Fragment key={`${line}-${index}`}>
{index > 0 && <br />}
<HighlightedText text={line} highlight={fallbackText(history.highlight, aboutContent.history.highlight)} />
</React.Fragment>
))}
</h2>
{/* Timeline */}
<div style={{ position: 'relative', paddingLeft: 28 }}>
{/* Vertical gold line */}
<div style={{ position: 'absolute', left: 0, top: 6, bottom: 6, width: 2, background: `linear-gradient(to bottom, ${GOLD}, rgba(239,191,4,0.15))` }} />
{[
{ year: '2005', title: 'Gründung', desc: 'Initiative bayerischer Wirtschaftsvertreter zur Stärkung des Mittelstands.' },
{ year: '2010', title: 'Erste mediale Partnerschaft', desc: 'Kooperation mit bayernweiten TV- und Printmedien.' },
{ year: '2015', title: '100 Preisträger', desc: 'Jubiläum: 100 ausgezeichnete Unternehmen seit der ersten Verleihung.' },
{ year: '2020', title: 'Digital-Transformation', desc: 'Erweiterung um digitale Bewerbungsprozesse und Online-Formate.' },
{ year: '2025', title: 'Jubiläumsjahr', desc: '20 Jahre BMP: stärker, sichtbarer und vernetzter denn je.' },
{ year: '2026', title: 'Bavarian Future Award', desc: 'Ergänzung des Portfolios um den Bavarian Future Award mit Fokus auf zukunftsweisende Unternehmen.' },
].map((item, i, arr) => (
<div key={i} style={{ position: 'relative', marginBottom: i < arr.length - 1 ? 32 : 0 }}>
{/* Gold dot */}
{fallbackArray(history.items, aboutContent.history.items).map((item, i, arr) => (
<div key={`${item.year}-${i}`} style={{ position: 'relative', marginBottom: i < arr.length - 1 ? 32 : 0 }}>
<div style={{ position: 'absolute', left: -34, top: 7, width: 10, height: 10, background: GOLD, outline: `3px solid ${NAVY}`, outlineOffset: 1, transform: 'rotate(45deg)' }} />
<div style={{ fontFamily: FF, fontSize: 'clamp(1.05rem, 1.6vw, 1.35rem)', fontWeight: 900, color: GOLD, letterSpacing: '-0.02em', lineHeight: 1, marginBottom: 4 }}>{item.year}</div>
<div style={{ fontFamily: FF, fontSize: 11, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 5 }}>{item.title}</div>
<div style={{ fontFamily: FB, fontSize: 16, color: 'rgba(255,255,255,0.38)', lineHeight: 1.65 }}>{item.desc}</div>
<div style={{ fontFamily: FF, fontSize: 'clamp(1.05rem, 1.6vw, 1.35rem)', fontWeight: 900, color: GOLD, letterSpacing: '-0.02em', lineHeight: 1, marginBottom: 4 }}>{fallbackText(item.year, '')}</div>
<div style={{ fontFamily: FF, fontSize: 11, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 5 }}>{fallbackText(item.title, '')}</div>
<div style={{ fontFamily: FB, fontSize: 16, color: 'rgba(255,255,255,0.38)', lineHeight: 1.65 }}>{fallbackText(item.desc, '')}</div>
</div>
))}
</div>
</div>
{/* RIGHT cream, Zielsetzung */}
<div style={{ background: '#EFE5E3', padding: isMobile ? '56px 24px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>Zielsetzung</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(goals.eyebrow, aboutContent.goals.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3vw, 2.8rem)', fontWeight: 900, color: NAVY, textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 1.0, margin: '0 0 40px' }}>
WOFÜR WIR<br />STEHEN
<Lines text={fallbackText(goals.heading, aboutContent.goals.heading)} />
</h2>
<div>
{[
{ n: '01', title: 'Sichtbarkeit', desc: 'Die Vielfalt und Leistungsstärke des bayerischen Mittelstands in der Öffentlichkeit sichtbar machen.' },
{ n: '02', title: 'Netzwerk', desc: 'Branchenübergreifende Verbindungen zwischen herausragenden Unternehmen und Entscheidern schaffen.' },
{ n: '03', title: 'Werte', desc: 'Wertebasierte Führung, nachhaltige Unternehmenskultur und gesellschaftliche Verantwortung fördern.' },
{ n: '04', title: 'Arbeitgebermarke', desc: 'Den Mittelstand als attraktiven Arbeitgeber stärken und Talente für Bayern gewinnen.' },
{ n: '05', title: 'Resilienz', desc: 'Die Widerstandsfähigkeit bayerischer Unternehmen würdigen, die auch in unsicheren Zeiten Stabilität und Verantwortung beweisen.' },
].map((item, i, arr) => (
<div
key={i}
style={{ display: 'flex', gap: 24, alignItems: 'flex-start', padding: '24px 0', borderTop: '1px solid rgba(17,29,85,0.1)', borderBottom: i === arr.length - 1 ? '1px solid rgba(17,29,85,0.1)' : 'none', cursor: 'default' }}
>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.12em', paddingTop: 3, flexShrink: 0, minWidth: 24 }}>{item.n}</span>
{fallbackArray(goals.items, aboutContent.goals.items).map((item, i, arr) => (
<div key={`${item.num}-${i}`} style={{ display: 'flex', gap: 24, alignItems: 'flex-start', padding: '24px 0', borderTop: '1px solid rgba(17,29,85,0.1)', borderBottom: i === arr.length - 1 ? '1px solid rgba(17,29,85,0.1)' : 'none', cursor: 'default' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.12em', paddingTop: 3, flexShrink: 0, minWidth: 24 }}>{fallbackText(item.num, '')}</span>
<div>
<div style={{ fontFamily: FF, fontSize: 17, fontWeight: 800, color: NAVY, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 7 }}>{item.title}</div>
<div style={{ fontFamily: FB, fontSize: 16, color: 'rgba(17,29,85,0.5)', lineHeight: 1.75 }}>{item.desc}</div>
<div style={{ fontFamily: FF, fontSize: 17, fontWeight: 800, color: NAVY, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 7 }}>{fallbackText(item.title, '')}</div>
<div style={{ fontFamily: FB, fontSize: 16, color: 'rgba(17,29,85,0.5)', lineHeight: 1.75 }}>{fallbackText(item.desc, '')}</div>
</div>
</div>
))}
@@ -310,111 +359,69 @@ const About: React.FC = () => {
</div>
</section>
{/* ── 5. VORTEILE & WERTE ──────────────────────────────────────────────── */}
<section id="vorteile" style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
{/* Header */}
<div style={{ padding: isMobile ? '48px 24px 32px' : '88px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 16 : 40, alignItems: 'flex-end', borderBottom: '1px solid #D0D5DD' }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>Vorteile &amp; Werte</span>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>{fallbackText(values.eyebrow, aboutContent.values.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.2vw, 2.9rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.04, margin: 0 }}>
WARUM DER<br />BMP ZÄHLT
<Lines text={fallbackText(values.heading, aboutContent.values.heading)} />
</h2>
</div>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.8, margin: 0, maxWidth: 440 }}>
Drei Dimensionen, die den Unterschied machen: für Ihr Unternehmen, Ihre Mitarbeitenden und Ihren Marktauftritt.
{fallbackText(values.description, aboutContent.values.description)}
</p>
</div>
{/* Editorial rows no cards, pure typography */}
{[
{
num: '01',
title: 'Exzellenz-Siegel',
},
{
num: '02',
title: 'Partner-Netzwerk',
},
{
num: '03',
title: 'Reichweite',
},
].map((item, i, arr) => (
<ValueRow key={i} item={item} last={i === arr.length - 1} />
{fallbackArray(values.items, aboutContent.values.items).map((item, i, arr) => (
<ValueRow key={`${item.num}-${i}`} item={item} last={i === arr.length - 1} />
))}
</section>
{/* ── 5. CTA ───────────────────────────────────────────────────────────── */}
<section style={{ background: NAVY, padding: isMobile ? '48px 24px' : '100px 80px', position: 'relative', overflow: 'hidden' }}>
{/* Gold decorative lines */}
<div style={{ position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '60%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.3 }} />
<div style={{ position: 'absolute', bottom: 0, left: '50%', transform: 'translateX(-50%)', width: '40%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.15 }} />
<div style={{ textAlign: 'center', position: 'relative', zIndex: 1, maxWidth: 700, margin: '0 auto' }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 10, marginBottom: 28 }}>
<div style={{ width: 28, height: 1, background: GOLD, opacity: 0.6 }} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04' }}>Starten Sie Ihre Reise</span>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04' }}>{fallbackText(cta.eyebrow, aboutContent.cta.eyebrow)}</span>
<div style={{ width: 28, height: 1, background: GOLD, opacity: 0.6 }} />
</div>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2.2rem, 5vw, 3.5rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1, margin: '0 0 24px' }}>
SCHREIBEN AUCH SIE GESCHICHTE
<Lines text={fallbackText(cta.heading, aboutContent.cta.heading)} />
</h2>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.55)', lineHeight: 1.8, marginBottom: 48, fontWeight: 300 }}>
Werden Sie Teil der Wall of Fame des bayerischen Mittelstands. Die Teilnahmegebühr beträgt 0 EUR und die Teilnahme lohnt sich in jeder Phase.
{fallbackText(cta.description, aboutContent.cta.description)}
</p>
<Link
to="/teilnahme"
to={fallbackText(cta.primaryCta?.url, aboutContent.cta.primaryCta.url)}
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: '#101828', background: GOLD, padding: '16px 40px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, transition: 'background 0.15s, box-shadow 0.2s' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
onMouseEnter={(e) => { e.currentTarget.style.background = '#FFD130'; e.currentTarget.style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)' }}
onMouseLeave={(e) => { e.currentTarget.style.background = GOLD; e.currentTarget.style.boxShadow = 'none' }}
>
Jetzt kostenlos bewerben <ChevronRight size={16} />
{fallbackText(cta.primaryCta?.label, aboutContent.cta.primaryCta.label)} <ChevronRight size={16} />
</Link>
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'center' }}>
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 11, color: 'rgba(255,255,255,0.3)', marginRight: 8 }}>Oder:</span>
<span style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.3)', marginRight: 8 }}>{fallbackText(cta.secondaryPrefix, aboutContent.cta.secondaryPrefix)}</span>
<Link
to="/mitglied-werden"
style={{
fontFamily: '"IBM Plex Sans", sans-serif',
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: 'rgba(239,191,4,0.7)',
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
borderBottom: '1px solid rgba(239,191,4,0.3)',
paddingBottom: 1,
transition: 'color 0.15s, borderColor 0.15s',
}}
onMouseEnter={e => {
(e.currentTarget as HTMLElement).style.color = '#EFBF04';
(e.currentTarget as HTMLElement).style.borderBottomColor = '#EFBF04';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.color = 'rgba(239,191,4,0.7)';
(e.currentTarget as HTMLElement).style.borderBottomColor = 'rgba(239,191,4,0.3)';
}}
to={fallbackText(cta.secondaryCta?.url, aboutContent.cta.secondaryCta.url)}
style={{ fontFamily: FF, fontSize: 11, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(239,191,4,0.7)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6, borderBottom: '1px solid rgba(239,191,4,0.3)', paddingBottom: 1, transition: 'color 0.15s, borderColor 0.15s' }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#EFBF04'; e.currentTarget.style.borderBottomColor = '#EFBF04' }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'rgba(239,191,4,0.7)'; e.currentTarget.style.borderBottomColor = 'rgba(239,191,4,0.3)' }}
>
Vereinsmitglied werden <ChevronRight size={10} />
{fallbackText(cta.secondaryCta?.label, aboutContent.cta.secondaryCta.label)} <ChevronRight size={10} />
</Link>
</div>
</div>
</section>
</div>
);
};
)
}
// ── HighlightCard ─────────────────────────────────────────────────────────────
function HighlightCard({ winner }: { winner: import('@/spa/data/winners').Winner }) {
const [hovered, setHovered] = useState(false);
const isMobile = useIsMobile();
function HighlightCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string }) {
const [hovered, setHovered] = useState(false)
const isMobile = useIsMobile()
return (
<Link
to={`/preistraeger/${winner.slug}`}
@@ -422,11 +429,7 @@ function HighlightCard({ winner }: { winner: import('@/spa/data/winners').Winner
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Image unoptimized
src={winner.img}
alt={winner.name}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }}
/>
<Image unoptimized src={winner.img} alt={winner.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
@@ -437,49 +440,33 @@ function HighlightCard({ winner }: { winner: import('@/spa/data/winners').Winner
{winner.year} · {winner.type}
</span>
</div>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', margin: '0 0 3px', lineHeight: 1.3, textShadow: '0 1px 4px rgba(0,0,0,0.5)' }}>
{winner.name}
</h3>
<p style={{ fontFamily: FF, fontSize: 14, color: 'rgba(255,255,255,0.75)', margin: 0, letterSpacing: '0.03em' }}>
{winner.category}
</p>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', margin: '0 0 3px', lineHeight: 1.3, textShadow: '0 1px 4px rgba(0,0,0,0.5)' }}>{winner.name}</h3>
<p style={{ fontFamily: FF, fontSize: 14, color: 'rgba(255,255,255,0.75)', margin: 0, letterSpacing: '0.03em' }}>{winner.category}</p>
</div>
{/* hover pill */}
<div style={{ position: 'absolute', bottom: 16, right: 16, background: GOLD, color: '#101828', padding: '6px 14px', borderRadius: 999, fontSize: 10, fontFamily: FF, fontWeight: 800, letterSpacing: '0.1em', textTransform: 'uppercase', opacity: hovered ? 1 : 0, transform: hovered ? 'translateY(0)' : 'translateY(6px)', transition: 'all 0.25s' }}>
Detail
{hoverLabel}
</div>
</Link>
);
)
}
// ── ValueRow ──────────────────────────────────────────────────────────────────
function ValueRow({ item, last }: { item: { num: string; title: string }; last: boolean }) {
const [hovered, setHovered] = useState(false);
const isMobileRow = useIsMobile();
function ValueRow({ item, last }: { item: { num?: string | null; title?: string | null }; last: boolean }) {
const [hovered, setHovered] = useState(false)
const isMobileRow = useIsMobile()
return (
<div
style={{
display: 'grid', gridTemplateColumns: isMobileRow ? '1fr' : '120px 1fr', gap: isMobileRow ? '16px 0' : '0 48px',
padding: isMobileRow ? '32px 24px' : '48px 80px',
borderBottom: last ? 'none' : '1px solid #D0D5DD',
background: hovered ? '#EAE7E6' : 'transparent',
transition: 'background 0.2s', cursor: 'default',
alignItems: 'center',
}}
style={{ display: 'grid', gridTemplateColumns: isMobileRow ? '1fr' : '120px 1fr', gap: isMobileRow ? '16px 0' : '0 48px', padding: isMobileRow ? '32px 24px' : '48px 80px', borderBottom: last ? 'none' : '1px solid #D0D5DD', background: hovered ? '#EAE7E6' : 'transparent', transition: 'background 0.2s', cursor: 'default', alignItems: 'center' }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* Left: number */}
<div>
<div style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3vw, 2.8rem)', fontWeight: 900, color: hovered ? GOLD : 'rgba(16,24,40,0.1)', lineHeight: 1, letterSpacing: '-0.04em', transition: 'color 0.2s' }}>{item.num}</div>
<div style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3vw, 2.8rem)', fontWeight: 900, color: hovered ? GOLD : 'rgba(16,24,40,0.1)', lineHeight: 1, letterSpacing: '-0.04em', transition: 'color 0.2s' }}>{fallbackText(item.num, '')}</div>
</div>
{/* Right: title (short label only) */}
<div>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.8rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: 0 }}>{item.title}</h3>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.3rem, 2vw, 1.8rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, margin: 0 }}>{fallbackText(item.title, '')}</h3>
</div>
</div>
);
)
}
export default About;
export default About