feat: add JuryMitglieder collection and migration for jury members
- Created a new collection `JuryMitglieder` with fields for name, image, quote, role, organization, bio, website, LinkedIn, sort order, active status, and published date. - Implemented access control for creating, reading, updating, and deleting jury members. - Added migration script to create necessary database tables and columns for jury members, including versioning support. - Updated existing `preistraeger` table to include new columns for award group, winner rank, sort order, and visibility settings. - Introduced a temporary global switch to control the visibility of testimonials in the SPA.
This commit is contained in:
@@ -16,7 +16,7 @@ type Args = {
|
|||||||
params: Promise<{ slug?: string[] }>
|
params: Promise<{ slug?: string[] }>
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteCollection = Exclude<CmsRouteCollection, 'partners'>
|
type RouteCollection = Exclude<CmsRouteCollection, 'partners' | 'jury-mitglieder'>
|
||||||
|
|
||||||
function routeTarget(pathname: string): { collection: RouteCollection; slug: string } {
|
function routeTarget(pathname: string): { collection: RouteCollection; slug: string } {
|
||||||
const parts = pathname.split('/').filter(Boolean)
|
const parts = pathname.split('/').filter(Boolean)
|
||||||
@@ -65,7 +65,7 @@ async function findList(payload: Awaited<ReturnType<typeof getPayload>>, collect
|
|||||||
overrideAccess: draft,
|
overrideAccess: draft,
|
||||||
limit: 300,
|
limit: 300,
|
||||||
pagination: false,
|
pagination: false,
|
||||||
...(collection === 'partners' ? { sort: 'sortOrder' } : {}),
|
...(collection === 'partners' || collection === 'jury-mitglieder' ? { sort: 'sortOrder' } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.docs
|
return result.docs
|
||||||
@@ -99,7 +99,19 @@ export default async function Page({ params }: Args) {
|
|||||||
const pathname = `/${slug.join('/')}`.replace(/\/$/, '') || '/'
|
const pathname = `/${slug.join('/')}`.replace(/\/$/, '') || '/'
|
||||||
const payload = await getPayload({ config: configPromise })
|
const payload = await getPayload({ config: configPromise })
|
||||||
const { isEnabled: draft } = await draftMode()
|
const { isEnabled: draft } = await draftMode()
|
||||||
const [header, footer, siteSettings, applicationPhase, route, supportPages, preistraeger, events, posts, partners] = await Promise.all([
|
const [
|
||||||
|
header,
|
||||||
|
footer,
|
||||||
|
siteSettings,
|
||||||
|
applicationPhase,
|
||||||
|
route,
|
||||||
|
supportPages,
|
||||||
|
preistraeger,
|
||||||
|
events,
|
||||||
|
posts,
|
||||||
|
partners,
|
||||||
|
juryMitglieder,
|
||||||
|
] = await Promise.all([
|
||||||
payload.findGlobal({ slug: 'header', depth: 1 }).catch(() => undefined),
|
payload.findGlobal({ slug: 'header', depth: 1 }).catch(() => undefined),
|
||||||
payload.findGlobal({ slug: 'footer', depth: 1 }).catch(() => undefined),
|
payload.findGlobal({ slug: 'footer', depth: 1 }).catch(() => undefined),
|
||||||
payload.findGlobal({ slug: 'site-settings', depth: 1 }).catch(() => undefined),
|
payload.findGlobal({ slug: 'site-settings', depth: 1 }).catch(() => undefined),
|
||||||
@@ -110,6 +122,7 @@ export default async function Page({ params }: Args) {
|
|||||||
findList(payload, 'events', draft).catch(() => []),
|
findList(payload, 'events', draft).catch(() => []),
|
||||||
findList(payload, 'posts', draft).catch(() => []),
|
findList(payload, 'posts', draft).catch(() => []),
|
||||||
findList(payload, 'partners', draft).catch(() => []),
|
findList(payload, 'partners', draft).catch(() => []),
|
||||||
|
findList(payload, 'jury-mitglieder', draft).catch(() => []),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!route) notFound()
|
if (!route) notFound()
|
||||||
@@ -130,6 +143,7 @@ export default async function Page({ params }: Args) {
|
|||||||
events: events as unknown as CmsRouteDoc[],
|
events: events as unknown as CmsRouteDoc[],
|
||||||
posts: posts as unknown as CmsRouteDoc[],
|
posts: posts as unknown as CmsRouteDoc[],
|
||||||
partners: partners as unknown as CmsRouteDoc[],
|
partners: partners as unknown as CmsRouteDoc[],
|
||||||
|
'jury-mitglieder': juryMitglieder as unknown as CmsRouteDoc[],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
111
src/collections/JuryMitglieder.ts
Normal file
111
src/collections/JuryMitglieder.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import { authenticated } from '@/access/authenticated'
|
||||||
|
import { authenticatedOrPublished } from '@/access/authenticatedOrPublished'
|
||||||
|
import { populatePublishedAt } from '@/hooks/populatePublishedAt'
|
||||||
|
|
||||||
|
export const JuryMitglieder: CollectionConfig = {
|
||||||
|
slug: 'jury-mitglieder',
|
||||||
|
labels: {
|
||||||
|
singular: 'Jury-Mitglied',
|
||||||
|
plural: 'Jury-Mitglieder',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
create: authenticated,
|
||||||
|
delete: authenticated,
|
||||||
|
read: authenticatedOrPublished,
|
||||||
|
update: authenticated,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
defaultColumns: ['name', 'role', 'sortOrder', 'active', 'updatedAt'],
|
||||||
|
group: 'Inhalte',
|
||||||
|
useAsTitle: 'name',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
label: 'Bild',
|
||||||
|
filterOptions: {
|
||||||
|
mimeType: {
|
||||||
|
contains: 'image',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'imageAlt',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Alternativtext',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quote',
|
||||||
|
type: 'textarea',
|
||||||
|
label: 'Zitat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Rolle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'organization',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Organisation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bio',
|
||||||
|
type: 'textarea',
|
||||||
|
label: 'Kurzbiografie',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'website',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Website',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin',
|
||||||
|
type: 'text',
|
||||||
|
label: 'LinkedIn URL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortOrder',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Sortierung',
|
||||||
|
defaultValue: 100,
|
||||||
|
admin: {
|
||||||
|
description: 'Niedrigere Zahlen erscheinen zuerst.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'active',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Aktiv',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'publishedAt',
|
||||||
|
type: 'date',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hooks: {
|
||||||
|
beforeChange: [populatePublishedAt],
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
drafts: {
|
||||||
|
autosave: {
|
||||||
|
interval: 100,
|
||||||
|
},
|
||||||
|
schedulePublish: true,
|
||||||
|
},
|
||||||
|
maxPerDoc: 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -133,9 +133,31 @@ export const homeFields: Field[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'winners',
|
||||||
|
label: '02 · Preisträger card grid',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Blue card grid with featured winner relationships.'),
|
||||||
|
fields: [
|
||||||
|
text('eyebrow', 'Eyebrow', homeContent.winners.eyebrow),
|
||||||
|
textarea('heading', 'Heading', homeContent.winners.heading),
|
||||||
|
{ name: 'cta', label: 'CTA', type: 'group', fields: linkGroup(homeContent.winners.cta.label, homeContent.winners.cta.url) },
|
||||||
|
uploadField('fallbackImage', 'Fallback card image', `Current frontend image: /images/${homeContent.winners.fallbackImageFilename}`),
|
||||||
|
text('cardFallbackTitle', 'Fallback card title', homeContent.winners.cardFallbackTitle),
|
||||||
|
text('hoverLabel', 'Card hover label', homeContent.winners.hoverLabel),
|
||||||
|
{
|
||||||
|
name: 'featured',
|
||||||
|
label: 'Featured winner cards',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'preistraeger',
|
||||||
|
hasMany: true,
|
||||||
|
admin: { description: 'Choose the winner cards shown on the home page. If empty, the latest 2025 winners are used.' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'quickCheck',
|
name: 'quickCheck',
|
||||||
label: '02 · Schnell-Check eligibility section',
|
label: '03 · Schnell-Check eligibility section',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
admin: sectionAdmin('White text panel plus right-side stage image and five eligibility rows.'),
|
admin: sectionAdmin('White text panel plus right-side stage image and five eligibility rows.'),
|
||||||
fields: [
|
fields: [
|
||||||
@@ -165,7 +187,7 @@ export const homeFields: Field[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'intro',
|
name: 'intro',
|
||||||
label: '03 · Award intro / value proposition',
|
label: '04 · Award intro / value proposition',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
admin: sectionAdmin('Jubilation image, stats, editorial quote, body copy, and CTA.'),
|
admin: sectionAdmin('Jubilation image, stats, editorial quote, body copy, and CTA.'),
|
||||||
fields: [
|
fields: [
|
||||||
@@ -189,28 +211,6 @@ export const homeFields: Field[] = [
|
|||||||
{ name: 'cta', label: 'CTA', type: 'group', fields: linkGroup(homeContent.intro.cta.label, homeContent.intro.cta.url) },
|
{ name: 'cta', label: 'CTA', type: 'group', fields: linkGroup(homeContent.intro.cta.label, homeContent.intro.cta.url) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'winners',
|
|
||||||
label: '04 · Preisträger card grid',
|
|
||||||
type: 'group',
|
|
||||||
admin: sectionAdmin('Blue card grid with featured winner relationships.'),
|
|
||||||
fields: [
|
|
||||||
text('eyebrow', 'Eyebrow', homeContent.winners.eyebrow),
|
|
||||||
textarea('heading', 'Heading', homeContent.winners.heading),
|
|
||||||
{ name: 'cta', label: 'CTA', type: 'group', fields: linkGroup(homeContent.winners.cta.label, homeContent.winners.cta.url) },
|
|
||||||
uploadField('fallbackImage', 'Fallback card image', `Current frontend image: /images/${homeContent.winners.fallbackImageFilename}`),
|
|
||||||
text('cardFallbackTitle', 'Fallback card title', homeContent.winners.cardFallbackTitle),
|
|
||||||
text('hoverLabel', 'Card hover label', homeContent.winners.hoverLabel),
|
|
||||||
{
|
|
||||||
name: 'featured',
|
|
||||||
label: 'Featured winner cards',
|
|
||||||
type: 'relationship',
|
|
||||||
relationTo: 'preistraeger',
|
|
||||||
hasMany: true,
|
|
||||||
admin: { description: 'Choose the winner cards shown on the home page. If empty, the latest 2025 winners are used.' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'testimonials',
|
name: 'testimonials',
|
||||||
label: '05 · Testimonials carousel',
|
label: '05 · Testimonials carousel',
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ const textarea = (name: string, label: string, defaultValue?: string): Field =>
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const number = (name: string, label: string, defaultValue: number, description?: string): Field => ({
|
||||||
|
name,
|
||||||
|
type: 'number',
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
min: 1,
|
||||||
|
admin: description ? { description } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
const ctaGroup = (name: string, label: string, labelDefault: string, urlDefault: string): Field => ({
|
const ctaGroup = (name: string, label: string, labelDefault: string, urlDefault: string): Field => ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
@@ -71,16 +80,49 @@ export const preistraegerIndexFields: Field[] = [
|
|||||||
admin: sectionAdmin('Label after the selected-year result count.'),
|
admin: sectionAdmin('Label after the selected-year result count.'),
|
||||||
fields: [text('label', 'Count label', preistraegerIndexContent.count.label)],
|
fields: [text('label', 'Count label', preistraegerIndexContent.count.label)],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
label: '04 · Gewinnerlogik',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Public page rules for the selected year. Winner records are still managed in the Preisträger collection.'),
|
||||||
|
fields: [
|
||||||
|
number('totalWinners', 'Gewinner gesamt', preistraegerIndexContent.rules.totalWinners, 'The public page displays at most this many winners per year.'),
|
||||||
|
number('goldeneBavariaCount', 'Goldene Bavaria Gewinner', preistraegerIndexContent.rules.goldeneBavariaCount, 'These top-ranked winners are shown in the separate Goldene Bavaria section.'),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'empty',
|
name: 'empty',
|
||||||
label: '04 · Empty state',
|
label: '05 · Empty state',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
admin: sectionAdmin('Message when the selected year returns no winners.'),
|
admin: sectionAdmin('Message when the selected year returns no winners.'),
|
||||||
fields: [textarea('message', 'Empty message', preistraegerIndexContent.empty.message)],
|
fields: [textarea('message', 'Empty message', preistraegerIndexContent.empty.message)],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'goldeneBavaria',
|
||||||
|
label: '06 · Goldene Bavaria section',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Copy for the separate section that highlights the best three winners.'),
|
||||||
|
fields: [
|
||||||
|
text('eyebrow', 'Eyebrow', preistraegerIndexContent.goldeneBavaria.eyebrow),
|
||||||
|
textarea('heading', 'Heading', preistraegerIndexContent.goldeneBavaria.heading),
|
||||||
|
textarea('description', 'Description', preistraegerIndexContent.goldeneBavaria.description),
|
||||||
|
textarea('emptyMessage', 'Empty message', preistraegerIndexContent.goldeneBavaria.emptyMessage),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'winners',
|
||||||
|
label: '07 · Winners section',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Copy for the remaining winners below the Goldene Bavaria section.'),
|
||||||
|
fields: [
|
||||||
|
text('eyebrow', 'Eyebrow', preistraegerIndexContent.winners.eyebrow),
|
||||||
|
textarea('heading', 'Heading', preistraegerIndexContent.winners.heading),
|
||||||
|
textarea('description', 'Description', preistraegerIndexContent.winners.description),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'card',
|
name: 'card',
|
||||||
label: '05 · Winner cards',
|
label: '08 · Winner cards',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
admin: sectionAdmin('Hover label shown on winner cards.'),
|
admin: sectionAdmin('Hover label shown on winner cards.'),
|
||||||
fields: [
|
fields: [
|
||||||
@@ -102,7 +144,7 @@ export const preistraegerIndexFields: Field[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cta',
|
name: 'cta',
|
||||||
label: '06 · Bottom CTA',
|
label: '09 · Bottom CTA',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
admin: sectionAdmin('Bottom application and membership CTA.'),
|
admin: sectionAdmin('Bottom application and membership CTA.'),
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import {
|
|||||||
PreviewField,
|
PreviewField,
|
||||||
} from '@payloadcms/plugin-seo/fields'
|
} from '@payloadcms/plugin-seo/fields'
|
||||||
|
|
||||||
|
const awardGroupOptions = [
|
||||||
|
{ label: 'Preisträger', value: 'preistraeger' },
|
||||||
|
{ label: 'Goldene Bavaria', value: 'goldene-bavaria' },
|
||||||
|
]
|
||||||
|
|
||||||
export const Preistraeger: CollectionConfig = {
|
export const Preistraeger: CollectionConfig = {
|
||||||
slug: 'preistraeger',
|
slug: 'preistraeger',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -64,6 +69,42 @@ export const Preistraeger: CollectionConfig = {
|
|||||||
name: 'year',
|
name: 'year',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'awardGroup',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Auszeichnungsgruppe',
|
||||||
|
defaultValue: 'preistraeger',
|
||||||
|
options: awardGroupOptions,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
'Steuert die Ausgabe auf der Preisträger-Übersicht. Pro Jahrgang werden 15 Gewinner angezeigt, davon maximal 3 in der Goldene-Bavaria-Section.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'winnerRank',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Rang im Jahrgang',
|
||||||
|
min: 1,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
'1 bis 3 sind die besten Gewinner und sollten als Goldene Bavaria markiert werden. Niedrigere Zahlen erscheinen zuerst.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortOrder',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Sortierung',
|
||||||
|
defaultValue: 100,
|
||||||
|
admin: {
|
||||||
|
description: 'Fallback-Sortierung innerhalb eines Jahrgangs, wenn kein Rang gesetzt ist.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showOnIndex',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Auf Preisträger-Übersicht anzeigen',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'awardType',
|
name: 'awardType',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
|
||||||
|
|
||||||
|
type MigrationDB = MigrateUpArgs['db']
|
||||||
|
|
||||||
|
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
|
||||||
|
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
|
||||||
|
|
||||||
|
async function tableExists(db: MigrationDB, tableName: string) {
|
||||||
|
const rows = (await db.all(
|
||||||
|
sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
|
||||||
|
)) as Array<{ name: string }>
|
||||||
|
|
||||||
|
return rows.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async function columnExists(db: MigrationDB, tableName: string, columnName: string) {
|
||||||
|
if (!(await tableExists(db, tableName))) return false
|
||||||
|
|
||||||
|
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
|
||||||
|
|
||||||
|
return columns.some((column) => column.name === columnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addColumn(db: MigrationDB, tableName: string, columnName: string, definition: string) {
|
||||||
|
if (await columnExists(db, tableName, columnName)) return
|
||||||
|
|
||||||
|
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} ADD ${ident(columnName)} ${definition};`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dropColumn(db: MigrationDB, tableName: string, columnName: string) {
|
||||||
|
if (!(await columnExists(db, tableName, columnName))) return
|
||||||
|
|
||||||
|
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} DROP COLUMN ${ident(columnName)};`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rankExistingPreistraeger(db: MigrationDB) {
|
||||||
|
if (!(await tableExists(db, 'preistraeger'))) return
|
||||||
|
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT
|
||||||
|
${ident('id')},
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY COALESCE(${ident('year')}, 0)
|
||||||
|
ORDER BY ${ident('id')} ASC
|
||||||
|
) AS display_rank
|
||||||
|
FROM ${ident('preistraeger')}
|
||||||
|
)
|
||||||
|
UPDATE ${ident('preistraeger')}
|
||||||
|
SET
|
||||||
|
${ident('award_group')} = CASE
|
||||||
|
WHEN (SELECT display_rank FROM ranked WHERE ranked.id = ${ident('preistraeger')}.${ident('id')}) <= 3 THEN ${literal('goldene-bavaria')}
|
||||||
|
ELSE ${literal('preistraeger')}
|
||||||
|
END,
|
||||||
|
${ident('winner_rank')} = (SELECT display_rank FROM ranked WHERE ranked.id = ${ident('preistraeger')}.${ident('id')}),
|
||||||
|
${ident('sort_order')} = (SELECT display_rank FROM ranked WHERE ranked.id = ${ident('preistraeger')}.${ident('id')}),
|
||||||
|
${ident('show_on_index')} = CASE
|
||||||
|
WHEN (SELECT display_rank FROM ranked WHERE ranked.id = ${ident('preistraeger')}.${ident('id')}) <= 15 THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
WHERE EXISTS (SELECT 1 FROM ranked WHERE ranked.id = ${ident('preistraeger')}.${ident('id')});
|
||||||
|
`))
|
||||||
|
|
||||||
|
if (await tableExists(db, '_preistraeger_v')) {
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
UPDATE ${ident('_preistraeger_v')}
|
||||||
|
SET
|
||||||
|
${ident('version_award_group')} = (
|
||||||
|
SELECT ${ident('award_group')} FROM ${ident('preistraeger')} WHERE ${ident('preistraeger')}.${ident('id')} = ${ident('_preistraeger_v')}.${ident('parent_id')}
|
||||||
|
),
|
||||||
|
${ident('version_winner_rank')} = (
|
||||||
|
SELECT ${ident('winner_rank')} FROM ${ident('preistraeger')} WHERE ${ident('preistraeger')}.${ident('id')} = ${ident('_preistraeger_v')}.${ident('parent_id')}
|
||||||
|
),
|
||||||
|
${ident('version_sort_order')} = (
|
||||||
|
SELECT ${ident('sort_order')} FROM ${ident('preistraeger')} WHERE ${ident('preistraeger')}.${ident('id')} = ${ident('_preistraeger_v')}.${ident('parent_id')}
|
||||||
|
),
|
||||||
|
${ident('version_show_on_index')} = (
|
||||||
|
SELECT ${ident('show_on_index')} FROM ${ident('preistraeger')} WHERE ${ident('preistraeger')}.${ident('id')} = ${ident('_preistraeger_v')}.${ident('parent_id')}
|
||||||
|
)
|
||||||
|
WHERE ${ident('parent_id')} IN (SELECT ${ident('id')} FROM ${ident('preistraeger')});
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createJuryMembersTables(db: MigrationDB) {
|
||||||
|
await db.run(sql.raw(`CREATE TABLE IF NOT EXISTS ${ident('jury_mitglieder')} (
|
||||||
|
${ident('id')} integer PRIMARY KEY NOT NULL,
|
||||||
|
${ident('name')} text,
|
||||||
|
${ident('image_id')} integer,
|
||||||
|
${ident('image_alt')} text,
|
||||||
|
${ident('quote')} text,
|
||||||
|
${ident('role')} text,
|
||||||
|
${ident('organization')} text,
|
||||||
|
${ident('bio')} text,
|
||||||
|
${ident('website')} text,
|
||||||
|
${ident('linkedin')} text,
|
||||||
|
${ident('sort_order')} numeric DEFAULT 100,
|
||||||
|
${ident('active')} integer DEFAULT true,
|
||||||
|
${ident('published_at')} text,
|
||||||
|
${ident('updated_at')} text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||||
|
${ident('created_at')} text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||||
|
${ident('_status')} text DEFAULT 'draft',
|
||||||
|
FOREIGN KEY (${ident('image_id')}) REFERENCES ${ident('media')}(${ident('id')}) ON UPDATE no action ON DELETE set null
|
||||||
|
);`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('jury_mitglieder_image_idx')} ON ${ident('jury_mitglieder')} (${ident('image_id')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('jury_mitglieder_updated_at_idx')} ON ${ident('jury_mitglieder')} (${ident('updated_at')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('jury_mitglieder_created_at_idx')} ON ${ident('jury_mitglieder')} (${ident('created_at')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('jury_mitglieder__status_idx')} ON ${ident('jury_mitglieder')} (${ident('_status')});`))
|
||||||
|
|
||||||
|
await db.run(sql.raw(`CREATE TABLE IF NOT EXISTS ${ident('_jury_mitglieder_v')} (
|
||||||
|
${ident('id')} integer PRIMARY KEY NOT NULL,
|
||||||
|
${ident('parent_id')} integer,
|
||||||
|
${ident('version_name')} text,
|
||||||
|
${ident('version_image_id')} integer,
|
||||||
|
${ident('version_image_alt')} text,
|
||||||
|
${ident('version_quote')} text,
|
||||||
|
${ident('version_role')} text,
|
||||||
|
${ident('version_organization')} text,
|
||||||
|
${ident('version_bio')} text,
|
||||||
|
${ident('version_website')} text,
|
||||||
|
${ident('version_linkedin')} text,
|
||||||
|
${ident('version_sort_order')} numeric DEFAULT 100,
|
||||||
|
${ident('version_active')} integer DEFAULT true,
|
||||||
|
${ident('version_published_at')} text,
|
||||||
|
${ident('version_updated_at')} text,
|
||||||
|
${ident('version_created_at')} text,
|
||||||
|
${ident('version__status')} text DEFAULT 'draft',
|
||||||
|
${ident('created_at')} text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||||
|
${ident('updated_at')} text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||||
|
${ident('latest')} integer,
|
||||||
|
${ident('autosave')} integer,
|
||||||
|
FOREIGN KEY (${ident('parent_id')}) REFERENCES ${ident('jury_mitglieder')}(${ident('id')}) ON UPDATE no action ON DELETE set null,
|
||||||
|
FOREIGN KEY (${ident('version_image_id')}) REFERENCES ${ident('media')}(${ident('id')}) ON UPDATE no action ON DELETE set null
|
||||||
|
);`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_parent_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('parent_id')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_version_version_image_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('version_image_id')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_version_version_updated_at_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('version_updated_at')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_version_version_created_at_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('version_created_at')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_version_version__status_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('version__status')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_created_at_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('created_at')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_updated_at_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('updated_at')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_latest_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('latest')});`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident('_jury_mitglieder_v_autosave_idx')} ON ${ident('_jury_mitglieder_v')} (${ident('autosave')});`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
|
||||||
|
const hadAwardGroup = await columnExists(db, 'preistraeger', 'award_group')
|
||||||
|
|
||||||
|
await addColumn(db, 'preistraeger', 'award_group', `text DEFAULT ${literal('preistraeger')}`)
|
||||||
|
await addColumn(db, 'preistraeger', 'winner_rank', 'numeric')
|
||||||
|
await addColumn(db, 'preistraeger', 'sort_order', 'numeric DEFAULT 100')
|
||||||
|
await addColumn(db, 'preistraeger', 'show_on_index', 'integer DEFAULT true')
|
||||||
|
await addColumn(db, '_preistraeger_v', 'version_award_group', `text DEFAULT ${literal('preistraeger')}`)
|
||||||
|
await addColumn(db, '_preistraeger_v', 'version_winner_rank', 'numeric')
|
||||||
|
await addColumn(db, '_preistraeger_v', 'version_sort_order', 'numeric DEFAULT 100')
|
||||||
|
await addColumn(db, '_preistraeger_v', 'version_show_on_index', 'integer DEFAULT true')
|
||||||
|
|
||||||
|
if (!hadAwardGroup) await rankExistingPreistraeger(db)
|
||||||
|
|
||||||
|
await addColumn(db, 'pages', 'preistraeger_index_rules_total_winners', 'numeric DEFAULT 15')
|
||||||
|
await addColumn(db, 'pages', 'preistraeger_index_rules_goldene_bavaria_count', 'numeric DEFAULT 3')
|
||||||
|
await addColumn(db, 'pages', 'preistraeger_index_goldene_bavaria_eyebrow', `text DEFAULT ${literal('Goldene Bavaria')}`)
|
||||||
|
await addColumn(db, 'pages', 'preistraeger_index_goldene_bavaria_heading', `text DEFAULT ${literal('Die drei Besten des Jahrgangs')}`)
|
||||||
|
await addColumn(
|
||||||
|
db,
|
||||||
|
'pages',
|
||||||
|
'preistraeger_index_goldene_bavaria_description',
|
||||||
|
`text DEFAULT ${literal('Aus den 15 Gewinnerinnen und Gewinnern erhalten die drei bestplatzierten Unternehmen die Goldene Bavaria.')}`,
|
||||||
|
)
|
||||||
|
await addColumn(
|
||||||
|
db,
|
||||||
|
'pages',
|
||||||
|
'preistraeger_index_goldene_bavaria_empty_message',
|
||||||
|
`text DEFAULT ${literal('Für diesen Jahrgang sind noch keine Goldene-Bavaria-Gewinner markiert.')}`,
|
||||||
|
)
|
||||||
|
await addColumn(db, 'pages', 'preistraeger_index_winners_eyebrow', `text DEFAULT ${literal('Preisträger:innen')}`)
|
||||||
|
await addColumn(db, 'pages', 'preistraeger_index_winners_heading', `text DEFAULT ${literal('Weitere Gewinnerinnen und Gewinner')}`)
|
||||||
|
await addColumn(
|
||||||
|
db,
|
||||||
|
'pages',
|
||||||
|
'preistraeger_index_winners_description',
|
||||||
|
`text DEFAULT ${literal('Der Bayerische Mittelstandspreis zeichnet pro Jahrgang insgesamt 15 Gewinner aus.')}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await addColumn(db, '_pages_v', 'version_preistraeger_index_rules_total_winners', 'numeric DEFAULT 15')
|
||||||
|
await addColumn(db, '_pages_v', 'version_preistraeger_index_rules_goldene_bavaria_count', 'numeric DEFAULT 3')
|
||||||
|
await addColumn(db, '_pages_v', 'version_preistraeger_index_goldene_bavaria_eyebrow', `text DEFAULT ${literal('Goldene Bavaria')}`)
|
||||||
|
await addColumn(db, '_pages_v', 'version_preistraeger_index_goldene_bavaria_heading', `text DEFAULT ${literal('Die drei Besten des Jahrgangs')}`)
|
||||||
|
await addColumn(
|
||||||
|
db,
|
||||||
|
'_pages_v',
|
||||||
|
'version_preistraeger_index_goldene_bavaria_description',
|
||||||
|
`text DEFAULT ${literal('Aus den 15 Gewinnerinnen und Gewinnern erhalten die drei bestplatzierten Unternehmen die Goldene Bavaria.')}`,
|
||||||
|
)
|
||||||
|
await addColumn(
|
||||||
|
db,
|
||||||
|
'_pages_v',
|
||||||
|
'version_preistraeger_index_goldene_bavaria_empty_message',
|
||||||
|
`text DEFAULT ${literal('Für diesen Jahrgang sind noch keine Goldene-Bavaria-Gewinner markiert.')}`,
|
||||||
|
)
|
||||||
|
await addColumn(db, '_pages_v', 'version_preistraeger_index_winners_eyebrow', `text DEFAULT ${literal('Preisträger:innen')}`)
|
||||||
|
await addColumn(db, '_pages_v', 'version_preistraeger_index_winners_heading', `text DEFAULT ${literal('Weitere Gewinnerinnen und Gewinner')}`)
|
||||||
|
await addColumn(
|
||||||
|
db,
|
||||||
|
'_pages_v',
|
||||||
|
'version_preistraeger_index_winners_description',
|
||||||
|
`text DEFAULT ${literal('Der Bayerische Mittelstandspreis zeichnet pro Jahrgang insgesamt 15 Gewinner aus.')}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await createJuryMembersTables(db)
|
||||||
|
|
||||||
|
if (!(await columnExists(db, 'payload_locked_documents_rels', 'jury_mitglieder_id'))) {
|
||||||
|
await db.run(
|
||||||
|
sql.raw(
|
||||||
|
`ALTER TABLE ${ident('payload_locked_documents_rels')} ADD ${ident('jury_mitglieder_id')} integer REFERENCES ${ident('jury_mitglieder')}(${ident('id')});`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await db.run(
|
||||||
|
sql.raw(
|
||||||
|
`CREATE INDEX IF NOT EXISTS ${ident('payload_locked_documents_rels_jury_mitglieder_id_idx')} ON ${ident('payload_locked_documents_rels')} (${ident('jury_mitglieder_id')});`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.run(sql.raw(`DROP INDEX IF EXISTS ${ident('payload_locked_documents_rels_jury_mitglieder_id_idx')};`))
|
||||||
|
await dropColumn(db, 'payload_locked_documents_rels', 'jury_mitglieder_id')
|
||||||
|
|
||||||
|
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident('_jury_mitglieder_v')};`))
|
||||||
|
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident('jury_mitglieder')};`))
|
||||||
|
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_winners_description')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_winners_heading')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_winners_eyebrow')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_goldene_bavaria_empty_message')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_goldene_bavaria_description')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_goldene_bavaria_heading')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_goldene_bavaria_eyebrow')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_rules_goldene_bavaria_count')
|
||||||
|
await dropColumn(db, '_pages_v', 'version_preistraeger_index_rules_total_winners')
|
||||||
|
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_winners_description')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_winners_heading')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_winners_eyebrow')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_goldene_bavaria_empty_message')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_goldene_bavaria_description')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_goldene_bavaria_heading')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_goldene_bavaria_eyebrow')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_rules_goldene_bavaria_count')
|
||||||
|
await dropColumn(db, 'pages', 'preistraeger_index_rules_total_winners')
|
||||||
|
|
||||||
|
await dropColumn(db, '_preistraeger_v', 'version_show_on_index')
|
||||||
|
await dropColumn(db, '_preistraeger_v', 'version_sort_order')
|
||||||
|
await dropColumn(db, '_preistraeger_v', 'version_winner_rank')
|
||||||
|
await dropColumn(db, '_preistraeger_v', 'version_award_group')
|
||||||
|
await dropColumn(db, 'preistraeger', 'show_on_index')
|
||||||
|
await dropColumn(db, 'preistraeger', 'sort_order')
|
||||||
|
await dropColumn(db, 'preistraeger', 'winner_rank')
|
||||||
|
await dropColumn(db, 'preistraeger', 'award_group')
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import * as migration_20260701_163206_participation_phase_form_fields from './20
|
|||||||
import * as migration_20260707_122300_preistraeger_zip_download from './20260707_122300_preistraeger_zip_download';
|
import * as migration_20260707_122300_preistraeger_zip_download from './20260707_122300_preistraeger_zip_download';
|
||||||
import * as migration_20260707_123500_preistraeger_gallery from './20260707_123500_preistraeger_gallery';
|
import * as migration_20260707_123500_preistraeger_gallery from './20260707_123500_preistraeger_gallery';
|
||||||
import * as migration_20260707_124500_site_settings_preistraeger_placeholder from './20260707_124500_site_settings_preistraeger_placeholder';
|
import * as migration_20260707_124500_site_settings_preistraeger_placeholder from './20260707_124500_site_settings_preistraeger_placeholder';
|
||||||
|
import * as migration_20260707_130500_preistraeger_awards_jury_members from './20260707_130500_preistraeger_awards_jury_members';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -54,4 +55,9 @@ export const migrations = [
|
|||||||
down: migration_20260707_124500_site_settings_preistraeger_placeholder.down,
|
down: migration_20260707_124500_site_settings_preistraeger_placeholder.down,
|
||||||
name: '20260707_124500_site_settings_preistraeger_placeholder',
|
name: '20260707_124500_site_settings_preistraeger_placeholder',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260707_130500_preistraeger_awards_jury_members.up,
|
||||||
|
down: migration_20260707_130500_preistraeger_awards_jury_members.down,
|
||||||
|
name: '20260707_130500_preistraeger_awards_jury_members',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export interface Config {
|
|||||||
collections: {
|
collections: {
|
||||||
pages: Page;
|
pages: Page;
|
||||||
preistraeger: Preistraeger;
|
preistraeger: Preistraeger;
|
||||||
|
'jury-mitglieder': JuryMitglieder;
|
||||||
partners: Partner;
|
partners: Partner;
|
||||||
posts: Post;
|
posts: Post;
|
||||||
events: Event;
|
events: Event;
|
||||||
@@ -89,6 +90,7 @@ export interface Config {
|
|||||||
collectionsSelect: {
|
collectionsSelect: {
|
||||||
pages: PagesSelect<false> | PagesSelect<true>;
|
pages: PagesSelect<false> | PagesSelect<true>;
|
||||||
preistraeger: PreistraegerSelect<false> | PreistraegerSelect<true>;
|
preistraeger: PreistraegerSelect<false> | PreistraegerSelect<true>;
|
||||||
|
'jury-mitglieder': JuryMitgliederSelect<false> | JuryMitgliederSelect<true>;
|
||||||
partners: PartnersSelect<false> | PartnersSelect<true>;
|
partners: PartnersSelect<false> | PartnersSelect<true>;
|
||||||
posts: PostsSelect<false> | PostsSelect<true>;
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
events: EventsSelect<false> | EventsSelect<true>;
|
events: EventsSelect<false> | EventsSelect<true>;
|
||||||
@@ -239,6 +241,27 @@ export interface Page {
|
|||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Blue card grid with featured winner relationships.
|
||||||
|
*/
|
||||||
|
winners?: {
|
||||||
|
eyebrow?: string | null;
|
||||||
|
heading?: string | null;
|
||||||
|
cta?: {
|
||||||
|
label?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Current frontend image: /images/gala-saal-overview.jpg
|
||||||
|
*/
|
||||||
|
fallbackImage?: (number | null) | Media;
|
||||||
|
cardFallbackTitle?: string | null;
|
||||||
|
hoverLabel?: string | null;
|
||||||
|
/**
|
||||||
|
* Choose the winner cards shown on the home page. If empty, the latest 2025 winners are used.
|
||||||
|
*/
|
||||||
|
featured?: (number | Preistraeger)[] | null;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* White text panel plus right-side stage image and five eligibility rows.
|
* White text panel plus right-side stage image and five eligibility rows.
|
||||||
*/
|
*/
|
||||||
@@ -289,27 +312,6 @@ export interface Page {
|
|||||||
url?: string | null;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Blue card grid with featured winner relationships.
|
|
||||||
*/
|
|
||||||
winners?: {
|
|
||||||
eyebrow?: string | null;
|
|
||||||
heading?: string | null;
|
|
||||||
cta?: {
|
|
||||||
label?: string | null;
|
|
||||||
url?: string | null;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Current frontend image: /images/gala-saal-overview.jpg
|
|
||||||
*/
|
|
||||||
fallbackImage?: (number | null) | Media;
|
|
||||||
cardFallbackTitle?: string | null;
|
|
||||||
hoverLabel?: string | null;
|
|
||||||
/**
|
|
||||||
* Choose the winner cards shown on the home page. If empty, the latest 2025 winners are used.
|
|
||||||
*/
|
|
||||||
featured?: (number | Preistraeger)[] | null;
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Portrait carousel and quote copy.
|
* Portrait carousel and quote copy.
|
||||||
*/
|
*/
|
||||||
@@ -1529,12 +1531,42 @@ export interface Page {
|
|||||||
count?: {
|
count?: {
|
||||||
label?: string | null;
|
label?: string | null;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Public page rules for the selected year. Winner records are still managed in the Preisträger collection.
|
||||||
|
*/
|
||||||
|
rules?: {
|
||||||
|
/**
|
||||||
|
* The public page displays at most this many winners per year.
|
||||||
|
*/
|
||||||
|
totalWinners?: number | null;
|
||||||
|
/**
|
||||||
|
* These top-ranked winners are shown in the separate Goldene Bavaria section.
|
||||||
|
*/
|
||||||
|
goldeneBavariaCount?: number | null;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Message when the selected year returns no winners.
|
* Message when the selected year returns no winners.
|
||||||
*/
|
*/
|
||||||
empty?: {
|
empty?: {
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Copy for the separate section that highlights the best three winners.
|
||||||
|
*/
|
||||||
|
goldeneBavaria?: {
|
||||||
|
eyebrow?: string | null;
|
||||||
|
heading?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
emptyMessage?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Copy for the remaining winners below the Goldene Bavaria section.
|
||||||
|
*/
|
||||||
|
winners?: {
|
||||||
|
eyebrow?: string | null;
|
||||||
|
heading?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Hover label shown on winner cards.
|
* Hover label shown on winner cards.
|
||||||
*/
|
*/
|
||||||
@@ -2576,6 +2608,19 @@ export interface Preistraeger {
|
|||||||
image?: (number | null) | Media;
|
image?: (number | null) | Media;
|
||||||
category?: string | null;
|
category?: string | null;
|
||||||
year?: number | null;
|
year?: number | null;
|
||||||
|
/**
|
||||||
|
* Steuert die Ausgabe auf der Preisträger-Übersicht. Pro Jahrgang werden 15 Gewinner angezeigt, davon maximal 3 in der Goldene-Bavaria-Section.
|
||||||
|
*/
|
||||||
|
awardGroup?: ('preistraeger' | 'goldene-bavaria') | null;
|
||||||
|
/**
|
||||||
|
* 1 bis 3 sind die besten Gewinner und sollten als Goldene Bavaria markiert werden. Niedrigere Zahlen erscheinen zuerst.
|
||||||
|
*/
|
||||||
|
winnerRank?: number | null;
|
||||||
|
/**
|
||||||
|
* Fallback-Sortierung innerhalb eines Jahrgangs, wenn kein Rang gesetzt ist.
|
||||||
|
*/
|
||||||
|
sortOrder?: number | null;
|
||||||
|
showOnIndex?: boolean | null;
|
||||||
awardType?: string | null;
|
awardType?: string | null;
|
||||||
shortDesc?: string | null;
|
shortDesc?: string | null;
|
||||||
longDesc?: string | null;
|
longDesc?: string | null;
|
||||||
@@ -2682,6 +2727,31 @@ export interface Preistraeger {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "jury-mitglieder".
|
||||||
|
*/
|
||||||
|
export interface JuryMitglieder {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
image?: (number | null) | Media;
|
||||||
|
imageAlt?: string | null;
|
||||||
|
quote?: string | null;
|
||||||
|
role?: string | null;
|
||||||
|
organization?: string | null;
|
||||||
|
bio?: string | null;
|
||||||
|
website?: string | null;
|
||||||
|
linkedin?: string | null;
|
||||||
|
/**
|
||||||
|
* Niedrigere Zahlen erscheinen zuerst.
|
||||||
|
*/
|
||||||
|
sortOrder?: number | null;
|
||||||
|
active?: boolean | null;
|
||||||
|
publishedAt?: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "partners".
|
* via the `definition` "partners".
|
||||||
@@ -2978,6 +3048,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'preistraeger';
|
relationTo: 'preistraeger';
|
||||||
value: number | Preistraeger;
|
value: number | Preistraeger;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'jury-mitglieder';
|
||||||
|
value: number | JuryMitglieder;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'partners';
|
relationTo: 'partners';
|
||||||
value: number | Partner;
|
value: number | Partner;
|
||||||
@@ -3112,6 +3186,22 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
winners?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
eyebrow?: T;
|
||||||
|
heading?: T;
|
||||||
|
cta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
fallbackImage?: T;
|
||||||
|
cardFallbackTitle?: T;
|
||||||
|
hoverLabel?: T;
|
||||||
|
featured?: T;
|
||||||
|
};
|
||||||
quickCheck?:
|
quickCheck?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -3158,22 +3248,6 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
url?: T;
|
url?: T;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
winners?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
eyebrow?: T;
|
|
||||||
heading?: T;
|
|
||||||
cta?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
label?: T;
|
|
||||||
url?: T;
|
|
||||||
};
|
|
||||||
fallbackImage?: T;
|
|
||||||
cardFallbackTitle?: T;
|
|
||||||
hoverLabel?: T;
|
|
||||||
featured?: T;
|
|
||||||
};
|
|
||||||
testimonials?:
|
testimonials?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -4209,11 +4283,32 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
| {
|
| {
|
||||||
label?: T;
|
label?: T;
|
||||||
};
|
};
|
||||||
|
rules?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
totalWinners?: T;
|
||||||
|
goldeneBavariaCount?: T;
|
||||||
|
};
|
||||||
empty?:
|
empty?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
message?: T;
|
message?: T;
|
||||||
};
|
};
|
||||||
|
goldeneBavaria?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
eyebrow?: T;
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
emptyMessage?: T;
|
||||||
|
};
|
||||||
|
winners?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
eyebrow?: T;
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
};
|
||||||
card?:
|
card?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -4860,6 +4955,10 @@ export interface PreistraegerSelect<T extends boolean = true> {
|
|||||||
image?: T;
|
image?: T;
|
||||||
category?: T;
|
category?: T;
|
||||||
year?: T;
|
year?: T;
|
||||||
|
awardGroup?: T;
|
||||||
|
winnerRank?: T;
|
||||||
|
sortOrder?: T;
|
||||||
|
showOnIndex?: T;
|
||||||
awardType?: T;
|
awardType?: T;
|
||||||
shortDesc?: T;
|
shortDesc?: T;
|
||||||
longDesc?: T;
|
longDesc?: T;
|
||||||
@@ -4962,6 +5061,27 @@ export interface PreistraegerSelect<T extends boolean = true> {
|
|||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
_status?: T;
|
_status?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "jury-mitglieder_select".
|
||||||
|
*/
|
||||||
|
export interface JuryMitgliederSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
image?: T;
|
||||||
|
imageAlt?: T;
|
||||||
|
quote?: T;
|
||||||
|
role?: T;
|
||||||
|
organization?: T;
|
||||||
|
bio?: T;
|
||||||
|
website?: T;
|
||||||
|
linkedin?: T;
|
||||||
|
sortOrder?: T;
|
||||||
|
active?: T;
|
||||||
|
publishedAt?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
_status?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "partners_select".
|
* via the `definition` "partners_select".
|
||||||
@@ -5785,6 +5905,10 @@ export interface TaskSchedulePublish {
|
|||||||
relationTo: 'preistraeger';
|
relationTo: 'preistraeger';
|
||||||
value: number | Preistraeger;
|
value: number | Preistraeger;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'jury-mitglieder';
|
||||||
|
value: number | JuryMitglieder;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'partners';
|
relationTo: 'partners';
|
||||||
value: number | Partner;
|
value: number | Partner;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { buildConfig, PayloadRequest } from 'payload'
|
|||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import { Events } from './collections/Events'
|
import { Events } from './collections/Events'
|
||||||
|
import { JuryMitglieder } from './collections/JuryMitglieder'
|
||||||
import { Media } from './collections/Media'
|
import { Media } from './collections/Media'
|
||||||
import { Pages } from './collections/Pages'
|
import { Pages } from './collections/Pages'
|
||||||
import { Partners } from './collections/Partners'
|
import { Partners } from './collections/Partners'
|
||||||
@@ -66,7 +67,7 @@ export default buildConfig({
|
|||||||
url: process.env.DATABASE_URL || '',
|
url: process.env.DATABASE_URL || '',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
collections: [Pages, Preistraeger, Partners, Posts, Events, Media, Users],
|
collections: [Pages, Preistraeger, JuryMitglieder, Partners, Posts, Events, Media, Users],
|
||||||
cors: [getServerSideURL()].filter(Boolean),
|
cors: [getServerSideURL()].filter(Boolean),
|
||||||
globals: [Header, Footer, SiteSettings, ApplicationPhase],
|
globals: [Header, Footer, SiteSettings, ApplicationPhase],
|
||||||
plugins,
|
plugins,
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ function preistraegerData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
|
|||||||
image: mediaID(mediaMap, winner?.img),
|
image: mediaID(mediaMap, winner?.img),
|
||||||
category: winner?.category,
|
category: winner?.category,
|
||||||
year: winner?.year,
|
year: winner?.year,
|
||||||
|
awardGroup: winner?.awardGroup,
|
||||||
|
winnerRank: winner?.winnerRank,
|
||||||
|
sortOrder: winner?.sortOrder,
|
||||||
|
showOnIndex: winner?.showOnIndex,
|
||||||
awardType: winner?.type,
|
awardType: winner?.type,
|
||||||
shortDesc: winner?.shortDesc || page.description,
|
shortDesc: winner?.shortDesc || page.description,
|
||||||
longDesc: winner?.longDesc,
|
longDesc: winner?.longDesc,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React, { createContext, useContext } from 'react'
|
|||||||
import type { SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
import type { SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
||||||
import type { SpaSiteSettingsData } from '@/spa/siteSettings'
|
import type { SpaSiteSettingsData } from '@/spa/siteSettings'
|
||||||
|
|
||||||
export type CmsRouteCollection = 'pages' | 'posts' | 'events' | 'preistraeger' | 'partners'
|
export type CmsRouteCollection = 'pages' | 'posts' | 'events' | 'preistraeger' | 'partners' | 'jury-mitglieder'
|
||||||
|
|
||||||
export type CmsRouteDoc = {
|
export type CmsRouteDoc = {
|
||||||
id: number | string
|
id: number | string
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
|||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
import { mediaUrl } from '@/spa/cmsMediaField'
|
import { mediaUrl } from '@/spa/cmsMediaField'
|
||||||
import { testimonialsContent } from '@/spa/testimonialsContent'
|
import { testimonialsContent } from '@/spa/testimonialsContent'
|
||||||
|
import { SHOW_TESTIMONIALS } from '@/spa/testimonialsVisibility'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
|
|
||||||
@@ -65,6 +66,10 @@ export default function TestimonialsSection({ data }: { data?: TestimonialsCms }
|
|||||||
const prev = () => setActive(i => (i - 1 + testimonials.length) % testimonials.length);
|
const prev = () => setActive(i => (i - 1 + testimonials.length) % testimonials.length);
|
||||||
const next = () => setActive(i => (i + 1) % testimonials.length);
|
const next = () => setActive(i => (i + 1) % testimonials.length);
|
||||||
|
|
||||||
|
if (!SHOW_TESTIMONIALS) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
<section style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||||||
<MunichSkylineBg />
|
<MunichSkylineBg />
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ type StepOption = (typeof netzwerkContent.sponsoringForm.steps)[number];
|
|||||||
|
|
||||||
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
|
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
|
||||||
|
const isPremiumPackage = (pkg: PackageOption) =>
|
||||||
|
[pkg.value, pkg.title].some((value) => typeof value === 'string' && value.toLowerCase().includes('premium'));
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }),
|
enter: (dir: number) => ({ x: dir > 0 ? 40 : -40, opacity: 0 }),
|
||||||
@@ -168,7 +170,7 @@ export default function SponsoringForm({ theme = 'dark', content }: { theme?: 'd
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const copy = { ...netzwerkContent.sponsoringForm, ...(content || {}) };
|
const copy = { ...netzwerkContent.sponsoringForm, ...(content || {}) };
|
||||||
const steps = fallbackArray<StepOption>(copy.steps, netzwerkContent.sponsoringForm.steps);
|
const steps = fallbackArray<StepOption>(copy.steps, netzwerkContent.sponsoringForm.steps);
|
||||||
const packages = fallbackArray<PackageOption>(copy.packages, netzwerkContent.sponsoringForm.packages);
|
const packages = fallbackArray<PackageOption>(copy.packages, netzwerkContent.sponsoringForm.packages).filter((pkg) => !isPremiumPackage(pkg));
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [dir, setDir] = useState(1);
|
const [dir, setDir] = useState(1);
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|||||||
@@ -49,9 +49,11 @@ const fittedTextSize = (text: string, preferred: number, compact = 12) => {
|
|||||||
export function PartnerLogoMark({
|
export function PartnerLogoMark({
|
||||||
partner,
|
partner,
|
||||||
defaultLogoColor = '#111D55',
|
defaultLogoColor = '#111D55',
|
||||||
|
scale = 1,
|
||||||
}: {
|
}: {
|
||||||
partner: PartnerLogoData
|
partner: PartnerLogoData
|
||||||
defaultLogoColor?: string
|
defaultLogoColor?: string
|
||||||
|
scale?: number
|
||||||
}) {
|
}) {
|
||||||
const realLogo = mediaUrl(partner.logo, '')
|
const realLogo = mediaUrl(partner.logo, '')
|
||||||
const alt = mediaAlt(partner.logo, fallbackText(partner.logoAlt, fallbackText(partner.name, 'Partner logo')))
|
const alt = mediaAlt(partner.logo, fallbackText(partner.logoAlt, fallbackText(partner.name, 'Partner logo')))
|
||||||
@@ -72,12 +74,13 @@ export function PartnerLogoMark({
|
|||||||
const color = fallbackText(partner.logoColor, defaultLogoColor)
|
const color = fallbackText(partner.logoColor, defaultLogoColor)
|
||||||
const text = fallbackText(partner.logoText, fallbackText(partner.name, 'Partner'))
|
const text = fallbackText(partner.logoText, fallbackText(partner.name, 'Partner'))
|
||||||
const subtext = fallbackText(partner.logoSubtext, '')
|
const subtext = fallbackText(partner.logoSubtext, '')
|
||||||
const textSize = fittedTextSize(text, partner.logoKind === 'serif' ? 21 : 16)
|
const size = (value: number) => Math.round(value * scale)
|
||||||
|
const textSize = size(fittedTextSize(text, partner.logoKind === 'serif' ? 21 : 16))
|
||||||
|
|
||||||
if (partner.logoKind === 'badge') {
|
if (partner.logoKind === 'badge') {
|
||||||
return (
|
return (
|
||||||
<span style={logoShellStyle}>
|
<span style={logoShellStyle}>
|
||||||
<span style={{ background: color, color: '#fff', fontWeight: 900, fontSize: 14, padding: '4px 10px', lineHeight: 1.25, display: 'inline-block', textAlign: 'center', fontFamily: FF }}>
|
<span style={{ background: color, color: '#fff', fontWeight: 900, fontSize: size(14), padding: `${size(4)}px ${size(10)}px`, lineHeight: 1.25, display: 'inline-block', textAlign: 'center', fontFamily: FF }}>
|
||||||
{text.split('\n').map((line, index) => (
|
{text.split('\n').map((line, index) => (
|
||||||
<React.Fragment key={`${line}-${index}`}>{index > 0 && <br />}{line}</React.Fragment>
|
<React.Fragment key={`${line}-${index}`}>{index > 0 && <br />}{line}</React.Fragment>
|
||||||
))}
|
))}
|
||||||
@@ -89,9 +92,9 @@ export function PartnerLogoMark({
|
|||||||
if (partner.logoKind === 'monogram') {
|
if (partner.logoKind === 'monogram') {
|
||||||
return (
|
return (
|
||||||
<span style={logoShellStyle}>
|
<span style={logoShellStyle}>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}>
|
||||||
<span style={{ width: 22, height: 22, background: '#3a3a3a', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 900, fontSize: 11, fontFamily: FF, flexShrink: 0 }}>{subtext || text.slice(0, 2)}</span>
|
<span style={{ width: size(22), height: size(22), background: '#3a3a3a', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 900, fontSize: size(11), fontFamily: FF, flexShrink: 0 }}>{subtext || text.slice(0, 2)}</span>
|
||||||
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: fittedTextSize(text, 15, 11), color, fontFamily: FF }}>{text}</span>
|
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FF }}>{text}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -100,9 +103,9 @@ export function PartnerLogoMark({
|
|||||||
if (partner.logoKind === 'dot') {
|
if (partner.logoKind === 'dot') {
|
||||||
return (
|
return (
|
||||||
<span style={logoShellStyle}>
|
<span style={logoShellStyle}>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}>
|
||||||
<span style={{ width: 20, height: 20, borderRadius: '50%', background: color, display: 'block', flexShrink: 0 }} />
|
<span style={{ width: size(20), height: size(20), borderRadius: '50%', background: color, display: 'block', flexShrink: 0 }} />
|
||||||
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: fittedTextSize(text, 16, 11), color, fontFamily: FF }}>{text}</span>
|
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 16, 11)), color, fontFamily: FF }}>{text}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -111,9 +114,9 @@ export function PartnerLogoMark({
|
|||||||
if (partner.logoKind === 'leaf') {
|
if (partner.logoKind === 'leaf') {
|
||||||
return (
|
return (
|
||||||
<span style={logoShellStyle}>
|
<span style={logoShellStyle}>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}>
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><circle cx="10" cy="10" r="9" fill={color} /><path d="M10 3 Q14 7 14 10 Q14 14 10 17 Q6 14 6 10 Q6 7 10 3Z" fill="white" opacity="0.8" /></svg>
|
<svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><circle cx="10" cy="10" r="9" fill={color} /><path d="M10 3 Q14 7 14 10 Q14 14 10 17 Q6 14 6 10 Q6 7 10 3Z" fill="white" opacity="0.8" /></svg>
|
||||||
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: fittedTextSize(text, 15, 11), color, fontFamily: FB }}>{text}</span>
|
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -122,13 +125,13 @@ export function PartnerLogoMark({
|
|||||||
if (partner.logoKind === 'rsm') {
|
if (partner.logoKind === 'rsm') {
|
||||||
return (
|
return (
|
||||||
<span style={logoShellStyle}>
|
<span style={logoShellStyle}>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}>
|
||||||
<span style={{ display: 'flex', gap: 2, flexShrink: 0 }}>
|
<span style={{ display: 'flex', gap: Math.max(2, size(2)), flexShrink: 0 }}>
|
||||||
<span style={{ width: 10, height: 10, background: '#6b7280', display: 'block' }} />
|
<span style={{ width: size(10), height: size(10), background: '#6b7280', display: 'block' }} />
|
||||||
<span style={{ width: 10, height: 10, background: '#22c55e', display: 'block' }} />
|
<span style={{ width: size(10), height: size(10), background: '#22c55e', display: 'block' }} />
|
||||||
<span style={{ width: 10, height: 10, background: '#3b82f6', display: 'block' }} />
|
<span style={{ width: size(10), height: size(10), background: '#3b82f6', display: 'block' }} />
|
||||||
</span>
|
</span>
|
||||||
<span style={{ ...containedTextStyle, fontWeight: 900, fontSize: 15, letterSpacing: 0, color, fontFamily: FF }}>{text} {subtext && <span style={{ color: '#6b7280', fontWeight: 600 }}>{subtext}</span>}</span>
|
<span style={{ ...containedTextStyle, fontWeight: 900, fontSize: size(15), letterSpacing: 0, color, fontFamily: FF }}>{text} {subtext && <span style={{ color: '#6b7280', fontWeight: 600 }}>{subtext}</span>}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -137,9 +140,9 @@ export function PartnerLogoMark({
|
|||||||
if (partner.logoKind === 'deutschebank') {
|
if (partner.logoKind === 'deutschebank') {
|
||||||
return (
|
return (
|
||||||
<span style={logoShellStyle}>
|
<span style={logoShellStyle}>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: size(8), minWidth: 0 }}>
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><rect x="1" y="1" width="18" height="18" fill="none" stroke={color} strokeWidth="1.5" /><line x1="5" y1="15" x2="15" y2="5" stroke={color} strokeWidth="2" /></svg>
|
<svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><rect x="1" y="1" width="18" height="18" fill="none" stroke={color} strokeWidth="1.5" /><line x1="5" y1="15" x2="15" y2="5" stroke={color} strokeWidth="2" /></svg>
|
||||||
<span style={{ ...containedTextStyle, fontWeight: 600, fontSize: fittedTextSize(text, 15, 11), color, fontFamily: FB }}>{text}</span>
|
<span style={{ ...containedTextStyle, fontWeight: 600, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export interface Winner {
|
|||||||
category: string;
|
category: string;
|
||||||
year: number;
|
year: number;
|
||||||
type: string;
|
type: string;
|
||||||
|
awardGroup?: 'preistraeger' | 'goldene-bavaria';
|
||||||
|
winnerRank?: number;
|
||||||
|
sortOrder?: number;
|
||||||
|
showOnIndex?: boolean;
|
||||||
img: string;
|
img: string;
|
||||||
shortDesc: string;
|
shortDesc: string;
|
||||||
longDesc: string;
|
longDesc: string;
|
||||||
@@ -17,7 +21,7 @@ export interface Winner {
|
|||||||
hasMedia: boolean;
|
hasMedia: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WINNERS: Winner[] = [
|
const winnerEntries: Omit<Winner, 'awardGroup' | 'winnerRank' | 'sortOrder' | 'showOnIndex'>[] = [
|
||||||
// ── 2025 ──────────────────────────────────────────────────────────────────
|
// ── 2025 ──────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: '1', slug: 'techvision-muenchen',
|
id: '1', slug: 'techvision-muenchen',
|
||||||
@@ -305,5 +309,21 @@ export const WINNERS: Winner[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const winnersPerYear = new Map<number, number>();
|
||||||
|
|
||||||
|
export const WINNERS: Winner[] = winnerEntries.map((winner) => {
|
||||||
|
const winnerRank = (winnersPerYear.get(winner.year) || 0) + 1;
|
||||||
|
winnersPerYear.set(winner.year, winnerRank);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...winner,
|
||||||
|
type: winnerRank <= 3 ? 'Goldene Bavaria' : winner.type,
|
||||||
|
awardGroup: winnerRank <= 3 ? 'goldene-bavaria' : 'preistraeger',
|
||||||
|
winnerRank,
|
||||||
|
sortOrder: winnerRank,
|
||||||
|
showOnIndex: winnerRank <= 15,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export const CATEGORIES = [...new Set(WINNERS.map(w => w.category))].sort();
|
export const CATEGORIES = [...new Set(WINNERS.map(w => w.category))].sort();
|
||||||
export const YEARS = [...new Set(WINNERS.map(w => w.year))].sort((a, b) => b - a);
|
export const YEARS = [...new Set(WINNERS.map(w => w.year))].sort((a, b) => b - a);
|
||||||
|
|||||||
@@ -121,14 +121,14 @@ export const netzwerkContent = {
|
|||||||
partners: {
|
partners: {
|
||||||
eyebrow: 'Netzwerkqualität',
|
eyebrow: 'Netzwerkqualität',
|
||||||
heading: 'PARTNER & SPONSOREN',
|
heading: 'PARTNER & SPONSOREN',
|
||||||
description: 'Partner in drei Kategorien: Hauptsponsoren, Medienpartner und weitere Sponsoren. Klicken für Details.',
|
description: 'Ein starkes Netzwerk aus Unternehmen und Medien begleitet den Bayerischen Mittelstandspreis. Klicken für Details.',
|
||||||
detailLabel: 'Details',
|
detailLabel: 'Details',
|
||||||
premiumLabel: 'Premium',
|
premiumLabel: '',
|
||||||
defaultLogoColor: '#111D55',
|
defaultLogoColor: '#111D55',
|
||||||
tiers: [
|
tiers: [
|
||||||
{ tier: 1, label: 'Hauptsponsoren', note: 'Premium' },
|
{ tier: 1, label: 'Große Darstellung', note: '' },
|
||||||
{ tier: 2, label: 'Medienpartner', note: '' },
|
{ tier: 2, label: 'Mittlere Darstellung', note: '' },
|
||||||
{ tier: 3, label: 'Weitere Sponsoren', note: '' },
|
{ tier: 3, label: 'Kompakte Darstellung', note: '' },
|
||||||
],
|
],
|
||||||
modal: {
|
modal: {
|
||||||
industryLabel: 'Branche',
|
industryLabel: 'Branche',
|
||||||
|
|||||||
@@ -401,6 +401,48 @@ const Home: React.FC = () => {
|
|||||||
{/* Status Phase Slider */}
|
{/* Status Phase Slider */}
|
||||||
<StatusSlider data={applicationPhase} />
|
<StatusSlider data={applicationPhase} />
|
||||||
|
|
||||||
|
{/* Winners Grid – neue Preisträger-Übersicht */}
|
||||||
|
<section style={{ background: '#111D55' }}>
|
||||||
|
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', justifyContent: 'space-between', gap: isMobile ? 16 : 0 }}>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>{fallbackText(winnersSection.eyebrow, homeContent.winners.eyebrow)}</span>
|
||||||
|
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: 0, lineHeight: 1 }}>
|
||||||
|
<Lines text={fallbackText(winnersSection.heading, homeContent.winners.heading)} />
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to={fallbackText(winnersSection.cta?.url, homeContent.winners.cta.url)}
|
||||||
|
style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#EFBF04', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: '1px solid #EFBF04', whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
{fallbackText(winnersSection.cta?.label, homeContent.winners.cta.label)} <ChevronRight size={14} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)',
|
||||||
|
gridAutoFlow: isMobile ? 'column' : undefined,
|
||||||
|
gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined,
|
||||||
|
gridAutoColumns: isMobile ? '66vw' : undefined,
|
||||||
|
overflowX: isMobile ? 'auto' : undefined,
|
||||||
|
scrollSnapType: isMobile ? 'x mandatory' : undefined,
|
||||||
|
gap: isMobile ? 8 : 0,
|
||||||
|
padding: isMobile ? '0 16px 8px' : 0,
|
||||||
|
WebkitOverflowScrolling: 'touch',
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
}}>
|
||||||
|
{homeWinners.map((w) => (
|
||||||
|
<HomeWinnerCard
|
||||||
|
key={(w as WinnerCardData).id || (w as WinnerCardData).slug}
|
||||||
|
winner={w as WinnerCardData}
|
||||||
|
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
|
||||||
|
fallbackImage={winnerFallbackImage}
|
||||||
|
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Schnell-Check Section */}
|
{/* Schnell-Check Section */}
|
||||||
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
<section style={{ overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||||||
<MunichSkylineBg />
|
<MunichSkylineBg />
|
||||||
@@ -550,48 +592,6 @@ const Home: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Winners Grid – neue Preisträger-Übersicht */}
|
|
||||||
<section style={{ background: '#111D55' }}>
|
|
||||||
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', justifyContent: 'space-between', gap: isMobile ? 16 : 0 }}>
|
|
||||||
<div>
|
|
||||||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>{fallbackText(winnersSection.eyebrow, homeContent.winners.eyebrow)}</span>
|
|
||||||
<h2 style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: 0, lineHeight: 1 }}>
|
|
||||||
<Lines text={fallbackText(winnersSection.heading, homeContent.winners.heading)} />
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
to={fallbackText(winnersSection.cta?.url, homeContent.winners.cta.url)}
|
|
||||||
style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#EFBF04', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: '1px solid #EFBF04', whiteSpace: 'nowrap' }}
|
|
||||||
>
|
|
||||||
{fallbackText(winnersSection.cta?.label, homeContent.winners.cta.label)} <ChevronRight size={14} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)',
|
|
||||||
gridAutoFlow: isMobile ? 'column' : undefined,
|
|
||||||
gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined,
|
|
||||||
gridAutoColumns: isMobile ? '66vw' : undefined,
|
|
||||||
overflowX: isMobile ? 'auto' : undefined,
|
|
||||||
scrollSnapType: isMobile ? 'x mandatory' : undefined,
|
|
||||||
gap: isMobile ? 8 : 0,
|
|
||||||
padding: isMobile ? '0 16px 8px' : 0,
|
|
||||||
WebkitOverflowScrolling: 'touch',
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
}}>
|
|
||||||
{homeWinners.map((w) => (
|
|
||||||
<HomeWinnerCard
|
|
||||||
key={(w as WinnerCardData).id || (w as WinnerCardData).slug}
|
|
||||||
winner={w as WinnerCardData}
|
|
||||||
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
|
|
||||||
fallbackImage={winnerFallbackImage}
|
|
||||||
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Testimonials Section */}
|
{/* Testimonials Section */}
|
||||||
<TestimonialsSection data={home.testimonials} />
|
<TestimonialsSection data={home.testimonials} />
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import MembershipWizard from '@/spa/components/membership/MembershipWizard';
|
|||||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
import { useCmsRoute } from '@/spa/cmsRoute';
|
||||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
import { mitgliedWerdenContent } from '@/spa/mitgliedWerdenContent';
|
import { mitgliedWerdenContent } from '@/spa/mitgliedWerdenContent';
|
||||||
|
import { SHOW_TESTIMONIALS } from '@/spa/testimonialsVisibility';
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
@@ -1242,7 +1243,7 @@ const MitgliedWerden: React.FC = () => {
|
|||||||
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
<div style={{ height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.25), transparent)', flexShrink: 0 }} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ── SECTION 7 – Testimonials ────────────────────────────────────────── */}
|
{SHOW_TESTIMONIALS && (
|
||||||
<section style={{ background: '#fff', position: 'relative', overflow: 'hidden' }}>
|
<section style={{ background: '#fff', position: 'relative', overflow: 'hidden' }}>
|
||||||
<div style={{ padding: isMobile ? '48px 24px' : '80px' }}>
|
<div style={{ padding: isMobile ? '48px 24px' : '80px' }}>
|
||||||
<span
|
<span
|
||||||
@@ -1288,6 +1289,7 @@ const MitgliedWerden: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── FAQ ─────────────────────────────────────────────────────────────── */}
|
{/* ── FAQ ─────────────────────────────────────────────────────────────── */}
|
||||||
<FAQSection isMobile={isMobile} content={faq} />
|
<FAQSection isMobile={isMobile} content={faq} />
|
||||||
|
|||||||
@@ -4,7 +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 { useCmsCollection, useCmsRoute } from '@/spa/cmsRoute';
|
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
import { homeContent } from '@/spa/homeContent';
|
import { homeContent } from '@/spa/homeContent';
|
||||||
import { netzwerkContent } from '@/spa/netzwerkContent';
|
import { netzwerkContent } from '@/spa/netzwerkContent';
|
||||||
@@ -18,14 +18,6 @@ const GOLD = '#EFBF04';
|
|||||||
const CREAM = '#E4E2E3';
|
const CREAM = '#E4E2E3';
|
||||||
const HIDE_MEMBERSHIP_NETWORK_CTA = true;
|
const HIDE_MEMBERSHIP_NETWORK_CTA = true;
|
||||||
|
|
||||||
const ROLE_COLOR: Record<string, string> = {
|
|
||||||
'Hauptsponsor': '#EFBF04',
|
|
||||||
'Sponsor': '#2563EB',
|
|
||||||
'Finanzpartner': '#16A34A',
|
|
||||||
'Partner': '#111D55',
|
|
||||||
'Medienpartner': '#DC2626',
|
|
||||||
};
|
|
||||||
|
|
||||||
type NetzwerkCms = Partial<typeof netzwerkContent> & {
|
type NetzwerkCms = Partial<typeof netzwerkContent> & {
|
||||||
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
|
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
|
||||||
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
|
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
|
||||||
@@ -44,7 +36,14 @@ type HomeVideoCms = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown }
|
type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown }
|
||||||
type JuryMember = (typeof netzwerkContent.jury.members)[number] & { imageUpload?: unknown }
|
type JuryMember = (typeof netzwerkContent.jury.members)[number] & {
|
||||||
|
active?: boolean | null
|
||||||
|
imageAlt?: string | null
|
||||||
|
imageUpload?: unknown
|
||||||
|
organization?: string | null
|
||||||
|
quote?: string | null
|
||||||
|
sortOrder?: number | null
|
||||||
|
}
|
||||||
type Partner = (typeof netzwerkContent.partners.items)[number] &
|
type Partner = (typeof netzwerkContent.partners.items)[number] &
|
||||||
PartnerLogoData & {
|
PartnerLogoData & {
|
||||||
active?: boolean | null
|
active?: boolean | null
|
||||||
@@ -65,11 +64,40 @@ type PartnerModalCopy = typeof netzwerkContent.partners.modal
|
|||||||
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
|
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
|
||||||
const managedImage = (upload: unknown, fallback: unknown) => mediaUrl(upload, fallbackText(fallback, ''))
|
const managedImage = (upload: unknown, fallback: unknown) => mediaUrl(upload, fallbackText(fallback, ''))
|
||||||
|
const deprecatedPartnerDescription = 'Partner in drei Kategorien: Hauptsponsoren, Medienpartner und weitere Sponsoren. Klicken für Details.'
|
||||||
|
const visiblePartnerDescription = (value: unknown) => {
|
||||||
|
const text = fallbackText(value, netzwerkContent.partners.description)
|
||||||
|
return text.trim() === deprecatedPartnerDescription ? netzwerkContent.partners.description : text
|
||||||
|
}
|
||||||
|
const juryImageFallback = '/images/preistraeger-placeholder.svg'
|
||||||
const sortedPartners = <T extends PartnerLogoData>(partners: T[]) =>
|
const sortedPartners = <T extends PartnerLogoData>(partners: T[]) =>
|
||||||
partners
|
partners
|
||||||
.map((partner, index) => ({ partner, index }))
|
.map((partner, index) => ({ partner, index }))
|
||||||
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
|
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
|
||||||
.map(({ partner }) => partner)
|
.map(({ partner }) => partner)
|
||||||
|
const normalizeJuryMember = (member: CmsRouteDoc): JuryMember => {
|
||||||
|
const organization = fallbackText(member.organization, '')
|
||||||
|
const rawRole = fallbackText(member.role, '')
|
||||||
|
const role = rawRole && organization && rawRole !== organization ? `${rawRole} / ${organization}` : fallbackText(rawRole, organization)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: fallbackText(member.name, ''),
|
||||||
|
role,
|
||||||
|
bio: fallbackText(member.bio, fallbackText(member.quote, '')),
|
||||||
|
image: mediaUrl(member.image, juryImageFallback),
|
||||||
|
imageAlt: fallbackText(member.imageAlt, fallbackText(member.name, 'Jury-Mitglied')),
|
||||||
|
imageUpload: member.image,
|
||||||
|
organization,
|
||||||
|
quote: fallbackText(member.quote, ''),
|
||||||
|
sortOrder: typeof member.sortOrder === 'number' ? member.sortOrder : undefined,
|
||||||
|
active: member.active !== false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sortedJuryMembers = (members: JuryMember[]) =>
|
||||||
|
members
|
||||||
|
.map((member, index) => ({ member, index }))
|
||||||
|
.sort((a, b) => Number(a.member.sortOrder ?? a.index) - Number(b.member.sortOrder ?? b.index))
|
||||||
|
.map(({ member }) => member)
|
||||||
const isMembershipCta = (cta?: { label?: string | null; url?: string | null } | null) => {
|
const isMembershipCta = (cta?: { label?: string | null; url?: string | null } | null) => {
|
||||||
const normalizedLabel = cta?.label?.trim().toLowerCase();
|
const normalizedLabel = cta?.label?.trim().toLowerCase();
|
||||||
|
|
||||||
@@ -138,24 +166,30 @@ function PersonCard({ imgSrc, imgAlt, name, titleLine1, titleLine2, institution,
|
|||||||
|
|
||||||
// ── JuryCard ──────────────────────────────────────────────────────────────────
|
// ── JuryCard ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const JuryCard: React.FC<{ member: JuryMember; idx: number }> = ({ member, idx }) => {
|
const JuryCard: React.FC<{ member: JuryMember }> = ({ member }) => {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const isMobileCard = useIsMobile();
|
const isMobileCard = useIsMobile();
|
||||||
|
const quote = fallbackText(member.quote, '');
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
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={{ display: 'flex', flexDirection: 'column', height: '100%', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.04)' : NAVY, transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '58vw', maxWidth: '58vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : { minWidth: 0, border: '1px solid rgba(255,255,255,0.07)' }) }}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: '3/4' }}>
|
<div style={{ position: 'relative', overflow: 'hidden', height: isMobileCard ? 142 : 170, flexShrink: 0 }}>
|
||||||
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, 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: '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 ? '18px 18px 22px' : '20px 20px 24px', flex: 1, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
|
||||||
<div style={{ fontFamily: FF, fontSize: 20, fontWeight: 700, color: '#fff', marginBottom: 5 }}>{member.name}</div>
|
<div style={{ fontFamily: FF, fontSize: isMobileCard ? 16 : 17, fontWeight: 700, color: '#fff', lineHeight: 1.15, marginBottom: 5, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>{member.name}</div>
|
||||||
<div style={{ fontFamily: FF, fontSize: 12, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.16em', fontWeight: 700, marginBottom: 14 }}>{member.role}</div>
|
<div style={{ fontFamily: FF, fontSize: isMobileCard ? 9 : 10, color: GOLD, textTransform: 'uppercase', letterSpacing: isMobileCard ? '0.1em' : '0.14em', fontWeight: 700, lineHeight: 1.35, marginBottom: 10, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>{member.role}</div>
|
||||||
<div style={{ width: hovered ? '100%' : 24, height: 1, background: GOLD, opacity: 0.4, transition: 'width 0.4s ease', marginBottom: 14 }} />
|
<div style={{ width: hovered ? '100%' : 20, height: 1, background: GOLD, opacity: 0.4, transition: 'width 0.4s ease', marginBottom: 10 }} />
|
||||||
<div style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.55)', lineHeight: 1.7 }}>{member.bio}</div>
|
<div style={{ fontFamily: FB, fontSize: isMobileCard ? 14 : 15, color: 'rgba(255,255,255,0.55)', lineHeight: 1.55, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>{member.bio}</div>
|
||||||
|
{quote ? (
|
||||||
|
<div style={{ borderLeft: `2px solid ${GOLD}`, color: 'rgba(255,255,255,0.72)', fontFamily: FB, fontSize: isMobileCard ? 13 : 14, fontStyle: 'italic', lineHeight: 1.5, marginTop: 14, paddingLeft: 10, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
|
||||||
|
{quote}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -166,6 +200,7 @@ const JuryCard: React.FC<{ member: JuryMember; idx: number }> = ({ member, idx }
|
|||||||
const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member, badge }) => {
|
const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member, badge }) => {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const isMobileCard = useIsMobile();
|
const isMobileCard = useIsMobile();
|
||||||
|
const quote = fallbackText(member.quote, '');
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
@@ -181,7 +216,7 @@ const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ 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' }}>
|
||||||
<Image unoptimized src={managedImage(member.imageUpload, member.image)} 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={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, 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' }}>
|
||||||
@@ -192,6 +227,11 @@ const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member
|
|||||||
<div style={{ fontFamily: FF, fontSize: 13, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.18em', fontWeight: 700, marginBottom: 24 }}>{member.role}</div>
|
<div style={{ fontFamily: FF, fontSize: 13, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.18em', fontWeight: 700, marginBottom: 24 }}>{member.role}</div>
|
||||||
<div style={{ width: 48, height: 2, background: GOLD, opacity: 0.5, marginBottom: 24 }} />
|
<div style={{ width: 48, height: 2, background: GOLD, opacity: 0.5, marginBottom: 24 }} />
|
||||||
<div style={{ fontFamily: FB, fontSize: isMobileCard ? 18 : 21, color: 'rgba(255,255,255,0.6)', lineHeight: 1.7, maxWidth: 620 }}>{member.bio}</div>
|
<div style={{ fontFamily: FB, fontSize: isMobileCard ? 18 : 21, color: 'rgba(255,255,255,0.6)', lineHeight: 1.7, maxWidth: 620 }}>{member.bio}</div>
|
||||||
|
{quote ? (
|
||||||
|
<div style={{ borderLeft: `3px solid ${GOLD}`, color: 'rgba(255,255,255,0.78)', fontFamily: FB, fontSize: isMobileCard ? 17 : 19, fontStyle: 'italic', lineHeight: 1.65, marginTop: 24, maxWidth: 620, paddingLeft: 18 }}>
|
||||||
|
{quote}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -201,35 +241,35 @@ const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member
|
|||||||
|
|
||||||
// ── SponsorCell (tiered) ──────────────────────────────────────────────────────
|
// ── SponsorCell (tiered) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; detailLabel: string; premiumLabel: string; onClick: () => void }> = ({ partner, variant, idx, cols, detailLabel, premiumLabel, onClick }) => {
|
const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; detailLabel: string; onClick: () => void }> = ({ partner, variant, idx, cols, detailLabel, onClick }) => {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const isMobileCell = useIsMobile();
|
const isMobileCell = useIsMobile();
|
||||||
const lg = variant === 'lg';
|
const lg = variant === 'lg';
|
||||||
const sm = variant === 'sm';
|
const sm = variant === 'sm';
|
||||||
const lastCol = (idx % cols) === cols - 1;
|
const lastCol = (idx % cols) === cols - 1;
|
||||||
|
const logoHeight = isMobileCell ? (lg ? 76 : sm ? 54 : 62) : lg ? 92 : sm ? 62 : 76;
|
||||||
|
const logoScale = lg ? 1.55 : sm ? 1.22 : 1.38;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
style={{
|
style={{
|
||||||
padding: isMobileCell ? '24px 20px' : lg ? '40px 36px' : sm ? '28px 28px' : '32px 32px',
|
padding: isMobileCell ? '26px 20px' : lg ? '44px 38px 40px' : sm ? '30px 28px' : '36px 32px',
|
||||||
borderRight: !lastCol && !isMobileCell ? '1px solid rgba(3,9,58,0.08)' : 'none',
|
borderRight: !lastCol && !isMobileCell ? '1px solid rgba(3,9,58,0.08)' : 'none',
|
||||||
borderBottom: '1px solid rgba(3,9,58,0.08)',
|
borderBottom: '1px solid rgba(3,9,58,0.08)',
|
||||||
display: 'flex', flexDirection: 'column', gap: 14,
|
display: 'flex', flexDirection: 'column', gap: lg ? 18 : 16,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
background: hovered ? '#E8E5E4' : 'transparent',
|
background: hovered ? '#E8E5E4' : 'transparent',
|
||||||
transition: 'background 0.15s',
|
transition: 'background 0.15s',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{lg && (
|
<div style={{ height: logoHeight, display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', minWidth: 0 }}>
|
||||||
<span style={{ position: 'absolute', top: 18, right: 18, fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: '#101828', background: GOLD, padding: '4px 10px', borderRadius: 100, zIndex: 1 }}>{premiumLabel}</span>
|
<PartnerLogoMark partner={partner} scale={logoScale} />
|
||||||
)}
|
</div>
|
||||||
<div style={{ height: lg ? 52 : 44, display: 'flex', alignItems: 'center' }}><PartnerLogoMark partner={partner} /></div>
|
<div style={{ width: hovered ? 56 : 24, height: 1, background: GOLD, transition: 'width 0.3s ease' }} />
|
||||||
<div style={{ width: hovered ? 48 : 20, height: 1, background: GOLD, transition: 'width 0.3s ease' }} />
|
|
||||||
<div style={{ fontFamily: FF, fontSize: lg ? 22 : 18, fontWeight: 700, color: '#101828', letterSpacing: '-0.01em', lineHeight: 1.15 }}>{partner.name}</div>
|
<div style={{ fontFamily: FF, fontSize: lg ? 22 : 18, fontWeight: 700, color: '#101828', letterSpacing: '-0.01em', lineHeight: 1.15 }}>{partner.name}</div>
|
||||||
<div style={{ fontFamily: FF, fontSize: sm ? 10 : 11, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.2em', fontWeight: 700 }}>{partner.role}</div>
|
|
||||||
{!sm && <div style={{ fontFamily: FB, fontSize: lg ? 17 : 16, color: 'rgba(16,24,40,0.45)', lineHeight: 1.6, flexGrow: 1 }}>{partner.desc}</div>}
|
{!sm && <div style={{ fontFamily: FB, fontSize: lg ? 17 : 16, 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 }}>
|
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.12em', color: GOLD, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
{detailLabel} <ArrowRight size={10} />
|
{detailLabel} <ArrowRight size={10} />
|
||||||
@@ -241,7 +281,7 @@ const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx
|
|||||||
// ── PartnerModal ─────────────────────────────────────────────────────────────
|
// ── PartnerModal ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: PartnerModalCopy; onClose: () => void }) {
|
function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: PartnerModalCopy; onClose: () => void }) {
|
||||||
const accentColor = ROLE_COLOR[partner.role] ?? GOLD;
|
const accentColor = GOLD;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
@@ -279,21 +319,6 @@ function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: Part
|
|||||||
{/* Gradient to white */}
|
{/* Gradient to white */}
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0) 35%, rgba(255,255,255,0.6) 75%, #fff 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0) 35%, rgba(255,255,255,0.6) 75%, #fff 100%)' }} />
|
||||||
|
|
||||||
{/* Category badge – top left */}
|
|
||||||
<div style={{ position: 'absolute', top: 20, left: 24 }}>
|
|
||||||
<span style={{
|
|
||||||
background: 'rgba(255,255,255,0.15)',
|
|
||||||
backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.25)',
|
|
||||||
color: '#fff', padding: '5px 13px',
|
|
||||||
fontFamily: FF, fontSize: 9, fontWeight: 700,
|
|
||||||
textTransform: 'uppercase', letterSpacing: '0.22em',
|
|
||||||
display: 'inline-block',
|
|
||||||
}}>
|
|
||||||
{partner.role}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Social + Close – top right */}
|
{/* Social + Close – top right */}
|
||||||
<div style={{ position: 'absolute', top: 16, right: 20, display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ position: 'absolute', top: 16, right: 20, display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
{partner.linkedin && (
|
{partner.linkedin && (
|
||||||
@@ -432,9 +457,6 @@ function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: Part
|
|||||||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.12em', color: 'rgba(16,24,40,0.3)' }}>
|
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.12em', color: 'rgba(16,24,40,0.3)' }}>
|
||||||
{fallbackText(copy.footerTitle, netzwerkContent.partners.modal.footerTitle)}
|
{fallbackText(copy.footerTitle, netzwerkContent.partners.modal.footerTitle)}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: accentColor }}>
|
|
||||||
{fallbackText(copy.footerRolePrefix, netzwerkContent.partners.modal.footerRolePrefix)} {partner.role}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -451,6 +473,7 @@ const Netzwerk: React.FC = () => {
|
|||||||
const cms = (useCmsRoute()?.doc?.netzwerk || {}) as NetzwerkCms;
|
const cms = (useCmsRoute()?.doc?.netzwerk || {}) as NetzwerkCms;
|
||||||
const homePage = useCmsCollection('pages').find((page) => page.spaPath === '/' || page.slug === 'startseite' || page.slug === 'home');
|
const homePage = useCmsCollection('pages').find((page) => page.spaPath === '/' || page.slug === 'startseite' || page.slug === 'home');
|
||||||
const cmsPartners = useCmsCollection('partners') as unknown as Partner[];
|
const cmsPartners = useCmsCollection('partners') as unknown as Partner[];
|
||||||
|
const cmsJuryMembers = useCmsCollection('jury-mitglieder');
|
||||||
const home = (homePage?.home || {}) as HomeVideoCms;
|
const home = (homePage?.home || {}) as HomeVideoCms;
|
||||||
const homeVideoModal = home.videoModal || {};
|
const homeVideoModal = home.videoModal || {};
|
||||||
const hero = { ...netzwerkContent.hero, ...(cms.hero || {}) };
|
const hero = { ...netzwerkContent.hero, ...(cms.hero || {}) };
|
||||||
@@ -462,7 +485,10 @@ const Netzwerk: React.FC = () => {
|
|||||||
const partnerModal = { ...netzwerkContent.partners.modal, ...((cms.partners as typeof netzwerkContent.partners | undefined)?.modal || {}) };
|
const partnerModal = { ...netzwerkContent.partners.modal, ...((cms.partners as typeof netzwerkContent.partners | undefined)?.modal || {}) };
|
||||||
const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) };
|
const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) };
|
||||||
const patronPeople = fallbackArray<PatronPerson>(patronage.people, netzwerkContent.patronage.people);
|
const patronPeople = fallbackArray<PatronPerson>(patronage.people, netzwerkContent.patronage.people);
|
||||||
const juryMembers = fallbackArray<JuryMember>(jury.members, netzwerkContent.jury.members);
|
const juryCollectionMembers = sortedJuryMembers(cmsJuryMembers.map(normalizeJuryMember).filter((member) => member.active !== false && member.name));
|
||||||
|
const juryMembers = juryCollectionMembers.length
|
||||||
|
? juryCollectionMembers
|
||||||
|
: fallbackArray<JuryMember>(jury.members, netzwerkContent.jury.members);
|
||||||
const expertiseRows = fallbackArray<ExpertiseRow>(expertise.rows, netzwerkContent.expertise.rows);
|
const expertiseRows = fallbackArray<ExpertiseRow>(expertise.rows, netzwerkContent.expertise.rows);
|
||||||
const defaultLogoColor = fallbackText(partnerSection.defaultLogoColor, netzwerkContent.partners.defaultLogoColor);
|
const defaultLogoColor = fallbackText(partnerSection.defaultLogoColor, netzwerkContent.partners.defaultLogoColor);
|
||||||
const partnerSource = cmsPartners.length ? cmsPartners : (netzwerkContent.partners.items as Partner[]);
|
const partnerSource = cmsPartners.length ? cmsPartners : (netzwerkContent.partners.items as Partner[]);
|
||||||
@@ -686,9 +712,9 @@ const Netzwerk: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */}
|
{/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */}
|
||||||
<div style={{ display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(3, 1fr)', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 14 : 0, padding: isMobile ? '24px 24px 12px' : '0', WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', borderTop: isMobile ? 'none' : '1px solid rgba(255,255,255,0.07)' }}>
|
<div style={{ display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(auto-fill, minmax(210px, 1fr))', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 12 : 0, padding: isMobile ? '24px 24px 12px' : '0', WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', borderTop: isMobile ? 'none' : '1px solid rgba(255,255,255,0.07)' }}>
|
||||||
{juryMembers.slice(1).map((member, idx) => (
|
{juryMembers.slice(1).map((member, idx) => (
|
||||||
<JuryCard key={idx} member={member} idx={idx} />
|
<JuryCard key={idx} member={member} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -741,7 +767,7 @@ const Netzwerk: React.FC = () => {
|
|||||||
<h2 style={{ fontFamily: FF, fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', fontSize: 'clamp(2rem, 3.5vw, 3rem)', lineHeight: 0.95, margin: 0 }}><Lines text={fallbackText(partnerSection.heading, netzwerkContent.partners.heading)} /></h2>
|
<h2 style={{ fontFamily: FF, fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', fontSize: 'clamp(2rem, 3.5vw, 3rem)', lineHeight: 0.95, margin: 0 }}><Lines text={fallbackText(partnerSection.heading, netzwerkContent.partners.heading)} /></h2>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.4)', maxWidth: isMobile ? '100%' : 220, textAlign: isMobile ? 'left' : 'right', lineHeight: 1.6, margin: 0 }}>
|
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.4)', maxWidth: isMobile ? '100%' : 220, textAlign: isMobile ? 'left' : 'right', lineHeight: 1.6, margin: 0 }}>
|
||||||
{fallbackText(partnerSection.description, netzwerkContent.partners.description)}
|
{visiblePartnerDescription(partnerSection.description)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -755,7 +781,7 @@ const Netzwerk: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={t.tier} style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)` }}>
|
<div key={t.tier} style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)` }}>
|
||||||
{items.map((p, i) => (
|
{items.map((p, i) => (
|
||||||
<SponsorCell key={p.id} partner={p} variant={variant} idx={i} cols={cols} detailLabel={fallbackText(partnerSection.detailLabel, netzwerkContent.partners.detailLabel)} premiumLabel={fallbackText(partnerSection.premiumLabel, netzwerkContent.partners.premiumLabel)} onClick={() => setSelectedPartner(p)} />
|
<SponsorCell key={p.id} partner={p} variant={variant} idx={i} cols={cols} detailLabel={fallbackText(partnerSection.detailLabel, netzwerkContent.partners.detailLabel)} onClick={() => setSelectedPartner(p)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Link } from '@/spa/router';
|
import { Link } from '@/spa/router';
|
||||||
import { Trophy, ArrowRight } from 'lucide-react';
|
import { Trophy, ArrowRight, Medal } from 'lucide-react';
|
||||||
import { WINNERS, type Winner } from '@/spa/data/winners';
|
import { WINNERS, type Winner } from '@/spa/data/winners';
|
||||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
@@ -16,6 +16,8 @@ const BORDER = '#D0D5DD';
|
|||||||
const GRAY = '#666666';
|
const GRAY = '#666666';
|
||||||
const BG_ALT = '#E4E2E3';
|
const BG_ALT = '#E4E2E3';
|
||||||
const SIMPLE_LIST_YEAR = 2023;
|
const SIMPLE_LIST_YEAR = 2023;
|
||||||
|
const GOLDENE_BAVARIA_GROUP = 'goldene-bavaria';
|
||||||
|
const PREISTRAEGER_GROUP = 'preistraeger';
|
||||||
|
|
||||||
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
|
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
|
||||||
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
|
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
|
||||||
@@ -23,6 +25,60 @@ type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
|
const optionalNumber = (value: unknown) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
|
};
|
||||||
|
const positiveNumber = (value: unknown, fallback: number) => {
|
||||||
|
const parsed = optionalNumber(value);
|
||||||
|
return parsed && parsed > 0 ? Math.floor(parsed) : fallback;
|
||||||
|
};
|
||||||
|
const normalizeAwardGroup = (value: unknown): Winner['awardGroup'] =>
|
||||||
|
value === GOLDENE_BAVARIA_GROUP || value === PREISTRAEGER_GROUP ? value : undefined;
|
||||||
|
|
||||||
|
const isGoldeneBavariaWinner = (winner: Winner) =>
|
||||||
|
winner.awardGroup === GOLDENE_BAVARIA_GROUP || winner.type.toLowerCase().includes('goldene bavaria');
|
||||||
|
|
||||||
|
const sortWinnersForDisplay = (winners: Winner[]) =>
|
||||||
|
winners
|
||||||
|
.map((winner, index) => ({ winner, index }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const rankDiff = (a.winner.winnerRank ?? Number.POSITIVE_INFINITY) - (b.winner.winnerRank ?? Number.POSITIVE_INFINITY);
|
||||||
|
if (rankDiff !== 0) return rankDiff;
|
||||||
|
|
||||||
|
const sortDiff = (a.winner.sortOrder ?? Number.POSITIVE_INFINITY) - (b.winner.sortOrder ?? Number.POSITIVE_INFINITY);
|
||||||
|
if (sortDiff !== 0) return sortDiff;
|
||||||
|
|
||||||
|
return a.index - b.index;
|
||||||
|
})
|
||||||
|
.map(({ winner }) => winner);
|
||||||
|
|
||||||
|
const splitWinnersForYear = (winners: Winner[], totalWinners: number, goldeneBavariaCount: number) => {
|
||||||
|
const visibleWinners = sortWinnersForDisplay(winners.filter((winner) => winner.showOnIndex !== false));
|
||||||
|
const maxGoldeneBavaria = Math.min(goldeneBavariaCount, totalWinners);
|
||||||
|
const goldeneBavariaWinners = visibleWinners
|
||||||
|
.filter((winner) => isGoldeneBavariaWinner(winner) || (winner.winnerRank || Number.POSITIVE_INFINITY) <= maxGoldeneBavaria)
|
||||||
|
.slice(0, maxGoldeneBavaria);
|
||||||
|
const goldIds = new Set(goldeneBavariaWinners.map((winner) => winner.id));
|
||||||
|
|
||||||
|
for (const winner of visibleWinners) {
|
||||||
|
if (goldeneBavariaWinners.length >= maxGoldeneBavaria) break;
|
||||||
|
if (goldIds.has(winner.id) || winner.awardGroup === PREISTRAEGER_GROUP) continue;
|
||||||
|
|
||||||
|
goldeneBavariaWinners.push(winner);
|
||||||
|
goldIds.add(winner.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingWinners = visibleWinners
|
||||||
|
.filter((winner) => !goldIds.has(winner.id))
|
||||||
|
.slice(0, Math.max(totalWinners - goldeneBavariaWinners.length, 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
goldeneBavariaWinners,
|
||||||
|
remainingWinners,
|
||||||
|
displayedWinners: [...goldeneBavariaWinners, ...remainingWinners],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeWinner = (
|
const normalizeWinner = (
|
||||||
doc: CmsRouteDoc,
|
doc: CmsRouteDoc,
|
||||||
@@ -34,7 +90,15 @@ const normalizeWinner = (
|
|||||||
name: String(doc.title || fallbackWinner.name || preistraegerIndexContent.card.fallbackWinner.name),
|
name: String(doc.title || fallbackWinner.name || preistraegerIndexContent.card.fallbackWinner.name),
|
||||||
category: String(doc.category || fallbackWinner.category || preistraegerIndexContent.card.fallbackWinner.category),
|
category: String(doc.category || fallbackWinner.category || preistraegerIndexContent.card.fallbackWinner.category),
|
||||||
year: Number(doc.year || new Date().getFullYear()),
|
year: Number(doc.year || new Date().getFullYear()),
|
||||||
type: String(doc.awardType || fallbackWinner.awardType || preistraegerIndexContent.card.fallbackWinner.awardType),
|
type: String(
|
||||||
|
doc.awardType ||
|
||||||
|
(normalizeAwardGroup(doc.awardGroup) === GOLDENE_BAVARIA_GROUP ? 'Goldene Bavaria' : fallbackWinner.awardType) ||
|
||||||
|
preistraegerIndexContent.card.fallbackWinner.awardType,
|
||||||
|
),
|
||||||
|
awardGroup: normalizeAwardGroup(doc.awardGroup),
|
||||||
|
winnerRank: optionalNumber(doc.winnerRank),
|
||||||
|
sortOrder: optionalNumber(doc.sortOrder),
|
||||||
|
showOnIndex: doc.showOnIndex !== false,
|
||||||
img: docImageUrl(doc, placeholderImage),
|
img: docImageUrl(doc, placeholderImage),
|
||||||
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
|
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
|
||||||
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
|
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
|
||||||
@@ -55,9 +119,17 @@ export default function Preistraeger() {
|
|||||||
const hero = { ...preistraegerIndexContent.hero, ...(cms.hero || {}) };
|
const hero = { ...preistraegerIndexContent.hero, ...(cms.hero || {}) };
|
||||||
const breadcrumb = { ...preistraegerIndexContent.breadcrumb, ...(cms.breadcrumb || {}) };
|
const breadcrumb = { ...preistraegerIndexContent.breadcrumb, ...(cms.breadcrumb || {}) };
|
||||||
const count = { ...preistraegerIndexContent.count, ...(cms.count || {}) };
|
const count = { ...preistraegerIndexContent.count, ...(cms.count || {}) };
|
||||||
|
const rules = { ...preistraegerIndexContent.rules, ...(cms.rules || {}) };
|
||||||
const empty = { ...preistraegerIndexContent.empty, ...(cms.empty || {}) };
|
const empty = { ...preistraegerIndexContent.empty, ...(cms.empty || {}) };
|
||||||
|
const goldeneBavaria = { ...preistraegerIndexContent.goldeneBavaria, ...(cms.goldeneBavaria || {}) };
|
||||||
|
const winnersSection = { ...preistraegerIndexContent.winners, ...(cms.winners || {}) };
|
||||||
const card = useMemo(() => ({ ...preistraegerIndexContent.card, ...(cms.card || {}) }), [cms.card]);
|
const card = useMemo(() => ({ ...preistraegerIndexContent.card, ...(cms.card || {}) }), [cms.card]);
|
||||||
const cta = { ...preistraegerIndexContent.cta, ...(cms.cta || {}) };
|
const cta = { ...preistraegerIndexContent.cta, ...(cms.cta || {}) };
|
||||||
|
const totalWinners = positiveNumber(rules.totalWinners, preistraegerIndexContent.rules.totalWinners);
|
||||||
|
const goldeneBavariaCount = Math.min(
|
||||||
|
positiveNumber(rules.goldeneBavariaCount, preistraegerIndexContent.rules.goldeneBavariaCount),
|
||||||
|
totalWinners,
|
||||||
|
);
|
||||||
const winners = useMemo(() => {
|
const winners = useMemo(() => {
|
||||||
const fallbackWinner = card.fallbackWinner || preistraegerIndexContent.card.fallbackWinner;
|
const fallbackWinner = card.fallbackWinner || preistraegerIndexContent.card.fallbackWinner;
|
||||||
if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug);
|
if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug);
|
||||||
@@ -68,6 +140,10 @@ export default function Preistraeger() {
|
|||||||
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
|
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
|
||||||
|
|
||||||
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]);
|
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]);
|
||||||
|
const winnerGroups = useMemo(
|
||||||
|
() => splitWinnersForYear(filtered, totalWinners, goldeneBavariaCount),
|
||||||
|
[filtered, goldeneBavariaCount, totalWinners],
|
||||||
|
);
|
||||||
const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR;
|
const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -120,17 +196,29 @@ export default function Preistraeger() {
|
|||||||
<div style={{ padding: isMobile ? '12px 24px' : '12px 80px', background: BG_ALT, borderTop: `1px solid ${BORDER}`, borderBottom: `1px solid ${BORDER}`, display: 'flex', alignItems: 'center', gap: 12 }}>
|
<div style={{ padding: isMobile ? '12px 24px' : '12px 80px', background: BG_ALT, borderTop: `1px solid ${BORDER}`, borderBottom: `1px solid ${BORDER}`, display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
<Trophy size={13} style={{ color: GOLD }} fill={GOLD} />
|
<Trophy size={13} style={{ color: GOLD }} fill={GOLD} />
|
||||||
<span style={{ fontFamily: FF, fontSize: 15, color: GRAY }}>
|
<span style={{ fontFamily: FF, fontSize: 15, color: GRAY }}>
|
||||||
<strong style={{ color: '#101828' }}>{selectedYear}</strong> – {filtered.length} {fallbackText(count.label, preistraegerIndexContent.count.label)}
|
<strong style={{ color: '#101828' }}>{selectedYear}</strong> – {winnerGroups.displayedWinners.length} {fallbackText(count.label, preistraegerIndexContent.count.label)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid */}
|
{winnerGroups.displayedWinners.length === 0 ? (
|
||||||
{filtered.length === 0 ? (
|
|
||||||
<div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}>
|
<div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}>
|
||||||
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
|
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
|
||||||
</div>
|
</div>
|
||||||
) : usesSimpleWinnerGrid ? (
|
) : (
|
||||||
<SimpleWinnerGrid winners={filtered} />
|
<>
|
||||||
|
{winnerGroups.goldeneBavariaWinners.length > 0 ? (
|
||||||
|
<GoldeneBavariaSection
|
||||||
|
copy={goldeneBavaria}
|
||||||
|
hoverLabel={fallbackText(card.hoverLabel, preistraegerIndexContent.card.hoverLabel)}
|
||||||
|
winners={winnerGroups.goldeneBavariaWinners}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{winnerGroups.remainingWinners.length > 0 ? (
|
||||||
|
<section style={{ background: '#fff' }}>
|
||||||
|
<SectionIntro copy={winnersSection} isMobile={isMobile} />
|
||||||
|
{usesSimpleWinnerGrid ? (
|
||||||
|
<SimpleWinnerGrid winners={winnerGroups.remainingWinners} />
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -145,11 +233,15 @@ export default function Preistraeger() {
|
|||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
}}>
|
}}>
|
||||||
{filtered.map(w => (
|
{winnerGroups.remainingWinners.map(w => (
|
||||||
<WinnerCard key={w.id} winner={w} hoverLabel={fallbackText(card.hoverLabel, preistraegerIndexContent.card.hoverLabel)} />
|
<WinnerCard key={w.id} winner={w} hoverLabel={fallbackText(card.hoverLabel, preistraegerIndexContent.card.hoverLabel)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Bottom CTA */}
|
{/* Bottom CTA */}
|
||||||
<div style={{ padding: isMobile ? '48px 24px' : '64px 80px', borderTop: `1px solid ${BORDER}`, textAlign: 'center' }}>
|
<div style={{ padding: isMobile ? '48px 24px' : '64px 80px', borderTop: `1px solid ${BORDER}`, textAlign: 'center' }}>
|
||||||
@@ -172,6 +264,101 @@ export default function Preistraeger() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SectionIntro({ copy, isMobile }: { copy: typeof preistraegerIndexContent.winners; isMobile: boolean }) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: isMobile ? '40px 24px 22px' : '56px 80px 28px', maxWidth: 900 }}>
|
||||||
|
<span style={{ color: GOLD, fontFamily: FF, fontSize: 12, fontWeight: 800, textTransform: 'uppercase', letterSpacing: 0 }}>
|
||||||
|
{fallbackText(copy.eyebrow, preistraegerIndexContent.winners.eyebrow)}
|
||||||
|
</span>
|
||||||
|
<h2 style={{ color: NAVY, fontFamily: FF, fontSize: isMobile ? 28 : 42, fontWeight: 900, lineHeight: 1.05, margin: '10px 0 12px', textTransform: 'uppercase', letterSpacing: 0 }}>
|
||||||
|
{fallbackText(copy.heading, preistraegerIndexContent.winners.heading)}
|
||||||
|
</h2>
|
||||||
|
<p style={{ color: GRAY, fontFamily: FF, fontSize: 17, lineHeight: 1.6, margin: 0, maxWidth: 680 }}>
|
||||||
|
{fallbackText(copy.description, preistraegerIndexContent.winners.description)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoldeneBavariaSection({
|
||||||
|
copy,
|
||||||
|
hoverLabel,
|
||||||
|
winners,
|
||||||
|
}: {
|
||||||
|
copy: typeof preistraegerIndexContent.goldeneBavaria;
|
||||||
|
hoverLabel: string;
|
||||||
|
winners: Winner[];
|
||||||
|
}) {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section style={{ background: NAVY, color: '#fff', borderTop: `1px solid ${BORDER}` }}>
|
||||||
|
<div style={{ padding: isMobile ? '44px 24px 26px' : '64px 80px 34px', maxWidth: 980 }}>
|
||||||
|
<span style={{ color: GOLD, fontFamily: FF, fontSize: 12, fontWeight: 800, textTransform: 'uppercase', letterSpacing: 0 }}>
|
||||||
|
{fallbackText(copy.eyebrow, preistraegerIndexContent.goldeneBavaria.eyebrow)}
|
||||||
|
</span>
|
||||||
|
<h2 style={{ color: '#fff', fontFamily: FF, fontSize: isMobile ? 30 : 48, fontWeight: 900, lineHeight: 1.02, margin: '10px 0 14px', textTransform: 'uppercase', letterSpacing: 0 }}>
|
||||||
|
{fallbackText(copy.heading, preistraegerIndexContent.goldeneBavaria.heading)}
|
||||||
|
</h2>
|
||||||
|
<p style={{ color: 'rgba(255,255,255,0.74)', fontFamily: FF, fontSize: 18, lineHeight: 1.6, margin: 0, maxWidth: 760 }}>
|
||||||
|
{fallbackText(copy.description, preistraegerIndexContent.goldeneBavaria.description)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))',
|
||||||
|
gap: isMobile ? 14 : 16,
|
||||||
|
padding: isMobile ? '0 24px 48px' : '0 80px 72px',
|
||||||
|
}}>
|
||||||
|
{winners.map((winner, index) => (
|
||||||
|
<GoldeneBavariaCard key={winner.id} winner={winner} rank={winner.winnerRank || index + 1} hoverLabel={hoverLabel} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoldeneBavariaCard({ winner, rank, hoverLabel }: { winner: Winner; rank: number; hoverLabel: string }) {
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/preistraeger/${winner.slug}`}
|
||||||
|
style={{ background: '#fff', color: '#101828', display: 'block', minHeight: isMobile ? 0 : 430, overflow: 'hidden', position: 'relative', textDecoration: 'none' }}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
>
|
||||||
|
<div style={{ height: isMobile ? 250 : 300, overflow: 'hidden', position: 'relative' }}>
|
||||||
|
<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.05)' : 'scale(1)' }}
|
||||||
|
/>
|
||||||
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.62), transparent 62%)' }} />
|
||||||
|
<div style={{ position: 'absolute', left: 18, bottom: 18, display: 'inline-flex', alignItems: 'center', gap: 8, background: GOLD, color: NAVY, padding: '8px 12px', fontFamily: FF, fontSize: 13, fontWeight: 900, textTransform: 'uppercase', letterSpacing: 0 }}>
|
||||||
|
<Medal size={16} /> #{rank} Goldene Bavaria
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: isMobile ? '20px' : '24px' }}>
|
||||||
|
<span style={{ color: GOLD, fontFamily: FF, fontSize: 12, fontWeight: 800, textTransform: 'uppercase', letterSpacing: 0 }}>
|
||||||
|
{winner.year} · {winner.category}
|
||||||
|
</span>
|
||||||
|
<h3 style={{ color: NAVY, fontFamily: FF, fontSize: isMobile ? 23 : 26, fontWeight: 900, lineHeight: 1.12, margin: '10px 0 10px', overflowWrap: 'anywhere' }}>
|
||||||
|
{winner.name}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: GRAY, fontFamily: FF, fontSize: 15, lineHeight: 1.55, margin: 0, overflowWrap: 'anywhere' }}>
|
||||||
|
{winner.shortDesc}
|
||||||
|
</p>
|
||||||
|
<span style={{ marginTop: 18, display: 'inline-flex', alignItems: 'center', gap: 7, color: NAVY, fontFamily: FF, fontSize: 14, fontWeight: 800, textTransform: 'uppercase', letterSpacing: 0 }}>
|
||||||
|
<Trophy size={14} /> {hoverLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── WinnerCard ───────────────────────────────────────────────────────────────
|
// ── WinnerCard ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string }) {
|
function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string }) {
|
||||||
|
|||||||
@@ -54,13 +54,14 @@ export default function PreistraegerDetail() {
|
|||||||
...((cmsWinner?.detailPage || {}) as PreistraegerDetailCms),
|
...((cmsWinner?.detailPage || {}) as PreistraegerDetailCms),
|
||||||
} as PreistraegerDetailCms;
|
} as PreistraegerDetailCms;
|
||||||
const fallbackWinner = cmsDetail.fallbackWinner || preistraegerDetailContent.fallbackWinner;
|
const fallbackWinner = cmsDetail.fallbackWinner || preistraegerDetailContent.fallbackWinner;
|
||||||
|
const cmsWinnerIsGoldeneBavaria = cmsWinner?.awardGroup === 'goldene-bavaria';
|
||||||
const winner = cmsWinner ? {
|
const winner = cmsWinner ? {
|
||||||
id: String(cmsWinner.id),
|
id: String(cmsWinner.id),
|
||||||
slug: String(cmsWinner.slug || slug),
|
slug: String(cmsWinner.slug || slug),
|
||||||
name: String(cmsWinner.title || fallbackWinner.name || preistraegerDetailContent.fallbackWinner.name),
|
name: String(cmsWinner.title || fallbackWinner.name || preistraegerDetailContent.fallbackWinner.name),
|
||||||
category: String(cmsWinner.category || fallbackWinner.category || preistraegerDetailContent.fallbackWinner.category),
|
category: String(cmsWinner.category || fallbackWinner.category || preistraegerDetailContent.fallbackWinner.category),
|
||||||
year: Number(cmsWinner.year || new Date().getFullYear()),
|
year: Number(cmsWinner.year || new Date().getFullYear()),
|
||||||
type: String(cmsWinner.awardType || fallbackWinner.awardType || preistraegerDetailContent.fallbackWinner.awardType),
|
type: String(cmsWinner.awardType || (cmsWinnerIsGoldeneBavaria ? 'Goldene Bavaria' : fallbackWinner.awardType) || preistraegerDetailContent.fallbackWinner.awardType),
|
||||||
img: docImageUrl(cmsWinner, placeholderImage),
|
img: docImageUrl(cmsWinner, placeholderImage),
|
||||||
shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''),
|
shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''),
|
||||||
longDesc: String(cmsWinner.longDesc || cmsWinner.description || cmsWinner.meta?.description || fallbackWinner.longDesc || preistraegerDetailContent.fallbackWinner.longDesc),
|
longDesc: String(cmsWinner.longDesc || cmsWinner.description || cmsWinner.meta?.description || fallbackWinner.longDesc || preistraegerDetailContent.fallbackWinner.longDesc),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Link } from '@/spa/router';
|
import { Calendar, MapPin } from 'lucide-react';
|
||||||
import { Calendar, MapPin, Download, ArrowRight, FileText } 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 { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||||
@@ -75,20 +74,17 @@ function Lines({ text }: { text: string }) {
|
|||||||
|
|
||||||
const fallbackCardImage = (item: PressEventItem | PressNewsItem) => mediaUrl(item.image, fallbackText(item.img, ''))
|
const fallbackCardImage = (item: PressEventItem | PressNewsItem) => mediaUrl(item.image, fallbackText(item.img, ''))
|
||||||
|
|
||||||
function NewsCard({ item, idx, isMobile, readMoreLabel, total }: { item: PressNewsItem; idx: number; isMobile: boolean; readMoreLabel: string; total: number }) {
|
function NewsCard({ item, idx, isMobile, total }: { item: PressNewsItem; idx: number; isMobile: boolean; total: number }) {
|
||||||
const [hovered, setHovered] = React.useState(false);
|
const [hovered, setHovered] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
<Link
|
<article
|
||||||
to={item.slug}
|
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
cursor: 'pointer',
|
|
||||||
borderLeft: isMobile ? 'none' : (idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none'),
|
borderLeft: isMobile ? 'none' : (idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none'),
|
||||||
borderBottom: isMobile && idx < total - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
|
borderBottom: isMobile && idx < total - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
|
||||||
textDecoration: 'none',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* image */}
|
{/* image */}
|
||||||
@@ -166,54 +162,8 @@ function NewsCard({ item, idx, isMobile, readMoreLabel, total }: { item: PressNe
|
|||||||
>
|
>
|
||||||
{item.excerpt}
|
{item.excerpt}
|
||||||
</p>
|
</p>
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.12em',
|
|
||||||
color: GOLD,
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{readMoreLabel} <ArrowRight size={12} />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</article>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +174,6 @@ const Press: React.FC = () => {
|
|||||||
const eventsSection = React.useMemo(() => ({ ...pressIndexContent.events, ...(cms.events || {}) }), [cms.events]);
|
const eventsSection = React.useMemo(() => ({ ...pressIndexContent.events, ...(cms.events || {}) }), [cms.events]);
|
||||||
const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]);
|
const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]);
|
||||||
const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]);
|
const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]);
|
||||||
const accreditation = React.useMemo(() => ({ ...pressIndexContent.accreditation, ...(cms.accreditation || {}) }), [cms.accreditation]);
|
|
||||||
const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n');
|
const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n');
|
||||||
const cmsEvents = useCmsCollection('events');
|
const cmsEvents = useCmsCollection('events');
|
||||||
const cmsPosts = useCmsCollection('posts');
|
const cmsPosts = useCmsCollection('posts');
|
||||||
@@ -248,22 +197,6 @@ const Press: React.FC = () => {
|
|||||||
if (cmsPosts.length) return cmsPosts.map((post) => postFromCms(post, newsCollectionFallbackImage, newsSection));
|
if (cmsPosts.length) return cmsPosts.map((post) => postFromCms(post, newsCollectionFallbackImage, newsSection));
|
||||||
return fallbackArray<PressNewsItem>(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
|
return fallbackArray<PressNewsItem>(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
|
||||||
}, [cmsPosts, newsCollectionFallbackImage, newsSection]);
|
}, [cmsPosts, newsCollectionFallbackImage, newsSection]);
|
||||||
const [hoveredEvent, setHoveredEvent] = useState<number | null>(null);
|
|
||||||
const [dlHovered, setDlHovered] = useState(false);
|
|
||||||
const [submitHovered, setSubmitHovered] = useState(false);
|
|
||||||
|
|
||||||
// Form state
|
|
||||||
const [medium, setMedium] = useState('');
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [submitted, setSubmitted] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (medium.trim() && name.trim() && email.trim()) {
|
|
||||||
setSubmitted(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
|
|
||||||
@@ -411,15 +344,11 @@ const Press: React.FC = () => {
|
|||||||
{combinedEvents.map((event, idx) => (
|
{combinedEvents.map((event, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
onMouseEnter={() => setHoveredEvent(idx)}
|
|
||||||
onMouseLeave={() => setHoveredEvent(null)}
|
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: isMobile ? '1fr' : '320px 1fr',
|
gridTemplateColumns: isMobile ? '1fr' : '320px 1fr',
|
||||||
borderBottom: '1px solid rgba(3,9,58,0.08)',
|
borderBottom: '1px solid rgba(3,9,58,0.08)',
|
||||||
background: hoveredEvent === idx ? '#EAE7E6' : 'transparent',
|
background: 'transparent',
|
||||||
transition: 'background 0.15s',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Left – image cell */}
|
{/* Left – image cell */}
|
||||||
@@ -444,9 +373,6 @@ const Press: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ width: 24, height: 1, background: 'rgba(239,191,4,0.4)', marginBottom: 14 }} />
|
<div style={{ width: 24, height: 1, background: 'rgba(239,191,4,0.4)', marginBottom: 14 }} />
|
||||||
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, flex: 1 }}>{event.desc}</p>
|
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, flex: 1 }}>{event.desc}</p>
|
||||||
<Link to={event.slug} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 16, fontFamily: FF, fontSize: 16, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', color: '#101828', textDecoration: 'none' }}>
|
|
||||||
{fallbackText(eventsSection.detailLabel, pressIndexContent.events.detailLabel)} <ArrowRight size={12} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -511,7 +437,7 @@ const Press: React.FC = () => {
|
|||||||
{/* 3-column news grid */}
|
{/* 3-column news grid */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)' }}>
|
||||||
{combinedNews.map((item, idx) => (
|
{combinedNews.map((item, idx) => (
|
||||||
<NewsCard key={idx} item={item} idx={idx} isMobile={isMobile} readMoreLabel={fallbackText(newsSection.readMoreLabel, pressIndexContent.news.readMoreLabel)} total={combinedNews.length} />
|
<NewsCard key={idx} item={item} idx={idx} isMobile={isMobile} total={combinedNews.length} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -519,7 +445,7 @@ const Press: React.FC = () => {
|
|||||||
{/* ── 4. PRESSE-MATERIAL & AKKREDITIERUNG ─────────────────────────── */}
|
{/* ── 4. PRESSE-MATERIAL & AKKREDITIERUNG ─────────────────────────── */}
|
||||||
<section id="downloads" style={{ background: CREAM, overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
<section id="downloads" style={{ background: CREAM, overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||||||
<MunichSkylineBg />
|
<MunichSkylineBg />
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 600 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr', minHeight: isMobile ? 'auto' : 600 }}>
|
||||||
|
|
||||||
{/* Left col */}
|
{/* Left col */}
|
||||||
<div
|
<div
|
||||||
@@ -529,8 +455,6 @@ const Press: React.FC = () => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRight: isMobile ? 'none' : '1px solid rgba(3,9,58,0.1)',
|
|
||||||
borderBottom: isMobile ? '1px solid rgba(3,9,58,0.1)' : 'none',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -571,62 +495,12 @@ const Press: React.FC = () => {
|
|||||||
color: 'rgba(16,24,40,0.5)',
|
color: 'rgba(16,24,40,0.5)',
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
maxWidth: 380,
|
maxWidth: 380,
|
||||||
marginBottom: 48,
|
marginBottom: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fallbackText(downloads.description, pressIndexContent.downloads.description)}
|
{fallbackText(downloads.description, pressIndexContent.downloads.description)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Download button */}
|
|
||||||
<a
|
|
||||||
href={mediaUrl(downloads.kitFile, fallbackText(downloads.kitUrl, pressIndexContent.downloads.kitUrl))}
|
|
||||||
download={fallbackText(downloads.kitDownloadName, pressIndexContent.downloads.kitDownloadName)}
|
|
||||||
onMouseEnter={() => setDlHovered(true)}
|
|
||||||
onMouseLeave={() => setDlHovered(false)}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 20,
|
|
||||||
padding: '24px 28px',
|
|
||||||
border: `1px solid ${dlHovered ? GOLD : 'rgba(3,9,58,0.12)'}`,
|
|
||||||
background: '#fff',
|
|
||||||
cursor: 'pointer',
|
|
||||||
marginBottom: 20,
|
|
||||||
transition: 'border-color 0.15s',
|
|
||||||
textDecoration: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
background: NAVY,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Download size={20} color={GOLD} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: '#101828',
|
|
||||||
marginBottom: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(downloads.kitLabel, pressIndexContent.downloads.kitLabel)}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontFamily: FB, fontSize: 17, color: 'rgba(16,24,40,0.4)' }}>
|
|
||||||
{fallbackText(downloads.kitMeta, pressIndexContent.downloads.kitMeta)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Press contact block */}
|
{/* Press contact block */}
|
||||||
<div style={{ borderLeft: `3px solid ${GOLD}`, paddingLeft: 24, marginTop: 32 }}>
|
<div style={{ borderLeft: `3px solid ${GOLD}`, paddingLeft: 24, marginTop: 32 }}>
|
||||||
<div
|
<div
|
||||||
@@ -671,204 +545,6 @@ const Press: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right col */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: NAVY,
|
|
||||||
padding: isMobile ? '48px 24px' : '72px 64px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{submitted ? (
|
|
||||||
/* Success state */
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
background: GOLD,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginBottom: 28,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FileText size={24} color={NAVY} />
|
|
||||||
</div>
|
|
||||||
<h3
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 'clamp(1.4rem, 2.2vw, 1.8rem)',
|
|
||||||
fontWeight: 900,
|
|
||||||
color: '#fff',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '-0.02em',
|
|
||||||
margin: '0 0 16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.successHeading, pressIndexContent.accreditation.successHeading)}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontFamily: FB,
|
|
||||||
fontSize: 18,
|
|
||||||
color: 'rgba(255,255,255,0.45)',
|
|
||||||
lineHeight: 1.8,
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.successMessage, pressIndexContent.accreditation.successMessage)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 10,
|
|
||||||
color: GOLD,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.32em',
|
|
||||||
fontWeight: 700,
|
|
||||||
marginBottom: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.eyebrow, pressIndexContent.accreditation.eyebrow)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 'clamp(1.4rem, 2.2vw, 1.8rem)',
|
|
||||||
fontWeight: 900,
|
|
||||||
color: '#fff',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '-0.02em',
|
|
||||||
margin: '0 0 16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Lines text={fallbackText(accreditation.heading, pressIndexContent.accreditation.heading)} />
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Gold divider */}
|
|
||||||
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
|
|
||||||
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontFamily: FB,
|
|
||||||
fontSize: 18,
|
|
||||||
color: 'rgba(255,255,255,0.45)',
|
|
||||||
lineHeight: 1.8,
|
|
||||||
marginBottom: 36,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.description, pressIndexContent.accreditation.description)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Form fields */}
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.2em',
|
|
||||||
color: 'rgba(255,255,255,0.35)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.mediumLabel, pressIndexContent.accreditation.mediumLabel)}
|
|
||||||
</label>
|
|
||||||
<DarkInput
|
|
||||||
type="text"
|
|
||||||
value={medium}
|
|
||||||
onChange={(e) => setMedium(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.2em',
|
|
||||||
color: 'rgba(255,255,255,0.35)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.nameLabel, pressIndexContent.accreditation.nameLabel)}
|
|
||||||
</label>
|
|
||||||
<DarkInput
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.2em',
|
|
||||||
color: 'rgba(255,255,255,0.35)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.emailLabel, pressIndexContent.accreditation.emailLabel)}
|
|
||||||
</label>
|
|
||||||
<DarkInput
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit button */}
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
onMouseEnter={() => setSubmitHovered(true)}
|
|
||||||
onMouseLeave={() => setSubmitHovered(false)}
|
|
||||||
style={{
|
|
||||||
background: submitHovered ? '#FFD130' : GOLD,
|
|
||||||
boxShadow: submitHovered ? '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)' : 'none',
|
|
||||||
color: '#101828',
|
|
||||||
border: 'none',
|
|
||||||
fontFamily: FF,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 700,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.12em',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '14px 28px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 8,
|
|
||||||
width: '100%',
|
|
||||||
transition: 'background 0.15s, box-shadow 0.2s',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fallbackText(accreditation.submitLabel, pressIndexContent.accreditation.submitLabel)} <ArrowRight size={13} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,11 +7,27 @@ export const preistraegerIndexContent = {
|
|||||||
label: 'Preisträger:innen',
|
label: 'Preisträger:innen',
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
label: 'Preisträger:innen',
|
label: 'Gewinner:innen',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
totalWinners: 15,
|
||||||
|
goldeneBavariaCount: 3,
|
||||||
},
|
},
|
||||||
empty: {
|
empty: {
|
||||||
message: 'Keine Preisträger für diese Auswahl.',
|
message: 'Keine Preisträger für diese Auswahl.',
|
||||||
},
|
},
|
||||||
|
goldeneBavaria: {
|
||||||
|
eyebrow: 'Goldene Bavaria',
|
||||||
|
heading: 'Die drei Besten des Jahrgangs',
|
||||||
|
description:
|
||||||
|
'Aus den 15 Gewinnerinnen und Gewinnern erhalten die drei bestplatzierten Unternehmen die Goldene Bavaria.',
|
||||||
|
emptyMessage: 'Für diesen Jahrgang sind noch keine Goldene-Bavaria-Gewinner markiert.',
|
||||||
|
},
|
||||||
|
winners: {
|
||||||
|
eyebrow: 'Preisträger:innen',
|
||||||
|
heading: 'Weitere Gewinnerinnen und Gewinner',
|
||||||
|
description: 'Der Bayerische Mittelstandspreis zeichnet pro Jahrgang insgesamt 15 Gewinner aus.',
|
||||||
|
},
|
||||||
card: {
|
card: {
|
||||||
fallbackImageFilename: 'gala-saal-overview.jpg',
|
fallbackImageFilename: 'gala-saal-overview.jpg',
|
||||||
hoverLabel: 'Mehr erfahren',
|
hoverLabel: 'Mehr erfahren',
|
||||||
|
|||||||
2
src/spa/testimonialsVisibility.ts
Normal file
2
src/spa/testimonialsVisibility.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Temporary global switch: keep CMS testimonial data intact while hiding sections.
|
||||||
|
export const SHOW_TESTIMONIALS: boolean = false
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user