feat: manage press index via payload

This commit is contained in:
syntaxbullet
2026-06-22 11:10:59 +02:00
parent aecdb65a14
commit 27b29a8a7a
7 changed files with 600 additions and 130 deletions

View File

@@ -24,6 +24,7 @@
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
"preload:preistraeger-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-preistraeger-index-page-cms.ts",
"preload:press-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-press-index-page-cms.ts",
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"test": "pnpm run test:int && pnpm run test:e2e",

View File

@@ -15,6 +15,7 @@ import { homeFields } from './homeFields'
import { impressumFields } from './impressumFields'
import { participationFields } from './participationFields'
import { preistraegerIndexFields } from './preistraegerIndexFields'
import { pressIndexFields } from './pressIndexFields'
import { slugField } from 'payload'
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
@@ -84,6 +85,14 @@ const isPreistraegerIndexPage = (_: unknown, siblingData?: { slug?: string; spaP
return spaPath === '/preistraeger' || slug === 'preistraeger' || title === 'preisträger' || title === 'preistraeger'
}
const isPressIndexPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
const slug = siblingData?.slug
const spaPath = siblingData?.spaPath
const title = siblingData?.title?.toLowerCase()
return spaPath === '/presse' || slug === 'presse' || slug === 'press' || title === 'presse' || title === 'press'
}
const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
isHomePage(undefined, siblingData) ||
isContactPage(undefined, siblingData) ||
@@ -91,7 +100,8 @@ const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: s
isAboutPage(undefined, siblingData) ||
isImpressumPage(undefined, siblingData) ||
isDatenschutzPage(undefined, siblingData) ||
isPreistraegerIndexPage(undefined, siblingData)
isPreistraegerIndexPage(undefined, siblingData) ||
isPressIndexPage(undefined, siblingData)
export const Pages: CollectionConfig<'pages'> = {
slug: 'pages',
@@ -208,6 +218,13 @@ export const Pages: CollectionConfig<'pages'> = {
fields: preistraegerIndexFields,
label: 'Preisträger Index Page',
},
{
admin: {
condition: (data) => isPressIndexPage(undefined, data),
},
fields: pressIndexFields,
label: 'Press Index Page',
},
{
name: 'meta',
label: 'SEO',

View File

@@ -0,0 +1,152 @@
import type { Field } from 'payload'
import { pressIndexContent } from '@/spa/pressIndexContent'
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 sectionAdmin = (description: string) => ({
description,
initCollapsed: true,
})
const eventFields: Field[] = [
text('title', 'Title'),
text('date', 'Date'),
text('location', 'Location'),
text('cat', 'Category'),
text('status', 'Status'),
text('img', 'Image URL or path'),
textarea('desc', 'Description'),
text('slug', 'Route'),
]
const newsFields: Field[] = [
text('title', 'Title'),
textarea('excerpt', 'Excerpt'),
text('cat', 'Category'),
text('img', 'Image URL or path'),
text('slug', 'Route'),
]
export const pressIndexFields: Field[] = [
{
name: 'pressIndex',
label: 'Press index page content',
type: 'group',
admin: {
description:
'Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections; fallback cards remain editable here.',
},
fields: [
{
name: 'hero',
label: '01 · Hero',
type: 'group',
admin: sectionAdmin('Hero image, eyebrow, heading, and intro copy.'),
fields: [
uploadField('image', 'Hero image', `Current frontend image: /images/${pressIndexContent.hero.imageFilename}`),
text('imageAlt', 'Image alt text', pressIndexContent.hero.imageAlt),
text('eyebrow', 'Eyebrow', pressIndexContent.hero.eyebrow),
textarea('heading', 'Heading', pressIndexContent.hero.heading),
textarea('description', 'Description', pressIndexContent.hero.description),
],
},
{
name: 'events',
label: '02 · Events section',
type: 'group',
admin: sectionAdmin('Section header, labels, and fallback event rows.'),
fields: [
text('eyebrow', 'Eyebrow', pressIndexContent.events.eyebrow),
textarea('heading', 'Heading', pressIndexContent.events.heading),
textarea('description', 'Description', pressIndexContent.events.description),
text('detailLabel', 'Detail link label', pressIndexContent.events.detailLabel),
text('openStatusLabel', 'Open status label', pressIndexContent.events.openStatusLabel),
{
name: 'fallbackItems',
label: 'Fallback event rows',
type: 'array',
dbName: 'press_events_fb',
defaultValue: pressIndexContent.events.fallbackItems,
fields: eventFields,
},
],
},
{
name: 'news',
label: '03 · Newsroom section',
type: 'group',
admin: sectionAdmin('Section header, read-more label, and fallback news cards.'),
fields: [
text('eyebrow', 'Eyebrow', pressIndexContent.news.eyebrow),
textarea('heading', 'Heading', pressIndexContent.news.heading),
textarea('description', 'Description', pressIndexContent.news.description),
text('readMoreLabel', 'Read more label', pressIndexContent.news.readMoreLabel),
{
name: 'fallbackItems',
label: 'Fallback news cards',
type: 'array',
dbName: 'press_news_fb',
defaultValue: pressIndexContent.news.fallbackItems,
fields: newsFields,
},
],
},
{
name: 'downloads',
label: '04 · Press material and contact',
type: 'group',
admin: sectionAdmin('Download button and press contact block.'),
fields: [
text('eyebrow', 'Eyebrow', pressIndexContent.downloads.eyebrow),
textarea('heading', 'Heading', pressIndexContent.downloads.heading),
textarea('description', 'Description', pressIndexContent.downloads.description),
text('kitLabel', 'Kit label', pressIndexContent.downloads.kitLabel),
text('kitMeta', 'Kit metadata', pressIndexContent.downloads.kitMeta),
text('kitUrl', 'Kit URL', pressIndexContent.downloads.kitUrl),
text('kitDownloadName', 'Kit download filename', pressIndexContent.downloads.kitDownloadName),
text('contactEyebrow', 'Contact eyebrow', pressIndexContent.downloads.contactEyebrow),
text('contactName', 'Contact name', pressIndexContent.downloads.contactName),
textarea('contactLines', 'Contact lines', pressIndexContent.downloads.contactLines),
],
},
{
name: 'accreditation',
label: '05 · Accreditation form copy',
type: 'group',
admin: sectionAdmin('Labels and success state for the press accreditation form.'),
fields: [
text('eyebrow', 'Eyebrow', pressIndexContent.accreditation.eyebrow),
textarea('heading', 'Heading', pressIndexContent.accreditation.heading),
textarea('description', 'Description', pressIndexContent.accreditation.description),
text('mediumLabel', 'Medium label', pressIndexContent.accreditation.mediumLabel),
text('nameLabel', 'Name label', pressIndexContent.accreditation.nameLabel),
text('emailLabel', 'Email label', pressIndexContent.accreditation.emailLabel),
text('submitLabel', 'Submit label', pressIndexContent.accreditation.submitLabel),
text('successHeading', 'Success heading', pressIndexContent.accreditation.successHeading),
textarea('successMessage', 'Success message', pressIndexContent.accreditation.successMessage),
],
},
],
},
]

View File

@@ -1242,6 +1242,95 @@ export interface Page {
};
};
};
/**
* Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections; fallback cards remain editable here.
*/
pressIndex?: {
/**
* Hero image, eyebrow, heading, and intro copy.
*/
hero?: {
/**
* Current frontend image: /images/gala-dinner.jpg
*/
image?: (number | null) | Media;
imageAlt?: string | null;
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
};
/**
* Section header, labels, and fallback event rows.
*/
events?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
detailLabel?: string | null;
openStatusLabel?: string | null;
fallbackItems?:
| {
title?: string | null;
date?: string | null;
location?: string | null;
cat?: string | null;
status?: string | null;
img?: string | null;
desc?: string | null;
slug?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Section header, read-more label, and fallback news cards.
*/
news?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
readMoreLabel?: string | null;
fallbackItems?:
| {
title?: string | null;
excerpt?: string | null;
cat?: string | null;
img?: string | null;
slug?: string | null;
id?: string | null;
}[]
| null;
};
/**
* Download button and press contact block.
*/
downloads?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
kitLabel?: string | null;
kitMeta?: string | null;
kitUrl?: string | null;
kitDownloadName?: string | null;
contactEyebrow?: string | null;
contactName?: string | null;
contactLines?: string | null;
};
/**
* Labels and success state for the press accreditation form.
*/
accreditation?: {
eyebrow?: string | null;
heading?: string | null;
description?: string | null;
mediumLabel?: string | null;
nameLabel?: string | null;
emailLabel?: string | null;
submitLabel?: string | null;
successHeading?: string | null;
successMessage?: string | null;
};
};
meta?: {
title?: string | null;
/**
@@ -3122,6 +3211,86 @@ export interface PagesSelect<T extends boolean = true> {
};
};
};
pressIndex?:
| T
| {
hero?:
| T
| {
image?: T;
imageAlt?: T;
eyebrow?: T;
heading?: T;
description?: T;
};
events?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
detailLabel?: T;
openStatusLabel?: T;
fallbackItems?:
| T
| {
title?: T;
date?: T;
location?: T;
cat?: T;
status?: T;
img?: T;
desc?: T;
slug?: T;
id?: T;
};
};
news?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
readMoreLabel?: T;
fallbackItems?:
| T
| {
title?: T;
excerpt?: T;
cat?: T;
img?: T;
slug?: T;
id?: T;
};
};
downloads?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
kitLabel?: T;
kitMeta?: T;
kitUrl?: T;
kitDownloadName?: T;
contactEyebrow?: T;
contactName?: T;
contactLines?: T;
};
accreditation?:
| T
| {
eyebrow?: T;
heading?: T;
description?: T;
mediumLabel?: T;
nameLabel?: T;
emailLabel?: T;
submitLabel?: T;
successHeading?: T;
successMessage?: T;
};
};
meta?:
| T
| {

View File

@@ -0,0 +1,60 @@
import 'dotenv/config'
import config from '@payload-config'
import { getPayload, type Payload } from 'payload'
import { pressIndexContent } from '@/spa/pressIndexContent'
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: '/presse' } }, { slug: { equals: 'presse' } }, { slug: { equals: 'press' } }],
},
})
const page = pageResult.docs[0]
if (!page) throw new Error('Press index page not found. Expected spaPath=/presse or slug=presse/press.')
const heroImage = await mediaByFilename(payload, pressIndexContent.hero.imageFilename)
const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero
await payload.update({
collection: 'pages',
id: page.id,
overrideAccess: true,
context: { disableRevalidate: true },
data: {
pressIndex: {
...pressIndexContent,
hero: {
...heroContent,
image: heroImage,
},
},
} as never,
})
payload.logger.info(`Preloaded Press index CMS fields for page ${page.id}`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})

View File

@@ -3,9 +3,10 @@ import { Link } from '@/spa/router';
import { Calendar, MapPin, Download, ArrowRight, FileText } from 'lucide-react';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { useCmsCollection } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField';
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import Image from '@/spa/components/ui/UnoptimizedImage'
import { pressIndexContent } from '@/spa/pressIndexContent';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
@@ -13,67 +14,52 @@ const CREAM = '#E4E2E3';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
const events = [
{
title: 'Preisverleihung 2026',
date: '22. Okt 2026',
location: 'München, Residenz',
cat: 'Gala / Event',
status: 'In Planung',
img: '/images/buehne-moderatoren.jpg',
desc: 'Der glanzvolle Höhepunkt des Jahres. Die Verleihung der Preise in der prachtvollen Kulisse der Residenz München.',
slug: '/presse/events/preisverleihung-2026',
},
{
title: 'Mittelstands-Gipfel',
date: '15. Juni 2026',
location: 'Nürnberg',
cat: 'Workshop',
status: 'Anmeldung offen',
img: '/images/networking-innenhof.jpg',
desc: 'Regionaler Austausch und Best-Practices für bayerische Unternehmen auf dem Weg zum Preis.',
slug: '/presse/events/mittelstands-gipfel-2026',
},
{
title: 'Nominierten-Auswahl',
date: '04. Aug 2026',
location: 'Regensburg',
cat: 'Jury-Sitzung',
status: 'Intern',
img: '/images/saal-gedeckt.jpg',
desc: 'Das Gremium sichtet die Ergebnisse der Audits und legt die Nominierten für die Hauptpreise fest.',
slug: '/presse/events/nominierten-auswahl-2026',
},
];
type PressIndexCms = Partial<typeof pressIndexContent> & {
hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown }
}
const news = [
{
title: 'Wie Bayerns KMU die KI nutzen',
excerpt:
'Ein Deep-Dive in die Bewerbungsunterlagen 2024 zeigt: Der Mittelstand treibt die Digitalisierung aktiv voran.',
cat: 'Innovation',
img: '/images/gala-saal-overview.jpg',
slug: '/presse/blog/ki-nutzung-bayerischer-kmu',
},
{
title: 'Start der Ehrenamts-Initiative',
excerpt:
'Gemeinsam mit unseren Partnern fördern wir soziale Projekte mittelständischer Unternehmen.',
cat: 'Engagement',
img: '/images/networking-innenhof.jpg',
slug: '/presse/blog/ehrenamts-initiative-2026',
},
{
title: 'Bayerische Wirtschaft wächst',
excerpt:
'Neue Prognosen zeigen ein stabiles Wachstum für den Mittelstand gute Aussichten für die Awards.',
cat: 'Wirtschaft',
img: '/images/gala-dinner.jpg',
slug: '/presse/blog/bayerische-wirtschaft-waechst',
},
];
type PressEventItem = (typeof pressIndexContent.events.fallbackItems)[number]
type PressNewsItem = (typeof pressIndexContent.news.fallbackItems)[number]
function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number; isMobile: boolean }) {
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;
const statusLabels: Record<string, string> = {
geplant: 'In Planung',
'anmeldung-offen': 'Anmeldung offen',
intern: 'Intern',
abgeschlossen: 'Abgeschlossen',
}
const normalizeStatus = (status: unknown) => {
const value = fallbackText(status, 'Geplant')
return statusLabels[value] || value
}
const eventFromCms = (event: CmsRouteDoc): PressEventItem => ({
title: String(event.title || 'Event'),
date: String(event.date || 'Termin folgt'),
location: String(event.location || event.venue || 'Bayern'),
cat: String(event.category || 'Event'),
status: normalizeStatus(event.status),
img: docImageUrl(event, '/images/buehne-moderatoren.jpg'),
desc: String(event.description || event.meta?.description || ''),
slug: String(event.spaPath || `/presse/events/${event.slug}`),
})
const postFromCms = (post: CmsRouteDoc): PressNewsItem => ({
title: String(post.title || 'Presse'),
excerpt: String(post.excerpt || post.meta?.description || ''),
cat: String(post.cat || 'Presse'),
img: docImageUrl(post, '/images/gala-saal-overview.jpg', 'heroImage'),
slug: String(post.spaPath || `/presse/blog/${post.slug}`),
})
function Lines({ text }: { text: string }) {
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>
}
function NewsCard({ item, idx, isMobile, readMoreLabel, total }: { item: PressNewsItem; idx: number; isMobile: boolean; readMoreLabel: string; total: number }) {
const [hovered, setHovered] = React.useState(false);
return (
<Link
@@ -85,7 +71,7 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
flexDirection: 'column',
cursor: 'pointer',
borderLeft: isMobile ? 'none' : (idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none'),
borderBottom: isMobile && idx < news.length - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
borderBottom: isMobile && idx < total - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
textDecoration: 'none',
}}
>
@@ -177,7 +163,7 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
gap: 5,
}}
>
Weiterlesen <ArrowRight size={12} />
{readMoreLabel} <ArrowRight size={12} />
</span>
</div>
</Link>
@@ -217,41 +203,23 @@ function DarkInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
const Press: React.FC = () => {
const isMobile = useIsMobile();
const cms = (useCmsRoute()?.doc?.pressIndex || {}) as PressIndexCms;
const hero = { ...pressIndexContent.hero, ...(cms.hero || {}) };
const eventsSection = { ...pressIndexContent.events, ...(cms.events || {}) };
const newsSection = { ...pressIndexContent.news, ...(cms.news || {}) };
const downloads = { ...pressIndexContent.downloads, ...(cms.downloads || {}) };
const accreditation = { ...pressIndexContent.accreditation, ...(cms.accreditation || {}) };
const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n');
const cmsEvents = useCmsCollection('events');
const cmsPosts = useCmsCollection('posts');
const combinedEvents = React.useMemo(() => {
const existing = new Set(events.map((event) => event.slug.split('/').pop()));
return [
...events,
...cmsEvents
.filter((event) => event.slug && !existing.has(String(event.slug)))
.map((event) => ({
title: String(event.title || 'Event'),
date: String(event.date || 'Termin folgt'),
location: String(event.location || event.venue || 'Bayern'),
cat: String(event.category || 'Event'),
status: String(event.status || 'Geplant'),
img: docImageUrl(event, '/images/buehne-moderatoren.jpg'),
desc: String(event.description || event.meta?.description || ''),
slug: String(event.spaPath || `/presse/events/${event.slug}`),
})),
];
}, [cmsEvents]);
if (cmsEvents.length) return cmsEvents.map(eventFromCms);
return fallbackArray<PressEventItem>(eventsSection.fallbackItems, pressIndexContent.events.fallbackItems);
}, [cmsEvents, eventsSection.fallbackItems]);
const combinedNews = React.useMemo(() => {
const existing = new Set(news.map((item) => item.slug.split('/').pop()));
return [
...news,
...cmsPosts
.filter((post) => post.slug && !existing.has(String(post.slug)))
.map((post) => ({
title: String(post.title || 'Presse'),
excerpt: String(post.excerpt || post.meta?.description || ''),
cat: String(post.cat || 'Presse'),
img: docImageUrl(post, '/images/gala-saal-overview.jpg', 'heroImage'),
slug: String(post.spaPath || `/presse/blog/${post.slug}`),
})),
];
}, [cmsPosts]);
if (cmsPosts.length) return cmsPosts.map(postFromCms);
return fallbackArray<PressNewsItem>(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
}, [cmsPosts, newsSection.fallbackItems]);
const [hoveredEvent, setHoveredEvent] = useState<number | null>(null);
const [dlHovered, setDlHovered] = useState(false);
const [submitHovered, setSubmitHovered] = useState(false);
@@ -283,8 +251,8 @@ const Press: React.FC = () => {
}}
>
<Image unoptimized
src="/images/gala-dinner.jpg"
alt="Presse BMP"
src={mediaUrl(hero.image, `/images/${pressIndexContent.hero.imageFilename}`)}
alt={mediaAlt(hero.image, fallbackText(hero.imageAlt, pressIndexContent.hero.imageAlt))}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
/>
<div
@@ -319,7 +287,7 @@ const Press: React.FC = () => {
color: GOLD,
}}
>
Presse &amp; Newsroom
{fallbackText(hero.eyebrow, pressIndexContent.hero.eyebrow)}
</span>
</div>
<h1
@@ -335,7 +303,7 @@ const Press: React.FC = () => {
overflowWrap: 'anywhere',
}}
>
EVENTS &amp;<br />BERICHTERSTATTUNG.
<Lines text={fallbackText(hero.heading, pressIndexContent.hero.heading)} />
</h1>
<div style={{ width: 48, height: 2, background: GOLD, marginBottom: 24 }} />
<p
@@ -349,7 +317,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
Alle Termine, Pressemitteilungen und Medienmaterial rund um den Bayerischen Mittelstandspreis.
{fallbackText(hero.description, pressIndexContent.hero.description)}
</p>
</div>
</section>
@@ -380,7 +348,7 @@ const Press: React.FC = () => {
marginBottom: 16,
}}
>
Termine 2026
{fallbackText(eventsSection.eyebrow, pressIndexContent.events.eyebrow)}
</div>
<h2
style={{
@@ -393,7 +361,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
EVENTS RUND UM DEN PREIS.
<Lines text={fallbackText(eventsSection.heading, pressIndexContent.events.heading)} />
</h2>
</div>
<div>
@@ -406,7 +374,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
Von der Jurysitzung bis zur festlichen Gala alle Veranstaltungen auf einen Blick.
{fallbackText(eventsSection.description, pressIndexContent.events.description)}
</p>
</div>
</div>
@@ -429,7 +397,7 @@ const Press: React.FC = () => {
{/* Left image cell */}
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 200 : 240 }}>
<Image unoptimized src={event.img} alt={event.title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', top: 20, left: 20, padding: '5px 12px', fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase' as const, letterSpacing: '0.18em', background: event.status === 'Anmeldung offen' ? GOLD : 'rgba(3,9,58,0.85)', color: event.status === 'Anmeldung offen' ? NAVY : 'rgba(255,255,255,0.6)' }}>
<div style={{ position: 'absolute', top: 20, left: 20, padding: '5px 12px', fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase' as const, letterSpacing: '0.18em', background: event.status === fallbackText(eventsSection.openStatusLabel, pressIndexContent.events.openStatusLabel) ? GOLD : 'rgba(3,9,58,0.85)', color: event.status === fallbackText(eventsSection.openStatusLabel, pressIndexContent.events.openStatusLabel) ? NAVY : 'rgba(255,255,255,0.6)' }}>
{event.status}
</div>
</div>
@@ -449,7 +417,7 @@ const Press: React.FC = () => {
<div style={{ width: 24, height: 1, background: 'rgba(239,191,4,0.4)', marginBottom: 14 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, flex: 1 }}>{event.desc}</p>
<Link to={event.slug} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 16, fontFamily: FF, fontSize: 16, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', color: '#101828', textDecoration: 'none' }}>
Details <ArrowRight size={12} />
{fallbackText(eventsSection.detailLabel, pressIndexContent.events.detailLabel)} <ArrowRight size={12} />
</Link>
</div>
</div>
@@ -481,7 +449,7 @@ const Press: React.FC = () => {
marginBottom: 16,
}}
>
Newsroom
{fallbackText(newsSection.eyebrow, pressIndexContent.news.eyebrow)}
</div>
<h2
style={{
@@ -494,7 +462,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
NACHRICHTEN &amp; EINBLICKE.
<Lines text={fallbackText(newsSection.heading, pressIndexContent.news.heading)} />
</h2>
</div>
<div>
@@ -507,7 +475,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.
{fallbackText(newsSection.description, pressIndexContent.news.description)}
</p>
</div>
</div>
@@ -515,7 +483,7 @@ const Press: React.FC = () => {
{/* 3-column news grid */}
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)' }}>
{combinedNews.map((item, idx) => (
<NewsCard key={idx} item={item} idx={idx} isMobile={isMobile} />
<NewsCard key={idx} item={item} idx={idx} isMobile={isMobile} readMoreLabel={fallbackText(newsSection.readMoreLabel, pressIndexContent.news.readMoreLabel)} total={combinedNews.length} />
))}
</div>
</section>
@@ -547,7 +515,7 @@ const Press: React.FC = () => {
marginBottom: 16,
}}
>
Pressekontakt &amp; Material
{fallbackText(downloads.eyebrow, pressIndexContent.downloads.eyebrow)}
</div>
<h2
@@ -561,7 +529,7 @@ const Press: React.FC = () => {
margin: '0 0 20px',
}}
>
PRESSE-MATERIAL &amp; KONTAKT.
<Lines text={fallbackText(downloads.heading, pressIndexContent.downloads.heading)} />
</h2>
{/* Gold divider */}
@@ -577,13 +545,13 @@ const Press: React.FC = () => {
marginBottom: 48,
}}
>
Laden Sie unser offizielles Material herunter oder kontaktieren Sie direkt unser Presseteam für individuelle Anfragen.
{fallbackText(downloads.description, pressIndexContent.downloads.description)}
</p>
{/* Download button */}
<a
href="/Print_BMP.zip"
download="Print_BMP.zip"
href={fallbackText(downloads.kitUrl, pressIndexContent.downloads.kitUrl)}
download={fallbackText(downloads.kitDownloadName, pressIndexContent.downloads.kitDownloadName)}
onMouseEnter={() => setDlHovered(true)}
onMouseLeave={() => setDlHovered(false)}
style={{
@@ -622,10 +590,10 @@ const Press: React.FC = () => {
marginBottom: 3,
}}
>
Download Presse-Kit
{fallbackText(downloads.kitLabel, pressIndexContent.downloads.kitLabel)}
</div>
<div style={{ fontFamily: FB, fontSize: 17, color: 'rgba(16,24,40,0.4)' }}>
Print_BMP.zip 1,5 MB
{fallbackText(downloads.kitMeta, pressIndexContent.downloads.kitMeta)}
</div>
</div>
</a>
@@ -643,7 +611,7 @@ const Press: React.FC = () => {
marginBottom: 12,
}}
>
Pressekontakt
{fallbackText(downloads.contactEyebrow, pressIndexContent.downloads.contactEyebrow)}
</div>
<div
style={{
@@ -654,7 +622,7 @@ const Press: React.FC = () => {
marginBottom: 4,
}}
>
Tanja Meier
{fallbackText(downloads.contactName, pressIndexContent.downloads.contactName)}
</div>
<div
style={{
@@ -665,9 +633,12 @@ const Press: React.FC = () => {
overflowWrap: 'anywhere',
}}
>
presse@bmp-bayern.de
<br />
+49 89 123 456 99
{contactLines.map((line, index) => (
<React.Fragment key={`${line}-${index}`}>
{line}
{index < contactLines.length - 1 && <br />}
</React.Fragment>
))}
</div>
</div>
</div>
@@ -717,7 +688,7 @@ const Press: React.FC = () => {
margin: '0 0 16px',
}}
>
Anfrage eingegangen
{fallbackText(accreditation.successHeading, pressIndexContent.accreditation.successHeading)}
</h3>
<p
style={{
@@ -728,7 +699,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
Vielen Dank für Ihre Akkreditierungsanfrage. Unser Presseteam meldet sich zeitnah bei Ihnen.
{fallbackText(accreditation.successMessage, pressIndexContent.accreditation.successMessage)}
</p>
</div>
) : (
@@ -744,7 +715,7 @@ const Press: React.FC = () => {
marginBottom: 20,
}}
>
Akkreditierung
{fallbackText(accreditation.eyebrow, pressIndexContent.accreditation.eyebrow)}
</div>
<h3
@@ -758,7 +729,7 @@ const Press: React.FC = () => {
margin: '0 0 16px',
}}
>
AKKREDITIERUNG ANFRAGEN.
<Lines text={fallbackText(accreditation.heading, pressIndexContent.accreditation.heading)} />
</h3>
{/* Gold divider */}
@@ -773,7 +744,7 @@ const Press: React.FC = () => {
marginBottom: 36,
}}
>
Melden Sie sich für unsere Presse-Verteiler an oder fordern Sie eine Akkreditierung für die Gala-Verleihung an.
{fallbackText(accreditation.description, pressIndexContent.accreditation.description)}
</p>
{/* Form fields */}
@@ -789,7 +760,7 @@ const Press: React.FC = () => {
color: 'rgba(255,255,255,0.35)',
}}
>
Medium / Redaktion *
{fallbackText(accreditation.mediumLabel, pressIndexContent.accreditation.mediumLabel)}
</label>
<DarkInput
type="text"
@@ -809,7 +780,7 @@ const Press: React.FC = () => {
color: 'rgba(255,255,255,0.35)',
}}
>
Ihr Name *
{fallbackText(accreditation.nameLabel, pressIndexContent.accreditation.nameLabel)}
</label>
<DarkInput
type="text"
@@ -829,7 +800,7 @@ const Press: React.FC = () => {
color: 'rgba(255,255,255,0.35)',
}}
>
E-Mail-Adresse *
{fallbackText(accreditation.emailLabel, pressIndexContent.accreditation.emailLabel)}
</label>
<DarkInput
type="email"
@@ -863,7 +834,7 @@ const Press: React.FC = () => {
transition: 'background 0.15s, box-shadow 0.2s',
}}
>
Akkreditierung anfragen <ArrowRight size={13} />
{fallbackText(accreditation.submitLabel, pressIndexContent.accreditation.submitLabel)} <ArrowRight size={13} />
</button>
</div>
</>

View File

@@ -0,0 +1,100 @@
export const pressIndexContent = {
hero: {
imageFilename: 'gala-dinner.jpg',
imageAlt: 'Presse BMP',
eyebrow: 'Presse & Newsroom',
heading: 'EVENTS &\nBERICHTERSTATTUNG.',
description: 'Alle Termine, Pressemitteilungen und Medienmaterial rund um den Bayerischen Mittelstandspreis.',
},
events: {
eyebrow: 'Termine 2026',
heading: 'EVENTS RUND UM DEN PREIS.',
description: 'Von der Jurysitzung bis zur festlichen Gala alle Veranstaltungen auf einen Blick.',
detailLabel: 'Details',
openStatusLabel: 'Anmeldung offen',
fallbackItems: [
{
title: 'Preisverleihung 2026',
date: '22. Okt 2026',
location: 'München, Residenz',
cat: 'Gala / Event',
status: 'In Planung',
img: '/images/buehne-moderatoren.jpg',
desc: 'Der glanzvolle Höhepunkt des Jahres. Die Verleihung der Preise in der prachtvollen Kulisse der Residenz München.',
slug: '/presse/events/preisverleihung-2026',
},
{
title: 'Mittelstands-Gipfel',
date: '15. Juni 2026',
location: 'Nürnberg',
cat: 'Workshop',
status: 'Anmeldung offen',
img: '/images/networking-innenhof.jpg',
desc: 'Regionaler Austausch und Best-Practices für bayerische Unternehmen auf dem Weg zum Preis.',
slug: '/presse/events/mittelstands-gipfel-2026',
},
{
title: 'Nominierten-Auswahl',
date: '04. Aug 2026',
location: 'Regensburg',
cat: 'Jury-Sitzung',
status: 'Intern',
img: '/images/saal-gedeckt.jpg',
desc: 'Das Gremium sichtet die Ergebnisse der Audits und legt die Nominierten für die Hauptpreise fest.',
slug: '/presse/events/nominierten-auswahl-2026',
},
],
},
news: {
eyebrow: 'Newsroom',
heading: 'NACHRICHTEN & EINBLICKE.',
description: 'Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.',
readMoreLabel: 'Weiterlesen',
fallbackItems: [
{
title: 'Wie Bayerns KMU die KI nutzen',
excerpt: 'Ein Deep-Dive in die Bewerbungsunterlagen 2024 zeigt: Der Mittelstand treibt die Digitalisierung aktiv voran.',
cat: 'Innovation',
img: '/images/gala-saal-overview.jpg',
slug: '/presse/blog/ki-nutzung-bayerischer-kmu',
},
{
title: 'Start der Ehrenamts-Initiative',
excerpt: 'Gemeinsam mit unseren Partnern fördern wir soziale Projekte mittelständischer Unternehmen.',
cat: 'Engagement',
img: '/images/networking-innenhof.jpg',
slug: '/presse/blog/ehrenamts-initiative-2026',
},
{
title: 'Bayerische Wirtschaft wächst',
excerpt: 'Neue Prognosen zeigen ein stabiles Wachstum für den Mittelstand gute Aussichten für die Awards.',
cat: 'Wirtschaft',
img: '/images/gala-dinner.jpg',
slug: '/presse/blog/bayerische-wirtschaft-waechst',
},
],
},
downloads: {
eyebrow: 'Pressekontakt & Material',
heading: 'PRESSE-MATERIAL & KONTAKT.',
description: 'Laden Sie unser offizielles Material herunter oder kontaktieren Sie direkt unser Presseteam für individuelle Anfragen.',
kitLabel: 'Download Presse-Kit',
kitMeta: 'Print_BMP.zip 1,5 MB',
kitUrl: '/Print_BMP.zip',
kitDownloadName: 'Print_BMP.zip',
contactEyebrow: 'Pressekontakt',
contactName: 'Tanja Meier',
contactLines: 'presse@bmp-bayern.de\n+49 89 123 456 99',
},
accreditation: {
eyebrow: 'Akkreditierung',
heading: 'AKKREDITIERUNG ANFRAGEN.',
description: 'Melden Sie sich für unsere Presse-Verteiler an oder fordern Sie eine Akkreditierung für die Gala-Verleihung an.',
mediumLabel: 'Medium / Redaktion *',
nameLabel: 'Ihr Name *',
emailLabel: 'E-Mail-Adresse *',
submitLabel: 'Akkreditierung anfragen',
successHeading: 'Anfrage eingegangen',
successMessage: 'Vielen Dank für Ihre Akkreditierungsanfrage. Unser Presseteam meldet sich zeitnah bei Ihnen.',
},
}