chore: fix linting errors and ts bugs.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 } : {})}>
|
||||
|
||||
@@ -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[],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
16
src/spa/components/ui/UnoptimizedImage.tsx
Normal file
16
src/spa/components/ui/UnoptimizedImage.tsx
Normal 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} />
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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%)' }}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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)' }}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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)' }}>
|
||||
|
||||
@@ -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)' }}
|
||||
|
||||
@@ -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"' }}>“</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"' }}>”</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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user