chore: fix linting errors and ts bugs.
This commit is contained in:
@@ -1,17 +1,27 @@
|
|||||||
import { dirname } from 'path'
|
import nextVitals from 'eslint-config-next/core-web-vitals'
|
||||||
import { fileURLToPath } from 'url'
|
import tseslint from 'typescript-eslint'
|
||||||
import { FlatCompat } from '@eslint/eslintrc'
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = dirname(__filename)
|
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
})
|
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
...nextVitals,
|
||||||
{
|
{
|
||||||
|
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
react: nextVitals[0].plugins.react,
|
||||||
|
'react-hooks': nextVitals[0].plugins['react-hooks'],
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'react/no-unescaped-entities': 'warn',
|
||||||
|
'react-hooks/immutability': 'warn',
|
||||||
|
'react-hooks/refs': 'warn',
|
||||||
|
'react-hooks/set-state-in-effect': 'warn',
|
||||||
|
'react-hooks/static-components': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx,mts,cts}'],
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint.plugin,
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
import type { Header } from '@/payload-types'
|
import type { Header } from '@/payload-types'
|
||||||
|
|
||||||
@@ -14,20 +14,13 @@ interface HeaderClientProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
||||||
/* Storing the value in a useState to avoid hydration errors */
|
|
||||||
const [theme, setTheme] = useState<string | null>(null)
|
|
||||||
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const theme = headerTheme || null
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHeaderTheme(null)
|
setHeaderTheme(null)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [pathname, setHeaderTheme])
|
||||||
}, [pathname])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (headerTheme && headerTheme !== theme) setTheme(headerTheme)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [headerTheme])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="container relative z-20 " {...(theme ? { 'data-theme': theme } : {})}>
|
<header className="container relative z-20 " {...(theme ? { 'data-theme': theme } : {})}>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { notFound } from 'next/navigation'
|
|||||||
|
|
||||||
import NextApp from '@/spa/NextApp'
|
import NextApp from '@/spa/NextApp'
|
||||||
import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation'
|
import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation'
|
||||||
|
import type { CmsRouteDoc } from '@/spa/cmsRoute'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ function routeTarget(pathname: string): { collection: RouteCollection; slug: str
|
|||||||
async function findRouteDoc(payload: Awaited<ReturnType<typeof getPayload>>, pathname: string, draft: boolean) {
|
async function findRouteDoc(payload: Awaited<ReturnType<typeof getPayload>>, pathname: string, draft: boolean) {
|
||||||
const target = routeTarget(pathname)
|
const target = routeTarget(pathname)
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: target.collection as any,
|
collection: target.collection,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
draft,
|
draft,
|
||||||
overrideAccess: draft,
|
overrideAccess: draft,
|
||||||
@@ -56,7 +57,7 @@ async function findRouteDoc(payload: Awaited<ReturnType<typeof getPayload>>, pat
|
|||||||
|
|
||||||
async function findList(payload: Awaited<ReturnType<typeof getPayload>>, collection: RouteCollection, draft: boolean) {
|
async function findList(payload: Awaited<ReturnType<typeof getPayload>>, collection: RouteCollection, draft: boolean) {
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: collection as any,
|
collection,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
draft,
|
draft,
|
||||||
overrideAccess: draft,
|
overrideAccess: draft,
|
||||||
@@ -90,11 +91,11 @@ export default async function Page({ params }: Args) {
|
|||||||
footer={normalizeFooterData(footer)}
|
footer={normalizeFooterData(footer)}
|
||||||
cmsRoute={{
|
cmsRoute={{
|
||||||
collection: route.collection,
|
collection: route.collection,
|
||||||
doc: route.doc as any,
|
doc: route.doc as unknown as CmsRouteDoc,
|
||||||
lists: {
|
lists: {
|
||||||
preistraeger: preistraeger as any,
|
preistraeger: preistraeger as unknown as CmsRouteDoc[],
|
||||||
events: events as any,
|
events: events as unknown as CmsRouteDoc[],
|
||||||
posts: posts as any,
|
posts: posts as unknown as CmsRouteDoc[],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -131,8 +131,9 @@ export const FormBlock: React.FC<
|
|||||||
{formFromProps &&
|
{formFromProps &&
|
||||||
formFromProps.fields &&
|
formFromProps.fields &&
|
||||||
formFromProps.fields?.map((field, index) => {
|
formFromProps.fields?.map((field, index) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const Field = fields?.[
|
||||||
const Field: React.FC<any> = fields?.[field.blockType as keyof typeof fields]
|
field.blockType as keyof typeof fields
|
||||||
|
] as React.ComponentType<Record<string, unknown>> | undefined
|
||||||
if (Field) {
|
if (Field) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 last:mb-0" key={index}>
|
<div className="mb-6 last:mb-0" key={index}>
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ export const Events: CollectionConfig = {
|
|||||||
url: ({ data, req }) =>
|
url: ({ data, req }) =>
|
||||||
generatePreviewPath({
|
generatePreviewPath({
|
||||||
slug: data?.spaPath || data?.slug,
|
slug: data?.spaPath || data?.slug,
|
||||||
collection: 'events' as any,
|
collection: 'events',
|
||||||
req,
|
req,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
preview: (data, { req }) =>
|
preview: (data, { req }) =>
|
||||||
generatePreviewPath({
|
generatePreviewPath({
|
||||||
slug: ((data?.spaPath as string | undefined) || (data?.slug as string)),
|
slug: ((data?.spaPath as string | undefined) || (data?.slug as string)),
|
||||||
collection: 'events' as any,
|
collection: 'events',
|
||||||
req,
|
req,
|
||||||
}),
|
}),
|
||||||
useAsTitle: 'title',
|
useAsTitle: 'title',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { User } from 'src/payload-types'
|
|||||||
// This means that we need to populate the authors manually here to protect user privacy
|
// This means that we need to populate the authors manually here to protect user privacy
|
||||||
// GraphQL will not return mutated user data that differs from the underlying schema
|
// GraphQL will not return mutated user data that differs from the underlying schema
|
||||||
// So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI
|
// So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI
|
||||||
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => {
|
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => {
|
||||||
if (doc?.authors && doc?.authors?.length > 0) {
|
if (doc?.authors && doc?.authors?.length > 0) {
|
||||||
const authorDocs: User[] = []
|
const authorDocs: User[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ export const Preistraeger: CollectionConfig = {
|
|||||||
url: ({ data, req }) =>
|
url: ({ data, req }) =>
|
||||||
generatePreviewPath({
|
generatePreviewPath({
|
||||||
slug: data?.spaPath || data?.slug,
|
slug: data?.spaPath || data?.slug,
|
||||||
collection: 'preistraeger' as any,
|
collection: 'preistraeger',
|
||||||
req,
|
req,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
preview: (data, { req }) =>
|
preview: (data, { req }) =>
|
||||||
generatePreviewPath({
|
generatePreviewPath({
|
||||||
slug: ((data?.spaPath as string | undefined) || (data?.slug as string)),
|
slug: ((data?.spaPath as string | undefined) || (data?.slug as string)),
|
||||||
collection: 'preistraeger' as any,
|
collection: 'preistraeger',
|
||||||
req,
|
req,
|
||||||
}),
|
}),
|
||||||
useAsTitle: 'title',
|
useAsTitle: 'title',
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export const Card: React.FC<{
|
|||||||
showCategories?: boolean
|
showCategories?: boolean
|
||||||
title?: string
|
title?: string
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { card, link } = useClickableCard({})
|
const {
|
||||||
|
card: { ref: cardRef },
|
||||||
|
link: { ref: linkRef },
|
||||||
|
} = useClickableCard({})
|
||||||
const { className, doc, relationTo, showCategories, title: titleFromProps } = props
|
const { className, doc, relationTo, showCategories, title: titleFromProps } = props
|
||||||
|
|
||||||
const { slug, categories, meta, title } = doc || {}
|
const { slug, categories, meta, title } = doc || {}
|
||||||
@@ -35,7 +38,7 @@ export const Card: React.FC<{
|
|||||||
'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer',
|
'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={card.ref}
|
ref={cardRef}
|
||||||
>
|
>
|
||||||
<div className="relative w-full ">
|
<div className="relative w-full ">
|
||||||
{!metaImage && <div className="">No image</div>}
|
{!metaImage && <div className="">No image</div>}
|
||||||
@@ -67,7 +70,7 @@ export const Card: React.FC<{
|
|||||||
{titleToUse && (
|
{titleToUse && (
|
||||||
<div className="prose">
|
<div className="prose">
|
||||||
<h3>
|
<h3>
|
||||||
<Link className="not-prose" href={href} ref={link.ref}>
|
<Link className="not-prose" href={href} ref={linkRef}>
|
||||||
{titleToUse}
|
{titleToUse}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -14,8 +15,7 @@ export const Logo = (props: Props) => {
|
|||||||
const priority = priorityFromProps || 'low'
|
const priority = priorityFromProps || 'low'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
/* eslint-disable @next/next/no-img-element */
|
<Image unoptimized
|
||||||
<img
|
|
||||||
alt="Payload Logo"
|
alt="Payload Logo"
|
||||||
width={193}
|
width={193}
|
||||||
height={34}
|
height={34}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export const seed = async ({
|
|||||||
|
|
||||||
payload.logger.info(`— Seeding pages...`)
|
payload.logger.info(`— Seeding pages...`)
|
||||||
|
|
||||||
const [_, contactPage] = await Promise.all([
|
await Promise.all([
|
||||||
payload.create({
|
payload.create({
|
||||||
collection: 'pages',
|
collection: 'pages',
|
||||||
depth: 0,
|
depth: 0,
|
||||||
@@ -222,11 +222,11 @@ export const seed = async ({
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
payload.updateGlobal({
|
payload.updateGlobal({
|
||||||
slug: 'header',
|
slug: 'header',
|
||||||
data: headerSeedData as any,
|
data: headerSeedData as never,
|
||||||
}),
|
}),
|
||||||
payload.updateGlobal({
|
payload.updateGlobal({
|
||||||
slug: 'footer',
|
slug: 'footer',
|
||||||
data: footerSeedData as any,
|
data: footerSeedData as never,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import { themeLocalStorageKey } from './types'
|
|||||||
|
|
||||||
export const ThemeSelector: React.FC = () => {
|
export const ThemeSelector: React.FC = () => {
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
const [value, setValue] = useState('')
|
const [value, setValue] = useState(() =>
|
||||||
|
typeof window === 'undefined' ? 'auto' : window.localStorage.getItem(themeLocalStorageKey) ?? 'auto',
|
||||||
|
)
|
||||||
|
|
||||||
const onThemeChange = (themeToSet: Theme & 'auto') => {
|
const onThemeChange = (themeToSet: Theme & 'auto') => {
|
||||||
if (themeToSet === 'auto') {
|
if (themeToSet === 'auto') {
|
||||||
@@ -28,10 +30,6 @@ export const ThemeSelector: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const preference = window.localStorage.getItem(themeLocalStorageKey)
|
|
||||||
setValue(preference ?? 'auto')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select onValueChange={onThemeChange} value={value}>
|
<Select onValueChange={onThemeChange} value={value}>
|
||||||
|
|||||||
@@ -16,9 +16,14 @@ const initialContext: ThemeContextType = {
|
|||||||
const ThemeContext = createContext(initialContext)
|
const ThemeContext = createContext(initialContext)
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [theme, setThemeState] = useState<Theme | undefined>(
|
const [theme, setThemeState] = useState<Theme | undefined>(() => {
|
||||||
canUseDOM ? (document.documentElement.getAttribute('data-theme') as Theme) : undefined,
|
if (!canUseDOM) return undefined
|
||||||
)
|
|
||||||
|
const preference = window.localStorage.getItem(themeLocalStorageKey)
|
||||||
|
if (themeIsValid(preference)) return preference
|
||||||
|
|
||||||
|
return getImplicitPreference() || defaultTheme
|
||||||
|
})
|
||||||
|
|
||||||
const setTheme = useCallback((themeToSet: Theme | null) => {
|
const setTheme = useCallback((themeToSet: Theme | null) => {
|
||||||
if (themeToSet === null) {
|
if (themeToSet === null) {
|
||||||
@@ -48,7 +53,6 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme', themeToSet)
|
document.documentElement.setAttribute('data-theme', themeToSet)
|
||||||
setThemeState(themeToSet)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <ThemeContext value={{ setTheme, theme }}>{children}</ThemeContext>
|
return <ThemeContext value={{ setTheme, theme }}>{children}</ThemeContext>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import crypto from 'crypto'
|
|||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getPayload, type File } from 'payload'
|
import { getPayload, type File, type Payload } from 'payload'
|
||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
const spaDir = path.join(root, 'src/spa')
|
const spaDir = path.join(root, 'src/spa')
|
||||||
@@ -127,7 +127,7 @@ function altForSource(source: string) {
|
|||||||
return basename.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ')
|
return basename.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findExistingMedia(payload: any, filename: string) {
|
async function findExistingMedia(payload: Payload, filename: string) {
|
||||||
const existing = await payload.find({
|
const existing = await payload.find({
|
||||||
collection: 'media',
|
collection: 'media',
|
||||||
depth: 0,
|
depth: 0,
|
||||||
@@ -169,7 +169,7 @@ async function main() {
|
|||||||
assetId('/bmp-logo.png')
|
assetId('/bmp-logo.png')
|
||||||
? payload.updateGlobal({
|
? payload.updateGlobal({
|
||||||
slug: 'header',
|
slug: 'header',
|
||||||
data: { logo: assetId('/bmp-logo.png') } as any,
|
data: { logo: assetId('/bmp-logo.png') } as never,
|
||||||
context: { disableRevalidate: true },
|
context: { disableRevalidate: true },
|
||||||
})
|
})
|
||||||
: Promise.resolve(),
|
: Promise.resolve(),
|
||||||
@@ -185,7 +185,7 @@ async function main() {
|
|||||||
image: assetId('/images/partner-logos.png'),
|
image: assetId('/images/partner-logos.png'),
|
||||||
},
|
},
|
||||||
].filter((item) => item.image),
|
].filter((item) => item.image),
|
||||||
} as any,
|
} as never,
|
||||||
context: { disableRevalidate: true },
|
context: { disableRevalidate: true },
|
||||||
})
|
})
|
||||||
: Promise.resolve(),
|
: Promise.resolve(),
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ function dataForPage(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
|
|||||||
|
|
||||||
async function findExistingDoc(payload: Payload, collection: TargetCollection, page: SpaPageRegistryEntry) {
|
async function findExistingDoc(payload: Payload, collection: TargetCollection, page: SpaPageRegistryEntry) {
|
||||||
const existing = await payload.find({
|
const existing = await payload.find({
|
||||||
collection: collection as any,
|
collection: collection as never,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
pagination: false,
|
pagination: false,
|
||||||
@@ -218,7 +218,7 @@ async function findExistingDoc(payload: Payload, collection: TargetCollection, p
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return existing.docs[0]
|
return existing.docs[0] as { id: string | number } | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeMovedPageDoc(payload: Payload, page: SpaPageRegistryEntry) {
|
async function removeMovedPageDoc(payload: Payload, page: SpaPageRegistryEntry) {
|
||||||
@@ -240,17 +240,17 @@ async function upsertPage(payload: Payload, page: SpaPageRegistryEntry, mediaMap
|
|||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await payload.update({
|
await payload.update({
|
||||||
collection: collection as any,
|
collection: collection as never,
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
data,
|
data: data as never,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
context: { disableRevalidate: true },
|
context: { disableRevalidate: true },
|
||||||
})
|
})
|
||||||
payload.logger.info(`updated ${collection}: ${page.path}`)
|
payload.logger.info(`updated ${collection}: ${page.path}`)
|
||||||
} else {
|
} else {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: collection as any,
|
collection: collection as never,
|
||||||
data,
|
data: data as never,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
context: { disableRevalidate: true },
|
context: { disableRevalidate: true },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ export default function NextApp({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = resolvePage(pathname)
|
const page = React.createElement(resolvePage(pathname))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CmsRouteProvider value={cmsRoute}>
|
<CmsRouteProvider value={cmsRoute}>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Layout header={header} footer={footer}>
|
<Layout header={header} footer={footer}>
|
||||||
<Page />
|
{page}
|
||||||
</Layout>
|
</Layout>
|
||||||
</CmsRouteProvider>
|
</CmsRouteProvider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Media } from '@/payload-types'
|
import type { Footer, Header, Media } from '@/payload-types'
|
||||||
|
|
||||||
export type SpaIconName = 'instagram' | 'linkedin' | 'facebook'
|
export type SpaIconName = 'instagram' | 'linkedin' | 'facebook'
|
||||||
export type SpaLink = { label: string; path: string }
|
export type SpaLink = { label: string; path: string }
|
||||||
@@ -167,29 +167,50 @@ const mediaToSpa = (value: unknown, fallback?: SpaMedia): SpaMedia | undefined =
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeHeaderData(header: any): SpaHeaderData {
|
type HeaderLanguage = string | { label?: string }
|
||||||
|
|
||||||
|
type FooterPartnerLogo = {
|
||||||
|
alt?: string
|
||||||
|
image?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeHeaderData(header?: Partial<Header>): SpaHeaderData {
|
||||||
return {
|
return {
|
||||||
...defaultHeaderData,
|
...defaultHeaderData,
|
||||||
...header,
|
...header,
|
||||||
logo: mediaToSpa(header?.logo, defaultHeaderData.logo),
|
logo: mediaToSpa(header?.logo, defaultHeaderData.logo),
|
||||||
navItems: header?.navItems?.length ? header.navItems : defaultHeaderData.navItems,
|
navItems: header?.navItems?.length
|
||||||
secondaryLinks: header?.secondaryLinks?.length ? header.secondaryLinks : defaultHeaderData.secondaryLinks,
|
? (header.navItems as unknown as SpaHeaderNavItem[])
|
||||||
socialLinks: header?.socialLinks?.length ? header.socialLinks : defaultHeaderData.socialLinks,
|
: defaultHeaderData.navItems,
|
||||||
ctas: header?.ctas?.length ? header.ctas : defaultHeaderData.ctas,
|
secondaryLinks: header?.secondaryLinks?.length
|
||||||
languages: header?.languages?.length ? header.languages.map((item: any) => item.label || item).filter(Boolean) : defaultHeaderData.languages,
|
? (header.secondaryLinks as unknown as SpaLink[])
|
||||||
|
: defaultHeaderData.secondaryLinks,
|
||||||
|
socialLinks: header?.socialLinks?.length
|
||||||
|
? (header.socialLinks as unknown as SpaSocialLink[])
|
||||||
|
: defaultHeaderData.socialLinks,
|
||||||
|
ctas: header?.ctas?.length ? (header.ctas as unknown as SpaCtaLink[]) : defaultHeaderData.ctas,
|
||||||
|
languages: header?.languages?.length
|
||||||
|
? (header.languages as HeaderLanguage[])
|
||||||
|
.map((item) => (typeof item === 'string' ? item : item.label))
|
||||||
|
.filter((label): label is string => Boolean(label))
|
||||||
|
: defaultHeaderData.languages,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeFooterData(footer: any): SpaFooterData {
|
export function normalizeFooterData(footer?: Partial<Footer>): SpaFooterData {
|
||||||
return {
|
return {
|
||||||
...defaultFooterData,
|
...defaultFooterData,
|
||||||
...footer,
|
...footer,
|
||||||
logo: mediaToSpa(footer?.logo, defaultFooterData.logo),
|
logo: mediaToSpa(footer?.logo, defaultFooterData.logo),
|
||||||
ctas: footer?.ctas?.length ? footer.ctas : defaultFooterData.ctas,
|
ctas: footer?.ctas?.length ? (footer.ctas as unknown as SpaCtaLink[]) : defaultFooterData.ctas,
|
||||||
linkColumns: footer?.linkColumns?.length ? footer.linkColumns : defaultFooterData.linkColumns,
|
linkColumns: footer?.linkColumns?.length
|
||||||
socialLinks: footer?.socialLinks?.length ? footer.socialLinks : defaultFooterData.socialLinks,
|
? (footer.linkColumns as unknown as SpaFooterData['linkColumns'])
|
||||||
|
: defaultFooterData.linkColumns,
|
||||||
|
socialLinks: footer?.socialLinks?.length
|
||||||
|
? (footer.socialLinks as unknown as SpaSocialLink[])
|
||||||
|
: defaultFooterData.socialLinks,
|
||||||
partnerLogos: footer?.partnerLogos?.length
|
partnerLogos: footer?.partnerLogos?.length
|
||||||
? footer.partnerLogos.map((item: any, index: number) => ({
|
? (footer.partnerLogos as FooterPartnerLogo[]).map((item, index) => ({
|
||||||
alt: item.alt || defaultFooterData.partnerLogos[index]?.alt || '',
|
alt: item.alt || defaultFooterData.partnerLogos[index]?.alt || '',
|
||||||
image: mediaToSpa(item.image, defaultFooterData.partnerLogos[index]?.image),
|
image: mediaToSpa(item.image, defaultFooterData.partnerLogos[index]?.image),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ export default function TestimonialsSection() {
|
|||||||
<div style={{ background: '#fff', borderRadius: 20, border: '1px solid rgba(17,29,85,0.10)', boxShadow: '0 16px 38px rgba(3,9,58,0.10)', overflow: 'hidden' }}>
|
<div style={{ background: '#fff', borderRadius: 20, border: '1px solid rgba(17,29,85,0.10)', boxShadow: '0 16px 38px rgba(3,9,58,0.10)', overflow: 'hidden' }}>
|
||||||
{/* Photo banner */}
|
{/* Photo banner */}
|
||||||
<div style={{ position: 'relative', height: 240, overflow: 'hidden' }}>
|
<div style={{ position: 'relative', height: 240, overflow: 'hidden' }}>
|
||||||
<img key={t.img} src={t.img} alt={t.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top', display: 'block' }} />
|
<Image unoptimized key={t.img} src={t.img} alt={t.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top', display: 'block' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.28) 0%, transparent 45%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.28) 0%, transparent 45%)' }} />
|
||||||
<span style={{ position: 'absolute', top: 14, left: 14, fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#101828', background: '#EFBF04', padding: '5px 10px', borderRadius: 100 }}>Preisträger {t.year}</span>
|
<span style={{ position: 'absolute', top: 14, left: 14, fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#101828', background: '#EFBF04', padding: '5px 10px', borderRadius: 100 }}>Preisträger {t.year}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +75,7 @@ export default function TestimonialsSection() {
|
|||||||
<div style={{ display: 'grid', gridTemplateColumns: '42% 58%', height: 560, position: 'relative', zIndex: 1 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '42% 58%', height: 560, position: 'relative', zIndex: 1 }}>
|
||||||
{/* Left – portrait photo */}
|
{/* Left – portrait photo */}
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 280 : 'auto' }}>
|
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 280 : 'auto' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
key={t.img}
|
key={t.img}
|
||||||
src={t.img}
|
src={t.img}
|
||||||
alt={t.name}
|
alt={t.name}
|
||||||
|
|||||||
@@ -261,6 +261,10 @@ function readSectionColors(section: HTMLElement): ModuleEdit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setElementText(el: HTMLElement, text: string) {
|
||||||
|
el.innerText = text;
|
||||||
|
}
|
||||||
|
|
||||||
function readElementProps(el: HTMLElement): ElementEdit {
|
function readElementProps(el: HTMLElement): ElementEdit {
|
||||||
const cs = window.getComputedStyle(el);
|
const cs = window.getComputedStyle(el);
|
||||||
const bg = cs.backgroundColor;
|
const bg = cs.backgroundColor;
|
||||||
@@ -326,7 +330,9 @@ function ColorRow({
|
|||||||
}) {
|
}) {
|
||||||
const pickerRef = useRef<HTMLInputElement>(null);
|
const pickerRef = useRef<HTMLInputElement>(null);
|
||||||
const [hex, setHex] = useState(value);
|
const [hex, setHex] = useState(value);
|
||||||
useEffect(() => { setHex(value); }, [value]);
|
useEffect(() => {
|
||||||
|
window.setTimeout(() => setHex(value), 0);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const commit = (raw: string) => {
|
const commit = (raw: string) => {
|
||||||
if (allowEmpty && raw === '') { onChange(''); setHex(''); return; }
|
if (allowEmpty && raw === '') { onChange(''); setHex(''); return; }
|
||||||
@@ -393,11 +399,6 @@ const FONT_WEIGHTS = [
|
|||||||
{ value: '900', label: 'Black'},
|
{ value: '900', label: 'Black'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ALIGN_OPTIONS = [
|
|
||||||
{ value: 'left', icon: '⬛' }, { value: 'center', icon: '≡' },
|
|
||||||
{ value: 'right', icon: '▶' }, { value: 'justify', icon: '▬' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function ElementPopup({
|
function ElementPopup({
|
||||||
popup, savedEdits, onSave, onClose,
|
popup, savedEdits, onSave, onClose,
|
||||||
}: {
|
}: {
|
||||||
@@ -414,17 +415,16 @@ function ElementPopup({
|
|||||||
}, [popup.sel, popup.el, savedEdits]);
|
}, [popup.sel, popup.el, savedEdits]);
|
||||||
|
|
||||||
const [props, setProps] = useState<ElementEdit>(init);
|
const [props, setProps] = useState<ElementEdit>(init);
|
||||||
useEffect(() => { setProps(init()); }, [popup.sel, init]);
|
|
||||||
|
|
||||||
const set = <K extends keyof ElementEdit>(key: K, val: ElementEdit[K]) =>
|
const set = <K extends keyof ElementEdit>(key: K, val: ElementEdit[K]) =>
|
||||||
setProps(p => ({ ...p, [key]: val }));
|
setProps(p => ({ ...p, [key]: val }));
|
||||||
|
|
||||||
// ── LIVE PREVIEW – apply every prop change directly to the element ──────
|
// ── LIVE PREVIEW – apply every prop change directly to the element ──────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
applyEditToElement(popup.el, props);
|
const element = popup.el;
|
||||||
if (props.text !== undefined) popup.el.innerText = props.text;
|
applyEditToElement(element, props);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
if (props.text !== undefined) setElementText(element, props.text);
|
||||||
}, [props]);
|
}, [popup.el, props]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onSave(popup.sel, props);
|
onSave(popup.sel, props);
|
||||||
@@ -644,14 +644,11 @@ export default function LayoutEditor() {
|
|||||||
try { localStorage.removeItem('bmp-editor-colors'); } catch {}
|
try { localStorage.removeItem('bmp-editor-colors'); } catch {}
|
||||||
const mods = loadModuleEdits();
|
const mods = loadModuleEdits();
|
||||||
const els = loadElementEdits();
|
const els = loadElementEdits();
|
||||||
setSavedModules(mods);
|
|
||||||
setSavedElements(els);
|
|
||||||
const p = location.pathname;
|
const p = location.pathname;
|
||||||
injectCSS(STYLE_MODULE, buildModuleCSS(mods, p));
|
injectCSS(STYLE_MODULE, buildModuleCSS(mods, p));
|
||||||
injectCSS(STYLE_ELEMENT, buildElementCSS(els, p));
|
injectCSS(STYLE_ELEMENT, buildElementCSS(els, p));
|
||||||
setTimeout(() => reapplyAllElementEdits(els, p), 80);
|
setTimeout(() => reapplyAllElementEdits(els, p), 80);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [location.pathname]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Rebuild CSS + re-apply when route changes ─────────────────────────────
|
// ── Rebuild CSS + re-apply when route changes ─────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -670,17 +667,15 @@ export default function LayoutEditor() {
|
|||||||
}
|
}
|
||||||
if (!editMode) {
|
if (!editMode) {
|
||||||
cancelModuleEdit();
|
cancelModuleEdit();
|
||||||
setPopup(null);
|
setTimeout(() => setPopup(null), 0);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [editMode, editTarget, location.pathname]);
|
||||||
}, [editMode, editTarget]);
|
|
||||||
|
|
||||||
// ── Live section preview ───────────────────────────────────────────────────
|
// ── Live section preview ───────────────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selSel) { injectCSS(STYLE_PREVIEW, ''); return; }
|
if (!selSel) { injectCSS(STYLE_PREVIEW, ''); return; }
|
||||||
injectCSS(STYLE_PREVIEW, buildModuleCSS({ [selSel]: preview }, location.pathname));
|
injectCSS(STYLE_PREVIEW, buildModuleCSS({ [selSel]: preview }, location.pathname));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [location.pathname, preview, selSel]);
|
||||||
}, [preview, selSel]);
|
|
||||||
|
|
||||||
// ── Click handler ─────────────────────────────────────────────────────────
|
// ── Click handler ─────────────────────────────────────────────────────────
|
||||||
const handleClick = useCallback((e: MouseEvent) => {
|
const handleClick = useCallback((e: MouseEvent) => {
|
||||||
@@ -709,7 +704,7 @@ export default function LayoutEditor() {
|
|||||||
const sel = getElementSelector(target, location.pathname);
|
const sel = getElementSelector(target, location.pathname);
|
||||||
const originalStyle = target.getAttribute('style') || '';
|
const originalStyle = target.getAttribute('style') || '';
|
||||||
setPopup({ x: e.clientX + 14, y: e.clientY + 14, el: target, sel, originalStyle });
|
setPopup({ x: e.clientX + 14, y: e.clientY + 14, el: target, sel, originalStyle });
|
||||||
}, [editMode, editTarget, savedModules]);
|
}, [editMode, editTarget, location.pathname, savedModules]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('click', handleClick, true);
|
document.addEventListener('click', handleClick, true);
|
||||||
@@ -1144,6 +1139,7 @@ export default function LayoutEditor() {
|
|||||||
{/* Element popup */}
|
{/* Element popup */}
|
||||||
{popup && (
|
{popup && (
|
||||||
<ElementPopup
|
<ElementPopup
|
||||||
|
key={popup.sel}
|
||||||
popup={popup}
|
popup={popup}
|
||||||
savedEdits={savedElements}
|
savedEdits={savedElements}
|
||||||
onSave={saveElementEdit}
|
onSave={saveElementEdit}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Link, useLocation } from '@/spa/router';
|
import { Link, useLocation } from '@/spa/router';
|
||||||
import { ArrowRight, Upload } from 'lucide-react';
|
import { ArrowRight, Upload } from 'lucide-react';
|
||||||
@@ -27,7 +29,6 @@ const ICONS: Record<SpaIconName, () => React.ReactElement> = {
|
|||||||
|
|
||||||
|
|
||||||
/* ── Tokens ────────────────────────────────────────────── */
|
/* ── Tokens ────────────────────────────────────────────── */
|
||||||
const NAVY = '#111D55';
|
|
||||||
const ANTHRA = '#1A2030'; // nav + footer shared bg
|
const ANTHRA = '#1A2030'; // nav + footer shared bg
|
||||||
const GOLD = '#EFBF04';
|
const GOLD = '#EFBF04';
|
||||||
const MUTED = '#D0D5DD';
|
const MUTED = '#D0D5DD';
|
||||||
@@ -77,9 +78,11 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
|
|
||||||
// Close everything on route change
|
// Close everything on route change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(false);
|
setTimeout(() => {
|
||||||
setMobileOpen(false);
|
setOpen(false);
|
||||||
setExpand(null);
|
setMobileOpen(false);
|
||||||
|
setExpand(null);
|
||||||
|
}, 0);
|
||||||
if (location.hash) {
|
if (location.hash) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const el = document.querySelector(location.hash);
|
const el = document.querySelector(location.hash);
|
||||||
@@ -134,7 +137,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
|
|
||||||
// Hide floating CTA when the form section (#bewerben) is in view – homepage only
|
// Hide floating CTA when the form section (#bewerben) is in view – homepage only
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormVisible(false);
|
setTimeout(() => setFormVisible(false), 0);
|
||||||
if (location.pathname !== '/') return;
|
if (location.pathname !== '/') return;
|
||||||
const el = document.getElementById('bewerben');
|
const el = document.getElementById('bewerben');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -182,7 +185,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
{/* LEFT – logo */}
|
{/* LEFT – logo */}
|
||||||
<div style={{ flex:1, display:'flex', alignItems:'center', padding:'0 24px', height:'100%', borderRight:`1px solid ${LINE}` }}>
|
<div style={{ flex:1, display:'flex', alignItems:'center', padding:'0 24px', height:'100%', borderRight:`1px solid ${LINE}` }}>
|
||||||
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={logo}
|
src={logo}
|
||||||
alt={header.logo?.alt || header.overlayLogoAlt}
|
alt={header.logo?.alt || header.overlayLogoAlt}
|
||||||
style={{ height:38, width:'auto', display:'block', objectFit:'contain' }}
|
style={{ height:38, width:'auto', display:'block', objectFit:'contain' }}
|
||||||
@@ -256,7 +259,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position:'absolute', top:48, right:80 }}>
|
<div style={{ position:'absolute', top:48, right:80 }}>
|
||||||
<img src={logo} alt={header.overlayLogoAlt} style={{ height:40, width:'auto', display:'block', opacity:0.9 }} />
|
<Image unoptimized src={logo} alt={header.overlayLogoAlt} style={{ height:40, width:'auto', display:'block', opacity:0.9 }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -435,7 +438,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
}}>
|
}}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
||||||
<img src={logo} alt={header.logo?.alt || header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
<Image unoptimized src={logo} alt={header.logo?.alt || header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Hamburger */}
|
{/* Hamburger */}
|
||||||
@@ -479,7 +482,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
{/* Header row: logo + close */}
|
{/* Header row: logo + close */}
|
||||||
<div style={{ position:'relative', zIndex:1, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 8px 0 20px', height:60, borderBottom:`1px solid ${SEP}`, flexShrink:0 }}>
|
<div style={{ position:'relative', zIndex:1, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 8px 0 20px', height:60, borderBottom:`1px solid ${SEP}`, flexShrink:0 }}>
|
||||||
<Link to="/" onClick={() => setMobileOpen(false)} style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
<Link to="/" onClick={() => setMobileOpen(false)} style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
|
||||||
<img src={logo} alt={header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
<Image unoptimized src={logo} alt={header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
|
||||||
</Link>
|
</Link>
|
||||||
<button onClick={() => setMobileOpen(false)} aria-label={header.closeLabel}
|
<button onClick={() => setMobileOpen(false)} aria-label={header.closeLabel}
|
||||||
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}>
|
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}>
|
||||||
@@ -709,7 +712,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
maxWidth: isMobile ? '100%' : 420,
|
maxWidth: isMobile ? '100%' : 420,
|
||||||
}}>
|
}}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<img
|
<Image unoptimized
|
||||||
src={footerLogo}
|
src={footerLogo}
|
||||||
alt={footer.logo?.alt || footer.logoAlt}
|
alt={footer.logo?.alt || footer.logoAlt}
|
||||||
style={{ width: 140, height: 'auto', marginBottom: 10, opacity: 0.9 }}
|
style={{ width: 140, height: 'auto', marginBottom: 10, opacity: 0.9 }}
|
||||||
@@ -895,7 +898,7 @@ const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, f
|
|||||||
<div style={{ width: 1, height: 20, background: 'rgba(239,191,4,0.2)', flexShrink: 0 }} />
|
<div style={{ width: 1, height: 20, background: 'rgba(239,191,4,0.2)', flexShrink: 0 }} />
|
||||||
{footer.partnerLogos.map((partner, index) => (
|
{footer.partnerLogos.map((partner, index) => (
|
||||||
partner.image?.url ? (
|
partner.image?.url ? (
|
||||||
<img
|
<Image unoptimized
|
||||||
key={`${partner.alt}-${index}`}
|
key={`${partner.alt}-${index}`}
|
||||||
src={partner.image.url}
|
src={partner.image.url}
|
||||||
alt={partner.image.alt || partner.alt}
|
alt={partner.image.alt || partner.alt}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useIsMobile } from "@/spa/hooks/useIsMobile";
|
import { useIsMobile } from "@/spa/hooks/useIsMobile";
|
||||||
|
|
||||||
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const NAVY = "#111D55";
|
const NAVY = "#111D55";
|
||||||
const GOLD = "#EFBF04";
|
const GOLD = "#EFBF04";
|
||||||
const CREAM = "#EFE5E3";
|
|
||||||
const BLUE = "#4A8FC9";
|
|
||||||
const WHITE = "#fff";
|
const WHITE = "#fff";
|
||||||
|
|
||||||
const TIERS = [
|
const TIERS = [
|
||||||
@@ -1125,7 +1123,7 @@ function MembershipWizard() {
|
|||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
const [slideDir, setSlideDir] = useState<"forward" | "back">("forward");
|
const [_slideDir, setSlideDir] = useState<"forward" | "back">("forward");
|
||||||
const [animating, setAnimating] = useState(false);
|
const [animating, setAnimating] = useState(false);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const GOLD = '#EFBF04';
|
const GOLD = '#EFBF04';
|
||||||
const DARK = '#101828';
|
const DARK = '#101828';
|
||||||
@@ -120,7 +121,7 @@ export default function LoadingScreen({ onDone }: { onDone: () => void }) {
|
|||||||
position: 'absolute', inset: 0,
|
position: 'absolute', inset: 0,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
}}>
|
}}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/bmp-logo.png"
|
src="/bmp-logo.png"
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden
|
aria-hidden
|
||||||
|
|||||||
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(() => {
|
React.useEffect(() => {
|
||||||
if (!api) return
|
if (!api) return
|
||||||
onSelect(api)
|
|
||||||
api.on("reInit", onSelect)
|
api.on("reInit", onSelect)
|
||||||
api.on("select", onSelect)
|
api.on("select", onSelect)
|
||||||
return () => { api?.off("select", onSelect) }
|
return () => {
|
||||||
|
api.off("reInit", onSelect)
|
||||||
|
api.off("select", onSelect)
|
||||||
|
}
|
||||||
}, [api, onSelect])
|
}, [api, onSelect])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cn } from "@/spa/lib/utils"
|
import { cn } from "@/spa/lib/utils"
|
||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cn } from "@/spa/lib/utils"
|
import { cn } from "@/spa/lib/utils"
|
||||||
|
|
||||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
export function useIsMobile(breakpoint = 768): boolean {
|
export function useIsMobile(breakpoint = 768): boolean {
|
||||||
const [isMobile, setIsMobile] = useState(() => window.innerWidth < breakpoint);
|
const query = `(max-width: ${breakpoint - 1}px)`;
|
||||||
useEffect(() => {
|
|
||||||
const mq = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
return useSyncExternalStore(
|
||||||
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
(onStoreChange) => {
|
||||||
mq.addEventListener('change', handler);
|
const mq = window.matchMedia(query);
|
||||||
setIsMobile(mq.matches);
|
mq.addEventListener('change', onStoreChange);
|
||||||
return () => mq.removeEventListener('change', handler);
|
return () => mq.removeEventListener('change', onStoreChange);
|
||||||
}, [breakpoint]);
|
},
|
||||||
return isMobile;
|
() => window.matchMedia(query).matches,
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import React, { useState } from 'react';
|
|||||||
import { Link } from '@/spa/router';
|
import { Link } from '@/spa/router';
|
||||||
import { Star, ChevronRight } from 'lucide-react';
|
import { Star, ChevronRight } from 'lucide-react';
|
||||||
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
||||||
import { Button } from '@/spa/components/ui/button';
|
|
||||||
import { WINNERS } from '@/spa/data/winners';
|
import { WINNERS } from '@/spa/data/winners';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const FB = '"Inter", sans-serif';
|
const FB = '"Inter", sans-serif';
|
||||||
@@ -23,7 +23,7 @@ const About: React.FC = () => {
|
|||||||
|
|
||||||
{/* ── 1. HERO ─────────────────────────────────────────────────────────── */}
|
{/* ── 1. HERO ─────────────────────────────────────────────────────────── */}
|
||||||
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
|
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/saal-leer-atmosphaere.jpg"
|
src="/images/saal-leer-atmosphaere.jpg"
|
||||||
alt="BMP Gala"
|
alt="BMP Gala"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -120,7 +120,7 @@ const About: React.FC = () => {
|
|||||||
|
|
||||||
{/* Right – full-bleed image (+ heading on mobile) */}
|
{/* Right – full-bleed image (+ heading on mobile) */}
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined, order: isMobile ? 1 : 0 }}>
|
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined, order: isMobile ? 1 : 0 }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/preistraeger-gruppe.jpg"
|
src="/images/preistraeger-gruppe.jpg"
|
||||||
alt="BMP Gala Preisverleihung"
|
alt="BMP Gala Preisverleihung"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
|
||||||
@@ -154,7 +154,7 @@ const About: React.FC = () => {
|
|||||||
|
|
||||||
{/* Left – full-bleed image (+ heading on mobile) */}
|
{/* Left – full-bleed image (+ heading on mobile) */}
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
|
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/networking-innenhof.jpg"
|
src="/images/networking-innenhof.jpg"
|
||||||
alt="Bayerischer Mittelstand"
|
alt="Bayerischer Mittelstand"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
|
||||||
@@ -422,7 +422,7 @@ function HighlightCard({ winner }: { winner: import('@/spa/data/winners').Winner
|
|||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={winner.img}
|
src={winner.img}
|
||||||
alt={winner.name}
|
alt={winner.name}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import React from 'react';
|
|||||||
import { useParams, Link } from '@/spa/router';
|
import { useParams, Link } from '@/spa/router';
|
||||||
import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react';
|
import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { getArticleBySlug, articles, type BlogArticle } from '@/spa/data/blog';
|
import { getArticleBySlug, articles, type BlogArticle, type BlogSection } from '@/spa/data/blog';
|
||||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
import { useCmsRoute } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
const GOLD = '#EFBF04';
|
const GOLD = '#EFBF04';
|
||||||
@@ -35,9 +36,17 @@ export default function BlogDetail() {
|
|||||||
readTime: String(cmsArticle.readTime || '1 min'),
|
readTime: String(cmsArticle.readTime || '1 min'),
|
||||||
img: docImageUrl(cmsArticle, '/images/gala-saal-overview.jpg', 'heroImage'),
|
img: docImageUrl(cmsArticle, '/images/gala-saal-overview.jpg', 'heroImage'),
|
||||||
excerpt: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.'),
|
excerpt: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.'),
|
||||||
sections: Array.isArray(cmsArticle.sections) && cmsArticle.sections.length > 0 ? cmsArticle.sections as any : [{ body: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.') }],
|
sections: Array.isArray(cmsArticle.sections) && cmsArticle.sections.length > 0 ? cmsArticle.sections as BlogSection[] : [{ body: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.') }],
|
||||||
tags: Array.isArray(cmsArticle.tags) ? cmsArticle.tags.map((item: any) => item.tag).filter(Boolean) : ['Presse'],
|
tags: Array.isArray(cmsArticle.tags)
|
||||||
relatedSlugs: Array.isArray(cmsArticle.relatedSlugs) ? cmsArticle.relatedSlugs.map((item: any) => item.slug).filter(Boolean) : [],
|
? cmsArticle.tags
|
||||||
|
.map((item: { tag?: string }) => item.tag)
|
||||||
|
.filter((tag): tag is string => Boolean(tag))
|
||||||
|
: ['Presse'],
|
||||||
|
relatedSlugs: Array.isArray(cmsArticle.relatedSlugs)
|
||||||
|
? cmsArticle.relatedSlugs
|
||||||
|
.map((item: { slug?: string }) => item.slug)
|
||||||
|
.filter((slug): slug is string => Boolean(slug))
|
||||||
|
: [],
|
||||||
} as BlogArticle : undefined);
|
} as BlogArticle : undefined);
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
@@ -60,7 +69,7 @@ export default function BlogDetail() {
|
|||||||
|
|
||||||
{/* ── HERO ──────────────────────────────────────────────────────────── */}
|
{/* ── HERO ──────────────────────────────────────────────────────────── */}
|
||||||
<section style={{ position: 'relative', height: isMobile ? 'auto' : '70vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
|
<section style={{ position: 'relative', height: isMobile ? 'auto' : '70vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={article.img}
|
src={article.img}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }}
|
||||||
@@ -288,7 +297,7 @@ function RelatedCard({ article, bordered, isMobile }: { article: ReturnType<type
|
|||||||
>
|
>
|
||||||
{/* Thumbnail */}
|
{/* Thumbnail */}
|
||||||
<div style={{ height: 180, overflow: 'hidden', marginBottom: 24 }}>
|
<div style={{ height: 180, overflow: 'hidden', marginBottom: 24 }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={article.img}
|
src={article.img}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MapPin, Mail, Phone, Clock, ChevronDown } from 'lucide-react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import KontaktForm from '@/spa/components/forms/KontaktForm';
|
import KontaktForm from '@/spa/components/forms/KontaktForm';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
const GOLD = '#EFBF04';
|
const GOLD = '#EFBF04';
|
||||||
@@ -43,7 +44,7 @@ const Contact: React.FC = () => {
|
|||||||
|
|
||||||
{/* ── Hero ───────────────────────────────────────────────────────── */}
|
{/* ── Hero ───────────────────────────────────────────────────────── */}
|
||||||
<section style={{ position: 'relative', height: '68vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
|
<section style={{ position: 'relative', height: '68vh', minHeight: 520, overflow: 'hidden', display: 'flex', alignItems: 'flex-end' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/gala-dinner.jpg"
|
src="/images/gala-dinner.jpg"
|
||||||
alt="BMP Gala"
|
alt="BMP Gala"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%' }}
|
||||||
@@ -130,7 +131,7 @@ const Contact: React.FC = () => {
|
|||||||
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div style={{ height: 200, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
<div style={{ height: 200, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
||||||
<img src="/images/preistraeger-gruppe.jpg" alt="BMP Gala 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
|
<Image unoptimized src="/images/preistraeger-gruppe.jpg" alt="BMP Gala 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function Datenschutz() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section id="recaptcha" title="Spamschutz mit Google reCAPTCHA">
|
<Section id="recaptcha" title="Spamschutz mit Google reCAPTCHA">
|
||||||
<p style={body}>Wir haben Google reCAPTCHA in Formularen auf unserer Webseite implementiert, um zu überprüfen, ob die in den Formularen eingegebenen Daten von menschlichen Besuchern stammen oder von Maschinen bzw. automatisierten Programmen („Bots").</p>
|
<p style={body}>Wir haben Google reCAPTCHA in Formularen auf unserer Webseite implementiert, um zu überprüfen, ob die in den Formularen eingegebenen Daten von menschlichen Besuchern stammen oder von Maschinen bzw. automatisierten Programmen („Bots“).</p>
|
||||||
<p style={body}>Dieses Tool analysiert automatisch das Verhalten der Webseitenbesucher, sobald sie mit der Webseite interagieren, unter Verwendung verschiedener Informationen wie IP-Adresse, Aufenthaltsdauer und Mausbewegungen.</p>
|
<p style={body}>Dieses Tool analysiert automatisch das Verhalten der Webseitenbesucher, sobald sie mit der Webseite interagieren, unter Verwendung verschiedener Informationen wie IP-Adresse, Aufenthaltsdauer und Mausbewegungen.</p>
|
||||||
<p style={{ ...body, fontWeight: 600, color: NAVY, marginBottom: 8 }}>Verarbeitete Daten</p>
|
<p style={{ ...body, fontWeight: 600, color: NAVY, marginBottom: 8 }}>Verarbeitete Daten</p>
|
||||||
<ul style={{ ...body, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 16 }}>
|
<ul style={{ ...body, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 16 }}>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useParams, Link } from '@/spa/router'
|
import { useParams, Link } from '@/spa/router'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight, Users, Tag } from 'lucide-react'
|
import { Calendar, MapPin, Clock, Bell, BellOff, ArrowLeft, ChevronRight } from 'lucide-react'
|
||||||
import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent } from '@/spa/data/events'
|
import { getEventBySlug, EventUpdate, UpdateType, type BmpEvent, type EventEckdaten } from '@/spa/data/events'
|
||||||
import { useCmsRoute } from '@/spa/cmsRoute'
|
import { useCmsRoute } from '@/spa/cmsRoute'
|
||||||
import { docImageUrl } from '@/spa/cmsMediaField'
|
import { docImageUrl } from '@/spa/cmsMediaField'
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile'
|
import { useIsMobile } from '@/spa/hooks/useIsMobile'
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const NAVY = '#111D55'
|
const NAVY = '#111D55'
|
||||||
const GOLD = '#EFBF04'
|
const GOLD = '#EFBF04'
|
||||||
const CREAM = '#EFE5E3'
|
const CREAM = '#EFE5E3'
|
||||||
const INK = '#3A3A3A'
|
|
||||||
|
|
||||||
// ── Timestamp formatter ──────────────────────────────────────────────────────
|
// ── Timestamp formatter ──────────────────────────────────────────────────────
|
||||||
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']
|
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']
|
||||||
@@ -88,12 +88,12 @@ export default function EventDetail() {
|
|||||||
location: String(cmsEvent.location || 'Bayern'),
|
location: String(cmsEvent.location || 'Bayern'),
|
||||||
venue: String(cmsEvent.venue || 'Ort folgt'),
|
venue: String(cmsEvent.venue || 'Ort folgt'),
|
||||||
category: String(cmsEvent.category || 'Event'),
|
category: String(cmsEvent.category || 'Event'),
|
||||||
status: (cmsEvent.status as any) || 'geplant',
|
status: (cmsEvent.status as BmpEvent['status']) || 'geplant',
|
||||||
img: docImageUrl(cmsEvent, '/images/buehne-moderatoren.jpg'),
|
img: docImageUrl(cmsEvent, '/images/buehne-moderatoren.jpg'),
|
||||||
description: String(cmsEvent.description || cmsEvent.meta?.description || ''),
|
description: String(cmsEvent.description || cmsEvent.meta?.description || ''),
|
||||||
longDescription: String(cmsEvent.longDescription || cmsEvent.description || cmsEvent.meta?.description || 'Dieses Event wird aus Payload CMS geladen.'),
|
longDescription: String(cmsEvent.longDescription || cmsEvent.description || cmsEvent.meta?.description || 'Dieses Event wird aus Payload CMS geladen.'),
|
||||||
eckdaten: Array.isArray(cmsEvent.eckdaten) ? cmsEvent.eckdaten as any : [],
|
eckdaten: Array.isArray(cmsEvent.eckdaten) ? cmsEvent.eckdaten as EventEckdaten[] : [],
|
||||||
updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as any : [],
|
updates: Array.isArray(cmsEvent.updates) ? cmsEvent.updates as EventUpdate[] : [],
|
||||||
} as BmpEvent : undefined)
|
} as BmpEvent : undefined)
|
||||||
const [subscribed, setSubscribed] = useState(false)
|
const [subscribed, setSubscribed] = useState(false)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
@@ -117,7 +117,7 @@ export default function EventDetail() {
|
|||||||
|
|
||||||
{/* ── Hero ─────────────────────────────────────────────────────────── */}
|
{/* ── Hero ─────────────────────────────────────────────────────────── */}
|
||||||
<section style={{ position: 'relative', height: isMobile ? 'auto' : '65vh', minHeight: isMobile ? 520 : 480, overflow: 'hidden' }}>
|
<section style={{ position: 'relative', height: isMobile ? 'auto' : '65vh', minHeight: isMobile ? 520 : 480, overflow: 'hidden' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={event.img}
|
src={event.img}
|
||||||
alt={event.title}
|
alt={event.title}
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center' }}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Button } from '@/spa/components/ui/button';
|
import { Button } from '@/spa/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/spa/components/ui/card';
|
import { Play, ChevronRight, X, Star } from 'lucide-react';
|
||||||
import { Badge } from '@/spa/components/ui/badge';
|
|
||||||
import { Award, Users, TrendingUp, Play, ChevronRight, Building2, ShieldCheck, Leaf, HeartHandshake, X, Star } from 'lucide-react';
|
|
||||||
import { Link } from '@/spa/router';
|
import { Link } from '@/spa/router';
|
||||||
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
|
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
|
||||||
import { WINNERS } from '@/spa/data/winners';
|
import { WINNERS } from '@/spa/data/winners';
|
||||||
@@ -10,6 +8,7 @@ import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
|||||||
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const PARTNERS = [
|
const PARTNERS = [
|
||||||
{ id: 'rsm', label: <><span style={{fontWeight:700,fontSize:11,letterSpacing:2}}>RSM</span><br/><span style={{fontWeight:300,fontSize:8,letterSpacing:1,opacity:.7}}>Ebner Stolz</span></> },
|
{ id: 'rsm', label: <><span style={{fontWeight:700,fontSize:11,letterSpacing:2}}>RSM</span><br/><span style={{fontWeight:300,fontSize:8,letterSpacing:1,opacity:.7}}>Ebner Stolz</span></> },
|
||||||
@@ -103,7 +102,7 @@ function HomeWinnerCard({ winner }: { winner: import('@/spa/data/winners').Winne
|
|||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
<img src={winner.img} alt={winner.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
|
<Image unoptimized src={winner.img} alt={winner.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
|
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
|
||||||
@@ -131,7 +130,7 @@ const Home: React.FC = () => {
|
|||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative flex items-center overflow-hidden" style={{ background: '#111D55', height: isMobile ? '88svh' : '100vh' }}>
|
<section className="relative flex items-center overflow-hidden" style={{ background: '#111D55', height: isMobile ? '88svh' : '100vh' }}>
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/gala-saal-overview.jpg"
|
src="/images/gala-saal-overview.jpg"
|
||||||
alt="Awards Gala"
|
alt="Awards Gala"
|
||||||
className="w-full h-full object-cover scale-105"
|
className="w-full h-full object-cover scale-105"
|
||||||
@@ -259,7 +258,7 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
{/* Right (desktop) / Top (mobile) – image; on mobile the heading is overlaid here */}
|
{/* Right (desktop) / Top (mobile) – image; on mobile the heading is overlaid here */}
|
||||||
<div style={{ position: 'relative', minHeight: isMobile ? 280 : 560, overflow: 'hidden', order: isMobile ? 1 : 0 }}>
|
<div style={{ position: 'relative', minHeight: isMobile ? 280 : 560, overflow: 'hidden', order: isMobile ? 1 : 0 }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/buehne-gewinner.jpg"
|
src="/images/buehne-gewinner.jpg"
|
||||||
alt="Preisverleihung Bühne"
|
alt="Preisverleihung Bühne"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: isMobile ? 'center right' : 'center center' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: isMobile ? 'center right' : 'center center' }}
|
||||||
@@ -289,7 +288,7 @@ const Home: React.FC = () => {
|
|||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '45% 55%' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '45% 55%' }}>
|
||||||
{/* Left – full-bleed image with stats overlay (+ heading on mobile) */}
|
{/* Left – full-bleed image with stats overlay (+ heading on mobile) */}
|
||||||
<div style={{ position: 'relative', minHeight: isMobile ? 300 : 600, overflow: 'hidden' }}>
|
<div style={{ position: 'relative', minHeight: isMobile ? 300 : 600, overflow: 'hidden' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/preistraeger-jubel.jpg"
|
src="/images/preistraeger-jubel.jpg"
|
||||||
alt="BMP Auszeichnung"
|
alt="BMP Auszeichnung"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top' }}
|
||||||
@@ -343,7 +342,7 @@ const Home: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
<div style={{ width: 40, height: 2, background: '#EFBF04', marginBottom: 28, display: isMobile ? 'none' : 'block' }} />
|
<div style={{ width: 40, height: 2, background: '#EFBF04', marginBottom: 28, display: isMobile ? 'none' : 'block' }} />
|
||||||
<blockquote style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 18, fontStyle: 'italic', color: '#101828', lineHeight: 1.7, borderLeft: '3px solid #EFBF04', paddingLeft: 20, margin: '0 0 24px' }}>
|
<blockquote style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 18, fontStyle: 'italic', color: '#101828', lineHeight: 1.7, borderLeft: '3px solid #EFBF04', paddingLeft: 20, margin: '0 0 24px' }}>
|
||||||
"Ein Unternehmen zu gründen erfordert nicht allein spezielle Fähigkeiten. Es erfordert Persönlichkeit!"
|
„Ein Unternehmen zu gründen erfordert nicht allein spezielle Fähigkeiten. Es erfordert Persönlichkeit!“
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, margin: '0 0 36px' }}>
|
<p style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 17, color: 'rgba(16,24,40,0.6)', lineHeight: 1.7, margin: '0 0 36px' }}>
|
||||||
Wir suchen Vorbilder, die sich trotz aller Risiken nicht scheuen, Visionen in Businesspläne und Ideen in Unternehmungen zu verwandeln, und das mit großem persönlichen Einsatz und Verantwortung für Ihre Mitarbeiter. Seit vielen Jahren würdigen wir herausragende unternehmerische Leistungen im Freistaat.
|
Wir suchen Vorbilder, die sich trotz aller Risiken nicht scheuen, Visionen in Businesspläne und Ideen in Unternehmungen zu verwandeln, und das mit großem persönlichen Einsatz und Verantwortung für Ihre Mitarbeiter. Seit vielen Jahren würdigen wir herausragende unternehmerische Leistungen im Freistaat.
|
||||||
@@ -444,7 +443,7 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
{/* Right – full-bleed photo (+ heading on mobile) */}
|
{/* Right – full-bleed photo (+ heading on mobile) */}
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : 'auto', order: isMobile ? 1 : 0 }}>
|
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : 'auto', order: isMobile ? 1 : 0 }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/networking-innenhof.jpg"
|
src="/images/networking-innenhof.jpg"
|
||||||
alt="BMP Gala Netzwerk"
|
alt="BMP Gala Netzwerk"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center center', display: 'block' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center center', display: 'block' }}
|
||||||
@@ -531,7 +530,7 @@ const Home: React.FC = () => {
|
|||||||
{/* Award ceremony photo */}
|
{/* Award ceremony photo */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/preisuebergabe.jpg"
|
src="/images/preisuebergabe.jpg"
|
||||||
alt="Preisverleihung Bayerischer Mittelstandspreis"
|
alt="Preisverleihung Bayerischer Mittelstandspreis"
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 25%', filter: 'sepia(0.18) brightness(0.92)' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 25%', filter: 'sepia(0.18) brightness(0.92)' }}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ChevronRight, ArrowLeft, Users, Star, Eye, Heart } from 'lucide-react';
|
|||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
import MembershipWizard from '@/spa/components/membership/MembershipWizard';
|
import MembershipWizard from '@/spa/components/membership/MembershipWizard';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
const GOLD = '#EFBF04';
|
const GOLD = '#EFBF04';
|
||||||
@@ -597,7 +598,7 @@ function QuoteCard({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
"{quote}"
|
„{quote}“
|
||||||
</p>
|
</p>
|
||||||
<div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20 }} />
|
<div style={{ width: 32, height: 1, background: GOLD, marginBottom: 20 }} />
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||||
@@ -708,7 +709,7 @@ const MitgliedWerden: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Background image */}
|
{/* Background image */}
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/mitglied-hero.jpg"
|
src="/images/mitglied-hero.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -1313,7 +1314,7 @@ const MitgliedWerden: React.FC = () => {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(0,0,0,0.18)', overflowY: isMobile ? 'visible' : 'auto', minHeight: 0 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(0,0,0,0.18)', overflowY: isMobile ? 'visible' : 'auto', minHeight: 0 }}>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div style={{ height: 180, position: 'relative', overflow: 'hidden', flexShrink: 0 }}>
|
<div style={{ height: 180, position: 'relative', overflow: 'hidden', flexShrink: 0 }}>
|
||||||
<img src="/images/gewinner-gruppenfoto.jpg" alt="BMP Mitglieder" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
|
<Image unoptimized src="/images/gewinner-gruppenfoto.jpg" alt="BMP Mitglieder" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(17,29,85,0.5) 0%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.45) 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(17,29,85,0.5) 0%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.45) 100%)' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, background: GOLD }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, background: GOLD }} />
|
||||||
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ChevronRight, X, ArrowRight, Globe, Linkedin, Building2, MapPin, Extern
|
|||||||
import SponsoringForm from '@/spa/components/forms/SponsoringForm';
|
import SponsoringForm from '@/spa/components/forms/SponsoringForm';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const FB = '"Inter", sans-serif';
|
const FB = '"Inter", sans-serif';
|
||||||
@@ -41,7 +42,7 @@ function PersonCard({ imgSrc, imgAlt, name, titleLine1, titleLine2, institution,
|
|||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative', height: isMobileCard ? 250 : 320, overflow: 'hidden', width: '100%' }}>
|
<div style={{ position: 'relative', height: isMobileCard ? 250 : 320, overflow: 'hidden', width: '100%' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={imgSrc}
|
src={imgSrc}
|
||||||
alt={imgAlt}
|
alt={imgAlt}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: hovered ? 'grayscale(0%)' : 'grayscale(100%)', transition: 'filter 0.55s ease, transform 0.55s ease', transform: hovered ? 'scale(1.04)' : 'scale(1)' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: hovered ? 'grayscale(0%)' : 'grayscale(100%)', transition: 'filter 0.55s ease, transform 0.55s ease', transform: hovered ? 'scale(1.04)' : 'scale(1)' }}
|
||||||
@@ -78,7 +79,7 @@ const JuryCard: React.FC<{ member: JuryMember; idx: number }> = ({ member, idx }
|
|||||||
style={{ borderLeft: !isMobileCard && idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none', display: 'flex', flexDirection: 'column', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.03)' : 'transparent', transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '74vw', maxWidth: '74vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : {}) }}
|
style={{ borderLeft: !isMobileCard && idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none', display: 'flex', flexDirection: 'column', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.03)' : 'transparent', transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '74vw', maxWidth: '74vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : {}) }}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: '3/4' }}>
|
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: '3/4' }}>
|
||||||
<img src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
<Image unoptimized src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: isMobileCard ? '28px 28px 32px' : '32px 32px 36px' }}>
|
<div style={{ padding: isMobileCard ? '28px 28px 32px' : '32px 32px 36px' }}>
|
||||||
@@ -111,7 +112,7 @@ const JuryChairCard: React.FC<{ member: JuryMember }> = ({ member }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto' }}>
|
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto' }}>
|
||||||
<img src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
<Image unoptimized src={member.img} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||||
@@ -144,30 +145,6 @@ interface Partner {
|
|||||||
linkedin?: string;
|
linkedin?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PartnerCell: React.FC<{ partner: Partner; idx: number; total: number; onClick: () => void }> = ({ partner, idx, total, onClick }) => {
|
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
const isMobileCell = useIsMobile();
|
|
||||||
const borderLeft = isMobileCell ? 'none' : (idx % 4 === 0 ? 'none' : '1px solid rgba(3,9,58,0.08)');
|
|
||||||
const showBottom = isMobileCell ? idx < total - 1 : idx < 8;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={onClick}
|
|
||||||
onMouseEnter={() => setHovered(true)}
|
|
||||||
onMouseLeave={() => setHovered(false)}
|
|
||||||
style={{ padding: isMobileCell ? '24px 20px' : '36px 32px', borderLeft, borderBottom: showBottom ? '1px solid rgba(3,9,58,0.08)' : 'none', display: 'flex', flexDirection: 'column', gap: 16, cursor: 'pointer', background: hovered ? '#E8E5E4' : 'transparent', transition: 'background 0.15s' }}
|
|
||||||
>
|
|
||||||
<div style={{ height: 48, display: 'flex', alignItems: 'center' }}>{partner.logo}</div>
|
|
||||||
<div style={{ width: hovered ? 48 : 20, height: 1, background: GOLD, transition: 'width 0.3s ease' }} />
|
|
||||||
<div style={{ fontFamily: FF, fontSize: 18, fontWeight: 700, color: '#101828' }}>{partner.name}</div>
|
|
||||||
<div style={{ fontFamily: FF, fontSize: 11, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.2em', fontWeight: 700 }}>{partner.role}</div>
|
|
||||||
<div style={{ fontFamily: FB, fontSize: 17, color: 'rgba(16,24,40,0.45)', lineHeight: 1.6, flexGrow: 1 }}>{partner.desc}</div>
|
|
||||||
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.12em', color: GOLD, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
||||||
Details <ArrowRight size={10} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── SponsorCell (tiered) ──────────────────────────────────────────────────────
|
// ── SponsorCell (tiered) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; onClick: () => void }> = ({ partner, variant, idx, cols, onClick }) => {
|
const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; onClick: () => void }> = ({ partner, variant, idx, cols, onClick }) => {
|
||||||
@@ -242,7 +219,7 @@ function PartnerModal({ partner, onClose }: { partner: Partner; onClose: () => v
|
|||||||
<div style={{ position: 'relative', height: 220, flexShrink: 0, overflow: 'hidden' }}>
|
<div style={{ position: 'relative', height: 220, flexShrink: 0, overflow: 'hidden' }}>
|
||||||
{/* Background */}
|
{/* Background */}
|
||||||
{partner.heroImg
|
{partner.heroImg
|
||||||
? <img src={partner.heroImg} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
|
? <Image unoptimized src={partner.heroImg} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||||
: <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(135deg, ${accentColor}33 0%, ${NAVY} 70%)` }} />
|
: <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(135deg, ${accentColor}33 0%, ${NAVY} 70%)` }} />
|
||||||
}
|
}
|
||||||
{/* Gradient to white */}
|
{/* Gradient to white */}
|
||||||
@@ -527,7 +504,7 @@ const Netzwerk: React.FC = () => {
|
|||||||
|
|
||||||
{/* ── 1. HERO ──────────────────────────────────────────────────────────── */}
|
{/* ── 1. HERO ──────────────────────────────────────────────────────────── */}
|
||||||
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
|
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
|
||||||
<img src="/images/netzwerk-hero.jpg" alt="BMP Netzwerk Preisverleihung" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
|
<Image unoptimized src="/images/netzwerk-hero.jpg" alt="BMP Netzwerk Preisverleihung" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: isMobile ? 'linear-gradient(to top, rgba(2,9,48,0.95) 0%, rgba(2,9,48,0.72) 38%, rgba(2,9,48,0.22) 72%, transparent 100%)' : 'linear-gradient(to right, #020930 0%, rgba(2,9,48,0.90) 38%, rgba(2,9,48,0.18) 65%, transparent 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: isMobile ? 'linear-gradient(to top, rgba(2,9,48,0.95) 0%, rgba(2,9,48,0.72) 38%, rgba(2,9,48,0.22) 72%, transparent 100%)' : 'linear-gradient(to right, #020930 0%, rgba(2,9,48,0.90) 38%, rgba(2,9,48,0.18) 65%, transparent 100%)' }} />
|
||||||
<div style={{ position: 'absolute', top: 0, left: 80, width: 2, height: '100%', background: `linear-gradient(to bottom, transparent, ${GOLD}, transparent)`, opacity: 0.4 }} />
|
<div style={{ position: 'absolute', top: 0, left: 80, width: 2, height: '100%', background: `linear-gradient(to bottom, transparent, ${GOLD}, transparent)`, opacity: 0.4 }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.3), transparent)', zIndex: 2 }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.3), transparent)', zIndex: 2 }} />
|
||||||
@@ -588,7 +565,7 @@ const Netzwerk: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<blockquote style={{ fontFamily: FF, fontSize: 18, fontStyle: 'italic', color: 'rgba(255,255,255,0.8)', lineHeight: 1.75, margin: 0, paddingLeft: 24, borderLeft: `3px solid ${GOLD}` }}>
|
<blockquote style={{ fontFamily: FF, fontSize: 18, fontStyle: 'italic', color: 'rgba(255,255,255,0.8)', lineHeight: 1.75, margin: 0, paddingLeft: 24, borderLeft: `3px solid ${GOLD}` }}>
|
||||||
"Der Bayerische Mittelstandspreis steht für das, was Bayern stark macht: Unternehmergeist, Verantwortung und Qualität. Mit großer Freude übernehme ich erneut die Schirmherrschaft für diesen bedeutenden Preis."
|
„Der Bayerische Mittelstandspreis steht für das, was Bayern stark macht: Unternehmergeist, Verantwortung und Qualität. Mit großer Freude übernehme ich erneut die Schirmherrschaft für diesen bedeutenden Preis.“
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<div style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 20, paddingLeft: 24 }}>
|
<div style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 20, paddingLeft: 24 }}>
|
||||||
– Ilse Aigner MdL, Präsidentin des Bayerischen Landtags
|
– Ilse Aigner MdL, Präsidentin des Bayerischen Landtags
|
||||||
@@ -813,7 +790,7 @@ const Netzwerk: React.FC = () => {
|
|||||||
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
||||||
<img src="/images/networking-innenhof.jpg" alt="BMP Partnernetzwerk" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%', filter: 'sepia(0.18) brightness(0.92)' }} />
|
<Image unoptimized src="/images/networking-innenhof.jpg" alt="BMP Partnernetzwerk" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%', filter: 'sepia(0.18) brightness(0.92)' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react';
|
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star, type LucideIcon } from 'lucide-react';
|
||||||
import { Link } from '@/spa/router';
|
import { Link } from '@/spa/router';
|
||||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
||||||
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
|
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const FB = '"Inter", sans-serif';
|
const FB = '"Inter", sans-serif';
|
||||||
@@ -21,7 +22,7 @@ const Participation: React.FC = () => {
|
|||||||
|
|
||||||
{/* ── HERO ─────────────────────────────────────────────────────────────── */}
|
{/* ── HERO ─────────────────────────────────────────────────────────────── */}
|
||||||
<section style={{ position: 'relative', minHeight: '65vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: NAVY }}>
|
<section style={{ position: 'relative', minHeight: '65vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: NAVY }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/preisuebergabe.jpg"
|
src="/images/preisuebergabe.jpg"
|
||||||
alt="Teilnahme BMP"
|
alt="Teilnahme BMP"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||||
@@ -146,7 +147,7 @@ const Participation: React.FC = () => {
|
|||||||
|
|
||||||
{/* Left – full-bleed image with gradient blend into navy on the right (+ heading on mobile) */}
|
{/* Left – full-bleed image with gradient blend into navy on the right (+ heading on mobile) */}
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
|
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/buehne-gewinner.jpg"
|
src="/images/buehne-gewinner.jpg"
|
||||||
alt="Jury-Bewertung BMP"
|
alt="Jury-Bewertung BMP"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
|
||||||
@@ -313,7 +314,7 @@ const Participation: React.FC = () => {
|
|||||||
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
|
||||||
<img src="/images/preistraeger-jubel.jpg" alt="Preisverleihung 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
|
<Image unoptimized src="/images/preistraeger-jubel.jpg" alt="Preisverleihung 2025" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'sepia(0.18) brightness(0.92)' }} />
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ position: 'absolute', bottom: 16, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
@@ -444,7 +445,7 @@ function BewerbungswegeSection() {
|
|||||||
{ weg: 'Weg 01', title: 'Vorschlag unterbreiten', desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.', Icon: Send, checkItems: INSTITUTIONEN, cta: 'Vorschlag unterbreiten', featured: false },
|
{ weg: 'Weg 01', title: 'Vorschlag unterbreiten', desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.', Icon: Send, checkItems: INSTITUTIONEN, cta: 'Vorschlag unterbreiten', featured: false },
|
||||||
{ weg: 'Weg 02', title: 'Initiativbewerbung', desc: 'Für Unternehmer, die selbst die Initiative ergreifen.', Icon: UserCheck, kvItems: [ { k: '0 EUR', v: 'Teilnahmegebühr' }, { k: 'Eigeninitiative', v: 'Direkt ohne Intermediär' }, { k: 'Für Unternehmer', v: 'Inhabergeführte KMU in Bayern' }, { k: 'Deadline', v: '30. Juni 2026' } ], cta: 'Jetzt bewerben', featured: true },
|
{ weg: 'Weg 02', title: 'Initiativbewerbung', desc: 'Für Unternehmer, die selbst die Initiative ergreifen.', Icon: UserCheck, kvItems: [ { k: '0 EUR', v: 'Teilnahmegebühr' }, { k: 'Eigeninitiative', v: 'Direkt ohne Intermediär' }, { k: 'Für Unternehmer', v: 'Inhabergeführte KMU in Bayern' }, { k: 'Deadline', v: '30. Juni 2026' } ], cta: 'Jetzt bewerben', featured: true },
|
||||||
{ weg: 'Weg 03 · Sonderpreis', title: 'Bavarian Future Award', desc: 'Sonderpreis, initiiert durch die Studierenden der HAM, Hochschule für angewandtes Management.', Icon: Star, kvItems: [ { k: 'Vorschlag möglich', v: 'Durch HAM & Partner' }, { k: 'Selbstbewerbung', v: 'Direkte Einreichung' }, { k: 'Initiiert von', v: 'Studierenden der HAM' }, { k: 'Kategorie', v: 'Zukunft & Innovation' } ], cta: 'Zum Formular', featured: false },
|
{ weg: 'Weg 03 · Sonderpreis', title: 'Bavarian Future Award', desc: 'Sonderpreis, initiiert durch die Studierenden der HAM, Hochschule für angewandtes Management.', Icon: Star, kvItems: [ { k: 'Vorschlag möglich', v: 'Durch HAM & Partner' }, { k: 'Selbstbewerbung', v: 'Direkte Einreichung' }, { k: 'Initiiert von', v: 'Studierenden der HAM' }, { k: 'Kategorie', v: 'Zukunft & Innovation' } ], cta: 'Zum Formular', featured: false },
|
||||||
] as { weg: string; title: string; desc: string; Icon: any; checkItems?: string[]; kvItems?: { k: string; v: string }[]; cta: string; featured: boolean }[]).map((w) => {
|
] as { weg: string; title: string; desc: string; Icon: LucideIcon; checkItems?: string[]; kvItems?: { k: string; v: string }[]; cta: string; featured: boolean }[]).map((w) => {
|
||||||
const Icon = w.Icon;
|
const Icon = w.Icon;
|
||||||
return (
|
return (
|
||||||
<div key={w.weg} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: w.featured ? `1.5px solid ${GOLD}` : '1px solid rgba(17,29,85,0.10)', boxShadow: w.featured ? '0 14px 34px rgba(239,191,4,0.20), 0 6px 16px rgba(3,9,58,0.10)' : '0 10px 28px rgba(3,9,58,0.08)' }}>
|
<div key={w.weg} style={{ flexShrink: 0, minWidth: '85vw', maxWidth: '85vw', scrollSnapAlign: 'center', display: 'flex', flexDirection: 'column', background: '#fff', borderRadius: 18, overflow: 'hidden', border: w.featured ? `1.5px solid ${GOLD}` : '1px solid rgba(17,29,85,0.10)', boxShadow: w.featured ? '0 14px 34px rgba(239,191,4,0.20), 0 6px 16px rgba(3,9,58,0.10)' : '0 10px 28px rgba(3,9,58,0.08)' }}>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { WINNERS, CATEGORIES, YEARS } from '@/spa/data/winners';
|
|||||||
import { useCmsCollection } from '@/spa/cmsRoute';
|
import { useCmsCollection } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
@@ -62,7 +63,7 @@ export default function Preistraeger() {
|
|||||||
<div style={{ background: '#fff', minHeight: '100vh' }}>
|
<div style={{ background: '#fff', minHeight: '100vh' }}>
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<div style={{ width: '100%', height: '50vh', overflow: 'hidden', position: 'relative' }}>
|
<div style={{ width: '100%', height: '50vh', overflow: 'hidden', position: 'relative' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=2000"
|
src="https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=2000"
|
||||||
alt="Preisverleihung"
|
alt="Preisverleihung"
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||||
@@ -250,7 +251,7 @@ function WinnerCard({ winner }: { winner: import('@/spa/data/winners').Winner })
|
|||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={winner.img}
|
src={winner.img}
|
||||||
alt={winner.name}
|
alt={winner.name}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.06)' : 'scale(1)' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.5s ease', transform: hovered ? 'scale(1.06)' : 'scale(1)' }}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { WINNERS } from '@/spa/data/winners';
|
|||||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
import { useCmsRoute } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const FB = '"Inter", sans-serif';
|
const FB = '"Inter", sans-serif';
|
||||||
@@ -40,14 +41,11 @@ export default function PreistraegerDetail() {
|
|||||||
|
|
||||||
if (!winner) return <Navigate to="/preistraeger" replace />;
|
if (!winner) return <Navigate to="/preistraeger" replace />;
|
||||||
|
|
||||||
// Pick a second photo (offset from primary)
|
|
||||||
const secondImg = winner.img.replace('w=800', 'w=900').replace('q=80', 'q=75');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: '#fff', minHeight: '100vh' }}>
|
<div style={{ background: '#fff', minHeight: '100vh' }}>
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<div style={{ width: '100%', height: '60vh', overflow: 'hidden', position: 'relative' }}>
|
<div style={{ width: '100%', height: '60vh', overflow: 'hidden', position: 'relative' }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={winner.img}
|
src={winner.img}
|
||||||
alt={winner.name}
|
alt={winner.name}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||||
@@ -110,9 +108,9 @@ export default function PreistraegerDetail() {
|
|||||||
{/* Quote */}
|
{/* Quote */}
|
||||||
<div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}>
|
<div style={{ borderLeft: `4px solid ${GOLD}`, paddingLeft: 24, marginBottom: 40 }}>
|
||||||
<p style={{ fontFamily: FB, fontStyle: 'italic', fontSize: 18, color: '#101828', lineHeight: 1.7, marginBottom: 12 }}>
|
<p style={{ fontFamily: FB, fontStyle: 'italic', fontSize: 18, color: '#101828', lineHeight: 1.7, marginBottom: 12 }}>
|
||||||
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginRight: 4, fontFamily: '"IBM Plex Sans"' }}>"</span>
|
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginRight: 4, fontFamily: '"IBM Plex Sans"' }}>“</span>
|
||||||
{winner.quote}
|
{winner.quote}
|
||||||
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>"</span>
|
<span style={{ color: GOLD, fontSize: 28, lineHeight: 1, marginLeft: 2, fontFamily: '"IBM Plex Sans"' }}>”</span>
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: GRAY, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
|
<p style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: GRAY, margin: 0, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
|
||||||
– {winner.quotePerson}, {winner.quoteRole}
|
– {winner.quotePerson}, {winner.quoteRole}
|
||||||
@@ -124,7 +122,7 @@ export default function PreistraegerDetail() {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||||||
{/* Photo */}
|
{/* Photo */}
|
||||||
<div style={{ borderRadius: 0, overflow: 'hidden', aspectRatio: '4/3' }}>
|
<div style={{ borderRadius: 0, overflow: 'hidden', aspectRatio: '4/3' }}>
|
||||||
<img src="/gala-preistraeger.jpg" alt="Gala Preisverleihung" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: 'brightness(0.95)' }} />
|
<Image unoptimized src="/gala-preistraeger.jpg" alt="Gala Preisverleihung" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: 'brightness(0.95)' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Kenndaten */}
|
{/* Kenndaten */}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
|||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
import { useCmsCollection } from '@/spa/cmsRoute';
|
import { useCmsCollection } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||||
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
const GOLD = '#EFBF04';
|
const GOLD = '#EFBF04';
|
||||||
@@ -90,7 +91,7 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
|
|||||||
>
|
>
|
||||||
{/* image */}
|
{/* image */}
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', height: 220 }}>
|
<div style={{ position: 'relative', overflow: 'hidden', height: 220 }}>
|
||||||
<img
|
<Image unoptimized
|
||||||
src={item.img}
|
src={item.img}
|
||||||
alt={item.title}
|
alt={item.title}
|
||||||
style={{
|
style={{
|
||||||
@@ -183,6 +184,37 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DarkInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
|
||||||
|
const [f, setF] = React.useState(false);
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
onFocus={(e) => {
|
||||||
|
setF(true);
|
||||||
|
props.onFocus?.(e);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setF(false);
|
||||||
|
props.onBlur?.(e);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: 44,
|
||||||
|
padding: '0 14px',
|
||||||
|
fontFamily: FF,
|
||||||
|
fontSize: 18,
|
||||||
|
color: '#fff',
|
||||||
|
border: `1px solid ${f ? 'rgba(239,191,4,0.8)' : 'rgba(255,255,255,0.12)'}`,
|
||||||
|
background: f ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.05)',
|
||||||
|
outline: 'none',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
transition: 'border-color 0.15s, background 0.15s',
|
||||||
|
...props.style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const Press: React.FC = () => {
|
const Press: React.FC = () => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const cmsEvents = useCmsCollection('events');
|
const cmsEvents = useCmsCollection('events');
|
||||||
@@ -236,38 +268,6 @@ const Press: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// DarkInput defined inline as required
|
|
||||||
function DarkInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
|
|
||||||
const [f, setF] = React.useState(false);
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
{...props}
|
|
||||||
onFocus={(e) => {
|
|
||||||
setF(true);
|
|
||||||
props.onFocus?.(e);
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
setF(false);
|
|
||||||
props.onBlur?.(e);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: 44,
|
|
||||||
padding: '0 14px',
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 18,
|
|
||||||
color: '#fff',
|
|
||||||
border: `1px solid ${f ? 'rgba(239,191,4,0.8)' : 'rgba(255,255,255,0.12)'}`,
|
|
||||||
background: f ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.05)',
|
|
||||||
outline: 'none',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
transition: 'border-color 0.15s, background 0.15s',
|
|
||||||
...props.style,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ const Press: React.FC = () => {
|
|||||||
background: NAVY,
|
background: NAVY,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<Image unoptimized
|
||||||
src="/images/gala-dinner.jpg"
|
src="/images/gala-dinner.jpg"
|
||||||
alt="Presse BMP"
|
alt="Presse BMP"
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -428,7 +428,7 @@ const Press: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{/* Left – image cell */}
|
{/* Left – image cell */}
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 200 : 240 }}>
|
<div style={{ position: 'relative', overflow: 'hidden', height: isMobile ? 200 : 240 }}>
|
||||||
<img src={event.img} alt={event.title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
<Image unoptimized src={event.img} alt={event.title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||||
<div style={{ position: 'absolute', top: 20, left: 20, padding: '5px 12px', fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase' as const, letterSpacing: '0.18em', background: event.status === 'Anmeldung offen' ? GOLD : 'rgba(3,9,58,0.85)', color: event.status === 'Anmeldung offen' ? NAVY : 'rgba(255,255,255,0.6)' }}>
|
<div style={{ position: 'absolute', top: 20, left: 20, padding: '5px 12px', fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase' as const, letterSpacing: '0.18em', background: event.status === 'Anmeldung offen' ? GOLD : 'rgba(3,9,58,0.85)', color: event.status === 'Anmeldung offen' ? NAVY : 'rgba(255,255,255,0.6)' }}>
|
||||||
{event.status}
|
{event.status}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ function useClickableCard<T extends HTMLElement>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[],
|
||||||
[router, card, link, timeDown],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleMouseUp = useCallback(
|
const handleMouseUp = useCallback(
|
||||||
@@ -71,8 +70,7 @@ function useClickableCard<T extends HTMLElement>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[external, newTab, router, scroll],
|
||||||
[router, card, link, timeDown],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -92,8 +90,7 @@ function useClickableCard<T extends HTMLElement>({
|
|||||||
return () => {
|
return () => {
|
||||||
abortController.abort()
|
abortController.abort()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [handleMouseDown, handleMouseUp])
|
||||||
}, [card, link, router])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
card: {
|
card: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { seedTestUser, cleanupTestUser, testUser } from '../helpers/seedUser'
|
|||||||
test.describe('Admin Panel', () => {
|
test.describe('Admin Panel', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
|
|
||||||
test.beforeAll(async ({ browser }, testInfo) => {
|
test.beforeAll(async ({ browser }) => {
|
||||||
await seedTestUser()
|
await seedTestUser()
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { test, expect, Page } from '@playwright/test'
|
|||||||
test.describe('Frontend', () => {
|
test.describe('Frontend', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
|
|
||||||
test.beforeAll(async ({ browser }, testInfo) => {
|
test.beforeAll(async ({ browser }) => {
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can load homepage', async ({ page }) => {
|
test('can load homepage', async () => {
|
||||||
await page.goto('http://localhost:3000')
|
await page.goto('http://localhost:3000')
|
||||||
await expect(page).toHaveTitle(/Payload Website Template/)
|
await expect(page).toHaveTitle(/Payload Website Template/)
|
||||||
const heading = page.locator('h1').first()
|
const heading = page.locator('h1').first()
|
||||||
|
|||||||
Reference in New Issue
Block a user