chore: fix linting errors and ts bugs.

This commit is contained in:
syntaxbullet
2026-06-17 17:41:21 +02:00
parent 3e04a3c7e3
commit 51494e25a8
41 changed files with 285 additions and 251 deletions

View File

@@ -1,17 +1,27 @@
import { dirname } from 'path' import nextVitals from 'eslint-config-next/core-web-vitals'
import { fileURLToPath } from 'url' import tseslint from 'typescript-eslint'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
const eslintConfig = [ const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'), ...nextVitals,
{ {
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
react: nextVitals[0].plugins.react,
'react-hooks': nextVitals[0].plugins['react-hooks'],
},
rules: {
'react/no-unescaped-entities': 'warn',
'react-hooks/immutability': 'warn',
'react-hooks/refs': 'warn',
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/static-components': 'warn',
},
},
{
files: ['**/*.{ts,tsx,mts,cts}'],
plugins: {
'@typescript-eslint': tseslint.plugin,
},
rules: { rules: {
'@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn', '@typescript-eslint/no-empty-object-type': 'warn',

View File

@@ -2,7 +2,7 @@
import { useHeaderTheme } from '@/providers/HeaderTheme' import { useHeaderTheme } from '@/providers/HeaderTheme'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import React, { useEffect, useState } from 'react' import React, { useEffect } from 'react'
import type { Header } from '@/payload-types' import type { Header } from '@/payload-types'
@@ -14,20 +14,13 @@ interface HeaderClientProps {
} }
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => { export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
/* Storing the value in a useState to avoid hydration errors */
const [theme, setTheme] = useState<string | null>(null)
const { headerTheme, setHeaderTheme } = useHeaderTheme() const { headerTheme, setHeaderTheme } = useHeaderTheme()
const pathname = usePathname() const pathname = usePathname()
const theme = headerTheme || null
useEffect(() => { useEffect(() => {
setHeaderTheme(null) setHeaderTheme(null)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname, setHeaderTheme])
}, [pathname])
useEffect(() => {
if (headerTheme && headerTheme !== theme) setTheme(headerTheme)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [headerTheme])
return ( return (
<header className="container relative z-20 " {...(theme ? { 'data-theme': theme } : {})}> <header className="container relative z-20 " {...(theme ? { 'data-theme': theme } : {})}>

View File

@@ -6,6 +6,7 @@ import { notFound } from 'next/navigation'
import NextApp from '@/spa/NextApp' import NextApp from '@/spa/NextApp'
import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation' import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation'
import type { CmsRouteDoc } from '@/spa/cmsRoute'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
@@ -36,7 +37,7 @@ function routeTarget(pathname: string): { collection: RouteCollection; slug: str
async function findRouteDoc(payload: Awaited<ReturnType<typeof getPayload>>, pathname: string, draft: boolean) { async function findRouteDoc(payload: Awaited<ReturnType<typeof getPayload>>, pathname: string, draft: boolean) {
const target = routeTarget(pathname) const target = routeTarget(pathname)
const result = await payload.find({ const result = await payload.find({
collection: target.collection as any, collection: target.collection,
depth: 1, depth: 1,
draft, draft,
overrideAccess: draft, overrideAccess: draft,
@@ -56,7 +57,7 @@ async function findRouteDoc(payload: Awaited<ReturnType<typeof getPayload>>, pat
async function findList(payload: Awaited<ReturnType<typeof getPayload>>, collection: RouteCollection, draft: boolean) { async function findList(payload: Awaited<ReturnType<typeof getPayload>>, collection: RouteCollection, draft: boolean) {
const result = await payload.find({ const result = await payload.find({
collection: collection as any, collection,
depth: 1, depth: 1,
draft, draft,
overrideAccess: draft, overrideAccess: draft,
@@ -90,11 +91,11 @@ export default async function Page({ params }: Args) {
footer={normalizeFooterData(footer)} footer={normalizeFooterData(footer)}
cmsRoute={{ cmsRoute={{
collection: route.collection, collection: route.collection,
doc: route.doc as any, doc: route.doc as unknown as CmsRouteDoc,
lists: { lists: {
preistraeger: preistraeger as any, preistraeger: preistraeger as unknown as CmsRouteDoc[],
events: events as any, events: events as unknown as CmsRouteDoc[],
posts: posts as any, posts: posts as unknown as CmsRouteDoc[],
}, },
}} }}
/> />

View File

@@ -131,8 +131,9 @@ export const FormBlock: React.FC<
{formFromProps && {formFromProps &&
formFromProps.fields && formFromProps.fields &&
formFromProps.fields?.map((field, index) => { formFromProps.fields?.map((field, index) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const Field = fields?.[
const Field: React.FC<any> = fields?.[field.blockType as keyof typeof fields] field.blockType as keyof typeof fields
] as React.ComponentType<Record<string, unknown>> | undefined
if (Field) { if (Field) {
return ( return (
<div className="mb-6 last:mb-0" key={index}> <div className="mb-6 last:mb-0" key={index}>

View File

@@ -31,14 +31,14 @@ export const Events: CollectionConfig = {
url: ({ data, req }) => url: ({ data, req }) =>
generatePreviewPath({ generatePreviewPath({
slug: data?.spaPath || data?.slug, slug: data?.spaPath || data?.slug,
collection: 'events' as any, collection: 'events',
req, req,
}), }),
}, },
preview: (data, { req }) => preview: (data, { req }) =>
generatePreviewPath({ generatePreviewPath({
slug: ((data?.spaPath as string | undefined) || (data?.slug as string)), slug: ((data?.spaPath as string | undefined) || (data?.slug as string)),
collection: 'events' as any, collection: 'events',
req, req,
}), }),
useAsTitle: 'title', useAsTitle: 'title',

View File

@@ -5,7 +5,7 @@ import { User } from 'src/payload-types'
// This means that we need to populate the authors manually here to protect user privacy // This means that we need to populate the authors manually here to protect user privacy
// GraphQL will not return mutated user data that differs from the underlying schema // GraphQL will not return mutated user data that differs from the underlying schema
// So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI // So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => { export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => {
if (doc?.authors && doc?.authors?.length > 0) { if (doc?.authors && doc?.authors?.length > 0) {
const authorDocs: User[] = [] const authorDocs: User[] = []

View File

@@ -31,14 +31,14 @@ export const Preistraeger: CollectionConfig = {
url: ({ data, req }) => url: ({ data, req }) =>
generatePreviewPath({ generatePreviewPath({
slug: data?.spaPath || data?.slug, slug: data?.spaPath || data?.slug,
collection: 'preistraeger' as any, collection: 'preistraeger',
req, req,
}), }),
}, },
preview: (data, { req }) => preview: (data, { req }) =>
generatePreviewPath({ generatePreviewPath({
slug: ((data?.spaPath as string | undefined) || (data?.slug as string)), slug: ((data?.spaPath as string | undefined) || (data?.slug as string)),
collection: 'preistraeger' as any, collection: 'preistraeger',
req, req,
}), }),
useAsTitle: 'title', useAsTitle: 'title',

View File

@@ -18,7 +18,10 @@ export const Card: React.FC<{
showCategories?: boolean showCategories?: boolean
title?: string title?: string
}> = (props) => { }> = (props) => {
const { card, link } = useClickableCard({}) const {
card: { ref: cardRef },
link: { ref: linkRef },
} = useClickableCard({})
const { className, doc, relationTo, showCategories, title: titleFromProps } = props const { className, doc, relationTo, showCategories, title: titleFromProps } = props
const { slug, categories, meta, title } = doc || {} const { slug, categories, meta, title } = doc || {}
@@ -35,7 +38,7 @@ export const Card: React.FC<{
'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer', 'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer',
className, className,
)} )}
ref={card.ref} ref={cardRef}
> >
<div className="relative w-full "> <div className="relative w-full ">
{!metaImage && <div className="">No image</div>} {!metaImage && <div className="">No image</div>}
@@ -67,7 +70,7 @@ export const Card: React.FC<{
{titleToUse && ( {titleToUse && (
<div className="prose"> <div className="prose">
<h3> <h3>
<Link className="not-prose" href={href} ref={link.ref}> <Link className="not-prose" href={href} ref={linkRef}>
{titleToUse} {titleToUse}
</Link> </Link>
</h3> </h3>

View File

@@ -1,5 +1,6 @@
import clsx from 'clsx' import clsx from 'clsx'
import React from 'react' import React from 'react'
import Image from '@/spa/components/ui/UnoptimizedImage'
interface Props { interface Props {
className?: string className?: string
@@ -14,8 +15,7 @@ export const Logo = (props: Props) => {
const priority = priorityFromProps || 'low' const priority = priorityFromProps || 'low'
return ( return (
/* eslint-disable @next/next/no-img-element */ <Image unoptimized
<img
alt="Payload Logo" alt="Payload Logo"
width={193} width={193}
height={34} height={34}

View File

@@ -201,7 +201,7 @@ export const seed = async ({
payload.logger.info(`— Seeding pages...`) payload.logger.info(`— Seeding pages...`)
const [_, contactPage] = await Promise.all([ await Promise.all([
payload.create({ payload.create({
collection: 'pages', collection: 'pages',
depth: 0, depth: 0,
@@ -222,11 +222,11 @@ export const seed = async ({
await Promise.all([ await Promise.all([
payload.updateGlobal({ payload.updateGlobal({
slug: 'header', slug: 'header',
data: headerSeedData as any, data: headerSeedData as never,
}), }),
payload.updateGlobal({ payload.updateGlobal({
slug: 'footer', slug: 'footer',
data: footerSeedData as any, data: footerSeedData as never,
}), }),
]) ])

View File

@@ -16,7 +16,9 @@ import { themeLocalStorageKey } from './types'
export const ThemeSelector: React.FC = () => { export const ThemeSelector: React.FC = () => {
const { setTheme } = useTheme() const { setTheme } = useTheme()
const [value, setValue] = useState('') const [value, setValue] = useState(() =>
typeof window === 'undefined' ? 'auto' : window.localStorage.getItem(themeLocalStorageKey) ?? 'auto',
)
const onThemeChange = (themeToSet: Theme & 'auto') => { const onThemeChange = (themeToSet: Theme & 'auto') => {
if (themeToSet === 'auto') { if (themeToSet === 'auto') {
@@ -28,10 +30,6 @@ export const ThemeSelector: React.FC = () => {
} }
} }
React.useEffect(() => {
const preference = window.localStorage.getItem(themeLocalStorageKey)
setValue(preference ?? 'auto')
}, [])
return ( return (
<Select onValueChange={onThemeChange} value={value}> <Select onValueChange={onThemeChange} value={value}>

View File

@@ -16,9 +16,14 @@ const initialContext: ThemeContextType = {
const ThemeContext = createContext(initialContext) const ThemeContext = createContext(initialContext)
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setThemeState] = useState<Theme | undefined>( const [theme, setThemeState] = useState<Theme | undefined>(() => {
canUseDOM ? (document.documentElement.getAttribute('data-theme') as Theme) : undefined, if (!canUseDOM) return undefined
)
const preference = window.localStorage.getItem(themeLocalStorageKey)
if (themeIsValid(preference)) return preference
return getImplicitPreference() || defaultTheme
})
const setTheme = useCallback((themeToSet: Theme | null) => { const setTheme = useCallback((themeToSet: Theme | null) => {
if (themeToSet === null) { if (themeToSet === null) {
@@ -48,7 +53,6 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
} }
document.documentElement.setAttribute('data-theme', themeToSet) document.documentElement.setAttribute('data-theme', themeToSet)
setThemeState(themeToSet)
}, []) }, [])
return <ThemeContext value={{ setTheme, theme }}>{children}</ThemeContext> return <ThemeContext value={{ setTheme, theme }}>{children}</ThemeContext>

View File

@@ -3,7 +3,7 @@ import crypto from 'crypto'
import 'dotenv/config' import 'dotenv/config'
import fs from 'fs/promises' import fs from 'fs/promises'
import path from 'path' import path from 'path'
import { getPayload, type File } from 'payload' import { getPayload, type File, type Payload } from 'payload'
const root = process.cwd() const root = process.cwd()
const spaDir = path.join(root, 'src/spa') const spaDir = path.join(root, 'src/spa')
@@ -127,7 +127,7 @@ function altForSource(source: string) {
return basename.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ') return basename.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ')
} }
async function findExistingMedia(payload: any, filename: string) { async function findExistingMedia(payload: Payload, filename: string) {
const existing = await payload.find({ const existing = await payload.find({
collection: 'media', collection: 'media',
depth: 0, depth: 0,
@@ -169,7 +169,7 @@ async function main() {
assetId('/bmp-logo.png') assetId('/bmp-logo.png')
? payload.updateGlobal({ ? payload.updateGlobal({
slug: 'header', slug: 'header',
data: { logo: assetId('/bmp-logo.png') } as any, data: { logo: assetId('/bmp-logo.png') } as never,
context: { disableRevalidate: true }, context: { disableRevalidate: true },
}) })
: Promise.resolve(), : Promise.resolve(),
@@ -185,7 +185,7 @@ async function main() {
image: assetId('/images/partner-logos.png'), image: assetId('/images/partner-logos.png'),
}, },
].filter((item) => item.image), ].filter((item) => item.image),
} as any, } as never,
context: { disableRevalidate: true }, context: { disableRevalidate: true },
}) })
: Promise.resolve(), : Promise.resolve(),

View File

@@ -188,7 +188,7 @@ function dataForPage(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
async function findExistingDoc(payload: Payload, collection: TargetCollection, page: SpaPageRegistryEntry) { async function findExistingDoc(payload: Payload, collection: TargetCollection, page: SpaPageRegistryEntry) {
const existing = await payload.find({ const existing = await payload.find({
collection: collection as any, collection: collection as never,
depth: 0, depth: 0,
limit: 1, limit: 1,
pagination: false, pagination: false,
@@ -218,7 +218,7 @@ async function findExistingDoc(payload: Payload, collection: TargetCollection, p
}, },
}) })
return existing.docs[0] return existing.docs[0] as { id: string | number } | undefined
} }
async function removeMovedPageDoc(payload: Payload, page: SpaPageRegistryEntry) { async function removeMovedPageDoc(payload: Payload, page: SpaPageRegistryEntry) {
@@ -240,17 +240,17 @@ async function upsertPage(payload: Payload, page: SpaPageRegistryEntry, mediaMap
if (existing) { if (existing) {
await payload.update({ await payload.update({
collection: collection as any, collection: collection as never,
id: existing.id, id: existing.id,
data, data: data as never,
depth: 0, depth: 0,
context: { disableRevalidate: true }, context: { disableRevalidate: true },
}) })
payload.logger.info(`updated ${collection}: ${page.path}`) payload.logger.info(`updated ${collection}: ${page.path}`)
} else { } else {
await payload.create({ await payload.create({
collection: collection as any, collection: collection as never,
data, data: data as never,
depth: 0, depth: 0,
context: { disableRevalidate: true }, context: { disableRevalidate: true },
}) })

View File

@@ -87,14 +87,14 @@ export default function NextApp({
) )
} }
const Page = resolvePage(pathname) const page = React.createElement(resolvePage(pathname))
return ( return (
<> <>
<CmsRouteProvider value={cmsRoute}> <CmsRouteProvider value={cmsRoute}>
<ScrollToTop /> <ScrollToTop />
<Layout header={header} footer={footer}> <Layout header={header} footer={footer}>
<Page /> {page}
</Layout> </Layout>
</CmsRouteProvider> </CmsRouteProvider>
</> </>

View File

@@ -1,4 +1,4 @@
import type { Media } from '@/payload-types' import type { Footer, Header, Media } from '@/payload-types'
export type SpaIconName = 'instagram' | 'linkedin' | 'facebook' export type SpaIconName = 'instagram' | 'linkedin' | 'facebook'
export type SpaLink = { label: string; path: string } export type SpaLink = { label: string; path: string }
@@ -167,29 +167,50 @@ const mediaToSpa = (value: unknown, fallback?: SpaMedia): SpaMedia | undefined =
return fallback return fallback
} }
export function normalizeHeaderData(header: any): SpaHeaderData { type HeaderLanguage = string | { label?: string }
type FooterPartnerLogo = {
alt?: string
image?: unknown
}
export function normalizeHeaderData(header?: Partial<Header>): SpaHeaderData {
return { return {
...defaultHeaderData, ...defaultHeaderData,
...header, ...header,
logo: mediaToSpa(header?.logo, defaultHeaderData.logo), logo: mediaToSpa(header?.logo, defaultHeaderData.logo),
navItems: header?.navItems?.length ? header.navItems : defaultHeaderData.navItems, navItems: header?.navItems?.length
secondaryLinks: header?.secondaryLinks?.length ? header.secondaryLinks : defaultHeaderData.secondaryLinks, ? (header.navItems as unknown as SpaHeaderNavItem[])
socialLinks: header?.socialLinks?.length ? header.socialLinks : defaultHeaderData.socialLinks, : defaultHeaderData.navItems,
ctas: header?.ctas?.length ? header.ctas : defaultHeaderData.ctas, secondaryLinks: header?.secondaryLinks?.length
languages: header?.languages?.length ? header.languages.map((item: any) => item.label || item).filter(Boolean) : defaultHeaderData.languages, ? (header.secondaryLinks as unknown as SpaLink[])
: defaultHeaderData.secondaryLinks,
socialLinks: header?.socialLinks?.length
? (header.socialLinks as unknown as SpaSocialLink[])
: defaultHeaderData.socialLinks,
ctas: header?.ctas?.length ? (header.ctas as unknown as SpaCtaLink[]) : defaultHeaderData.ctas,
languages: header?.languages?.length
? (header.languages as HeaderLanguage[])
.map((item) => (typeof item === 'string' ? item : item.label))
.filter((label): label is string => Boolean(label))
: defaultHeaderData.languages,
} }
} }
export function normalizeFooterData(footer: any): SpaFooterData { export function normalizeFooterData(footer?: Partial<Footer>): SpaFooterData {
return { return {
...defaultFooterData, ...defaultFooterData,
...footer, ...footer,
logo: mediaToSpa(footer?.logo, defaultFooterData.logo), logo: mediaToSpa(footer?.logo, defaultFooterData.logo),
ctas: footer?.ctas?.length ? footer.ctas : defaultFooterData.ctas, ctas: footer?.ctas?.length ? (footer.ctas as unknown as SpaCtaLink[]) : defaultFooterData.ctas,
linkColumns: footer?.linkColumns?.length ? footer.linkColumns : defaultFooterData.linkColumns, linkColumns: footer?.linkColumns?.length
socialLinks: footer?.socialLinks?.length ? footer.socialLinks : defaultFooterData.socialLinks, ? (footer.linkColumns as unknown as SpaFooterData['linkColumns'])
: defaultFooterData.linkColumns,
socialLinks: footer?.socialLinks?.length
? (footer.socialLinks as unknown as SpaSocialLink[])
: defaultFooterData.socialLinks,
partnerLogos: footer?.partnerLogos?.length partnerLogos: footer?.partnerLogos?.length
? footer.partnerLogos.map((item: any, index: number) => ({ ? (footer.partnerLogos as FooterPartnerLogo[]).map((item, index) => ({
alt: item.alt || defaultFooterData.partnerLogos[index]?.alt || '', alt: item.alt || defaultFooterData.partnerLogos[index]?.alt || '',
image: mediaToSpa(item.image, defaultFooterData.partnerLogos[index]?.image), image: mediaToSpa(item.image, defaultFooterData.partnerLogos[index]?.image),
})) }))

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { ChevronRight } from 'lucide-react'; import { ChevronRight } from 'lucide-react';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
@@ -38,7 +39,7 @@ export default function TestimonialsSection() {
<div style={{ background: '#fff', borderRadius: 20, border: '1px solid rgba(17,29,85,0.10)', boxShadow: '0 16px 38px rgba(3,9,58,0.10)', overflow: 'hidden' }}> <div style={{ background: '#fff', borderRadius: 20, border: '1px solid rgba(17,29,85,0.10)', boxShadow: '0 16px 38px rgba(3,9,58,0.10)', overflow: 'hidden' }}>
{/* Photo banner */} {/* Photo banner */}
<div style={{ position: 'relative', height: 240, overflow: 'hidden' }}> <div style={{ position: 'relative', height: 240, overflow: 'hidden' }}>
<img key={t.img} src={t.img} alt={t.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top', display: 'block' }} /> <Image unoptimized key={t.img} src={t.img} alt={t.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.28) 0%, transparent 45%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.28) 0%, transparent 45%)' }} />
<span style={{ position: 'absolute', top: 14, left: 14, fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#101828', background: '#EFBF04', padding: '5px 10px', borderRadius: 100 }}>Preisträger {t.year}</span> <span style={{ position: 'absolute', top: 14, left: 14, fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#101828', background: '#EFBF04', padding: '5px 10px', borderRadius: 100 }}>Preisträger {t.year}</span>
</div> </div>
@@ -74,7 +75,7 @@ export default function TestimonialsSection() {
<div style={{ display: 'grid', gridTemplateColumns: '42% 58%', height: 560, position: 'relative', zIndex: 1 }}> <div style={{ display: 'grid', gridTemplateColumns: '42% 58%', height: 560, position: 'relative', zIndex: 1 }}>
{/* Left – portrait photo */} {/* Left – portrait photo */}
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 280 : 'auto' }}> <div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 280 : 'auto' }}>
<img <Image unoptimized
key={t.img} key={t.img}
src={t.img} src={t.img}
alt={t.name} alt={t.name}

View File

@@ -261,6 +261,10 @@ function readSectionColors(section: HTMLElement): ModuleEdit {
}; };
} }
function setElementText(el: HTMLElement, text: string) {
el.innerText = text;
}
function readElementProps(el: HTMLElement): ElementEdit { function readElementProps(el: HTMLElement): ElementEdit {
const cs = window.getComputedStyle(el); const cs = window.getComputedStyle(el);
const bg = cs.backgroundColor; const bg = cs.backgroundColor;
@@ -326,7 +330,9 @@ function ColorRow({
}) { }) {
const pickerRef = useRef<HTMLInputElement>(null); const pickerRef = useRef<HTMLInputElement>(null);
const [hex, setHex] = useState(value); const [hex, setHex] = useState(value);
useEffect(() => { setHex(value); }, [value]); useEffect(() => {
window.setTimeout(() => setHex(value), 0);
}, [value]);
const commit = (raw: string) => { const commit = (raw: string) => {
if (allowEmpty && raw === '') { onChange(''); setHex(''); return; } if (allowEmpty && raw === '') { onChange(''); setHex(''); return; }
@@ -393,11 +399,6 @@ const FONT_WEIGHTS = [
{ value: '900', label: 'Black'}, { value: '900', label: 'Black'},
]; ];
const ALIGN_OPTIONS = [
{ value: 'left', icon: '⬛' }, { value: 'center', icon: '≡' },
{ value: 'right', icon: '▶' }, { value: 'justify', icon: '▬' },
];
function ElementPopup({ function ElementPopup({
popup, savedEdits, onSave, onClose, popup, savedEdits, onSave, onClose,
}: { }: {
@@ -414,17 +415,16 @@ function ElementPopup({
}, [popup.sel, popup.el, savedEdits]); }, [popup.sel, popup.el, savedEdits]);
const [props, setProps] = useState<ElementEdit>(init); const [props, setProps] = useState<ElementEdit>(init);
useEffect(() => { setProps(init()); }, [popup.sel, init]);
const set = <K extends keyof ElementEdit>(key: K, val: ElementEdit[K]) => const set = <K extends keyof ElementEdit>(key: K, val: ElementEdit[K]) =>
setProps(p => ({ ...p, [key]: val })); setProps(p => ({ ...p, [key]: val }));
// ── LIVE PREVIEW – apply every prop change directly to the element ────── // ── LIVE PREVIEW – apply every prop change directly to the element ──────
useEffect(() => { useEffect(() => {
applyEditToElement(popup.el, props); const element = popup.el;
if (props.text !== undefined) popup.el.innerText = props.text; applyEditToElement(element, props);
// eslint-disable-next-line react-hooks/exhaustive-deps if (props.text !== undefined) setElementText(element, props.text);
}, [props]); }, [popup.el, props]);
const handleSave = () => { const handleSave = () => {
onSave(popup.sel, props); onSave(popup.sel, props);
@@ -644,14 +644,11 @@ export default function LayoutEditor() {
try { localStorage.removeItem('bmp-editor-colors'); } catch {} try { localStorage.removeItem('bmp-editor-colors'); } catch {}
const mods = loadModuleEdits(); const mods = loadModuleEdits();
const els = loadElementEdits(); const els = loadElementEdits();
setSavedModules(mods);
setSavedElements(els);
const p = location.pathname; const p = location.pathname;
injectCSS(STYLE_MODULE, buildModuleCSS(mods, p)); injectCSS(STYLE_MODULE, buildModuleCSS(mods, p));
injectCSS(STYLE_ELEMENT, buildElementCSS(els, p)); injectCSS(STYLE_ELEMENT, buildElementCSS(els, p));
setTimeout(() => reapplyAllElementEdits(els, p), 80); setTimeout(() => reapplyAllElementEdits(els, p), 80);
// eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]);
}, []);
// ── Rebuild CSS + re-apply when route changes ───────────────────────────── // ── Rebuild CSS + re-apply when route changes ─────────────────────────────
useEffect(() => { useEffect(() => {
@@ -670,17 +667,15 @@ export default function LayoutEditor() {
} }
if (!editMode) { if (!editMode) {
cancelModuleEdit(); cancelModuleEdit();
setPopup(null); setTimeout(() => setPopup(null), 0);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [editMode, editTarget, location.pathname]);
}, [editMode, editTarget]);
// ── Live section preview ─────────────────────────────────────────────────── // ── Live section preview ───────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!selSel) { injectCSS(STYLE_PREVIEW, ''); return; } if (!selSel) { injectCSS(STYLE_PREVIEW, ''); return; }
injectCSS(STYLE_PREVIEW, buildModuleCSS({ [selSel]: preview }, location.pathname)); injectCSS(STYLE_PREVIEW, buildModuleCSS({ [selSel]: preview }, location.pathname));
// eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname, preview, selSel]);
}, [preview, selSel]);
// ── Click handler ───────────────────────────────────────────────────────── // ── Click handler ─────────────────────────────────────────────────────────
const handleClick = useCallback((e: MouseEvent) => { const handleClick = useCallback((e: MouseEvent) => {
@@ -709,7 +704,7 @@ export default function LayoutEditor() {
const sel = getElementSelector(target, location.pathname); const sel = getElementSelector(target, location.pathname);
const originalStyle = target.getAttribute('style') || ''; const originalStyle = target.getAttribute('style') || '';
setPopup({ x: e.clientX + 14, y: e.clientY + 14, el: target, sel, originalStyle }); setPopup({ x: e.clientX + 14, y: e.clientY + 14, el: target, sel, originalStyle });
}, [editMode, editTarget, savedModules]); }, [editMode, editTarget, location.pathname, savedModules]);
useEffect(() => { useEffect(() => {
document.addEventListener('click', handleClick, true); document.addEventListener('click', handleClick, true);
@@ -1144,6 +1139,7 @@ export default function LayoutEditor() {
{/* Element popup */} {/* Element popup */}
{popup && ( {popup && (
<ElementPopup <ElementPopup
key={popup.sel}
popup={popup} popup={popup}
savedEdits={savedElements} savedEdits={savedElements}
onSave={saveElementEdit} onSave={saveElementEdit}

View File

@@ -1,5 +1,7 @@
'use client' 'use client'
import Image from '@/spa/components/ui/UnoptimizedImage'
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from '@/spa/router'; import { Link, useLocation } from '@/spa/router';
import { ArrowRight, Upload } from 'lucide-react'; import { ArrowRight, Upload } from 'lucide-react';
@@ -27,7 +29,6 @@ const ICONS: Record<SpaIconName, () => React.ReactElement> = {
/* ── Tokens ────────────────────────────────────────────── */ /* ── Tokens ────────────────────────────────────────────── */
const NAVY = '#111D55';
const ANTHRA = '#1A2030'; // nav + footer shared bg const ANTHRA = '#1A2030'; // nav + footer shared bg
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
const MUTED = '#D0D5DD'; const MUTED = '#D0D5DD';
@@ -77,9 +78,11 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
// Close everything on route change // Close everything on route change
useEffect(() => { useEffect(() => {
setOpen(false); setTimeout(() => {
setMobileOpen(false); setOpen(false);
setExpand(null); setMobileOpen(false);
setExpand(null);
}, 0);
if (location.hash) { if (location.hash) {
setTimeout(() => { setTimeout(() => {
const el = document.querySelector(location.hash); const el = document.querySelector(location.hash);
@@ -134,7 +137,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
// Hide floating CTA when the form section (#bewerben) is in view – homepage only // Hide floating CTA when the form section (#bewerben) is in view – homepage only
useEffect(() => { useEffect(() => {
setFormVisible(false); setTimeout(() => setFormVisible(false), 0);
if (location.pathname !== '/') return; if (location.pathname !== '/') return;
const el = document.getElementById('bewerben'); const el = document.getElementById('bewerben');
if (!el) return; if (!el) return;
@@ -182,7 +185,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
{/* LEFT – logo */} {/* LEFT – logo */}
<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 <Image unoptimized
src={logo} src={logo}
alt={header.logo?.alt || header.overlayLogoAlt} alt={header.logo?.alt || header.overlayLogoAlt}
style={{ height:38, width:'auto', display:'block', objectFit:'contain' }} style={{ height:38, width:'auto', display:'block', objectFit:'contain' }}
@@ -256,7 +259,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
</div> </div>
</div> </div>
<div style={{ position:'absolute', top:48, right:80 }}> <div style={{ position:'absolute', top:48, right:80 }}>
<img src={logo} alt={header.overlayLogoAlt} style={{ height:40, width:'auto', display:'block', opacity:0.9 }} /> <Image unoptimized src={logo} alt={header.overlayLogoAlt} style={{ height:40, width:'auto', display:'block', opacity:0.9 }} />
</div> </div>
</div> </div>
@@ -435,7 +438,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
}}> }}>
{/* Logo */} {/* Logo */}
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}> <Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
<img src={logo} alt={header.logo?.alt || header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} /> <Image unoptimized src={logo} alt={header.logo?.alt || header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
</Link> </Link>
{/* Hamburger */} {/* Hamburger */}
@@ -479,7 +482,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
{/* 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={logo} alt={header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} /> <Image unoptimized src={logo} alt={header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
</Link> </Link>
<button onClick={() => setMobileOpen(false)} aria-label={header.closeLabel} <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 }}>
@@ -709,7 +712,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
maxWidth: isMobile ? '100%' : 420, maxWidth: isMobile ? '100%' : 420,
}}> }}>
{/* Logo */} {/* Logo */}
<img <Image unoptimized
src={footerLogo} src={footerLogo}
alt={footer.logo?.alt || footer.logoAlt} 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 }}
@@ -895,7 +898,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
<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) => ( {footer.partnerLogos.map((partner, index) => (
partner.image?.url ? ( partner.image?.url ? (
<img <Image unoptimized
key={`${partner.alt}-${index}`} key={`${partner.alt}-${index}`}
src={partner.image.url} src={partner.image.url}
alt={partner.image.alt || partner.alt} alt={partner.image.alt || partner.alt}

View File

@@ -1,12 +1,10 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef } from "react";
import { useIsMobile } from "@/spa/hooks/useIsMobile"; import { useIsMobile } from "@/spa/hooks/useIsMobile";
// ─── CONSTANTS ──────────────────────────────────────────────────────────────── // ─── CONSTANTS ────────────────────────────────────────────────────────────────
const NAVY = "#111D55"; const NAVY = "#111D55";
const GOLD = "#EFBF04"; const GOLD = "#EFBF04";
const CREAM = "#EFE5E3";
const BLUE = "#4A8FC9";
const WHITE = "#fff"; const WHITE = "#fff";
const TIERS = [ const TIERS = [
@@ -1125,7 +1123,7 @@ function MembershipWizard() {
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const [errors, setErrors] = useState<Errors>({}); const [errors, setErrors] = useState<Errors>({});
const [slideDir, setSlideDir] = useState<"forward" | "back">("forward"); const [_slideDir, setSlideDir] = useState<"forward" | "back">("forward");
const [animating, setAnimating] = useState(false); const [animating, setAnimating] = useState(false);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react'; import { motion, AnimatePresence } from 'motion/react';
import Image from '@/spa/components/ui/UnoptimizedImage'
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
const DARK = '#101828'; const DARK = '#101828';
@@ -120,7 +121,7 @@ export default function LoadingScreen({ onDone }: { onDone: () => void }) {
position: 'absolute', inset: 0, position: 'absolute', inset: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
<img <Image unoptimized
src="/bmp-logo.png" src="/bmp-logo.png"
alt="" alt=""
aria-hidden aria-hidden

View File

@@ -0,0 +1,16 @@
import type { ImgHTMLAttributes } from 'react'
type UnoptimizedImageProps = ImgHTMLAttributes<HTMLImageElement> & {
unoptimized?: boolean
}
export default function UnoptimizedImage({
alt = '',
unoptimized: _unoptimized,
...props
}: UnoptimizedImageProps) {
// This project has legacy SPA image markup with CSS-driven dimensions. Keeping a plain
// img preserves that behavior while avoiding Next/Image's required width/height contract.
// eslint-disable-next-line @next/next/no-img-element
return <img alt={alt} {...props} />
}

View File

@@ -79,10 +79,12 @@ const Carousel = React.forwardRef<
React.useEffect(() => { React.useEffect(() => {
if (!api) return if (!api) return
onSelect(api)
api.on("reInit", onSelect) api.on("reInit", onSelect)
api.on("select", onSelect) api.on("select", onSelect)
return () => { api?.off("select", onSelect) } return () => {
api.off("reInit", onSelect)
api.off("select", onSelect)
}
}, [api, onSelect]) }, [api, onSelect])
return ( return (

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react"
import { cn } from "@/spa/lib/utils" import { cn } from "@/spa/lib/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react"
import { cn } from "@/spa/lib/utils" import { cn } from "@/spa/lib/utils"
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {

View File

@@ -1,13 +1,15 @@
import { useState, useEffect } from 'react'; import { useSyncExternalStore } from 'react';
export function useIsMobile(breakpoint = 768): boolean { export function useIsMobile(breakpoint = 768): boolean {
const [isMobile, setIsMobile] = useState(() => window.innerWidth < breakpoint); const query = `(max-width: ${breakpoint - 1}px)`;
useEffect(() => {
const mq = window.matchMedia(`(max-width: ${breakpoint - 1}px)`); return useSyncExternalStore(
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches); (onStoreChange) => {
mq.addEventListener('change', handler); const mq = window.matchMedia(query);
setIsMobile(mq.matches); mq.addEventListener('change', onStoreChange);
return () => mq.removeEventListener('change', handler); return () => mq.removeEventListener('change', onStoreChange);
}, [breakpoint]); },
return isMobile; () => window.matchMedia(query).matches,
() => false,
);
} }

View File

@@ -2,10 +2,10 @@ import React, { useState } from 'react';
import { Link } from '@/spa/router'; import { Link } from '@/spa/router';
import { Star, ChevronRight } from 'lucide-react'; import { Star, ChevronRight } from 'lucide-react';
import TestimonialsSection from '@/spa/components/TestimonialsSection'; import TestimonialsSection from '@/spa/components/TestimonialsSection';
import { Button } from '@/spa/components/ui/button';
import { WINNERS } from '@/spa/data/winners'; import { WINNERS } from '@/spa/data/winners';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
@@ -23,7 +23,7 @@ const About: React.FC = () => {
{/* ── 1. HERO ─────────────────────────────────────────────────────────── */} {/* ── 1. HERO ─────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}> <section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
<img <Image unoptimized
src="/images/saal-leer-atmosphaere.jpg" src="/images/saal-leer-atmosphaere.jpg"
alt="BMP Gala" alt="BMP Gala"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
@@ -120,7 +120,7 @@ const About: React.FC = () => {
{/* Right – full-bleed image (+ heading on mobile) */} {/* Right – full-bleed image (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined, order: isMobile ? 1 : 0 }}> <div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined, order: isMobile ? 1 : 0 }}>
<img <Image unoptimized
src="/images/preistraeger-gruppe.jpg" src="/images/preistraeger-gruppe.jpg"
alt="BMP Gala Preisverleihung" alt="BMP Gala Preisverleihung"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
@@ -154,7 +154,7 @@ const About: React.FC = () => {
{/* Left – full-bleed image (+ heading on mobile) */} {/* Left – full-bleed image (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}> <div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
<img <Image unoptimized
src="/images/networking-innenhof.jpg" src="/images/networking-innenhof.jpg"
alt="Bayerischer Mittelstand" alt="Bayerischer Mittelstand"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
@@ -422,7 +422,7 @@ function HighlightCard({ winner }: { winner: import('@/spa/data/winners').Winner
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
> >
<img <Image unoptimized
src={winner.img} src={winner.img}
alt={winner.name} alt={winner.name}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }}

View File

@@ -2,10 +2,11 @@ import React from 'react';
import { useParams, Link } from '@/spa/router'; import { useParams, Link } from '@/spa/router';
import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react'; import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { getArticleBySlug, articles, type BlogArticle } from '@/spa/data/blog'; import { getArticleBySlug, articles, type BlogArticle, type BlogSection } from '@/spa/data/blog';
import { useCmsRoute } from '@/spa/cmsRoute'; import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField'; import { docImageUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
@@ -35,9 +36,17 @@ export default function BlogDetail() {
readTime: String(cmsArticle.readTime || '1 min'), readTime: String(cmsArticle.readTime || '1 min'),
img: docImageUrl(cmsArticle, '/images/gala-saal-overview.jpg', 'heroImage'), img: docImageUrl(cmsArticle, '/images/gala-saal-overview.jpg', 'heroImage'),
excerpt: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.'), excerpt: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.'),
sections: Array.isArray(cmsArticle.sections) && cmsArticle.sections.length > 0 ? cmsArticle.sections as any : [{ body: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.') }], sections: Array.isArray(cmsArticle.sections) && cmsArticle.sections.length > 0 ? cmsArticle.sections as BlogSection[] : [{ body: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.') }],
tags: Array.isArray(cmsArticle.tags) ? cmsArticle.tags.map((item: any) => item.tag).filter(Boolean) : ['Presse'], tags: Array.isArray(cmsArticle.tags)
relatedSlugs: Array.isArray(cmsArticle.relatedSlugs) ? cmsArticle.relatedSlugs.map((item: any) => item.slug).filter(Boolean) : [], ? cmsArticle.tags
.map((item: { tag?: string }) => item.tag)
.filter((tag): tag is string => Boolean(tag))
: ['Presse'],
relatedSlugs: Array.isArray(cmsArticle.relatedSlugs)
? cmsArticle.relatedSlugs
.map((item: { slug?: string }) => item.slug)
.filter((slug): slug is string => Boolean(slug))
: [],
} as BlogArticle : undefined); } as BlogArticle : undefined);
if (!article) { if (!article) {
@@ -60,7 +69,7 @@ export default function BlogDetail() {
{/* ── HERO ──────────────────────────────────────────────────────────── */} {/* ── HERO ──────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', height: isMobile ? 'auto' : '70vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}> <section style={{ position: 'relative', height: isMobile ? 'auto' : '70vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
<img <Image unoptimized
src={article.img} src={article.img}
alt={article.title} alt={article.title}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }}
@@ -288,7 +297,7 @@ function RelatedCard({ article, bordered, isMobile }: { article: ReturnType<type
> >
{/* Thumbnail */} {/* Thumbnail */}
<div style={{ height: 180, overflow: 'hidden', marginBottom: 24 }}> <div style={{ height: 180, overflow: 'hidden', marginBottom: 24 }}>
<img <Image unoptimized
src={article.img} src={article.img}
alt={article.title} alt={article.title}
style={{ style={{

View File

@@ -3,6 +3,7 @@ import { MapPin, Mail, Phone, Clock, ChevronDown } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import KontaktForm from '@/spa/components/forms/KontaktForm'; import KontaktForm from '@/spa/components/forms/KontaktForm';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
@@ -43,7 +44,7 @@ const Contact: React.FC = () => {
{/* ── Hero ───────────────────────────────────────────────────────── */} {/* ── Hero ───────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', height: '68vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}> <section style={{ position: 'relative', height: '68vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
<img <Image unoptimized
src="/images/gala-dinner.jpg" src="/images/gala-dinner.jpg"
alt="BMP Gala" alt="BMP Gala"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }}
@@ -130,7 +131,7 @@ const Contact: React.FC = () => {
{!isMobile && ( {!isMobile && (
<div style={{ height: 200, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}> <div style={{ height: 200, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<img src="/images/preistraeger-gruppe.jpg" alt="BMP Gala 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} /> <Image unoptimized src="/images/preistraeger-gruppe.jpg" alt="BMP Gala 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>

View File

@@ -175,7 +175,7 @@ export default function Datenschutz() {
</Section> </Section>
<Section id="recaptcha" title="Spamschutz mit Google reCAPTCHA"> <Section id="recaptcha" title="Spamschutz mit Google reCAPTCHA">
<p style={body}>Wir haben Google reCAPTCHA in Formularen auf unserer Webseite implementiert, um zu überprüfen, ob die in den Formularen eingegebenen Daten von menschlichen Besuchern stammen oder von Maschinen bzw. automatisierten Programmen („Bots").</p> <p style={body}>Wir haben Google reCAPTCHA in Formularen auf unserer Webseite implementiert, um zu überprüfen, ob die in den Formularen eingegebenen Daten von menschlichen Besuchern stammen oder von Maschinen bzw. automatisierten Programmen („Bots“).</p>
<p style={body}>Dieses Tool analysiert automatisch das Verhalten der Webseitenbesucher, sobald sie mit der Webseite interagieren, unter Verwendung verschiedener Informationen wie IP-Adresse, Aufenthaltsdauer und Mausbewegungen.</p> <p style={body}>Dieses Tool analysiert automatisch das Verhalten der Webseitenbesucher, sobald sie mit der Webseite interagieren, unter Verwendung verschiedener Informationen wie IP-Adresse, Aufenthaltsdauer und Mausbewegungen.</p>
<p style={{ ...body, fontWeight: 600, color: NAVY, marginBottom: 8 }}>Verarbeitete Daten</p> <p style={{ ...body, fontWeight: 600, color: NAVY, marginBottom: 8 }}>Verarbeitete Daten</p>
<ul style={{ ...body, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 16 }}> <ul style={{ ...body, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 16 }}>

View File

@@ -1,16 +1,16 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useParams, Link } from '@/spa/router' import { useParams, Link } from '@/spa/router'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight, Users, Tag } from 'lucide-react' import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight } from 'lucide-react'
import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent } from '@/spa/data/events' import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent, type EventEckdaten } from '@/spa/data/events'
import { useCmsRoute } from '@/spa/cmsRoute' import { useCmsRoute } from '@/spa/cmsRoute'
import { docImageUrl } from '@/spa/cmsMediaField' import { docImageUrl } from '@/spa/cmsMediaField'
import { useIsMobile } from '@/spa/hooks/useIsMobile' import { useIsMobile } from '@/spa/hooks/useIsMobile'
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55' const NAVY = '#111D55'
const GOLD = '#EFBF04' const GOLD = '#EFBF04'
const CREAM = '#EFE5E3' const CREAM = '#EFE5E3'
const INK = '#3A3A3A'
// ── Timestamp formatter ────────────────────────────────────────────────────── // ── Timestamp formatter ──────────────────────────────────────────────────────
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'] const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']
@@ -88,12 +88,12 @@ export default function EventDetail() {
location: String(cmsEvent.location || 'Bayern'), location: String(cmsEvent.location || 'Bayern'),
venue: String(cmsEvent.venue || 'Ort folgt'), venue: String(cmsEvent.venue || 'Ort folgt'),
category: String(cmsEvent.category || 'Event'), category: String(cmsEvent.category || 'Event'),
status: (cmsEvent.status as any) || 'geplant', status: (cmsEvent.status as BmpEvent['status']) || 'geplant',
img: docImageUrl(cmsEvent, '/images/buehne-moderatoren.jpg'), img: docImageUrl(cmsEvent, '/images/buehne-moderatoren.jpg'),
description: String(cmsEvent.description || cmsEvent.meta?.description || ''), description: String(cmsEvent.description || cmsEvent.meta?.description || ''),
longDescription: String(cmsEvent.longDescription || cmsEvent.description || cmsEvent.meta?.description || 'Dieses Event wird aus Payload CMS geladen.'), longDescription: String(cmsEvent.longDescription || cmsEvent.description || cmsEvent.meta?.description || 'Dieses Event wird aus Payload CMS geladen.'),
eckdaten: Array.isArray(cmsEvent.eckdaten) ? cmsEvent.eckdaten as any : [], eckdaten: Array.isArray(cmsEvent.eckdaten) ? cmsEvent.eckdaten as EventEckdaten[] : [],
updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as any : [], updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as EventUpdate[] : [],
} as BmpEvent : undefined) } as BmpEvent : undefined)
const [subscribed, setSubscribed] = useState(false) const [subscribed, setSubscribed] = useState(false)
const isMobile = useIsMobile() const isMobile = useIsMobile()
@@ -117,7 +117,7 @@ export default function EventDetail() {
{/* ── Hero ─────────────────────────────────────────────────────────── */} {/* ── Hero ─────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', height: isMobile ? 'auto' : '65vh', minHeight: isMobile ? 520 : 480, overflow: 'hidden' }}> <section style={{ position: 'relative', height: isMobile ? 'auto' : '65vh', minHeight: isMobile ? 520 : 480, overflow: 'hidden' }}>
<img <Image unoptimized
src={event.img} src={event.img}
alt={event.title} alt={event.title}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center' }}

View File

@@ -1,8 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Button } from '@/spa/components/ui/button'; import { Button } from '@/spa/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/spa/components/ui/card'; import { Play, ChevronRight, X, Star } from 'lucide-react';
import { Badge } from '@/spa/components/ui/badge';
import { Award, Users, TrendingUp, Play, ChevronRight, Building2, ShieldCheck, Leaf, HeartHandshake, X, Star } from 'lucide-react';
import { Link } from '@/spa/router'; import { Link } from '@/spa/router';
import { PartnerTicker } from '@/spa/components/ui/partner-ticker'; import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
import { WINNERS } from '@/spa/data/winners'; import { WINNERS } from '@/spa/data/winners';
@@ -10,6 +8,7 @@ import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import TestimonialsSection from '@/spa/components/TestimonialsSection'; import TestimonialsSection from '@/spa/components/TestimonialsSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const PARTNERS = [ const PARTNERS = [
{ id: 'rsm', label: <><span style={{fontWeight:700,fontSize:11,letterSpacing:2}}>RSM</span><br/><span style={{fontWeight:300,fontSize:8,letterSpacing:1,opacity:.7}}>Ebner Stolz</span></> }, { id: 'rsm', label: <><span style={{fontWeight:700,fontSize:11,letterSpacing:2}}>RSM</span><br/><span style={{fontWeight:300,fontSize:8,letterSpacing:1,opacity:.7}}>Ebner Stolz</span></> },
@@ -103,7 +102,7 @@ function HomeWinnerCard({ winner }: { winner: import('@/spa/data/winners').Winne
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
> >
<img src={winner.img} alt={winner.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} /> <Image unoptimized src={winner.img} alt={winner.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} /> <div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} /> <div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
@@ -131,7 +130,7 @@ const Home: React.FC = () => {
{/* Hero Section */} {/* Hero Section */}
<section className="relative flex items-center overflow-hidden" style={{ background: '#111D55', height: isMobile ? '88svh' : '100vh' }}> <section className="relative flex items-center overflow-hidden" style={{ background: '#111D55', height: isMobile ? '88svh' : '100vh' }}>
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<img <Image unoptimized
src="/images/gala-saal-overview.jpg" src="/images/gala-saal-overview.jpg"
alt="Awards Gala" alt="Awards Gala"
className="w-full h-full object-cover scale-105" className="w-full h-full object-cover scale-105"
@@ -259,7 +258,7 @@ const Home: React.FC = () => {
{/* Right (desktop) / Top (mobile) – image; on mobile the heading is overlaid here */} {/* Right (desktop) / Top (mobile) – image; on mobile the heading is overlaid here */}
<div style={{ position: 'relative', minHeight: isMobile ? 280 : 560, overflow: 'hidden', order: isMobile ? 1 : 0 }}> <div style={{ position: 'relative', minHeight: isMobile ? 280 : 560, overflow: 'hidden', order: isMobile ? 1 : 0 }}>
<img <Image unoptimized
src="/images/buehne-gewinner.jpg" src="/images/buehne-gewinner.jpg"
alt="Preisverleihung Bühne" alt="Preisverleihung Bühne"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: isMobile ? 'center right' : 'center center' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: isMobile ? 'center right' : 'center center' }}
@@ -289,7 +288,7 @@ const Home: React.FC = () => {
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '45% 55%' }}> <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '45% 55%' }}>
{/* Left – full-bleed image with stats overlay (+ heading on mobile) */} {/* Left – full-bleed image with stats overlay (+ heading on mobile) */}
<div style={{ position: 'relative', minHeight: isMobile ? 300 : 600, overflow: 'hidden' }}> <div style={{ position: 'relative', minHeight: isMobile ? 300 : 600, overflow: 'hidden' }}>
<img <Image unoptimized
src="/images/preistraeger-jubel.jpg" src="/images/preistraeger-jubel.jpg"
alt="BMP Auszeichnung" alt="BMP Auszeichnung"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top' }}
@@ -343,7 +342,7 @@ const Home: React.FC = () => {
)} )}
<div style={{ width: 40, height: 2, background: '#EFBF04', marginBottom: 28, display: isMobile ? 'none' : 'block' }} /> <div style={{ width: 40, height: 2, background: '#EFBF04', marginBottom: 28, display: isMobile ? 'none' : 'block' }} />
<blockquote style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 18, fontStyle: 'italic', color: '#101828', lineHeight: 1.7, borderLeft: '3px solid #EFBF04', paddingLeft: 20, margin: '0 0 24px' }}> <blockquote style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 18, fontStyle: 'italic', color: '#101828', lineHeight: 1.7, borderLeft: '3px solid #EFBF04', paddingLeft: 20, margin: '0 0 24px' }}>
"Ein Unternehmen zu gründen erfordert nicht allein spezielle Fähigkeiten. Es erfordert Persönlichkeit!" „Ein Unternehmen zu gründen erfordert nicht allein spezielle Fähigkeiten. Es erfordert Persönlichkeit!“
</blockquote> </blockquote>
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, margin: '0 0 36px' }}> <p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, margin: '0 0 36px' }}>
Wir suchen Vorbilder, die sich trotz aller Risiken nicht scheuen, Visionen in Businesspläne und Ideen in Unternehmungen zu verwandeln, und das mit großem persönlichen Einsatz und Verantwortung für Ihre Mitarbeiter. Seit vielen Jahren würdigen wir herausragende unternehmerische Leistungen im Freistaat. Wir suchen Vorbilder, die sich trotz aller Risiken nicht scheuen, Visionen in Businesspläne und Ideen in Unternehmungen zu verwandeln, und das mit großem persönlichen Einsatz und Verantwortung für Ihre Mitarbeiter. Seit vielen Jahren würdigen wir herausragende unternehmerische Leistungen im Freistaat.
@@ -444,7 +443,7 @@ const Home: React.FC = () => {
{/* Right – full-bleed photo (+ heading on mobile) */} {/* Right – full-bleed photo (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : 'auto', order: isMobile ? 1 : 0 }}> <div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : 'auto', order: isMobile ? 1 : 0 }}>
<img <Image unoptimized
src="/images/networking-innenhof.jpg" src="/images/networking-innenhof.jpg"
alt="BMP Gala Netzwerk" alt="BMP Gala Netzwerk"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center center', display: 'block' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center center', display: 'block' }}
@@ -531,7 +530,7 @@ const Home: React.FC = () => {
{/* Award ceremony photo */} {/* Award ceremony photo */}
{!isMobile && ( {!isMobile && (
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}> <div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<img <Image unoptimized
src="/images/preisuebergabe.jpg" src="/images/preisuebergabe.jpg"
alt="Preisverleihung Bayerischer Mittelstandspreis" alt="Preisverleihung Bayerischer Mittelstandspreis"
style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 25%', filter: 'sepia(0.18) brightness(0.92)' }} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 25%', filter: 'sepia(0.18) brightness(0.92)' }}

View File

@@ -4,6 +4,7 @@ import { ChevronRight, ArrowLeft, Users, Star, Eye, Heart } from 'lucide-react';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import MembershipWizard from '@/spa/components/membership/MembershipWizard'; import MembershipWizard from '@/spa/components/membership/MembershipWizard';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
@@ -597,7 +598,7 @@ function QuoteCard({
flex: 1, flex: 1,
}} }}
> >
"{quote}" „{quote}“
</p> </p>
<div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20 }} /> <div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20 }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
@@ -708,7 +709,7 @@ const MitgliedWerden: React.FC = () => {
}} }}
> >
{/* Background image */} {/* Background image */}
<img <Image unoptimized
src="/images/mitglied-hero.jpg" src="/images/mitglied-hero.jpg"
alt="" alt=""
aria-hidden="true" aria-hidden="true"
@@ -1313,7 +1314,7 @@ const MitgliedWerden: React.FC = () => {
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(0,0,0,0.18)', overflowY: isMobile ? 'visible' : 'auto', minHeight: 0 }}> <div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(0,0,0,0.18)', overflowY: isMobile ? 'visible' : 'auto', minHeight: 0 }}>
{!isMobile && ( {!isMobile && (
<div style={{ height: 180, position: 'relative', overflow: 'hidden', flexShrink: 0 }}> <div style={{ height: 180, position: 'relative', overflow: 'hidden', flexShrink: 0 }}>
<img src="/images/gewinner-gruppenfoto.jpg" alt="BMP Mitglieder" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} /> <Image unoptimized src="/images/gewinner-gruppenfoto.jpg" alt="BMP Mitglieder" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(17,29,85,0.5) 0%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.45) 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(17,29,85,0.5) 0%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.45) 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, background: GOLD }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, background: GOLD }} />
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>

View File

@@ -4,6 +4,7 @@ import { ChevronRight, X, ArrowRight, Globe, Linkedin, Building2, MapPin, Extern
import SponsoringForm from '@/spa/components/forms/SponsoringForm'; import SponsoringForm from '@/spa/components/forms/SponsoringForm';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
@@ -41,7 +42,7 @@ function PersonCard({ imgSrc, imgAlt, name, titleLine1, titleLine2, institution,
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
> >
<div style={{ position: 'relative', height: isMobileCard ? 250 : 320, overflow: 'hidden', width: '100%' }}> <div style={{ position: 'relative', height: isMobileCard ? 250 : 320, overflow: 'hidden', width: '100%' }}>
<img <Image unoptimized
src={imgSrc} src={imgSrc}
alt={imgAlt} alt={imgAlt}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: hovered ? 'grayscale(0%)' : 'grayscale(100%)', transition: 'filter 0.55s ease, transform 0.55s ease', transform: hovered ? 'scale(1.04)' : 'scale(1)' }} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: hovered ? 'grayscale(0%)' : 'grayscale(100%)', transition: 'filter 0.55s ease, transform 0.55s ease', transform: hovered ? 'scale(1.04)' : 'scale(1)' }}
@@ -78,7 +79,7 @@ const JuryCard: React.FC<{ member: JuryMember; idx: number }> = ({ member, idx }
style={{ borderLeft: !isMobileCard && idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none', display: 'flex', flexDirection: 'column', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.03)' : 'transparent', transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '74vw', maxWidth: '74vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : {}) }} style={{ borderLeft: !isMobileCard && idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none', display: 'flex', flexDirection: 'column', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.03)' : 'transparent', transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '74vw', maxWidth: '74vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : {}) }}
> >
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: '3/4' }}> <div style={{ position: 'relative', overflow: 'hidden', aspectRatio: '3/4' }}>
<img src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} /> <Image unoptimized src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} />
</div> </div>
<div style={{ padding: isMobileCard ? '28px 28px 32px' : '32px 32px 36px' }}> <div style={{ padding: isMobileCard ? '28px 28px 32px' : '32px 32px 36px' }}>
@@ -111,7 +112,7 @@ const JuryChairCard: React.FC<{ member: JuryMember }> = ({ member }) => {
}} }}
> >
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto' }}> <div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto' }}>
<img src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} /> <Image unoptimized src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} />
</div> </div>
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}> <div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
@@ -144,30 +145,6 @@ interface Partner {
linkedin?: string; linkedin?: string;
} }
const PartnerCell: React.FC<{ partner: Partner; idx: number; total: number; onClick: () => void }> = ({ partner, idx, total, onClick }) => {
const [hovered, setHovered] = useState(false);
const isMobileCell = useIsMobile();
const borderLeft = isMobileCell ? 'none' : (idx % 4 === 0 ? 'none' : '1px solid rgba(3,9,58,0.08)');
const showBottom = isMobileCell ? idx < total - 1 : idx < 8;
return (
<div
onClick={onClick}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ padding: isMobileCell ? '24px 20px' : '36px 32px', borderLeft, borderBottom: showBottom ? '1px solid rgba(3,9,58,0.08)' : 'none', display: 'flex', flexDirection: 'column', gap: 16, cursor: 'pointer', background: hovered ? '#E8E5E4' : 'transparent', transition: 'background 0.15s' }}
>
<div style={{ height: 48, display: 'flex', alignItems: 'center' }}>{partner.logo}</div>
<div style={{ width: hovered ? 48 : 20, height: 1, background: GOLD, transition: 'width 0.3s ease' }} />
<div style={{ fontFamily: FF, fontSize: 18, fontWeight: 700, color: '#101828' }}>{partner.name}</div>
<div style={{ fontFamily: FF, fontSize: 11, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.2em', fontWeight: 700 }}>{partner.role}</div>
<div style={{ fontFamily: FB, fontSize: 17, color: 'rgba(16,24,40,0.45)', lineHeight: 1.6, flexGrow: 1 }}>{partner.desc}</div>
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.12em', color: GOLD, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 4 }}>
Details <ArrowRight size={10} />
</div>
</div>
);
};
// ── SponsorCell (tiered) ────────────────────────────────────────────────────── // ── SponsorCell (tiered) ──────────────────────────────────────────────────────
const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; onClick: () => void }> = ({ partner, variant, idx, cols, onClick }) => { const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; onClick: () => void }> = ({ partner, variant, idx, cols, onClick }) => {
@@ -242,7 +219,7 @@ function PartnerModal({ partner, onClose }: { partner: Partner; onClose: () => v
<div style={{ position: 'relative', height: 220, flexShrink: 0, overflow: 'hidden' }}> <div style={{ position: 'relative', height: 220, flexShrink: 0, overflow: 'hidden' }}>
{/* Background */} {/* Background */}
{partner.heroImg {partner.heroImg
? <img src={partner.heroImg} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} /> ? <Image unoptimized src={partner.heroImg} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
: <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(135deg, ${accentColor}33 0%, ${NAVY} 70%)` }} /> : <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(135deg, ${accentColor}33 0%, ${NAVY} 70%)` }} />
} }
{/* Gradient to white */} {/* Gradient to white */}
@@ -527,7 +504,7 @@ const Netzwerk: React.FC = () => {
{/* ── 1. HERO ──────────────────────────────────────────────────────────── */} {/* ── 1. HERO ──────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}> <section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
<img src="/images/netzwerk-hero.jpg" alt="BMP Netzwerk Preisverleihung" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} /> <Image unoptimized src="/images/netzwerk-hero.jpg" alt="BMP Netzwerk Preisverleihung" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
<div style={{ position: 'absolute', inset: 0, background: isMobile ? 'linear-gradient(to top, rgba(2,9,48,0.95) 0%, rgba(2,9,48,0.72) 38%, rgba(2,9,48,0.22) 72%, transparent 100%)' : 'linear-gradient(to right, #020930 0%, rgba(2,9,48,0.90) 38%, rgba(2,9,48,0.18) 65%, transparent 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: isMobile ? 'linear-gradient(to top, rgba(2,9,48,0.95) 0%, rgba(2,9,48,0.72) 38%, rgba(2,9,48,0.22) 72%, transparent 100%)' : 'linear-gradient(to right, #020930 0%, rgba(2,9,48,0.90) 38%, rgba(2,9,48,0.18) 65%, transparent 100%)' }} />
<div style={{ position: 'absolute', top: 0, left: 80, width: 2, height: '100%', background: `linear-gradient(to bottom, transparent, ${GOLD}, transparent)`, opacity: 0.4 }} /> <div style={{ position: 'absolute', top: 0, left: 80, width: 2, height: '100%', background: `linear-gradient(to bottom, transparent, ${GOLD}, transparent)`, opacity: 0.4 }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.3), transparent)', zIndex: 2 }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.3), transparent)', zIndex: 2 }} />
@@ -588,7 +565,7 @@ const Netzwerk: React.FC = () => {
</div> </div>
<div> <div>
<blockquote style={{ fontFamily: FF, fontSize: 18, fontStyle: 'italic', color: 'rgba(255,255,255,0.8)', lineHeight: 1.75, margin: 0, paddingLeft: 24, borderLeft: `3px solid ${GOLD}` }}> <blockquote style={{ fontFamily: FF, fontSize: 18, fontStyle: 'italic', color: 'rgba(255,255,255,0.8)', lineHeight: 1.75, margin: 0, paddingLeft: 24, borderLeft: `3px solid ${GOLD}` }}>
"Der Bayerische Mittelstandspreis steht für das, was Bayern stark macht: Unternehmergeist, Verantwortung und Qualität. Mit großer Freude übernehme ich erneut die Schirmherrschaft für diesen bedeutenden Preis." „Der Bayerische Mittelstandspreis steht für das, was Bayern stark macht: Unternehmergeist, Verantwortung und Qualität. Mit großer Freude übernehme ich erneut die Schirmherrschaft für diesen bedeutenden Preis.“
</blockquote> </blockquote>
<div style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 20, paddingLeft: 24 }}> <div style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 20, paddingLeft: 24 }}>
– Ilse Aigner MdL, Präsidentin des Bayerischen Landtags – Ilse Aigner MdL, Präsidentin des Bayerischen Landtags
@@ -813,7 +790,7 @@ const Netzwerk: React.FC = () => {
{!isMobile && ( {!isMobile && (
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}> <div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<img src="/images/networking-innenhof.jpg" alt="BMP Partnernetzwerk" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%', filter: 'sepia(0.18) brightness(0.92)' }} /> <Image unoptimized src="/images/networking-innenhof.jpg" alt="BMP Partnernetzwerk" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%', filter: 'sepia(0.18) brightness(0.92)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>

View File

@@ -1,10 +1,11 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react'; import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star, type LucideIcon } from 'lucide-react';
import { Link } from '@/spa/router'; import { Link } from '@/spa/router';
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm'; import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection'; import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
@@ -21,7 +22,7 @@ const Participation: React.FC = () => {
{/* ── HERO ─────────────────────────────────────────────────────────────── */} {/* ── HERO ─────────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', minHeight: '65vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: NAVY }}> <section style={{ position: 'relative', minHeight: '65vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: NAVY }}>
<img <Image unoptimized
src="/images/preisuebergabe.jpg" src="/images/preisuebergabe.jpg"
alt="Teilnahme BMP" alt="Teilnahme BMP"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
@@ -146,7 +147,7 @@ const Participation: React.FC = () => {
{/* Left – full-bleed image with gradient blend into navy on the right (+ heading on mobile) */} {/* Left – full-bleed image with gradient blend into navy on the right (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}> <div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
<img <Image unoptimized
src="/images/buehne-gewinner.jpg" src="/images/buehne-gewinner.jpg"
alt="Jury-Bewertung BMP" alt="Jury-Bewertung BMP"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
@@ -313,7 +314,7 @@ const Participation: React.FC = () => {
{!isMobile && ( {!isMobile && (
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}> <div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<img src="/images/preistraeger-jubel.jpg" alt="Preisverleihung 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} /> <Image unoptimized src="/images/preistraeger-jubel.jpg" alt="Preisverleihung 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
@@ -444,7 +445,7 @@ function BewerbungswegeSection() {
{ weg: 'Weg 01', title: 'Vorschlag unterbreiten', desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.', Icon: Send, checkItems: INSTITUTIONEN, cta: 'Vorschlag unterbreiten', featured: false }, { weg: 'Weg 01', title: 'Vorschlag unterbreiten', desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.', Icon: Send, checkItems: INSTITUTIONEN, cta: 'Vorschlag unterbreiten', featured: false },
{ weg: 'Weg 02', title: 'Initiativbewerbung', desc: 'Für Unternehmer, die selbst die Initiative ergreifen.', Icon: UserCheck, kvItems: [ { k: '0 EUR', v: 'Teilnahmegebühr' }, { k: 'Eigeninitiative', v: 'Direkt ohne Intermediär' }, { k: 'Für Unternehmer', v: 'Inhabergeführte KMU in Bayern' }, { k: 'Deadline', v: '30. Juni 2026' } ], cta: 'Jetzt bewerben', featured: true }, { weg: 'Weg 02', title: 'Initiativbewerbung', desc: 'Für Unternehmer, die selbst die Initiative ergreifen.', Icon: UserCheck, kvItems: [ { k: '0 EUR', v: 'Teilnahmegebühr' }, { k: 'Eigeninitiative', v: 'Direkt ohne Intermediär' }, { k: 'Für Unternehmer', v: 'Inhabergeführte KMU in Bayern' }, { k: 'Deadline', v: '30. Juni 2026' } ], cta: 'Jetzt bewerben', featured: true },
{ weg: 'Weg 03 · Sonderpreis', title: 'Bavarian Future Award', desc: 'Sonderpreis, initiiert durch die Studierenden der HAM, Hochschule für angewandtes Management.', Icon: Star, kvItems: [ { k: 'Vorschlag möglich', v: 'Durch HAM & Partner' }, { k: 'Selbstbewerbung', v: 'Direkte Einreichung' }, { k: 'Initiiert von', v: 'Studierenden der HAM' }, { k: 'Kategorie', v: 'Zukunft & Innovation' } ], cta: 'Zum Formular', featured: false }, { weg: 'Weg 03 · Sonderpreis', title: 'Bavarian Future Award', desc: 'Sonderpreis, initiiert durch die Studierenden der HAM, Hochschule für angewandtes Management.', Icon: Star, kvItems: [ { k: 'Vorschlag möglich', v: 'Durch HAM & Partner' }, { k: 'Selbstbewerbung', v: 'Direkte Einreichung' }, { k: 'Initiiert von', v: 'Studierenden der HAM' }, { k: 'Kategorie', v: 'Zukunft & Innovation' } ], cta: 'Zum Formular', featured: false },
] as { weg: string; title: string; desc: string; Icon: any; checkItems?: string[]; kvItems?: { k: string; v: string }[]; cta: string; featured: boolean }[]).map((w) => { ] as { weg: string; title: string; desc: string; Icon: LucideIcon; checkItems?: string[]; kvItems?: { k: string; v: string }[]; cta: string; featured: boolean }[]).map((w) => {
const Icon = w.Icon; const Icon = w.Icon;
return ( return (
<div key={w.weg} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: w.featured ? `1.5px solid ${GOLD}` : '1px solid rgba(17,29,85,0.10)', boxShadow: w.featured ? '0 14px 34px rgba(239,191,4,0.20), 0 6px 16px rgba(3,9,58,0.10)' : '0 10px 28px rgba(3,9,58,0.08)' }}> <div key={w.weg} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: w.featured ? `1.5px solid ${GOLD}` : '1px solid rgba(17,29,85,0.10)', boxShadow: w.featured ? '0 14px 34px rgba(239,191,4,0.20), 0 6px 16px rgba(3,9,58,0.10)' : '0 10px 28px rgba(3,9,58,0.08)' }}>

View File

@@ -5,6 +5,7 @@ import { WINNERS, CATEGORIES, YEARS } from '@/spa/data/winners';
import { useCmsCollection } from '@/spa/cmsRoute'; import { useCmsCollection } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField'; import { docImageUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
const NAVY = '#111D55'; const NAVY = '#111D55';
@@ -62,7 +63,7 @@ export default function Preistraeger() {
<div style={{ background: '#fff', minHeight: '100vh' }}> <div style={{ background: '#fff', minHeight: '100vh' }}>
{/* Hero */} {/* Hero */}
<div style={{ width: '100%', height: '50vh', overflow: 'hidden', position: 'relative' }}> <div style={{ width: '100%', height: '50vh', overflow: 'hidden', position: 'relative' }}>
<img <Image unoptimized
src="https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=2000" src="https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=2000"
alt="Preisverleihung" alt="Preisverleihung"
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
@@ -250,7 +251,7 @@ function WinnerCard({ winner }: { winner: import('@/spa/data/winners').Winner })
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
> >
<img <Image unoptimized
src={winner.img} src={winner.img}
alt={winner.name} alt={winner.name}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.06)' : 'scale(1)' }} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.06)' : 'scale(1)' }}

View File

@@ -5,6 +5,7 @@ import { WINNERS } from '@/spa/data/winners';
import { useCmsRoute } from '@/spa/cmsRoute'; import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField'; import { docImageUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif'; const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
@@ -40,14 +41,11 @@ export default function PreistraegerDetail() {
if (!winner) return <Navigate to="/preistraeger" replace />; if (!winner) return <Navigate to="/preistraeger" replace />;
// Pick a second photo (offset from primary)
const secondImg = winner.img.replace('w=800', 'w=900').replace('q=80', 'q=75');
return ( return (
<div style={{ background: '#fff', minHeight: '100vh' }}> <div style={{ background: '#fff', minHeight: '100vh' }}>
{/* Hero */} {/* Hero */}
<div style={{ width: '100%', height: '60vh', overflow: 'hidden', position: 'relative' }}> <div style={{ width: '100%', height: '60vh', overflow: 'hidden', position: 'relative' }}>
<img <Image unoptimized
src={winner.img} src={winner.img}
alt={winner.name} alt={winner.name}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
@@ -110,9 +108,9 @@ export default function PreistraegerDetail() {
{/* Quote */} {/* Quote */}
<div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}> <div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}>
<p style={{ fontFamily: FB, fontStyle: 'italic', fontSize: 18, color: '#101828', lineHeight: 1.7, marginBottom: 12 }}> <p style={{ fontFamily: FB, fontStyle: 'italic', fontSize: 18, color: '#101828', lineHeight: 1.7, marginBottom: 12 }}>
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginRight: 4, fontFamily: '"IBM Plex Sans"' }}>"</span> <span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginRight: 4, fontFamily: '"IBM Plex Sans"' }}>&ldquo;</span>
{winner.quote} {winner.quote}
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>"</span> <span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>&rdquo;</span>
</p> </p>
<p style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: GRAY, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}> <p style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: GRAY, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
– {winner.quotePerson}, {winner.quoteRole} – {winner.quotePerson}, {winner.quoteRole}
@@ -124,7 +122,7 @@ export default function PreistraegerDetail() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{/* Photo */} {/* Photo */}
<div style={{ borderRadius: 0, overflow: 'hidden', aspectRatio: '4/3' }}> <div style={{ borderRadius: 0, overflow: 'hidden', aspectRatio: '4/3' }}>
<img src="/gala-preistraeger.jpg" alt="Gala Preisverleihung" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: 'brightness(0.95)' }} /> <Image unoptimized src="/gala-preistraeger.jpg" alt="Gala Preisverleihung" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: 'brightness(0.95)' }} />
</div> </div>
{/* Kenndaten */} {/* Kenndaten */}

View File

@@ -5,6 +5,7 @@ import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { useCmsCollection } from '@/spa/cmsRoute'; import { useCmsCollection } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField'; import { docImageUrl } from '@/spa/cmsMediaField';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
@@ -90,7 +91,7 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
> >
{/* image */} {/* image */}
<div style={{ position: 'relative', overflow: 'hidden', height: 220 }}> <div style={{ position: 'relative', overflow: 'hidden', height: 220 }}>
<img <Image unoptimized
src={item.img} src={item.img}
alt={item.title} alt={item.title}
style={{ style={{
@@ -183,6 +184,37 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
); );
} }
function DarkInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
const [f, setF] = React.useState(false);
return (
<input
{...props}
onFocus={(e) => {
setF(true);
props.onFocus?.(e);
}}
onBlur={(e) => {
setF(false);
props.onBlur?.(e);
}}
style={{
width: '100%',
height: 44,
padding: '0 14px',
fontFamily: FF,
fontSize: 18,
color: '#fff',
border: `1px solid ${f ? 'rgba(239,191,4,0.8)' : 'rgba(255,255,255,0.12)'}`,
background: f ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.05)',
outline: 'none',
boxSizing: 'border-box',
transition: 'border-color 0.15s, background 0.15s',
...props.style,
}}
/>
);
}
const Press: React.FC = () => { const Press: React.FC = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const cmsEvents = useCmsCollection('events'); const cmsEvents = useCmsCollection('events');
@@ -236,38 +268,6 @@ const Press: React.FC = () => {
} }
}; };
// DarkInput defined inline as required
function DarkInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
const [f, setF] = React.useState(false);
return (
<input
{...props}
onFocus={(e) => {
setF(true);
props.onFocus?.(e);
}}
onBlur={(e) => {
setF(false);
props.onBlur?.(e);
}}
style={{
width: '100%',
height: 44,
padding: '0 14px',
fontFamily: FF,
fontSize: 18,
color: '#fff',
border: `1px solid ${f ? 'rgba(239,191,4,0.8)' : 'rgba(255,255,255,0.12)'}`,
background: f ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.05)',
outline: 'none',
boxSizing: 'border-box',
transition: 'border-color 0.15s, background 0.15s',
...props.style,
}}
/>
);
}
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
@@ -282,7 +282,7 @@ const Press: React.FC = () => {
background: NAVY, background: NAVY,
}} }}
> >
<img <Image unoptimized
src="/images/gala-dinner.jpg" src="/images/gala-dinner.jpg"
alt="Presse BMP" alt="Presse BMP"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
@@ -428,7 +428,7 @@ const Press: React.FC = () => {
> >
{/* Left – image cell */} {/* Left – image cell */}
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 200 : 240 }}> <div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 200 : 240 }}>
<img src={event.img} alt={event.title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> <Image unoptimized src={event.img} alt={event.title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', top: 20, left: 20, padding: '5px 12px', fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase' as const, letterSpacing: '0.18em', background: event.status === 'Anmeldung offen' ? GOLD : 'rgba(3,9,58,0.85)', color: event.status === 'Anmeldung offen' ? NAVY : 'rgba(255,255,255,0.6)' }}> <div style={{ position: 'absolute', top: 20, left: 20, padding: '5px 12px', fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase' as const, letterSpacing: '0.18em', background: event.status === 'Anmeldung offen' ? GOLD : 'rgba(3,9,58,0.85)', color: event.status === 'Anmeldung offen' ? NAVY : 'rgba(255,255,255,0.6)' }}>
{event.status} {event.status}
</div> </div>

View File

@@ -49,8 +49,7 @@ function useClickableCard<T extends HTMLElement>({
} }
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps [],
[router, card, link, timeDown],
) )
const handleMouseUp = useCallback( const handleMouseUp = useCallback(
@@ -71,8 +70,7 @@ function useClickableCard<T extends HTMLElement>({
} }
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps [external, newTab, router, scroll],
[router, card, link, timeDown],
) )
useEffect(() => { useEffect(() => {
@@ -92,8 +90,7 @@ function useClickableCard<T extends HTMLElement>({
return () => { return () => {
abortController.abort() abortController.abort()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [handleMouseDown, handleMouseUp])
}, [card, link, router])
return { return {
card: { card: {

View File

@@ -5,7 +5,7 @@ import { seedTestUser, cleanupTestUser, testUser } from '../helpers/seedUser'
test.describe('Admin Panel', () => { test.describe('Admin Panel', () => {
let page: Page let page: Page
test.beforeAll(async ({ browser }, testInfo) => { test.beforeAll(async ({ browser }) => {
await seedTestUser() await seedTestUser()
const context = await browser.newContext() const context = await browser.newContext()

View File

@@ -3,12 +3,12 @@ import { test, expect, Page } from '@playwright/test'
test.describe('Frontend', () => { test.describe('Frontend', () => {
let page: Page let page: Page
test.beforeAll(async ({ browser }, testInfo) => { test.beforeAll(async ({ browser }) => {
const context = await browser.newContext() const context = await browser.newContext()
page = await context.newPage() page = await context.newPage()
}) })
test('can load homepage', async ({ page }) => { test('can load homepage', async () => {
await page.goto('http://localhost:3000') await page.goto('http://localhost:3000')
await expect(page).toHaveTitle(/Payload Website Template/) await expect(page).toHaveTitle(/Payload Website Template/)
const heading = page.locator('h1').first() const heading = page.locator('h1').first()