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 { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
import nextVitals from 'eslint-config-next/core-web-vitals'
import tseslint from 'typescript-eslint'
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: {
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',

View File

@@ -2,7 +2,7 @@
import { useHeaderTheme } from '@/providers/HeaderTheme'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import React, { useEffect, useState } from 'react'
import React, { useEffect } from 'react'
import type { Header } from '@/payload-types'
@@ -14,20 +14,13 @@ interface HeaderClientProps {
}
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 pathname = usePathname()
const theme = headerTheme || null
useEffect(() => {
setHeaderTheme(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
useEffect(() => {
if (headerTheme && headerTheme !== theme) setTheme(headerTheme)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [headerTheme])
}, [pathname, setHeaderTheme])
return (
<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 { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation'
import type { CmsRouteDoc } from '@/spa/cmsRoute'
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) {
const target = routeTarget(pathname)
const result = await payload.find({
collection: target.collection as any,
collection: target.collection,
depth: 1,
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) {
const result = await payload.find({
collection: collection as any,
collection,
depth: 1,
draft,
overrideAccess: draft,
@@ -90,11 +91,11 @@ export default async function Page({ params }: Args) {
footer={normalizeFooterData(footer)}
cmsRoute={{
collection: route.collection,
doc: route.doc as any,
doc: route.doc as unknown as CmsRouteDoc,
lists: {
preistraeger: preistraeger as any,
events: events as any,
posts: posts as any,
preistraeger: preistraeger as unknown as CmsRouteDoc[],
events: events as unknown as CmsRouteDoc[],
posts: posts as unknown as CmsRouteDoc[],
},
}}
/>

View File

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

View File

@@ -31,14 +31,14 @@ export const Events: CollectionConfig = {
url: ({ data, req }) =>
generatePreviewPath({
slug: data?.spaPath || data?.slug,
collection: 'events' as any,
collection: 'events',
req,
}),
},
preview: (data, { req }) =>
generatePreviewPath({
slug: ((data?.spaPath as string | undefined) || (data?.slug as string)),
collection: 'events' as any,
collection: 'events',
req,
}),
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
// 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
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => {
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => {
if (doc?.authors && doc?.authors?.length > 0) {
const authorDocs: User[] = []

View File

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

View File

@@ -18,7 +18,10 @@ export const Card: React.FC<{
showCategories?: boolean
title?: string
}> = (props) => {
const { card, link } = useClickableCard({})
const {
card: { ref: cardRef },
link: { ref: linkRef },
} = useClickableCard({})
const { className, doc, relationTo, showCategories, title: titleFromProps } = props
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',
className,
)}
ref={card.ref}
ref={cardRef}
>
<div className="relative w-full ">
{!metaImage && <div className="">No image</div>}
@@ -67,7 +70,7 @@ export const Card: React.FC<{
{titleToUse && (
<div className="prose">
<h3>
<Link className="not-prose" href={href} ref={link.ref}>
<Link className="not-prose" href={href} ref={linkRef}>
{titleToUse}
</Link>
</h3>

View File

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

View File

@@ -201,7 +201,7 @@ export const seed = async ({
payload.logger.info(`— Seeding pages...`)
const [_, contactPage] = await Promise.all([
await Promise.all([
payload.create({
collection: 'pages',
depth: 0,
@@ -222,11 +222,11 @@ export const seed = async ({
await Promise.all([
payload.updateGlobal({
slug: 'header',
data: headerSeedData as any,
data: headerSeedData as never,
}),
payload.updateGlobal({
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 = () => {
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') => {
if (themeToSet === 'auto') {
@@ -28,10 +30,6 @@ export const ThemeSelector: React.FC = () => {
}
}
React.useEffect(() => {
const preference = window.localStorage.getItem(themeLocalStorageKey)
setValue(preference ?? 'auto')
}, [])
return (
<Select onValueChange={onThemeChange} value={value}>

View File

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

View File

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

View File

@@ -188,7 +188,7 @@ function dataForPage(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
async function findExistingDoc(payload: Payload, collection: TargetCollection, page: SpaPageRegistryEntry) {
const existing = await payload.find({
collection: collection as any,
collection: collection as never,
depth: 0,
limit: 1,
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) {
@@ -240,17 +240,17 @@ async function upsertPage(payload: Payload, page: SpaPageRegistryEntry, mediaMap
if (existing) {
await payload.update({
collection: collection as any,
collection: collection as never,
id: existing.id,
data,
data: data as never,
depth: 0,
context: { disableRevalidate: true },
})
payload.logger.info(`updated ${collection}: ${page.path}`)
} else {
await payload.create({
collection: collection as any,
data,
collection: collection as never,
data: data as never,
depth: 0,
context: { disableRevalidate: true },
})

View File

@@ -87,14 +87,14 @@ export default function NextApp({
)
}
const Page = resolvePage(pathname)
const page = React.createElement(resolvePage(pathname))
return (
<>
<CmsRouteProvider value={cmsRoute}>
<ScrollToTop />
<Layout header={header} footer={footer}>
<Page />
{page}
</Layout>
</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 SpaLink = { label: string; path: string }
@@ -167,29 +167,50 @@ const mediaToSpa = (value: unknown, fallback?: SpaMedia): SpaMedia | undefined =
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 {
...defaultHeaderData,
...header,
logo: mediaToSpa(header?.logo, defaultHeaderData.logo),
navItems: header?.navItems?.length ? header.navItems : defaultHeaderData.navItems,
secondaryLinks: header?.secondaryLinks?.length ? header.secondaryLinks : defaultHeaderData.secondaryLinks,
socialLinks: header?.socialLinks?.length ? header.socialLinks : defaultHeaderData.socialLinks,
ctas: header?.ctas?.length ? header.ctas : defaultHeaderData.ctas,
languages: header?.languages?.length ? header.languages.map((item: any) => item.label || item).filter(Boolean) : defaultHeaderData.languages,
navItems: header?.navItems?.length
? (header.navItems as unknown as SpaHeaderNavItem[])
: defaultHeaderData.navItems,
secondaryLinks: header?.secondaryLinks?.length
? (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 {
...defaultFooterData,
...footer,
logo: mediaToSpa(footer?.logo, defaultFooterData.logo),
ctas: footer?.ctas?.length ? footer.ctas : defaultFooterData.ctas,
linkColumns: footer?.linkColumns?.length ? footer.linkColumns : defaultFooterData.linkColumns,
socialLinks: footer?.socialLinks?.length ? footer.socialLinks : defaultFooterData.socialLinks,
ctas: footer?.ctas?.length ? (footer.ctas as unknown as SpaCtaLink[]) : defaultFooterData.ctas,
linkColumns: footer?.linkColumns?.length
? (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
? footer.partnerLogos.map((item: any, index: number) => ({
? (footer.partnerLogos as FooterPartnerLogo[]).map((item, index) => ({
alt: item.alt || defaultFooterData.partnerLogos[index]?.alt || '',
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 MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
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' }}>
{/* Photo banner */}
<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%)' }} />
<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>
@@ -74,7 +75,7 @@ export default function TestimonialsSection() {
<div style={{ display: 'grid', gridTemplateColumns: '42% 58%', height: 560, position: 'relative', zIndex: 1 }}>
{/* Left – portrait photo */}
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 280 : 'auto' }}>
<img
<Image unoptimized
key={t.img}
src={t.img}
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 {
const cs = window.getComputedStyle(el);
const bg = cs.backgroundColor;
@@ -326,7 +330,9 @@ function ColorRow({
}) {
const pickerRef = useRef<HTMLInputElement>(null);
const [hex, setHex] = useState(value);
useEffect(() => { setHex(value); }, [value]);
useEffect(() => {
window.setTimeout(() => setHex(value), 0);
}, [value]);
const commit = (raw: string) => {
if (allowEmpty && raw === '') { onChange(''); setHex(''); return; }
@@ -393,11 +399,6 @@ const FONT_WEIGHTS = [
{ value: '900', label: 'Black'},
];
const ALIGN_OPTIONS = [
{ value: 'left', icon: '⬛' }, { value: 'center', icon: '≡' },
{ value: 'right', icon: '▶' }, { value: 'justify', icon: '▬' },
];
function ElementPopup({
popup, savedEdits, onSave, onClose,
}: {
@@ -414,17 +415,16 @@ function ElementPopup({
}, [popup.sel, popup.el, savedEdits]);
const [props, setProps] = useState<ElementEdit>(init);
useEffect(() => { setProps(init()); }, [popup.sel, init]);
const set = <K extends keyof ElementEdit>(key: K, val: ElementEdit[K]) =>
setProps(p => ({ ...p, [key]: val }));
// ── LIVE PREVIEW – apply every prop change directly to the element ──────
useEffect(() => {
applyEditToElement(popup.el, props);
if (props.text !== undefined) popup.el.innerText = props.text;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props]);
const element = popup.el;
applyEditToElement(element, props);
if (props.text !== undefined) setElementText(element, props.text);
}, [popup.el, props]);
const handleSave = () => {
onSave(popup.sel, props);
@@ -644,14 +644,11 @@ export default function LayoutEditor() {
try { localStorage.removeItem('bmp-editor-colors'); } catch {}
const mods = loadModuleEdits();
const els = loadElementEdits();
setSavedModules(mods);
setSavedElements(els);
const p = location.pathname;
injectCSS(STYLE_MODULE, buildModuleCSS(mods, p));
injectCSS(STYLE_ELEMENT, buildElementCSS(els, p));
setTimeout(() => reapplyAllElementEdits(els, p), 80);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [location.pathname]);
// ── Rebuild CSS + re-apply when route changes ─────────────────────────────
useEffect(() => {
@@ -670,17 +667,15 @@ export default function LayoutEditor() {
}
if (!editMode) {
cancelModuleEdit();
setPopup(null);
setTimeout(() => setPopup(null), 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editMode, editTarget]);
}, [editMode, editTarget, location.pathname]);
// ── Live section preview ───────────────────────────────────────────────────
useEffect(() => {
if (!selSel) { injectCSS(STYLE_PREVIEW, ''); return; }
injectCSS(STYLE_PREVIEW, buildModuleCSS({ [selSel]: preview }, location.pathname));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [preview, selSel]);
}, [location.pathname, preview, selSel]);
// ── Click handler ─────────────────────────────────────────────────────────
const handleClick = useCallback((e: MouseEvent) => {
@@ -709,7 +704,7 @@ export default function LayoutEditor() {
const sel = getElementSelector(target, location.pathname);
const originalStyle = target.getAttribute('style') || '';
setPopup({ x: e.clientX + 14, y: e.clientY + 14, el: target, sel, originalStyle });
}, [editMode, editTarget, savedModules]);
}, [editMode, editTarget, location.pathname, savedModules]);
useEffect(() => {
document.addEventListener('click', handleClick, true);
@@ -1144,6 +1139,7 @@ export default function LayoutEditor() {
{/* Element popup */}
{popup && (
<ElementPopup
key={popup.sel}
popup={popup}
savedEdits={savedElements}
onSave={saveElementEdit}

View File

@@ -1,5 +1,7 @@
'use client'
import Image from '@/spa/components/ui/UnoptimizedImage'
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from '@/spa/router';
import { ArrowRight, Upload } from 'lucide-react';
@@ -27,7 +29,6 @@ const ICONS: Record<SpaIconName, () => React.ReactElement> = {
/* ── Tokens ────────────────────────────────────────────── */
const NAVY = '#111D55';
const ANTHRA = '#1A2030'; // nav + footer shared bg
const GOLD = '#EFBF04';
const MUTED = '#D0D5DD';
@@ -77,9 +78,11 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
// Close everything on route change
useEffect(() => {
setTimeout(() => {
setOpen(false);
setMobileOpen(false);
setExpand(null);
}, 0);
if (location.hash) {
setTimeout(() => {
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
useEffect(() => {
setFormVisible(false);
setTimeout(() => setFormVisible(false), 0);
if (location.pathname !== '/') return;
const el = document.getElementById('bewerben');
if (!el) return;
@@ -182,7 +185,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
{/* LEFT – logo */}
<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' }}>
<img
<Image unoptimized
src={logo}
alt={header.logo?.alt || header.overlayLogoAlt}
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 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>
@@ -435,7 +438,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
}}>
{/* Logo */}
<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>
{/* Hamburger */}
@@ -479,7 +482,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
{/* 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 }}>
<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>
<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 }}>
@@ -709,7 +712,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
maxWidth: isMobile ? '100%' : 420,
}}>
{/* Logo */}
<img
<Image unoptimized
src={footerLogo}
alt={footer.logo?.alt || footer.logoAlt}
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 }} />
{footer.partnerLogos.map((partner, index) => (
partner.image?.url ? (
<img
<Image unoptimized
key={`${partner.alt}-${index}`}
src={partner.image.url}
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";
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
const NAVY = "#111D55";
const GOLD = "#EFBF04";
const CREAM = "#EFE5E3";
const BLUE = "#4A8FC9";
const WHITE = "#fff";
const TIERS = [
@@ -1125,7 +1123,7 @@ function MembershipWizard() {
const [step, setStep] = useState(1);
const [submitted, setSubmitted] = useState(false);
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 contentRef = useRef<HTMLDivElement>(null);

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import Image from '@/spa/components/ui/UnoptimizedImage'
const GOLD = '#EFBF04';
const DARK = '#101828';
@@ -120,7 +121,7 @@ export default function LoadingScreen({ onDone }: { onDone: () => void }) {
position: 'absolute', inset: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<img
<Image unoptimized
src="/bmp-logo.png"
alt=""
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(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => { api?.off("select", onSelect) }
return () => {
api.off("reInit", onSelect)
api.off("select", onSelect)
}
}, [api, onSelect])
return (

View File

@@ -1,7 +1,7 @@
import * as React from "react"
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>(
({ className, type, ...props }, ref) => {

View File

@@ -1,7 +1,7 @@
import * as React from "react"
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>(
({ className, ...props }, ref) => {

View File

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

View File

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

View File

@@ -2,10 +2,11 @@ import React from 'react';
import { useParams, Link } from '@/spa/router';
import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react';
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 { docImageUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55';
const GOLD = '#EFBF04';
@@ -35,9 +36,17 @@ export default function BlogDetail() {
readTime: String(cmsArticle.readTime || '1 min'),
img: docImageUrl(cmsArticle, '/images/gala-saal-overview.jpg', 'heroImage'),
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.') }],
tags: Array.isArray(cmsArticle.tags) ? cmsArticle.tags.map((item: any) => item.tag).filter(Boolean) : ['Presse'],
relatedSlugs: Array.isArray(cmsArticle.relatedSlugs) ? cmsArticle.relatedSlugs.map((item: any) => item.slug).filter(Boolean) : [],
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: { 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);
if (!article) {
@@ -60,7 +69,7 @@ export default function BlogDetail() {
{/* ── HERO ──────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', height: isMobile ? 'auto' : '70vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
<img
<Image unoptimized
src={article.img}
alt={article.title}
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 */}
<div style={{ height: 180, overflow: 'hidden', marginBottom: 24 }}>
<img
<Image unoptimized
src={article.img}
alt={article.title}
style={{

View File

@@ -3,6 +3,7 @@ import { MapPin, Mail, Phone, Clock, ChevronDown } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import KontaktForm from '@/spa/components/forms/KontaktForm';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55';
const GOLD = '#EFBF04';
@@ -43,7 +44,7 @@ const Contact: React.FC = () => {
{/* ── Hero ───────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', height: '68vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
<img
<Image unoptimized
src="/images/gala-dinner.jpg"
alt="BMP Gala"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }}
@@ -130,7 +131,7 @@ const Contact: React.FC = () => {
{!isMobile && (
<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', 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 }}>

View File

@@ -175,7 +175,7 @@ export default function Datenschutz() {
</Section>
<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, fontWeight: 600, color: NAVY, marginBottom: 8 }}>Verarbeitete Daten</p>
<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 { useParams, Link } from '@/spa/router'
import { motion } from 'framer-motion'
import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight, Users, Tag } from 'lucide-react'
import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent } from '@/spa/data/events'
import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight } from 'lucide-react'
import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent, type EventEckdaten } from '@/spa/data/events'
import { useCmsRoute } from '@/spa/cmsRoute'
import { docImageUrl } from '@/spa/cmsMediaField'
import { useIsMobile } from '@/spa/hooks/useIsMobile'
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55'
const GOLD = '#EFBF04'
const CREAM = '#EFE5E3'
const INK = '#3A3A3A'
// ── Timestamp formatter ──────────────────────────────────────────────────────
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'),
venue: String(cmsEvent.venue || 'Ort folgt'),
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'),
description: String(cmsEvent.description || cmsEvent.meta?.description || ''),
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 : [],
updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as any : [],
eckdaten: Array.isArray(cmsEvent.eckdaten) ? cmsEvent.eckdaten as EventEckdaten[] : [],
updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as EventUpdate[] : [],
} as BmpEvent : undefined)
const [subscribed, setSubscribed] = useState(false)
const isMobile = useIsMobile()
@@ -117,7 +117,7 @@ export default function EventDetail() {
{/* ── Hero ─────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', height: isMobile ? 'auto' : '65vh', minHeight: isMobile ? 520 : 480, overflow: 'hidden' }}>
<img
<Image unoptimized
src={event.img}
alt={event.title}
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 { Button } from '@/spa/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/spa/components/ui/card';
import { Badge } from '@/spa/components/ui/badge';
import { Award, Users, TrendingUp, Play, ChevronRight, Building2, ShieldCheck, Leaf, HeartHandshake, X, Star } from 'lucide-react';
import { Play, ChevronRight, X, Star } from 'lucide-react';
import { Link } from '@/spa/router';
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
import { WINNERS } from '@/spa/data/winners';
@@ -10,6 +8,7 @@ import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import TestimonialsSection from '@/spa/components/TestimonialsSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
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></> },
@@ -103,7 +102,7 @@ function HomeWinnerCard({ winner }: { winner: import('@/spa/data/winners').Winne
onMouseEnter={() => setHovered(true)}
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: '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)' }} />
@@ -131,7 +130,7 @@ const Home: React.FC = () => {
{/* Hero Section */}
<section className="relative flex items-center overflow-hidden" style={{ background: '#111D55', height: isMobile ? '88svh' : '100vh' }}>
<div className="absolute inset-0 z-0">
<img
<Image unoptimized
src="/images/gala-saal-overview.jpg"
alt="Awards Gala"
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 */}
<div style={{ position: 'relative', minHeight: isMobile ? 280 : 560, overflow: 'hidden', order: isMobile ? 1 : 0 }}>
<img
<Image unoptimized
src="/images/buehne-gewinner.jpg"
alt="Preisverleihung Bühne"
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%' }}>
{/* Left – full-bleed image with stats overlay (+ heading on mobile) */}
<div style={{ position: 'relative', minHeight: isMobile ? 300 : 600, overflow: 'hidden' }}>
<img
<Image unoptimized
src="/images/preistraeger-jubel.jpg"
alt="BMP Auszeichnung"
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' }} />
<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>
<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.
@@ -444,7 +443,7 @@ const Home: React.FC = () => {
{/* Right – full-bleed photo (+ heading on mobile) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : 'auto', order: isMobile ? 1 : 0 }}>
<img
<Image unoptimized
src="/images/networking-innenhof.jpg"
alt="BMP Gala Netzwerk"
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 */}
{!isMobile && (
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<img
<Image unoptimized
src="/images/preisuebergabe.jpg"
alt="Preisverleihung Bayerischer Mittelstandspreis"
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 { useIsMobile } from '@/spa/hooks/useIsMobile';
import MembershipWizard from '@/spa/components/membership/MembershipWizard';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55';
const GOLD = '#EFBF04';
@@ -597,7 +598,7 @@ function QuoteCard({
flex: 1,
}}
>
"{quote}"
„{quote}“
</p>
<div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20 }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
@@ -708,7 +709,7 @@ const MitgliedWerden: React.FC = () => {
}}
>
{/* Background image */}
<img
<Image unoptimized
src="/images/mitglied-hero.jpg"
alt=""
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 }}>
{!isMobile && (
<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', bottom: 0, left: 0, right: 0, height: 3, background: GOLD }} />
<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 MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
@@ -41,7 +42,7 @@ function PersonCard({ imgSrc, imgAlt, name, titleLine1, titleLine2, institution,
onMouseLeave={() => setHovered(false)}
>
<div style={{ position: 'relative', height: isMobileCard ? 250 : 320, overflow: 'hidden', width: '100%' }}>
<img
<Image unoptimized
src={imgSrc}
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)' }}
@@ -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' } : {}) }}
>
<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>
<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' }}>
<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>
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
@@ -144,30 +145,6 @@ interface Partner {
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) ──────────────────────────────────────────────────────
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' }}>
{/* Background */}
{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%)` }} />
}
{/* Gradient to white */}
@@ -527,7 +504,7 @@ const Netzwerk: React.FC = () => {
{/* ── 1. HERO ──────────────────────────────────────────────────────────── */}
<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', 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 }} />
@@ -588,7 +565,7 @@ const Netzwerk: React.FC = () => {
</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}` }}>
"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>
<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
@@ -813,7 +790,7 @@ const Netzwerk: React.FC = () => {
{!isMobile && (
<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', 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 }}>

View File

@@ -1,10 +1,11 @@
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 BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
@@ -21,7 +22,7 @@ const Participation: React.FC = () => {
{/* ── HERO ─────────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', minHeight: '65vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: NAVY }}>
<img
<Image unoptimized
src="/images/preisuebergabe.jpg"
alt="Teilnahme BMP"
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) */}
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
<img
<Image unoptimized
src="/images/buehne-gewinner.jpg"
alt="Jury-Bewertung BMP"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
@@ -313,7 +314,7 @@ const Participation: React.FC = () => {
{!isMobile && (
<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', 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 }}>
@@ -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 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 },
] 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;
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)' }}>

View File

@@ -5,6 +5,7 @@ import { WINNERS, CATEGORIES, YEARS } from '@/spa/data/winners';
import { useCmsCollection } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif';
const NAVY = '#111D55';
@@ -62,7 +63,7 @@ export default function Preistraeger() {
<div style={{ background: '#fff', minHeight: '100vh' }}>
{/* Hero */}
<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"
alt="Preisverleihung"
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)}
onMouseLeave={() => setHovered(false)}
>
<img
<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.06)' : 'scale(1)' }}

View File

@@ -5,6 +5,7 @@ import { WINNERS } from '@/spa/data/winners';
import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
@@ -40,14 +41,11 @@ export default function PreistraegerDetail() {
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 (
<div style={{ background: '#fff', minHeight: '100vh' }}>
{/* Hero */}
<div style={{ width: '100%', height: '60vh', overflow: 'hidden', position: 'relative' }}>
<img
<Image unoptimized
src={winner.img}
alt={winner.name}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
@@ -110,9 +108,9 @@ export default function PreistraegerDetail() {
{/* Quote */}
<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 }}>
<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}
<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 style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: GRAY, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
– {winner.quotePerson}, {winner.quoteRole}
@@ -124,7 +122,7 @@ export default function PreistraegerDetail() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{/* Photo */}
<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>
{/* Kenndaten */}

View File

@@ -5,6 +5,7 @@ import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { useCmsCollection } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField';
import Image from '@/spa/components/ui/UnoptimizedImage'
const NAVY = '#111D55';
const GOLD = '#EFBF04';
@@ -90,7 +91,7 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
>
{/* image */}
<div style={{ position: 'relative', overflow: 'hidden', height: 220 }}>
<img
<Image unoptimized
src={item.img}
alt={item.title}
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 isMobile = useIsMobile();
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 (
<div className="animate-fade-in">
@@ -282,7 +282,7 @@ const Press: React.FC = () => {
background: NAVY,
}}
>
<img
<Image unoptimized
src="/images/gala-dinner.jpg"
alt="Presse BMP"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
@@ -428,7 +428,7 @@ const Press: React.FC = () => {
>
{/* Left – image cell */}
<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)' }}>
{event.status}
</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(
@@ -71,8 +70,7 @@ function useClickableCard<T extends HTMLElement>({
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[router, card, link, timeDown],
[external, newTab, router, scroll],
)
useEffect(() => {
@@ -92,8 +90,7 @@ function useClickableCard<T extends HTMLElement>({
return () => {
abortController.abort()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [card, link, router])
}, [handleMouseDown, handleMouseUp])
return {
card: {

View File

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

View File

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