feat: manage about page via payload
This commit is contained in:
@@ -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",
|
||||
|
||||
254
src/collections/Pages/aboutFields.ts
Normal file
254
src/collections/Pages/aboutFields.ts
Normal 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),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
| {
|
||||
|
||||
89
src/scripts/preload-about-page-cms.ts
Normal file
89
src/scripts/preload-about-page-cms.ts
Normal 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
142
src/spa/aboutContent.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
@@ -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 & 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 & 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 & 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 & 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
|
||||
|
||||
Reference in New Issue
Block a user