feat: connect header and footer to cms as globals
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,9 @@ node_modules
|
|||||||
.next
|
.next
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.vercel
|
.vercel
|
||||||
|
content.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
# Payload default media upload directory
|
# Payload default media upload directory
|
||||||
public/media/
|
public/media/
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import Link from 'next/link'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { ThemeSelector } from '@/providers/Theme/ThemeSelector'
|
import { ThemeSelector } from '@/providers/Theme/ThemeSelector'
|
||||||
import { CMSLink } from '@/components/Link'
|
|
||||||
import { Logo } from '@/components/Logo/Logo'
|
import { Logo } from '@/components/Logo/Logo'
|
||||||
|
|
||||||
export async function Footer() {
|
export async function Footer() {
|
||||||
const footerData = await getCachedGlobal('footer', 1)()
|
const footerData = await getCachedGlobal('footer', 1)()
|
||||||
|
|
||||||
const navItems = footerData?.navItems || []
|
const linkColumns = footerData?.linkColumns || []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="mt-auto border-t border-border bg-black dark:bg-card text-white">
|
<footer className="mt-auto border-t border-border bg-black dark:bg-card text-white">
|
||||||
@@ -21,9 +20,11 @@ export async function Footer() {
|
|||||||
<div className="flex flex-col-reverse items-start md:flex-row gap-4 md:items-center">
|
<div className="flex flex-col-reverse items-start md:flex-row gap-4 md:items-center">
|
||||||
<ThemeSelector />
|
<ThemeSelector />
|
||||||
<nav className="flex flex-col md:flex-row gap-4">
|
<nav className="flex flex-col md:flex-row gap-4">
|
||||||
{navItems.map(({ link }, i) => {
|
{linkColumns.flatMap((column) => column.links || []).map((link, i) => (
|
||||||
return <CMSLink className="text-white" key={i} {...link} />
|
<Link className="text-white" key={`${link.path}-${i}`} href={link.path || '/'}>
|
||||||
})}
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Header } from '@/payload-types'
|
import { Footer } from '@/payload-types'
|
||||||
import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
|
import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
|
||||||
|
|
||||||
export const RowLabel: React.FC<RowLabelProps> = () => {
|
export const RowLabel: React.FC<RowLabelProps> = () => {
|
||||||
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
|
const data = useRowLabel<NonNullable<Footer['linkColumns']>[number]>()
|
||||||
|
|
||||||
const label = data?.data?.link?.label
|
const label = data?.data?.label
|
||||||
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}`
|
? `Link column ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data.data.label}`
|
||||||
: 'Row'
|
: 'Link column'
|
||||||
|
|
||||||
return <div>{label}</div>
|
return <div>{label}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,90 @@
|
|||||||
import type { GlobalConfig } from 'payload'
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
import { link } from '@/fields/link'
|
|
||||||
import { revalidateFooter } from './hooks/revalidateFooter'
|
import { revalidateFooter } from './hooks/revalidateFooter'
|
||||||
|
import { defaultFooterData } from '@/spa/cmsNavigation'
|
||||||
|
|
||||||
|
const linkFields = [
|
||||||
|
{ name: 'label', type: 'text' as const, required: true },
|
||||||
|
{ name: 'path', type: 'text' as const, required: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const socialFields = [
|
||||||
|
{ name: 'label', type: 'text' as const, required: true },
|
||||||
|
{ name: 'href', type: 'text' as const, required: true },
|
||||||
|
{
|
||||||
|
name: 'icon',
|
||||||
|
type: 'select' as const,
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: 'Instagram', value: 'instagram' },
|
||||||
|
{ label: 'LinkedIn', value: 'linkedin' },
|
||||||
|
{ label: 'Facebook', value: 'facebook' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const Footer: GlobalConfig = {
|
export const Footer: GlobalConfig = {
|
||||||
slug: 'footer',
|
slug: 'footer',
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
},
|
},
|
||||||
|
admin: {
|
||||||
|
livePreview: {
|
||||||
|
url: () => '/',
|
||||||
|
},
|
||||||
|
preview: () => '/',
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'navItems',
|
type: 'tabs',
|
||||||
type: 'array',
|
tabs: [
|
||||||
|
{
|
||||||
|
label: 'Brand & Copy',
|
||||||
fields: [
|
fields: [
|
||||||
link({
|
{ name: 'logo', type: 'upload', relationTo: 'media', admin: { description: 'Falls leer, wird /bmp-logo.png verwendet.' } },
|
||||||
appearances: false,
|
{ name: 'logoAlt', type: 'text', defaultValue: defaultFooterData.logoAlt, required: true },
|
||||||
}),
|
{ name: 'headlinePrefix', type: 'text', defaultValue: defaultFooterData.headlinePrefix, required: true },
|
||||||
|
{ name: 'headlineAccent', type: 'text', defaultValue: defaultFooterData.headlineAccent, required: true },
|
||||||
|
{ name: 'headlineSuffix', type: 'text', defaultValue: defaultFooterData.headlineSuffix, required: true },
|
||||||
|
{ name: 'sinceLabel', type: 'text', defaultValue: defaultFooterData.sinceLabel, required: true },
|
||||||
|
{ name: 'year', type: 'text', defaultValue: defaultFooterData.year, required: true },
|
||||||
|
{ name: 'description', type: 'textarea', defaultValue: defaultFooterData.description, required: true },
|
||||||
|
{ name: 'copyright', type: 'text', defaultValue: defaultFooterData.copyright, required: true },
|
||||||
],
|
],
|
||||||
maxRows: 6,
|
},
|
||||||
|
{
|
||||||
|
label: 'Links',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'ctas',
|
||||||
|
label: 'CTA links',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultFooterData.ctas,
|
||||||
|
fields: [
|
||||||
|
...linkFields,
|
||||||
|
{
|
||||||
|
name: 'variant',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'primary',
|
||||||
|
options: [
|
||||||
|
{ label: 'Primary gold button', value: 'primary' },
|
||||||
|
{ label: 'Secondary outline button', value: 'secondary' },
|
||||||
|
{ label: 'Subtle text link', value: 'subtle' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
maxRows: 4,
|
||||||
|
admin: { initCollapsed: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkColumns',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultFooterData.linkColumns,
|
||||||
|
fields: [
|
||||||
|
{ name: 'label', type: 'text', required: true },
|
||||||
|
{ name: 'links', type: 'array', fields: linkFields, required: true, admin: { initCollapsed: true } },
|
||||||
|
],
|
||||||
|
maxRows: 4,
|
||||||
admin: {
|
admin: {
|
||||||
initCollapsed: true,
|
initCollapsed: true,
|
||||||
components: {
|
components: {
|
||||||
@@ -26,6 +93,32 @@ export const Footer: GlobalConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Partner & Social',
|
||||||
|
fields: [
|
||||||
|
{ name: 'partnerLabel', type: 'text', defaultValue: defaultFooterData.partnerLabel, required: true },
|
||||||
|
{
|
||||||
|
name: 'partnerLogos',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{ name: 'image', type: 'upload', relationTo: 'media', required: true },
|
||||||
|
{ name: 'alt', type: 'text', required: true },
|
||||||
|
],
|
||||||
|
admin: { initCollapsed: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'socialLinks',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultFooterData.socialLinks,
|
||||||
|
fields: socialFields,
|
||||||
|
admin: { initCollapsed: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
afterChange: [revalidateFooter],
|
afterChange: [revalidateFooter],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import React from 'react'
|
|||||||
|
|
||||||
import type { Header as HeaderType } from '@/payload-types'
|
import type { Header as HeaderType } from '@/payload-types'
|
||||||
|
|
||||||
import { CMSLink } from '@/components/Link'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { SearchIcon } from 'lucide-react'
|
import { SearchIcon } from 'lucide-react'
|
||||||
|
|
||||||
@@ -13,8 +12,8 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex gap-3 items-center">
|
<nav className="flex gap-3 items-center">
|
||||||
{navItems.map(({ link }, i) => {
|
{navItems.map((item, i) => {
|
||||||
return <CMSLink key={i} {...link} appearance="link" />
|
return <Link key={i} href={item.path || '/'}>{item.label}</Link>
|
||||||
})}
|
})}
|
||||||
<Link href="/search">
|
<Link href="/search">
|
||||||
<span className="sr-only">Search</span>
|
<span className="sr-only">Search</span>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
|
|||||||
export const RowLabel: React.FC<RowLabelProps> = () => {
|
export const RowLabel: React.FC<RowLabelProps> = () => {
|
||||||
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
|
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
|
||||||
|
|
||||||
const label = data?.data?.link?.label
|
const label = data?.data?.label
|
||||||
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}`
|
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data.data.label}`
|
||||||
: 'Row'
|
: 'Nav item'
|
||||||
|
|
||||||
return <div>{label}</div>
|
return <div>{label}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,77 @@
|
|||||||
import type { GlobalConfig } from 'payload'
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
import { link } from '@/fields/link'
|
|
||||||
import { revalidateHeader } from './hooks/revalidateHeader'
|
import { revalidateHeader } from './hooks/revalidateHeader'
|
||||||
|
import { defaultHeaderData } from '@/spa/cmsNavigation'
|
||||||
|
|
||||||
|
const linkFields = [
|
||||||
|
{ name: 'label', type: 'text' as const, required: true },
|
||||||
|
{ name: 'path', type: 'text' as const, required: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const socialFields = [
|
||||||
|
{ name: 'label', type: 'text' as const, required: true },
|
||||||
|
{ name: 'href', type: 'text' as const, required: true },
|
||||||
|
{
|
||||||
|
name: 'icon',
|
||||||
|
type: 'select' as const,
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: 'Instagram', value: 'instagram' },
|
||||||
|
{ label: 'LinkedIn', value: 'linkedin' },
|
||||||
|
{ label: 'Facebook', value: 'facebook' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const Header: GlobalConfig = {
|
export const Header: GlobalConfig = {
|
||||||
slug: 'header',
|
slug: 'header',
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
},
|
},
|
||||||
|
admin: {
|
||||||
|
livePreview: {
|
||||||
|
url: () => '/',
|
||||||
|
},
|
||||||
|
preview: () => '/',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
label: 'Brand & Overlay',
|
||||||
|
fields: [
|
||||||
|
{ name: 'logo', type: 'upload', relationTo: 'media', admin: { description: 'Falls leer, wird /bmp-logo.png verwendet.' } },
|
||||||
|
{ name: 'overlayLogoAlt', type: 'text', defaultValue: defaultHeaderData.overlayLogoAlt, required: true },
|
||||||
|
{ name: 'overlayKicker', type: 'text', defaultValue: defaultHeaderData.overlayKicker, required: true },
|
||||||
|
{ name: 'overlayHeadline', type: 'textarea', defaultValue: defaultHeaderData.overlayHeadline, required: true },
|
||||||
|
{ name: 'mobileWatermark', type: 'textarea', defaultValue: defaultHeaderData.mobileWatermark, required: true },
|
||||||
|
{ name: 'menuLabel', type: 'text', defaultValue: defaultHeaderData.menuLabel, required: true },
|
||||||
|
{ name: 'openLabel', type: 'text', defaultValue: defaultHeaderData.openLabel, required: true },
|
||||||
|
{ name: 'closeLabel', type: 'text', defaultValue: defaultHeaderData.closeLabel, required: true },
|
||||||
|
{ name: 'overviewLabel', type: 'text', defaultValue: defaultHeaderData.overviewLabel, required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Navigation',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'navItems',
|
name: 'navItems',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
defaultValue: defaultHeaderData.navItems,
|
||||||
fields: [
|
fields: [
|
||||||
link({
|
{ name: 'key', type: 'text', required: true, admin: { description: 'Stable internal key, e.g. presse.' } },
|
||||||
appearances: false,
|
...linkFields,
|
||||||
}),
|
{ name: 'subLabel', type: 'text' },
|
||||||
|
{
|
||||||
|
name: 'sub',
|
||||||
|
label: 'Sub navigation',
|
||||||
|
type: 'array',
|
||||||
|
fields: linkFields,
|
||||||
|
admin: { initCollapsed: true },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
maxRows: 6,
|
maxRows: 8,
|
||||||
admin: {
|
admin: {
|
||||||
initCollapsed: true,
|
initCollapsed: true,
|
||||||
components: {
|
components: {
|
||||||
@@ -25,6 +79,57 @@ export const Header: GlobalConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'secondaryLinks',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultHeaderData.secondaryLinks,
|
||||||
|
fields: linkFields,
|
||||||
|
admin: { initCollapsed: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctas',
|
||||||
|
label: 'CTA links',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultHeaderData.ctas,
|
||||||
|
fields: [
|
||||||
|
...linkFields,
|
||||||
|
{
|
||||||
|
name: 'variant',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'primary',
|
||||||
|
options: [
|
||||||
|
{ label: 'Primary gold button', value: 'primary' },
|
||||||
|
{ label: 'Secondary outline button', value: 'secondary' },
|
||||||
|
{ label: 'Upload dashed link', value: 'upload' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
maxRows: 3,
|
||||||
|
admin: { initCollapsed: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Social & Language',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'socialLinks',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultHeaderData.socialLinks,
|
||||||
|
fields: socialFields,
|
||||||
|
admin: { initCollapsed: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultHeaderData.languages.map((label) => ({ label })),
|
||||||
|
fields: [{ name: 'label', type: 'text', required: true }],
|
||||||
|
maxRows: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
afterChange: [revalidateHeader],
|
afterChange: [revalidateHeader],
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
|
||||||
import NextApp from '@/spa/NextApp'
|
import NextApp from '@/spa/NextApp'
|
||||||
|
import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
params: Promise<{ slug?: string[] }>
|
params: Promise<{ slug?: string[] }>
|
||||||
@@ -7,6 +13,11 @@ type Args = {
|
|||||||
export default async function Page({ params }: Args) {
|
export default async function Page({ params }: Args) {
|
||||||
const { slug = [] } = await params
|
const { slug = [] } = await params
|
||||||
const pathname = `/${slug.join('/')}`.replace(/\/$/, '') || '/'
|
const pathname = `/${slug.join('/')}`.replace(/\/$/, '') || '/'
|
||||||
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
const [header, footer] = await Promise.all([
|
||||||
|
payload.findGlobal({ slug: 'header', depth: 1 }).catch(() => undefined),
|
||||||
|
payload.findGlobal({ slug: 'footer', depth: 1 }).catch(() => undefined),
|
||||||
|
])
|
||||||
|
|
||||||
return <NextApp pathname={pathname} />
|
return <NextApp pathname={pathname} header={normalizeHeaderData(header)} footer={normalizeFooterData(footer)} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { imageHero1 } from './image-hero-1'
|
|||||||
import { post1 } from './post-1'
|
import { post1 } from './post-1'
|
||||||
import { post2 } from './post-2'
|
import { post2 } from './post-2'
|
||||||
import { post3 } from './post-3'
|
import { post3 } from './post-3'
|
||||||
|
import { defaultFooterData, defaultHeaderData } from '@/spa/cmsNavigation'
|
||||||
|
|
||||||
const collections: CollectionSlug[] = [
|
const collections: CollectionSlug[] = [
|
||||||
'categories',
|
'categories',
|
||||||
@@ -48,9 +49,7 @@ export const seed = async ({
|
|||||||
globals.map((global) =>
|
globals.map((global) =>
|
||||||
payload.updateGlobal({
|
payload.updateGlobal({
|
||||||
slug: global,
|
slug: global,
|
||||||
data: {
|
data: {},
|
||||||
navItems: [],
|
|
||||||
},
|
|
||||||
depth: 0,
|
depth: 0,
|
||||||
context: {
|
context: {
|
||||||
disableRevalidate: true,
|
disableRevalidate: true,
|
||||||
@@ -217,60 +216,17 @@ export const seed = async ({
|
|||||||
|
|
||||||
payload.logger.info(`— Seeding globals...`)
|
payload.logger.info(`— Seeding globals...`)
|
||||||
|
|
||||||
|
const { logo: _headerLogo, ...headerSeedData } = defaultHeaderData
|
||||||
|
const { logo: _footerLogo, partnerLogos: _partnerLogos, ...footerSeedData } = defaultFooterData
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
payload.updateGlobal({
|
payload.updateGlobal({
|
||||||
slug: 'header',
|
slug: 'header',
|
||||||
data: {
|
data: headerSeedData as any,
|
||||||
navItems: [
|
|
||||||
{
|
|
||||||
link: {
|
|
||||||
type: 'custom',
|
|
||||||
label: 'Posts',
|
|
||||||
url: '/posts',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: {
|
|
||||||
type: 'reference',
|
|
||||||
label: 'Contact',
|
|
||||||
reference: {
|
|
||||||
relationTo: 'pages',
|
|
||||||
value: contactPage.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
payload.updateGlobal({
|
payload.updateGlobal({
|
||||||
slug: 'footer',
|
slug: 'footer',
|
||||||
data: {
|
data: footerSeedData as any,
|
||||||
navItems: [
|
|
||||||
{
|
|
||||||
link: {
|
|
||||||
type: 'custom',
|
|
||||||
label: 'Admin',
|
|
||||||
url: '/admin',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: {
|
|
||||||
type: 'custom',
|
|
||||||
label: 'Source Code',
|
|
||||||
newTab: true,
|
|
||||||
url: 'https://github.com/payloadcms/payload/tree/3.x/templates/website',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: {
|
|
||||||
type: 'custom',
|
|
||||||
label: 'Payload',
|
|
||||||
newTab: true,
|
|
||||||
url: 'https://payloadcms.com/',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export interface Config {
|
|||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
};
|
};
|
||||||
db: {
|
db: {
|
||||||
defaultIDType: string;
|
defaultIDType: number;
|
||||||
};
|
};
|
||||||
fallbackLocale: null;
|
fallbackLocale: null;
|
||||||
globals: {
|
globals: {
|
||||||
@@ -156,7 +156,7 @@ export interface UserAuthOperations {
|
|||||||
* via the `definition` "pages".
|
* via the `definition` "pages".
|
||||||
*/
|
*/
|
||||||
export interface Page {
|
export interface Page {
|
||||||
id: string;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
hero: {
|
hero: {
|
||||||
type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact';
|
type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact';
|
||||||
@@ -183,11 +183,11 @@ export interface Page {
|
|||||||
reference?:
|
reference?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: string | Page;
|
value: number | Page;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
} | null);
|
} | null);
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -199,7 +199,7 @@ export interface Page {
|
|||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
media?: (string | null) | Media;
|
media?: (number | null) | Media;
|
||||||
};
|
};
|
||||||
layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[];
|
layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[];
|
||||||
meta?: {
|
meta?: {
|
||||||
@@ -207,7 +207,7 @@ export interface Page {
|
|||||||
/**
|
/**
|
||||||
* Maximum upload file size: 12MB. Recommended file size for images is <500KB.
|
* Maximum upload file size: 12MB. Recommended file size for images is <500KB.
|
||||||
*/
|
*/
|
||||||
image?: (string | null) | Media;
|
image?: (number | null) | Media;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
};
|
};
|
||||||
publishedAt?: string | null;
|
publishedAt?: string | null;
|
||||||
@@ -225,9 +225,9 @@ export interface Page {
|
|||||||
* via the `definition` "posts".
|
* via the `definition` "posts".
|
||||||
*/
|
*/
|
||||||
export interface Post {
|
export interface Post {
|
||||||
id: string;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
heroImage?: (string | null) | Media;
|
heroImage?: (number | null) | Media;
|
||||||
content: {
|
content: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -243,18 +243,18 @@ export interface Post {
|
|||||||
};
|
};
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
relatedPosts?: (string | Post)[] | null;
|
relatedPosts?: (number | Post)[] | null;
|
||||||
categories?: (string | Category)[] | null;
|
categories?: (number | Category)[] | null;
|
||||||
meta?: {
|
meta?: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
/**
|
/**
|
||||||
* Maximum upload file size: 12MB. Recommended file size for images is <500KB.
|
* Maximum upload file size: 12MB. Recommended file size for images is <500KB.
|
||||||
*/
|
*/
|
||||||
image?: (string | null) | Media;
|
image?: (number | null) | Media;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
};
|
};
|
||||||
publishedAt?: string | null;
|
publishedAt?: string | null;
|
||||||
authors?: (string | User)[] | null;
|
authors?: (number | User)[] | null;
|
||||||
populatedAuthors?:
|
populatedAuthors?:
|
||||||
| {
|
| {
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
@@ -275,7 +275,7 @@ export interface Post {
|
|||||||
* via the `definition` "media".
|
* via the `definition` "media".
|
||||||
*/
|
*/
|
||||||
export interface Media {
|
export interface Media {
|
||||||
id: string;
|
id: number;
|
||||||
alt?: string | null;
|
alt?: string | null;
|
||||||
caption?: {
|
caption?: {
|
||||||
root: {
|
root: {
|
||||||
@@ -292,7 +292,7 @@ export interface Media {
|
|||||||
};
|
};
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
} | null;
|
} | null;
|
||||||
folder?: (string | null) | FolderInterface;
|
folder?: (number | null) | FolderInterface;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -368,18 +368,18 @@ export interface Media {
|
|||||||
* via the `definition` "payload-folders".
|
* via the `definition` "payload-folders".
|
||||||
*/
|
*/
|
||||||
export interface FolderInterface {
|
export interface FolderInterface {
|
||||||
id: string;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
folder?: (string | null) | FolderInterface;
|
folder?: (number | null) | FolderInterface;
|
||||||
documentsAndFolders?: {
|
documentsAndFolders?: {
|
||||||
docs?: (
|
docs?: (
|
||||||
| {
|
| {
|
||||||
relationTo?: 'payload-folders';
|
relationTo?: 'payload-folders';
|
||||||
value: string | FolderInterface;
|
value: number | FolderInterface;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
relationTo?: 'media';
|
relationTo?: 'media';
|
||||||
value: string | Media;
|
value: number | Media;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
@@ -394,17 +394,17 @@ export interface FolderInterface {
|
|||||||
* via the `definition` "categories".
|
* via the `definition` "categories".
|
||||||
*/
|
*/
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: string;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
/**
|
/**
|
||||||
* When enabled, the slug will auto-generate from the title field on save and autosave.
|
* When enabled, the slug will auto-generate from the title field on save and autosave.
|
||||||
*/
|
*/
|
||||||
generateSlug?: boolean | null;
|
generateSlug?: boolean | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
parent?: (string | null) | Category;
|
parent?: (number | null) | Category;
|
||||||
breadcrumbs?:
|
breadcrumbs?:
|
||||||
| {
|
| {
|
||||||
doc?: (string | null) | Category;
|
doc?: (number | null) | Category;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
label?: string | null;
|
label?: string | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
@@ -418,7 +418,7 @@ export interface Category {
|
|||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
*/
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: number;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -467,11 +467,11 @@ export interface CallToActionBlock {
|
|||||||
reference?:
|
reference?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: string | Page;
|
value: number | Page;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
} | null);
|
} | null);
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -517,11 +517,11 @@ export interface ContentBlock {
|
|||||||
reference?:
|
reference?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: string | Page;
|
value: number | Page;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
} | null);
|
} | null);
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -542,7 +542,7 @@ export interface ContentBlock {
|
|||||||
* via the `definition` "MediaBlock".
|
* via the `definition` "MediaBlock".
|
||||||
*/
|
*/
|
||||||
export interface MediaBlock {
|
export interface MediaBlock {
|
||||||
media: string | Media;
|
media: number | Media;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'mediaBlock';
|
blockType: 'mediaBlock';
|
||||||
@@ -569,12 +569,12 @@ export interface ArchiveBlock {
|
|||||||
} | null;
|
} | null;
|
||||||
populateBy?: ('collection' | 'selection') | null;
|
populateBy?: ('collection' | 'selection') | null;
|
||||||
relationTo?: 'posts' | null;
|
relationTo?: 'posts' | null;
|
||||||
categories?: (string | Category)[] | null;
|
categories?: (number | Category)[] | null;
|
||||||
limit?: number | null;
|
limit?: number | null;
|
||||||
selectedDocs?:
|
selectedDocs?:
|
||||||
| {
|
| {
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
@@ -586,7 +586,7 @@ export interface ArchiveBlock {
|
|||||||
* via the `definition` "FormBlock".
|
* via the `definition` "FormBlock".
|
||||||
*/
|
*/
|
||||||
export interface FormBlock {
|
export interface FormBlock {
|
||||||
form: string | Form;
|
form: number | Form;
|
||||||
enableIntro?: boolean | null;
|
enableIntro?: boolean | null;
|
||||||
introContent?: {
|
introContent?: {
|
||||||
root: {
|
root: {
|
||||||
@@ -612,7 +612,7 @@ export interface FormBlock {
|
|||||||
* via the `definition` "forms".
|
* via the `definition` "forms".
|
||||||
*/
|
*/
|
||||||
export interface Form {
|
export interface Form {
|
||||||
id: string;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
fields?:
|
fields?:
|
||||||
| (
|
| (
|
||||||
@@ -786,7 +786,7 @@ export interface Form {
|
|||||||
* via the `definition` "redirects".
|
* via the `definition` "redirects".
|
||||||
*/
|
*/
|
||||||
export interface Redirect {
|
export interface Redirect {
|
||||||
id: string;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* You will need to rebuild the website when changing this field.
|
* You will need to rebuild the website when changing this field.
|
||||||
*/
|
*/
|
||||||
@@ -796,11 +796,11 @@ export interface Redirect {
|
|||||||
reference?:
|
reference?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: string | Page;
|
value: number | Page;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
} | null);
|
} | null);
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
@@ -812,8 +812,8 @@ export interface Redirect {
|
|||||||
* via the `definition` "form-submissions".
|
* via the `definition` "form-submissions".
|
||||||
*/
|
*/
|
||||||
export interface FormSubmission {
|
export interface FormSubmission {
|
||||||
id: string;
|
id: number;
|
||||||
form: string | Form;
|
form: number | Form;
|
||||||
submissionData?:
|
submissionData?:
|
||||||
| {
|
| {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -831,18 +831,18 @@ export interface FormSubmission {
|
|||||||
* via the `definition` "search".
|
* via the `definition` "search".
|
||||||
*/
|
*/
|
||||||
export interface Search {
|
export interface Search {
|
||||||
id: string;
|
id: number;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
priority?: number | null;
|
priority?: number | null;
|
||||||
doc: {
|
doc: {
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
};
|
};
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
meta?: {
|
meta?: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
image?: (string | null) | Media;
|
image?: (number | null) | Media;
|
||||||
};
|
};
|
||||||
categories?:
|
categories?:
|
||||||
| {
|
| {
|
||||||
@@ -860,7 +860,7 @@ export interface Search {
|
|||||||
* via the `definition` "payload-kv".
|
* via the `definition` "payload-kv".
|
||||||
*/
|
*/
|
||||||
export interface PayloadKv {
|
export interface PayloadKv {
|
||||||
id: string;
|
id: number;
|
||||||
key: string;
|
key: string;
|
||||||
data:
|
data:
|
||||||
| {
|
| {
|
||||||
@@ -877,7 +877,7 @@ export interface PayloadKv {
|
|||||||
* via the `definition` "payload-jobs".
|
* via the `definition` "payload-jobs".
|
||||||
*/
|
*/
|
||||||
export interface PayloadJob {
|
export interface PayloadJob {
|
||||||
id: string;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Input data provided to the job
|
* Input data provided to the job
|
||||||
*/
|
*/
|
||||||
@@ -969,52 +969,52 @@ export interface PayloadJob {
|
|||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
*/
|
*/
|
||||||
export interface PayloadLockedDocument {
|
export interface PayloadLockedDocument {
|
||||||
id: string;
|
id: number;
|
||||||
document?:
|
document?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: string | Page;
|
value: number | Page;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'media';
|
relationTo: 'media';
|
||||||
value: string | Media;
|
value: number | Media;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'categories';
|
relationTo: 'categories';
|
||||||
value: string | Category;
|
value: number | Category;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: number | User;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'redirects';
|
relationTo: 'redirects';
|
||||||
value: string | Redirect;
|
value: number | Redirect;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'forms';
|
relationTo: 'forms';
|
||||||
value: string | Form;
|
value: number | Form;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'form-submissions';
|
relationTo: 'form-submissions';
|
||||||
value: string | FormSubmission;
|
value: number | FormSubmission;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'search';
|
relationTo: 'search';
|
||||||
value: string | Search;
|
value: number | Search;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'payload-folders';
|
relationTo: 'payload-folders';
|
||||||
value: string | FolderInterface;
|
value: number | FolderInterface;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: number | User;
|
||||||
};
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -1024,10 +1024,10 @@ export interface PayloadLockedDocument {
|
|||||||
* via the `definition` "payload-preferences".
|
* via the `definition` "payload-preferences".
|
||||||
*/
|
*/
|
||||||
export interface PayloadPreference {
|
export interface PayloadPreference {
|
||||||
id: string;
|
id: number;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: number | User;
|
||||||
};
|
};
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
value?:
|
value?:
|
||||||
@@ -1047,7 +1047,7 @@ export interface PayloadPreference {
|
|||||||
* via the `definition` "payload-migrations".
|
* via the `definition` "payload-migrations".
|
||||||
*/
|
*/
|
||||||
export interface PayloadMigration {
|
export interface PayloadMigration {
|
||||||
id: string;
|
id: number;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
batch?: number | null;
|
batch?: number | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -1636,24 +1636,64 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
|||||||
* via the `definition` "header".
|
* via the `definition` "header".
|
||||||
*/
|
*/
|
||||||
export interface Header {
|
export interface Header {
|
||||||
id: string;
|
id: number;
|
||||||
|
/**
|
||||||
|
* Falls leer, wird /bmp-logo.png verwendet.
|
||||||
|
*/
|
||||||
|
logo?: (number | null) | Media;
|
||||||
|
overlayLogoAlt: string;
|
||||||
|
overlayKicker: string;
|
||||||
|
overlayHeadline: string;
|
||||||
|
mobileWatermark: string;
|
||||||
|
menuLabel: string;
|
||||||
|
openLabel: string;
|
||||||
|
closeLabel: string;
|
||||||
|
overviewLabel: string;
|
||||||
navItems?:
|
navItems?:
|
||||||
| {
|
| {
|
||||||
link: {
|
/**
|
||||||
type?: ('reference' | 'custom') | null;
|
* Stable internal key, e.g. presse.
|
||||||
newTab?: boolean | null;
|
*/
|
||||||
reference?:
|
key: string;
|
||||||
| ({
|
label: string;
|
||||||
relationTo: 'pages';
|
path: string;
|
||||||
value: string | Page;
|
subLabel?: string | null;
|
||||||
} | null)
|
sub?:
|
||||||
| ({
|
| {
|
||||||
relationTo: 'posts';
|
label: string;
|
||||||
value: string | Post;
|
path: string;
|
||||||
} | null);
|
id?: string | null;
|
||||||
url?: string | null;
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
secondaryLinks?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
ctas?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
variant?: ('primary' | 'secondary' | 'upload') | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
socialLinks?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: 'instagram' | 'linkedin' | 'facebook';
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
languages?:
|
||||||
|
| {
|
||||||
label: string;
|
label: string;
|
||||||
};
|
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
@@ -1665,24 +1705,51 @@ export interface Header {
|
|||||||
* via the `definition` "footer".
|
* via the `definition` "footer".
|
||||||
*/
|
*/
|
||||||
export interface Footer {
|
export interface Footer {
|
||||||
id: string;
|
id: number;
|
||||||
navItems?:
|
/**
|
||||||
|
* Falls leer, wird /bmp-logo.png verwendet.
|
||||||
|
*/
|
||||||
|
logo?: (number | null) | Media;
|
||||||
|
logoAlt: string;
|
||||||
|
headlinePrefix: string;
|
||||||
|
headlineAccent: string;
|
||||||
|
headlineSuffix: string;
|
||||||
|
sinceLabel: string;
|
||||||
|
year: string;
|
||||||
|
description: string;
|
||||||
|
copyright: string;
|
||||||
|
ctas?:
|
||||||
| {
|
| {
|
||||||
link: {
|
|
||||||
type?: ('reference' | 'custom') | null;
|
|
||||||
newTab?: boolean | null;
|
|
||||||
reference?:
|
|
||||||
| ({
|
|
||||||
relationTo: 'pages';
|
|
||||||
value: string | Page;
|
|
||||||
} | null)
|
|
||||||
| ({
|
|
||||||
relationTo: 'posts';
|
|
||||||
value: string | Post;
|
|
||||||
} | null);
|
|
||||||
url?: string | null;
|
|
||||||
label: string;
|
label: string;
|
||||||
};
|
path: string;
|
||||||
|
variant?: ('primary' | 'secondary' | 'subtle') | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
linkColumns?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
links: {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[];
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
partnerLabel: string;
|
||||||
|
partnerLogos?:
|
||||||
|
| {
|
||||||
|
image: number | Media;
|
||||||
|
alt: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
socialLinks?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: 'instagram' | 'linkedin' | 'facebook';
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
@@ -1694,20 +1761,60 @@ export interface Footer {
|
|||||||
* via the `definition` "header_select".
|
* via the `definition` "header_select".
|
||||||
*/
|
*/
|
||||||
export interface HeaderSelect<T extends boolean = true> {
|
export interface HeaderSelect<T extends boolean = true> {
|
||||||
|
logo?: T;
|
||||||
|
overlayLogoAlt?: T;
|
||||||
|
overlayKicker?: T;
|
||||||
|
overlayHeadline?: T;
|
||||||
|
mobileWatermark?: T;
|
||||||
|
menuLabel?: T;
|
||||||
|
openLabel?: T;
|
||||||
|
closeLabel?: T;
|
||||||
|
overviewLabel?: T;
|
||||||
navItems?:
|
navItems?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
link?:
|
key?: T;
|
||||||
|
label?: T;
|
||||||
|
path?: T;
|
||||||
|
subLabel?: T;
|
||||||
|
sub?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
type?: T;
|
|
||||||
newTab?: T;
|
|
||||||
reference?: T;
|
|
||||||
url?: T;
|
|
||||||
label?: T;
|
label?: T;
|
||||||
|
path?: T;
|
||||||
|
id?: T;
|
||||||
};
|
};
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
|
secondaryLinks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
path?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
ctas?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
path?: T;
|
||||||
|
variant?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
socialLinks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
href?: T;
|
||||||
|
icon?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
languages?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
globalType?: T;
|
globalType?: T;
|
||||||
@@ -1717,18 +1824,50 @@ export interface HeaderSelect<T extends boolean = true> {
|
|||||||
* via the `definition` "footer_select".
|
* via the `definition` "footer_select".
|
||||||
*/
|
*/
|
||||||
export interface FooterSelect<T extends boolean = true> {
|
export interface FooterSelect<T extends boolean = true> {
|
||||||
navItems?:
|
logo?: T;
|
||||||
|
logoAlt?: T;
|
||||||
|
headlinePrefix?: T;
|
||||||
|
headlineAccent?: T;
|
||||||
|
headlineSuffix?: T;
|
||||||
|
sinceLabel?: T;
|
||||||
|
year?: T;
|
||||||
|
description?: T;
|
||||||
|
copyright?: T;
|
||||||
|
ctas?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
link?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
type?: T;
|
|
||||||
newTab?: T;
|
|
||||||
reference?: T;
|
|
||||||
url?: T;
|
|
||||||
label?: T;
|
label?: T;
|
||||||
|
path?: T;
|
||||||
|
variant?: T;
|
||||||
|
id?: T;
|
||||||
};
|
};
|
||||||
|
linkColumns?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
links?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
path?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
partnerLabel?: T;
|
||||||
|
partnerLogos?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
image?: T;
|
||||||
|
alt?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
socialLinks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
href?: T;
|
||||||
|
icon?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
@@ -1756,14 +1895,14 @@ export interface TaskSchedulePublish {
|
|||||||
doc?:
|
doc?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: string | Page;
|
value: number | Page;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
} | null);
|
} | null);
|
||||||
global?: string | null;
|
global?: string | null;
|
||||||
user?: (string | null) | User;
|
user?: (number | null) | User;
|
||||||
};
|
};
|
||||||
output?: unknown;
|
output?: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react'
|
|||||||
|
|
||||||
import Layout from '@/spa/components/layout/Layout'
|
import Layout from '@/spa/components/layout/Layout'
|
||||||
import ScrollToTop from '@/spa/components/layout/ScrollToTop'
|
import ScrollToTop from '@/spa/components/layout/ScrollToTop'
|
||||||
|
import type { SpaFooterData, SpaHeaderData } from '@/spa/cmsNavigation'
|
||||||
import About from '@/spa/pages/About'
|
import About from '@/spa/pages/About'
|
||||||
import BlogDetail from '@/spa/pages/BlogDetail'
|
import BlogDetail from '@/spa/pages/BlogDetail'
|
||||||
import Contact from '@/spa/pages/Contact'
|
import Contact from '@/spa/pages/Contact'
|
||||||
@@ -41,7 +42,15 @@ function resolvePage(pathname: string) {
|
|||||||
return Home
|
return Home
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NextApp({ pathname }: { pathname: string }) {
|
export default function NextApp({
|
||||||
|
pathname,
|
||||||
|
header,
|
||||||
|
footer,
|
||||||
|
}: {
|
||||||
|
pathname: string
|
||||||
|
header?: SpaHeaderData
|
||||||
|
footer?: SpaFooterData
|
||||||
|
}) {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,7 +79,7 @@ export default function NextApp({ pathname }: { pathname: string }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Layout>
|
<Layout header={header} footer={footer}>
|
||||||
<Page />
|
<Page />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|||||||
198
src/spa/cmsNavigation.ts
Normal file
198
src/spa/cmsNavigation.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import type { Media } from '@/payload-types'
|
||||||
|
|
||||||
|
export type SpaIconName = 'instagram' | 'linkedin' | 'facebook'
|
||||||
|
export type SpaLink = { label: string; path: string }
|
||||||
|
export type SpaSocialLink = { label: string; href: string; icon: SpaIconName }
|
||||||
|
export type SpaCtaLink = SpaLink & { variant?: 'primary' | 'secondary' | 'subtle' | 'upload' }
|
||||||
|
export type SpaMedia = { url?: string; alt?: string }
|
||||||
|
|
||||||
|
export type SpaHeaderNavItem = SpaLink & {
|
||||||
|
key: string
|
||||||
|
subLabel?: string
|
||||||
|
sub?: SpaLink[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SpaHeaderData = {
|
||||||
|
logo?: SpaMedia
|
||||||
|
overlayKicker: string
|
||||||
|
overlayHeadline: string
|
||||||
|
overlayLogoAlt: string
|
||||||
|
mobileWatermark: string
|
||||||
|
menuLabel: string
|
||||||
|
openLabel: string
|
||||||
|
closeLabel: string
|
||||||
|
overviewLabel: string
|
||||||
|
navItems: SpaHeaderNavItem[]
|
||||||
|
secondaryLinks: SpaLink[]
|
||||||
|
socialLinks: SpaSocialLink[]
|
||||||
|
ctas: SpaCtaLink[]
|
||||||
|
languages: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SpaFooterLinkColumn = { label: string; links: SpaLink[] }
|
||||||
|
export type SpaPartnerLogo = { image?: SpaMedia; alt: string }
|
||||||
|
|
||||||
|
export type SpaFooterData = {
|
||||||
|
logo?: SpaMedia
|
||||||
|
logoAlt: string
|
||||||
|
headlinePrefix: string
|
||||||
|
headlineAccent: string
|
||||||
|
headlineSuffix: string
|
||||||
|
sinceLabel: string
|
||||||
|
year: string
|
||||||
|
description: string
|
||||||
|
ctas: SpaCtaLink[]
|
||||||
|
linkColumns: SpaFooterLinkColumn[]
|
||||||
|
partnerLabel: string
|
||||||
|
partnerLogos: SpaPartnerLogo[]
|
||||||
|
copyright: string
|
||||||
|
socialLinks: SpaSocialLink[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultSocialLinks: SpaSocialLink[] = [
|
||||||
|
{ href: 'https://instagram.com', icon: 'instagram', label: 'Instagram' },
|
||||||
|
{ href: 'https://linkedin.com', icon: 'linkedin', label: 'LinkedIn' },
|
||||||
|
{ href: 'https://facebook.com', icon: 'facebook', label: 'Facebook' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const defaultHeaderData: SpaHeaderData = {
|
||||||
|
logo: { url: '/bmp-logo.png', alt: 'Der Bayerische Mittelstandspreis 2026' },
|
||||||
|
overlayKicker: 'Bayerischer Mittelstandspreis',
|
||||||
|
overlayHeadline: 'EXZELLENZ HAT\nEINEN NAMEN.',
|
||||||
|
overlayLogoAlt: 'BMP Logo',
|
||||||
|
mobileWatermark: 'Exzellenz\nseit 2005',
|
||||||
|
menuLabel: 'Menü',
|
||||||
|
openLabel: 'Navigation öffnen',
|
||||||
|
closeLabel: 'Menü schließen',
|
||||||
|
overviewLabel: 'Überblick',
|
||||||
|
languages: ['DE', 'EN'],
|
||||||
|
socialLinks: defaultSocialLinks,
|
||||||
|
navItems: [
|
||||||
|
{
|
||||||
|
key: 'preistraeger', label: 'Preisträger:innen', path: '/preistraeger', subLabel: 'PREISTRÄGER:INNEN',
|
||||||
|
sub: [
|
||||||
|
{ label: 'Alle Preisträger:innen', path: '/preistraeger' },
|
||||||
|
{ label: 'Sieger & Aufsteiger 2025', path: '/preistraeger' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'teilnahme', label: 'Ihr Weg zum Preis', path: '/teilnahme', subLabel: 'IHR WEG ZUM PREIS',
|
||||||
|
sub: [
|
||||||
|
{ label: 'Teilnahmevoraussetzungen', path: '/teilnahme#voraussetzungen' },
|
||||||
|
{ label: 'Kriterien & Bewertung', path: '/teilnahme#kriterien' },
|
||||||
|
{ label: 'Fristen & Termine', path: '/teilnahme#fristen' },
|
||||||
|
{ label: 'Bewerbungsformular', path: '/teilnahme#bewerben' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bmp', label: 'Der BMP', path: '/der-bmp', subLabel: 'DER BMP',
|
||||||
|
sub: [
|
||||||
|
{ label: 'Was ist der Preis?', path: '/der-bmp#was-ist' },
|
||||||
|
{ label: 'Geschichte & Werte', path: '/der-bmp#geschichte' },
|
||||||
|
{ label: 'Vorteile der Auszeichnung', path: '/der-bmp#vorteile' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'netzwerk', label: 'Netzwerk', path: '/netzwerk', subLabel: 'NETZWERK',
|
||||||
|
sub: [
|
||||||
|
{ label: 'Jury-Mitglieder', path: '/netzwerk#jury' },
|
||||||
|
{ label: 'Partner & Sponsoren', path: '/netzwerk#partner' },
|
||||||
|
{ label: 'Schirmherrschaft', path: '/netzwerk#schirmherrschaft' },
|
||||||
|
{ label: 'Mitglied werden', path: '/mitglied-werden' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'presse', label: 'Presse', path: '/presse', subLabel: 'PRESSE',
|
||||||
|
sub: [
|
||||||
|
{ label: 'Events & Gala', path: '/presse#events' },
|
||||||
|
{ label: 'Downloads & Impressionen', path: '/presse#downloads' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
secondaryLinks: [
|
||||||
|
{ label: 'Startseite', path: '/' },
|
||||||
|
{ label: 'Kontakt', path: '/kontakt' },
|
||||||
|
{ label: 'Impressum', path: '/impressum' },
|
||||||
|
{ label: 'Datenschutz', path: '/datenschutz' },
|
||||||
|
],
|
||||||
|
ctas: [
|
||||||
|
{ label: 'Jetzt bewerben', path: '/teilnahme#bewerben', variant: 'primary' },
|
||||||
|
{ label: 'Mitglied werden', path: '/mitglied-werden', variant: 'secondary' },
|
||||||
|
{ label: 'Formular hochladen', path: '/formular-hochladen', variant: 'upload' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFooterData: SpaFooterData = {
|
||||||
|
logo: { url: '/bmp-logo.png', alt: 'Bayerischer Mittelstandspreis' },
|
||||||
|
logoAlt: 'Bayerischer Mittelstandspreis',
|
||||||
|
headlinePrefix: 'Der Preis des',
|
||||||
|
headlineAccent: 'Bayerischen',
|
||||||
|
headlineSuffix: 'Mittelstands',
|
||||||
|
sinceLabel: 'Exzellenz seit 2005',
|
||||||
|
year: '2026',
|
||||||
|
description: 'Die renommierteste Auszeichnung für herausragende Unternehmen des bayerischen Mittelstands.',
|
||||||
|
ctas: [
|
||||||
|
{ label: 'Kostenlos bewerben', path: '/teilnahme#bewerben', variant: 'primary' },
|
||||||
|
{ label: 'Mitglied werden', path: '/mitglied-werden', variant: 'secondary' },
|
||||||
|
{ label: 'Sponsoring-Anfrage →', path: '/netzwerk#sponsoring', variant: 'subtle' },
|
||||||
|
],
|
||||||
|
linkColumns: [
|
||||||
|
{ label: 'BMP 2026', links: [
|
||||||
|
{ label: 'Startseite', path: '/' },
|
||||||
|
{ label: 'Preisträger', path: '/preistraeger' },
|
||||||
|
{ label: 'Teilnahme', path: '/teilnahme' },
|
||||||
|
] },
|
||||||
|
{ label: 'Engagement', links: [
|
||||||
|
{ label: 'Netzwerk & Jury', path: '/netzwerk' },
|
||||||
|
{ label: 'Presse', path: '/presse' },
|
||||||
|
{ label: 'Kontakt', path: '/kontakt' },
|
||||||
|
] },
|
||||||
|
{ label: 'Legal', links: [
|
||||||
|
{ label: 'Datenschutz', path: '/datenschutz' },
|
||||||
|
{ label: 'Impressum', path: '/impressum' },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
partnerLabel: 'Partner',
|
||||||
|
partnerLogos: [
|
||||||
|
{ alt: 'EWIF – Europäisches Wirtschaftsforum', image: { url: '/images/ewif-logo.png' } },
|
||||||
|
{ alt: 'Hochschule für angewandtes Management · Gipfeldialog Altaussee', image: { url: '/images/partner-logos.png' } },
|
||||||
|
],
|
||||||
|
copyright: '© 2026 Bayerischer Mittelstandspreis · Alle Rechte vorbehalten',
|
||||||
|
socialLinks: defaultSocialLinks,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMedia = (value: unknown): value is Media => Boolean(value && typeof value === 'object' && 'url' in value)
|
||||||
|
const mediaToSpa = (value: unknown, fallback?: SpaMedia): SpaMedia | undefined => {
|
||||||
|
if (isMedia(value)) return { url: value.url || fallback?.url, alt: value.alt || fallback?.alt }
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeHeaderData(header: any): SpaHeaderData {
|
||||||
|
return {
|
||||||
|
...defaultHeaderData,
|
||||||
|
...header,
|
||||||
|
logo: mediaToSpa(header?.logo, defaultHeaderData.logo),
|
||||||
|
navItems: header?.navItems?.length ? header.navItems : defaultHeaderData.navItems,
|
||||||
|
secondaryLinks: header?.secondaryLinks?.length ? header.secondaryLinks : defaultHeaderData.secondaryLinks,
|
||||||
|
socialLinks: header?.socialLinks?.length ? header.socialLinks : defaultHeaderData.socialLinks,
|
||||||
|
ctas: header?.ctas?.length ? header.ctas : defaultHeaderData.ctas,
|
||||||
|
languages: header?.languages?.length ? header.languages.map((item: any) => item.label || item).filter(Boolean) : defaultHeaderData.languages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeFooterData(footer: any): SpaFooterData {
|
||||||
|
return {
|
||||||
|
...defaultFooterData,
|
||||||
|
...footer,
|
||||||
|
logo: mediaToSpa(footer?.logo, defaultFooterData.logo),
|
||||||
|
ctas: footer?.ctas?.length ? footer.ctas : defaultFooterData.ctas,
|
||||||
|
linkColumns: footer?.linkColumns?.length ? footer.linkColumns : defaultFooterData.linkColumns,
|
||||||
|
socialLinks: footer?.socialLinks?.length ? footer.socialLinks : defaultFooterData.socialLinks,
|
||||||
|
partnerLogos: footer?.partnerLogos?.length
|
||||||
|
? footer.partnerLogos.map((item: any, index: number) => ({
|
||||||
|
alt: item.alt || defaultFooterData.partnerLogos[index]?.alt || '',
|
||||||
|
image: mediaToSpa(item.image, defaultFooterData.partnerLogos[index]?.image),
|
||||||
|
}))
|
||||||
|
: defaultFooterData.partnerLogos,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,79 +5,26 @@ import { Link, useLocation } from '@/spa/router';
|
|||||||
import { ArrowRight, Upload } from 'lucide-react';
|
import { ArrowRight, Upload } from 'lucide-react';
|
||||||
import LoadingScreen from '@/spa/components/ui/LoadingScreen';
|
import LoadingScreen from '@/spa/components/ui/LoadingScreen';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import {
|
||||||
|
defaultFooterData,
|
||||||
|
defaultHeaderData,
|
||||||
|
type SpaFooterData,
|
||||||
|
type SpaHeaderData,
|
||||||
|
type SpaIconName,
|
||||||
|
} from '@/spa/cmsNavigation';
|
||||||
|
|
||||||
interface LayoutProps { children: React.ReactNode; }
|
interface LayoutProps { children: React.ReactNode; header?: SpaHeaderData; footer?: SpaFooterData; }
|
||||||
|
|
||||||
/* ── SVG icons ─────────────────────────────────────────── */
|
/* ── SVG icons ─────────────────────────────────────────── */
|
||||||
const IgIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r=".8" fill="currentColor" stroke="none"/></svg>;
|
const IgIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r=".8" fill="currentColor" stroke="none"/></svg>;
|
||||||
const LiIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg>;
|
const LiIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg>;
|
||||||
const FbIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z"/></svg>;
|
const FbIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z"/></svg>;
|
||||||
const SOCIAL = [
|
const ICONS: Record<SpaIconName, () => React.ReactElement> = {
|
||||||
{ href:'https://instagram.com', Icon:IgIcon, label:'Instagram' },
|
instagram: IgIcon,
|
||||||
{ href:'https://linkedin.com', Icon:LiIcon, label:'LinkedIn' },
|
linkedin: LiIcon,
|
||||||
{ href:'https://facebook.com', Icon:FbIcon, label:'Facebook' },
|
facebook: FbIcon,
|
||||||
];
|
};
|
||||||
|
|
||||||
/* ── Nav data ──────────────────────────────────────────── */
|
|
||||||
interface NavItem {
|
|
||||||
key: string; label: string; path: string;
|
|
||||||
subLabel?: string;
|
|
||||||
sub?: { label: string; path: string }[];
|
|
||||||
}
|
|
||||||
const NAV: NavItem[] = [
|
|
||||||
{
|
|
||||||
key: 'preistraeger', label: 'Preisträger:innen', path: '/preistraeger',
|
|
||||||
subLabel: 'PREISTRÄGER:INNEN',
|
|
||||||
sub: [
|
|
||||||
{ label: 'Alle Preisträger:innen', path: '/preistraeger' },
|
|
||||||
{ label: 'Sieger & Aufsteiger 2025', path: '/preistraeger' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'teilnahme', label: 'Ihr Weg zum Preis', path: '/teilnahme',
|
|
||||||
subLabel: 'IHR WEG ZUM PREIS',
|
|
||||||
sub: [
|
|
||||||
{ label: 'Teilnahmevoraussetzungen', path: '/teilnahme#voraussetzungen' },
|
|
||||||
{ label: 'Kriterien & Bewertung', path: '/teilnahme#kriterien' },
|
|
||||||
{ label: 'Fristen & Termine', path: '/teilnahme#fristen' },
|
|
||||||
{ label: 'Bewerbungsformular', path: '/teilnahme#bewerben' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'bmp', label: 'Der BMP', path: '/der-bmp',
|
|
||||||
subLabel: 'DER BMP',
|
|
||||||
sub: [
|
|
||||||
{ label: 'Was ist der Preis?', path: '/der-bmp#was-ist' },
|
|
||||||
{ label: 'Geschichte & Werte', path: '/der-bmp#geschichte' },
|
|
||||||
{ label: 'Vorteile der Auszeichnung', path: '/der-bmp#vorteile' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'netzwerk', label: 'Netzwerk', path: '/netzwerk',
|
|
||||||
subLabel: 'NETZWERK',
|
|
||||||
sub: [
|
|
||||||
{ label: 'Jury-Mitglieder', path: '/netzwerk#jury' },
|
|
||||||
{ label: 'Partner & Sponsoren', path: '/netzwerk#partner' },
|
|
||||||
{ label: 'Schirmherrschaft', path: '/netzwerk#schirmherrschaft' },
|
|
||||||
{ label: 'Mitglied werden', path: '/mitglied-werden' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'presse', label: 'Presse', path: '/presse',
|
|
||||||
subLabel: 'PRESSE',
|
|
||||||
sub: [
|
|
||||||
{ label: 'Events & Gala', path: '/presse#events' },
|
|
||||||
{ label: 'Downloads & Impressionen', path: '/presse#downloads' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const SECONDARY = [
|
|
||||||
{ label:'Startseite', path:'/' },
|
|
||||||
{ label:'Kontakt', path:'/kontakt' },
|
|
||||||
{ label:'Impressum', path:'/impressum' },
|
|
||||||
{ label:'Datenschutz', path:'/datenschutz' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/* ── Tokens ────────────────────────────────────────────── */
|
/* ── Tokens ────────────────────────────────────────────── */
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
@@ -90,22 +37,32 @@ const FF_DISPLAY = '"IBM Plex Sans", sans-serif';
|
|||||||
const FF_BODY = '"Inter", sans-serif';
|
const FF_BODY = '"Inter", sans-serif';
|
||||||
|
|
||||||
/* ── SocialLink helper ─────────────────────────────────── */
|
/* ── SocialLink helper ─────────────────────────────────── */
|
||||||
const SocialLink: React.FC<{ href:string; Icon:()=>React.ReactElement; label:string }> = ({ href, Icon, label }) => (
|
const SocialLink: React.FC<{ href:string; icon:SpaIconName; label:string }> = ({ href, icon, label }) => {
|
||||||
|
const Icon = ICONS[icon] || LiIcon;
|
||||||
|
return (
|
||||||
<a href={href} target="_blank" rel="noopener noreferrer" aria-label={label}
|
<a href={href} target="_blank" rel="noopener noreferrer" aria-label={label}
|
||||||
style={{ color:MUTED, display:'flex', textDecoration:'none', transition:'color .2s' }}
|
style={{ color:MUTED, display:'flex', textDecoration:'none', transition:'color .2s' }}
|
||||||
onMouseEnter={e=>(e.currentTarget.style.color=GOLD)}
|
onMouseEnter={e=>(e.currentTarget.style.color=GOLD)}
|
||||||
onMouseLeave={e=>(e.currentTarget.style.color=MUTED)}
|
onMouseLeave={e=>(e.currentTarget.style.color=MUTED)}
|
||||||
><Icon /></a>
|
><Icon /></a>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/* ── Component ──────────────────────────────────────────── */
|
/* ── Component ──────────────────────────────────────────── */
|
||||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, footer = defaultFooterData }) => {
|
||||||
|
const NAV = header.navItems;
|
||||||
|
const SECONDARY = header.secondaryLinks;
|
||||||
|
const SOCIAL = header.socialLinks;
|
||||||
|
const logo = header.logo?.url || '/bmp-logo.png';
|
||||||
|
const footerLogo = footer.logo?.url || '/bmp-logo.png';
|
||||||
|
const headerCtas = header.ctas;
|
||||||
|
const footerCtas = footer.ctas;
|
||||||
const [open, setOpen] = useState(false); // desktop overlay
|
const [open, setOpen] = useState(false); // desktop overlay
|
||||||
const [activeKey, setActive] = useState<string | null>(null); // desktop hover subnav
|
const [activeKey, setActive] = useState<string | null>(null); // desktop hover subnav
|
||||||
const [mobileOpen, setMobileOpen] = useState(false); // mobile sidebar overlay
|
const [mobileOpen, setMobileOpen] = useState(false); // mobile sidebar overlay
|
||||||
const [expandKey, setExpand] = useState<string | null>(null); // mobile accordion
|
const [expandKey, setExpand] = useState<string | null>(null); // mobile accordion
|
||||||
const [lang, setLang] = useState<'DE'|'EN'>('DE');
|
const [lang, setLang] = useState(header.languages[0] || 'DE');
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [showLoader, setShowLoader] = useState(() => {
|
const [showLoader, setShowLoader] = useState(() => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
@@ -226,8 +183,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
<div style={{ flex:1, display:'flex', alignItems:'center', padding:'0 24px', height:'100%', borderRight:`1px solid ${LINE}` }}>
|
<div style={{ flex:1, display:'flex', alignItems:'center', padding:'0 24px', height:'100%', borderRight:`1px solid ${LINE}` }}>
|
||||||
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
||||||
<img
|
<img
|
||||||
src="/bmp-logo.png"
|
src={logo}
|
||||||
alt="Der Bayerische Mittelstandspreis 2026"
|
alt={header.logo?.alt || header.overlayLogoAlt}
|
||||||
style={{ height:38, width:'auto', display:'block', objectFit:'contain' }}
|
style={{ height:38, width:'auto', display:'block', objectFit:'contain' }}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -239,7 +196,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setOpen(v => !v)}
|
onClick={() => setOpen(v => !v)}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-label="Navigation öffnen"
|
aria-label={header.openLabel}
|
||||||
style={{ display:'flex', alignItems:'center', gap:14, background:'none', border:'none', cursor:'pointer', padding:'8px 16px' }}
|
style={{ display:'flex', alignItems:'center', gap:14, background:'none', border:'none', cursor:'pointer', padding:'8px 16px' }}
|
||||||
>
|
>
|
||||||
<div style={{ width:20, height:15, position:'relative' }}>
|
<div style={{ width:20, height:15, position:'relative' }}>
|
||||||
@@ -247,7 +204,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
<span style={{ position:'absolute', top:'50%', left:0, width:'100%', height:1.5, background:MUTED, marginTop:'-0.75px', transition:'opacity .3s', opacity: open ? 0 : 1 }} />
|
<span style={{ position:'absolute', top:'50%', left:0, width:'100%', height:1.5, background:MUTED, marginTop:'-0.75px', transition:'opacity .3s', opacity: open ? 0 : 1 }} />
|
||||||
<span style={{ position:'absolute', bottom:0, left:0, width:'100%', height:1.5, background:MUTED, transformOrigin:'center', transition:'transform .3s ease', transform: open ? 'translateY(-6.5px) rotate(-45deg)' : 'none' }} />
|
<span style={{ position:'absolute', bottom:0, left:0, width:'100%', height:1.5, background:MUTED, transformOrigin:'center', transition:'transform .3s ease', transform: open ? 'translateY(-6.5px) rotate(-45deg)' : 'none' }} />
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize:9, fontWeight:600, textTransform:'uppercase', letterSpacing:'.22em', color:MUTED, lineHeight:1, fontFamily:FF_DISPLAY }}>Menü</span>
|
<span style={{ fontSize:9, fontWeight:600, textTransform:'uppercase', letterSpacing:'.22em', color:MUTED, lineHeight:1, fontFamily:FF_DISPLAY }}>{header.menuLabel}</span>
|
||||||
</button>
|
</button>
|
||||||
<span style={{ fontSize:8, color:'rgba(5,26,135,0.5)', letterSpacing:2, userSelect:'none' }}>◆</span>
|
<span style={{ fontSize:8, color:'rgba(5,26,135,0.5)', letterSpacing:2, userSelect:'none' }}>◆</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,7 +216,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ width:1, height:20, background:'rgba(239,191,4,0.3)' }} />
|
<div style={{ width:1, height:20, background:'rgba(239,191,4,0.3)' }} />
|
||||||
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
||||||
{(['DE','EN'] as const).map((l, i) => (
|
{header.languages.map((l, i) => (
|
||||||
<React.Fragment key={l}>
|
<React.Fragment key={l}>
|
||||||
{i===1 && <span style={{ color:'rgba(239,191,4,.35)', fontSize: 14 }}>|</span>}
|
{i===1 && <span style={{ color:'rgba(239,191,4,.35)', fontSize: 14 }}>|</span>}
|
||||||
<button
|
<button
|
||||||
@@ -292,14 +249,14 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
<div style={{ flex:'1 1 50%', display:'flex', alignItems:'flex-end', padding:'0 80px 48px', position:'relative', minHeight:0, zIndex:1 }}>
|
<div style={{ flex:'1 1 50%', display:'flex', alignItems:'flex-end', padding:'0 80px 48px', position:'relative', minHeight:0, zIndex:1 }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize:12, color:'rgba(255,255,255,.4)', letterSpacing:'.12em', textTransform:'uppercase', marginBottom:12, fontFamily:FF_DISPLAY }}>
|
<div style={{ fontSize:12, color:'rgba(255,255,255,.4)', letterSpacing:'.12em', textTransform:'uppercase', marginBottom:12, fontFamily:FF_DISPLAY }}>
|
||||||
Bayerischer Mittelstandspreis
|
{header.overlayKicker}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize:'clamp(42px,6.5vw,88px)', fontWeight:900, color:'#fff', textTransform:'uppercase', lineHeight:.95, letterSpacing:'-.01em', fontFamily:FF_DISPLAY }}>
|
<div style={{ fontSize:'clamp(42px,6.5vw,88px)', fontWeight:900, color:'#fff', textTransform:'uppercase', lineHeight:.95, letterSpacing:'-.01em', fontFamily:FF_DISPLAY }}>
|
||||||
EXZELLENZ HAT<br/>EINEN NAMEN.
|
{header.overlayHeadline.split('\n').map((line, index) => <React.Fragment key={line + index}>{index > 0 && <br />}{line}</React.Fragment>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position:'absolute', top:48, right:80 }}>
|
<div style={{ position:'absolute', top:48, right:80 }}>
|
||||||
<img src="/bmp-logo.png" alt="BMP Logo" style={{ height:40, width:'auto', display:'block', opacity:0.9 }} />
|
<img src={logo} alt={header.overlayLogoAlt} style={{ height:40, width:'auto', display:'block', opacity:0.9 }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -359,7 +316,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
pointerEvents: activeKey===item.key ? 'all' : 'none',
|
pointerEvents: activeKey===item.key ? 'all' : 'none',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'.22em', color:'#009EE0', marginBottom:16, fontFamily:FF_DISPLAY }}>
|
<div style={{ fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'.22em', color:'#009EE0', marginBottom:16, fontFamily:FF_DISPLAY }}>
|
||||||
Überblick
|
{header.overviewLabel}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'.1em', color:'#fff', marginBottom:20, fontFamily:FF_DISPLAY }}>
|
<div style={{ fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'.1em', color:'#fff', marginBottom:20, fontFamily:FF_DISPLAY }}>
|
||||||
{item.subLabel}
|
{item.subLabel}
|
||||||
@@ -385,7 +342,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
|
|
||||||
<div style={{ display:'flex', alignItems:'center', gap:20, flexWrap:'wrap' }}>
|
<div style={{ display:'flex', alignItems:'center', gap:20, flexWrap:'wrap' }}>
|
||||||
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
||||||
{(['DE','EN'] as const).map((l,i) => (
|
{header.languages.map((l,i) => (
|
||||||
<React.Fragment key={l}>
|
<React.Fragment key={l}>
|
||||||
{i===1 && <span style={{ color:'rgba(239,191,4,.35)', fontSize: 14 }}>|</span>}
|
{i===1 && <span style={{ color:'rgba(239,191,4,.35)', fontSize: 14 }}>|</span>}
|
||||||
<button onClick={() => setLang(l)}
|
<button onClick={() => setLang(l)}
|
||||||
@@ -412,20 +369,20 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display:'flex', flexDirection:'column', gap:10 }}>
|
<div style={{ display:'flex', flexDirection:'column', gap:10 }}>
|
||||||
<Link to="/teilnahme#bewerben" onClick={() => setOpen(false)}
|
<Link to={headerCtas[0]?.path || '/teilnahme#bewerben'} onClick={() => setOpen(false)}
|
||||||
style={{ display:'inline-block', background:'#EFBF04', color:'#101828', fontSize: 15, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', padding:'11px 24px', textDecoration:'none', transition:'background .2s, box-shadow .2s', borderRadius:0, fontFamily:FF_DISPLAY }}
|
style={{ display:'inline-block', background:'#EFBF04', color:'#101828', fontSize: 15, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', padding:'11px 24px', textDecoration:'none', transition:'background .2s, box-shadow .2s', borderRadius:0, fontFamily:FF_DISPLAY }}
|
||||||
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)'; }}
|
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='#EFBF04'; e.currentTarget.style.boxShadow='none'; }}
|
onMouseLeave={e=>{ e.currentTarget.style.background='#EFBF04'; e.currentTarget.style.boxShadow='none'; }}
|
||||||
>Jetzt bewerben</Link>
|
>{headerCtas[0]?.label || 'Jetzt bewerben'}</Link>
|
||||||
<Link to="/mitglied-werden" onClick={() => setOpen(false)}
|
<Link to={headerCtas[1]?.path || '/mitglied-werden'} onClick={() => setOpen(false)}
|
||||||
style={{ display:'inline-block', background:'transparent', color:MUTED, fontSize: 15, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', padding:'10px 24px', textDecoration:'none', transition:'border-color .2s, color .2s', borderRadius:0, fontFamily:FF_DISPLAY, border:'1.5px solid rgba(239,191,4,0.35)' }}
|
style={{ display:'inline-block', background:'transparent', color:MUTED, fontSize: 15, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', padding:'10px 24px', textDecoration:'none', transition:'border-color .2s, color .2s', borderRadius:0, fontFamily:FF_DISPLAY, border:'1.5px solid rgba(239,191,4,0.35)' }}
|
||||||
onMouseEnter={e=>{ e.currentTarget.style.borderColor='#EFBF04'; e.currentTarget.style.color='#EFBF04'; }}
|
onMouseEnter={e=>{ e.currentTarget.style.borderColor='#EFBF04'; e.currentTarget.style.color='#EFBF04'; }}
|
||||||
onMouseLeave={e=>{ e.currentTarget.style.borderColor='rgba(239,191,4,0.35)'; e.currentTarget.style.color=MUTED; }}
|
onMouseLeave={e=>{ e.currentTarget.style.borderColor='rgba(239,191,4,0.35)'; e.currentTarget.style.color=MUTED; }}
|
||||||
>Mitglied werden</Link>
|
>{headerCtas[1]?.label || 'Mitglied werden'}</Link>
|
||||||
|
|
||||||
<div style={{ height:'1px', background:'rgba(239,191,4,0.12)', margin:'4px 0' }} />
|
<div style={{ height:'1px', background:'rgba(239,191,4,0.12)', margin:'4px 0' }} />
|
||||||
|
|
||||||
<Link to="/formular-hochladen" onClick={() => setOpen(false)}
|
<Link to={headerCtas[2]?.path || '/formular-hochladen'} onClick={() => setOpen(false)}
|
||||||
style={{
|
style={{
|
||||||
display:'flex', alignItems:'center', gap:10,
|
display:'flex', alignItems:'center', gap:10,
|
||||||
background:'rgba(255,255,255,0.04)',
|
background:'rgba(255,255,255,0.04)',
|
||||||
@@ -449,7 +406,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
e.currentTarget.style.background='rgba(255,255,255,0.04)';
|
e.currentTarget.style.background='rgba(255,255,255,0.04)';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Upload size={12} strokeWidth={2} /> Formular hochladen
|
<Upload size={12} strokeWidth={2} /> {headerCtas[2]?.label || 'Formular hochladen'}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -478,14 +435,14 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
}}>
|
}}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
||||||
<img src="/bmp-logo.png" alt="Bayerischer Mittelstandspreis 2026" style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
<img src={logo} alt={header.logo?.alt || header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Hamburger */}
|
{/* Hamburger */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileOpen(true)}
|
onClick={() => setMobileOpen(true)}
|
||||||
aria-expanded={mobileOpen}
|
aria-expanded={mobileOpen}
|
||||||
aria-label="Navigation öffnen"
|
aria-label={header.openLabel}
|
||||||
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}
|
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}
|
||||||
>
|
>
|
||||||
<div style={{ width:24, position:'relative', display:'flex', flexDirection:'column', gap:5 }}>
|
<div style={{ width:24, position:'relative', display:'flex', flexDirection:'column', gap:5 }}>
|
||||||
@@ -517,14 +474,14 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
textTransform:'uppercase', letterSpacing:'-.02em',
|
textTransform:'uppercase', letterSpacing:'-.02em',
|
||||||
color:'rgba(255,255,255,.04)',
|
color:'rgba(255,255,255,.04)',
|
||||||
pointerEvents:'none', userSelect:'none', zIndex:0,
|
pointerEvents:'none', userSelect:'none', zIndex:0,
|
||||||
}}>Exzellenz<br/>seit 2005</div>
|
}}>{header.mobileWatermark.split('\n').map((line, index) => <React.Fragment key={line + index}>{index > 0 && <br />}{line}</React.Fragment>)}</div>
|
||||||
|
|
||||||
{/* Header row: logo + close */}
|
{/* Header row: logo + close */}
|
||||||
<div style={{ position:'relative', zIndex:1, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 8px 0 20px', height:60, borderBottom:`1px solid ${SEP}`, flexShrink:0 }}>
|
<div style={{ position:'relative', zIndex:1, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 8px 0 20px', height:60, borderBottom:`1px solid ${SEP}`, flexShrink:0 }}>
|
||||||
<Link to="/" onClick={() => setMobileOpen(false)} style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
<Link to="/" onClick={() => setMobileOpen(false)} style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
||||||
<img src="/bmp-logo.png" alt="BMP Logo" style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
<img src={logo} alt={header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
||||||
</Link>
|
</Link>
|
||||||
<button onClick={() => setMobileOpen(false)} aria-label="Menü schließen"
|
<button onClick={() => setMobileOpen(false)} aria-label={header.closeLabel}
|
||||||
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}>
|
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}>
|
||||||
<div style={{ width:26, height:26, position:'relative', display:'flex', alignItems:'center', justifyContent:'center' }}>
|
<div style={{ width:26, height:26, position:'relative', display:'flex', alignItems:'center', justifyContent:'center' }}>
|
||||||
<span style={{ position:'absolute', width:22, height:2, background:MUTED, borderRadius:2, transform:'rotate(45deg)' }} />
|
<span style={{ position:'absolute', width:22, height:2, background:MUTED, borderRadius:2, transform:'rotate(45deg)' }} />
|
||||||
@@ -589,15 +546,15 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
|
|
||||||
{/* CTAs */}
|
{/* CTAs */}
|
||||||
<div style={{ padding:'24px 24px 8px', display:'flex', flexDirection:'column', gap:12 }}>
|
<div style={{ padding:'24px 24px 8px', display:'flex', flexDirection:'column', gap:12 }}>
|
||||||
<Link to="/teilnahme#bewerben" onClick={() => setMobileOpen(false)}
|
<Link to={headerCtas[0]?.path || '/teilnahme#bewerben'} onClick={() => setMobileOpen(false)}
|
||||||
style={{ display:'flex', alignItems:'center', justifyContent:'center', minHeight:52, background:GOLD, color:'#101828', fontSize:14, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', textDecoration:'none', fontFamily:FF_DISPLAY }}
|
style={{ display:'flex', alignItems:'center', justifyContent:'center', minHeight:52, background:GOLD, color:'#101828', fontSize:14, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', textDecoration:'none', fontFamily:FF_DISPLAY }}
|
||||||
>Jetzt bewerben</Link>
|
>{headerCtas[0]?.label || 'Jetzt bewerben'}</Link>
|
||||||
<Link to="/mitglied-werden" onClick={() => setMobileOpen(false)}
|
<Link to={headerCtas[1]?.path || '/mitglied-werden'} onClick={() => setMobileOpen(false)}
|
||||||
style={{ display:'flex', alignItems:'center', justifyContent:'center', minHeight:50, background:'transparent', color:'#fff', fontSize:14, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', textDecoration:'none', fontFamily:FF_DISPLAY, border:`1.5px solid ${LINE}` }}
|
style={{ display:'flex', alignItems:'center', justifyContent:'center', minHeight:50, background:'transparent', color:'#fff', fontSize:14, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', textDecoration:'none', fontFamily:FF_DISPLAY, border:`1.5px solid ${LINE}` }}
|
||||||
>Mitglied werden</Link>
|
>{headerCtas[1]?.label || 'Mitglied werden'}</Link>
|
||||||
<Link to="/formular-hochladen" onClick={() => setMobileOpen(false)}
|
<Link to={headerCtas[2]?.path || '/formular-hochladen'} onClick={() => setMobileOpen(false)}
|
||||||
style={{ display:'flex', alignItems:'center', justifyContent:'center', gap:8, minHeight:46, background:'none', color:'rgba(255,255,255,0.5)', fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'.14em', textDecoration:'none', fontFamily:FF_DISPLAY, border:'1px dashed rgba(255,255,255,0.2)' }}
|
style={{ display:'flex', alignItems:'center', justifyContent:'center', gap:8, minHeight:46, background:'none', color:'rgba(255,255,255,0.5)', fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'.14em', textDecoration:'none', fontFamily:FF_DISPLAY, border:'1px dashed rgba(255,255,255,0.2)' }}
|
||||||
><Upload size={13} strokeWidth={2} /> Formular hochladen</Link>
|
><Upload size={13} strokeWidth={2} /> {headerCtas[2]?.label || 'Formular hochladen'}</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer zone: lang + social + secondary */}
|
{/* Footer zone: lang + social + secondary */}
|
||||||
@@ -605,7 +562,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
{/* Lang + social row */}
|
{/* Lang + social row */}
|
||||||
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between' }}>
|
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between' }}>
|
||||||
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
|
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
|
||||||
{(['DE','EN'] as const).map((l, i) => (
|
{header.languages.map((l, i) => (
|
||||||
<React.Fragment key={l}>
|
<React.Fragment key={l}>
|
||||||
{i === 1 && <span style={{ color:LINE, fontSize:13 }}>|</span>}
|
{i === 1 && <span style={{ color:LINE, fontSize:13 }}>|</span>}
|
||||||
<button onClick={() => setLang(l)}
|
<button onClick={() => setLang(l)}
|
||||||
@@ -652,7 +609,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#101828'; }}
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#101828'; }}
|
||||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#111D55'; (e.currentTarget as HTMLElement).style.color = '#fff'; }}
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#111D55'; (e.currentTarget as HTMLElement).style.color = '#fff'; }}
|
||||||
>
|
>
|
||||||
Jetzt bewerben <ArrowRight size={13} />
|
{headerCtas[0]?.label || 'Jetzt bewerben'} <ArrowRight size={13} />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -693,7 +650,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
whiteSpace: isMobile ? 'normal' : 'nowrap',
|
whiteSpace: isMobile ? 'normal' : 'nowrap',
|
||||||
}}>
|
}}>
|
||||||
Der Preis des <span style={{ color: GOLD }}>Bayerischen</span> Mittelstands
|
{footer.headlinePrefix} <span style={{ color: GOLD }}>{footer.headlineAccent}</span> {footer.headlineSuffix}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Gold rule beneath headline */}
|
{/* Gold rule beneath headline */}
|
||||||
@@ -711,7 +668,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.35em',
|
letterSpacing: '0.35em',
|
||||||
color: 'rgba(239,191,4,0.55)',
|
color: 'rgba(239,191,4,0.55)',
|
||||||
}}>Exzellenz seit 2005</span>
|
}}>{footer.sinceLabel}</span>
|
||||||
<div style={{ flex: 1, height: 1, background: 'rgba(239,191,4,0.18)' }} />
|
<div style={{ flex: 1, height: 1, background: 'rgba(239,191,4,0.18)' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -741,7 +698,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
}}>2026</div>
|
}}>{footer.year}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── LEFT COLUMN: copy + CTAs ── */}
|
{/* ── LEFT COLUMN: copy + CTAs ── */}
|
||||||
@@ -753,8 +710,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
}}>
|
}}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<img
|
<img
|
||||||
src="/bmp-logo.png"
|
src={footerLogo}
|
||||||
alt="Bayerischer Mittelstandspreis"
|
alt={footer.logo?.alt || footer.logoAlt}
|
||||||
style={{ width: 140, height: 'auto', marginBottom: 10, opacity: 0.9 }}
|
style={{ width: 140, height: 'auto', marginBottom: 10, opacity: 0.9 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -766,7 +723,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
margin: '0 0 16px 0',
|
margin: '0 0 16px 0',
|
||||||
maxWidth: 300,
|
maxWidth: 300,
|
||||||
}}>
|
}}>
|
||||||
Die renommierteste Auszeichnung für herausragende Unternehmen des bayerischen Mittelstands.
|
{footer.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* CTA stack */}
|
{/* CTA stack */}
|
||||||
@@ -777,7 +734,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
}}>
|
}}>
|
||||||
<Link
|
<Link
|
||||||
to="/teilnahme#bewerben"
|
to={footerCtas[0]?.path || '/teilnahme#bewerben'}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: FF_DISPLAY,
|
fontFamily: FF_DISPLAY,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -800,10 +757,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
e.currentTarget.style.background = GOLD;
|
e.currentTarget.style.background = GOLD;
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
}}
|
}}
|
||||||
>Kostenlos bewerben</Link>
|
>{footerCtas[0]?.label || 'Kostenlos bewerben'}</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/mitglied-werden"
|
to={footerCtas[1]?.path || '/mitglied-werden'}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: FF_DISPLAY,
|
fontFamily: FF_DISPLAY,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -826,10 +783,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
e.currentTarget.style.borderColor = 'rgba(239,191,4,0.35)';
|
e.currentTarget.style.borderColor = 'rgba(239,191,4,0.35)';
|
||||||
e.currentTarget.style.color = MUTED;
|
e.currentTarget.style.color = MUTED;
|
||||||
}}
|
}}
|
||||||
>Mitglied werden</Link>
|
>{footerCtas[1]?.label || 'Mitglied werden'}</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/netzwerk#sponsoring"
|
to={footerCtas[2]?.path || '/netzwerk#sponsoring'}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: FF_DISPLAY,
|
fontFamily: FF_DISPLAY,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -843,7 +800,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
|
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
|
||||||
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.4)'; }}
|
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.4)'; }}
|
||||||
>Sponsoring-Anfrage →</Link>
|
>{footerCtas[2]?.label || 'Sponsoring-Anfrage →'}</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -868,8 +825,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
{/* BMP 2026 column */}
|
{footer.linkColumns.map((column, index) => (
|
||||||
<div style={{ minWidth: isMobile ? 'auto' : 160 }}>
|
<React.Fragment key={column.label + index}>
|
||||||
|
<div style={{ minWidth: isMobile ? 'auto' : index === footer.linkColumns.length - 1 ? 120 : 160 }}>
|
||||||
<p style={{
|
<p style={{
|
||||||
fontFamily: FF_DISPLAY,
|
fontFamily: FF_DISPLAY,
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
@@ -878,16 +836,12 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
letterSpacing: '0.28em',
|
letterSpacing: '0.28em',
|
||||||
color: 'rgba(255,255,255,0.3)',
|
color: 'rgba(255,255,255,0.3)',
|
||||||
margin: '0 0 10px 0',
|
margin: '0 0 10px 0',
|
||||||
}}>BMP 2026</p>
|
}}>{column.label}</p>
|
||||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{[
|
{column.links.map(item => (
|
||||||
{ l: 'Startseite', p: '/' },
|
<li key={item.path + item.label}>
|
||||||
{ l: 'Preisträger', p: '/preistraeger' },
|
|
||||||
{ l: 'Teilnahme', p: '/teilnahme' },
|
|
||||||
].map(item => (
|
|
||||||
<li key={item.p}>
|
|
||||||
<Link
|
<Link
|
||||||
to={item.p}
|
to={item.path}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: FF_BODY,
|
fontFamily: FF_BODY,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -898,14 +852,13 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
|
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
|
||||||
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.55)'; }}
|
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.55)'; }}
|
||||||
>{item.l}</Link>
|
>{item.label}</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gold vertical separator – desktop only */}
|
{!isMobile && index < footer.linkColumns.length - 1 && (
|
||||||
{!isMobile && (
|
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 1,
|
width: 1,
|
||||||
alignSelf: 'stretch',
|
alignSelf: 'stretch',
|
||||||
@@ -914,89 +867,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}} />
|
}} />
|
||||||
)}
|
)}
|
||||||
|
</React.Fragment>
|
||||||
{/* Engagement column */}
|
|
||||||
<div style={{ minWidth: isMobile ? 'auto' : 160 }}>
|
|
||||||
<p style={{
|
|
||||||
fontFamily: FF_DISPLAY,
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.28em',
|
|
||||||
color: 'rgba(255,255,255,0.3)',
|
|
||||||
margin: '0 0 10px 0',
|
|
||||||
}}>Engagement</p>
|
|
||||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
||||||
{[
|
|
||||||
{ l: 'Netzwerk & Jury', p: '/netzwerk' },
|
|
||||||
{ l: 'Presse', p: '/presse' },
|
|
||||||
{ l: 'Kontakt', p: '/kontakt' },
|
|
||||||
].map(item => (
|
|
||||||
<li key={item.p}>
|
|
||||||
<Link
|
|
||||||
to={item.p}
|
|
||||||
style={{
|
|
||||||
fontFamily: FF_BODY,
|
|
||||||
fontSize: 16,
|
|
||||||
color: 'rgba(255,255,255,0.55)',
|
|
||||||
textDecoration: 'none',
|
|
||||||
transition: 'color 0.2s',
|
|
||||||
display: 'inline-block',
|
|
||||||
}}
|
|
||||||
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
|
|
||||||
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.55)'; }}
|
|
||||||
>{item.l}</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Gold vertical separator – desktop only */}
|
|
||||||
{!isMobile && (
|
|
||||||
<div style={{
|
|
||||||
width: 1,
|
|
||||||
alignSelf: 'stretch',
|
|
||||||
background: 'rgba(239,191,4,0.22)',
|
|
||||||
margin: '0 40px',
|
|
||||||
flexShrink: 0,
|
|
||||||
}} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Legal column */}
|
|
||||||
<div style={{ minWidth: isMobile ? 'auto' : 120 }}>
|
|
||||||
<p style={{
|
|
||||||
fontFamily: FF_DISPLAY,
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.28em',
|
|
||||||
color: 'rgba(255,255,255,0.3)',
|
|
||||||
margin: '0 0 10px 0',
|
|
||||||
}}>Legal</p>
|
|
||||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
||||||
{[
|
|
||||||
{ l: 'Datenschutz', p: '/datenschutz' },
|
|
||||||
{ l: 'Impressum', p: '/impressum' },
|
|
||||||
].map(item => (
|
|
||||||
<li key={item.p}>
|
|
||||||
<Link
|
|
||||||
to={item.p}
|
|
||||||
style={{
|
|
||||||
fontFamily: FF_BODY,
|
|
||||||
fontSize: 16,
|
|
||||||
color: 'rgba(255,255,255,0.55)',
|
|
||||||
textDecoration: 'none',
|
|
||||||
transition: 'color 0.2s',
|
|
||||||
display: 'inline-block',
|
|
||||||
}}
|
|
||||||
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
|
|
||||||
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.55)'; }}
|
|
||||||
>{item.l}</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1019,18 +891,18 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
letterSpacing: '0.32em',
|
letterSpacing: '0.32em',
|
||||||
color: 'rgba(255,255,255,0.22)',
|
color: 'rgba(255,255,255,0.22)',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>Partner</span>
|
}}>{footer.partnerLabel}</span>
|
||||||
<div style={{ width: 1, height: 20, background: 'rgba(239,191,4,0.2)', flexShrink: 0 }} />
|
<div style={{ width: 1, height: 20, background: 'rgba(239,191,4,0.2)', flexShrink: 0 }} />
|
||||||
|
{footer.partnerLogos.map((partner, index) => (
|
||||||
|
partner.image?.url ? (
|
||||||
<img
|
<img
|
||||||
src="/images/ewif-logo.png"
|
key={`${partner.alt}-${index}`}
|
||||||
alt="EWIF – Europäisches Wirtschaftsforum"
|
src={partner.image.url}
|
||||||
style={{ height: 32, width: 'auto', opacity: 0.5, filter: 'brightness(2)', display: 'block' }}
|
alt={partner.image.alt || partner.alt}
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="/images/partner-logos.png"
|
|
||||||
alt="Hochschule für angewandtes Management · Gipfeldialog Altaussee"
|
|
||||||
style={{ height: 32, width: 'auto', opacity: 0.5, filter: 'brightness(2)', display: 'block' }}
|
style={{ height: 32, width: 'auto', opacity: 0.5, filter: 'brightness(2)', display: 'block' }}
|
||||||
/>
|
/>
|
||||||
|
) : null
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── BOTTOM BAR ── */}
|
{/* ── BOTTOM BAR ── */}
|
||||||
@@ -1051,7 +923,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
color: 'rgba(255,255,255,0.3)',
|
color: 'rgba(255,255,255,0.3)',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
}}>
|
}}>
|
||||||
© 2026 Bayerischer Mittelstandspreis · Alle Rechte vorbehalten
|
{footer.copyright}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
|
||||||
{SOCIAL.map(s => <SocialLink key={s.label} {...s} />)}
|
{SOCIAL.map(s => <SocialLink key={s.label} {...s} />)}
|
||||||
|
|||||||
Reference in New Issue
Block a user