feat: manage preistraeger index via payload
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
"preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts",
|
"preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts",
|
||||||
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",
|
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",
|
||||||
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
|
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
|
||||||
|
"preload:preistraeger-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-preistraeger-index-page-cms.ts",
|
||||||
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
|
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
|
||||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
||||||
"test": "pnpm run test:int && pnpm run test:e2e",
|
"test": "pnpm run test:int && pnpm run test:e2e",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { datenschutzFields } from './datenschutzFields'
|
|||||||
import { homeFields } from './homeFields'
|
import { homeFields } from './homeFields'
|
||||||
import { impressumFields } from './impressumFields'
|
import { impressumFields } from './impressumFields'
|
||||||
import { participationFields } from './participationFields'
|
import { participationFields } from './participationFields'
|
||||||
|
import { preistraegerIndexFields } from './preistraegerIndexFields'
|
||||||
import { slugField } from 'payload'
|
import { slugField } from 'payload'
|
||||||
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
||||||
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
|
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
|
||||||
@@ -75,13 +76,22 @@ const isDatenschutzPage = (_: unknown, siblingData?: { slug?: string; spaPath?:
|
|||||||
return spaPath === '/datenschutz' || slug === 'datenschutz' || title === 'datenschutz'
|
return spaPath === '/datenschutz' || slug === 'datenschutz' || title === 'datenschutz'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPreistraegerIndexPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
|
||||||
|
const slug = siblingData?.slug
|
||||||
|
const spaPath = siblingData?.spaPath
|
||||||
|
const title = siblingData?.title?.toLowerCase()
|
||||||
|
|
||||||
|
return spaPath === '/preistraeger' || slug === 'preistraeger' || title === 'preisträger' || title === 'preistraeger'
|
||||||
|
}
|
||||||
|
|
||||||
const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
|
const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
|
||||||
isHomePage(undefined, siblingData) ||
|
isHomePage(undefined, siblingData) ||
|
||||||
isContactPage(undefined, siblingData) ||
|
isContactPage(undefined, siblingData) ||
|
||||||
isParticipationPage(undefined, siblingData) ||
|
isParticipationPage(undefined, siblingData) ||
|
||||||
isAboutPage(undefined, siblingData) ||
|
isAboutPage(undefined, siblingData) ||
|
||||||
isImpressumPage(undefined, siblingData) ||
|
isImpressumPage(undefined, siblingData) ||
|
||||||
isDatenschutzPage(undefined, siblingData)
|
isDatenschutzPage(undefined, siblingData) ||
|
||||||
|
isPreistraegerIndexPage(undefined, siblingData)
|
||||||
|
|
||||||
export const Pages: CollectionConfig<'pages'> = {
|
export const Pages: CollectionConfig<'pages'> = {
|
||||||
slug: 'pages',
|
slug: 'pages',
|
||||||
@@ -191,6 +201,13 @@ export const Pages: CollectionConfig<'pages'> = {
|
|||||||
fields: datenschutzFields,
|
fields: datenschutzFields,
|
||||||
label: 'Datenschutz Page',
|
label: 'Datenschutz Page',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
admin: {
|
||||||
|
condition: (data) => isPreistraegerIndexPage(undefined, data),
|
||||||
|
},
|
||||||
|
fields: preistraegerIndexFields,
|
||||||
|
label: 'Preisträger Index Page',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'meta',
|
name: 'meta',
|
||||||
label: 'SEO',
|
label: 'SEO',
|
||||||
|
|||||||
116
src/collections/Pages/preistraegerIndexFields.ts
Normal file
116
src/collections/Pages/preistraegerIndexFields.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import type { Field } from 'payload'
|
||||||
|
|
||||||
|
import { preistraegerIndexContent } from '@/spa/preistraegerIndexContent'
|
||||||
|
|
||||||
|
const uploadField = (name: string, label: string, description?: string): Field => ({
|
||||||
|
name,
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
label,
|
||||||
|
admin: description ? { description } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = (name: string, label: string, defaultValue?: string): Field => ({
|
||||||
|
name,
|
||||||
|
type: 'text',
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
})
|
||||||
|
|
||||||
|
const textarea = (name: string, label: string, defaultValue?: string): Field => ({
|
||||||
|
name,
|
||||||
|
type: 'textarea',
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ctaGroup = (name: string, label: string, labelDefault: string, urlDefault: string): Field => ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
type: 'group',
|
||||||
|
fields: [text('label', 'Label', labelDefault), text('url', 'URL', urlDefault)],
|
||||||
|
})
|
||||||
|
|
||||||
|
const sectionAdmin = (description: string) => ({
|
||||||
|
description,
|
||||||
|
initCollapsed: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const preistraegerIndexFields: Field[] = [
|
||||||
|
{
|
||||||
|
name: 'preistraegerIndex',
|
||||||
|
label: 'Preisträger index page content',
|
||||||
|
type: 'group',
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
'Edit the Preisträger listing page chrome. Individual winner cards are managed through the Preisträger collection.',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'hero',
|
||||||
|
label: '01 · Hero image',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Large image above the filters. Upload an image or keep the migrated URL fallback.'),
|
||||||
|
fields: [
|
||||||
|
uploadField('image', 'Hero image'),
|
||||||
|
text('imageUrl', 'Fallback image URL', preistraegerIndexContent.hero.imageUrl),
|
||||||
|
text('imageAlt', 'Image alt text', preistraegerIndexContent.hero.imageAlt),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'breadcrumb',
|
||||||
|
label: '02 · Breadcrumb bar',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Small page label below the image.'),
|
||||||
|
fields: [text('label', 'Label', preistraegerIndexContent.breadcrumb.label)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filters',
|
||||||
|
label: '03 · Filters',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Filter placeholders, toggle label, and reset label.'),
|
||||||
|
fields: [
|
||||||
|
text('categoryPlaceholder', 'Category placeholder', preistraegerIndexContent.filters.categoryPlaceholder),
|
||||||
|
text('searchPlaceholder', 'Search placeholder', preistraegerIndexContent.filters.searchPlaceholder),
|
||||||
|
textarea('storyHeading', 'Story heading', preistraegerIndexContent.filters.storyHeading),
|
||||||
|
text('mediaIcon', 'Media icon', preistraegerIndexContent.filters.mediaIcon),
|
||||||
|
text('mediaLabel', 'Media label', preistraegerIndexContent.filters.mediaLabel),
|
||||||
|
text('resetLabel', 'Reset label', preistraegerIndexContent.filters.resetLabel),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
label: '04 · Count bar',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Label after the filtered result count.'),
|
||||||
|
fields: [text('label', 'Count label', preistraegerIndexContent.count.label)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'empty',
|
||||||
|
label: '05 · Empty state',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Message when filters return no winners.'),
|
||||||
|
fields: [textarea('message', 'Empty message', preistraegerIndexContent.empty.message)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'card',
|
||||||
|
label: '06 · Winner cards',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Hover label shown on winner cards.'),
|
||||||
|
fields: [text('hoverLabel', 'Hover label', preistraegerIndexContent.card.hoverLabel)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cta',
|
||||||
|
label: '07 · Bottom CTA',
|
||||||
|
type: 'group',
|
||||||
|
admin: sectionAdmin('Bottom application and membership CTA.'),
|
||||||
|
fields: [
|
||||||
|
textarea('text', 'CTA text', preistraegerIndexContent.cta.text),
|
||||||
|
ctaGroup('primaryCta', 'Primary CTA', preistraegerIndexContent.cta.primaryCta.label, preistraegerIndexContent.cta.primaryCta.url),
|
||||||
|
text('secondaryPrefix', 'Secondary prefix', preistraegerIndexContent.cta.secondaryPrefix),
|
||||||
|
ctaGroup('secondaryCta', 'Secondary CTA', preistraegerIndexContent.cta.secondaryCta.label, preistraegerIndexContent.cta.secondaryCta.url),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1179,6 +1179,69 @@ export interface Page {
|
|||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Edit the Preisträger listing page chrome. Individual winner cards are managed through the Preisträger collection.
|
||||||
|
*/
|
||||||
|
preistraegerIndex?: {
|
||||||
|
/**
|
||||||
|
* Large image above the filters. Upload an image or keep the migrated URL fallback.
|
||||||
|
*/
|
||||||
|
hero?: {
|
||||||
|
image?: (number | null) | Media;
|
||||||
|
imageUrl?: string | null;
|
||||||
|
imageAlt?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Small page label below the image.
|
||||||
|
*/
|
||||||
|
breadcrumb?: {
|
||||||
|
label?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Filter placeholders, toggle label, and reset label.
|
||||||
|
*/
|
||||||
|
filters?: {
|
||||||
|
categoryPlaceholder?: string | null;
|
||||||
|
searchPlaceholder?: string | null;
|
||||||
|
storyHeading?: string | null;
|
||||||
|
mediaIcon?: string | null;
|
||||||
|
mediaLabel?: string | null;
|
||||||
|
resetLabel?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Label after the filtered result count.
|
||||||
|
*/
|
||||||
|
count?: {
|
||||||
|
label?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Message when filters return no winners.
|
||||||
|
*/
|
||||||
|
empty?: {
|
||||||
|
message?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Hover label shown on winner cards.
|
||||||
|
*/
|
||||||
|
card?: {
|
||||||
|
hoverLabel?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Bottom application and membership CTA.
|
||||||
|
*/
|
||||||
|
cta?: {
|
||||||
|
text?: string | null;
|
||||||
|
primaryCta?: {
|
||||||
|
label?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
secondaryPrefix?: string | null;
|
||||||
|
secondaryCta?: {
|
||||||
|
label?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
meta?: {
|
meta?: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
/**
|
/**
|
||||||
@@ -3000,6 +3063,65 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
items?: T;
|
items?: T;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
preistraegerIndex?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
hero?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
image?: T;
|
||||||
|
imageUrl?: T;
|
||||||
|
imageAlt?: T;
|
||||||
|
};
|
||||||
|
breadcrumb?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
};
|
||||||
|
filters?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
categoryPlaceholder?: T;
|
||||||
|
searchPlaceholder?: T;
|
||||||
|
storyHeading?: T;
|
||||||
|
mediaIcon?: T;
|
||||||
|
mediaLabel?: T;
|
||||||
|
resetLabel?: T;
|
||||||
|
};
|
||||||
|
count?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
};
|
||||||
|
empty?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
message?: T;
|
||||||
|
};
|
||||||
|
card?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
hoverLabel?: T;
|
||||||
|
};
|
||||||
|
cta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
text?: T;
|
||||||
|
primaryCta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
secondaryPrefix?: T;
|
||||||
|
secondaryCta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
meta?:
|
meta?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
|||||||
41
src/scripts/preload-preistraeger-index-page-cms.ts
Normal file
41
src/scripts/preload-preistraeger-index-page-cms.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
|
||||||
|
import { preistraegerIndexContent } from '@/spa/preistraegerIndexContent'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
const pageResult = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
draft: true,
|
||||||
|
overrideAccess: true,
|
||||||
|
where: {
|
||||||
|
or: [{ spaPath: { equals: '/preistraeger' } }, { slug: { equals: 'preistraeger' } }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const page = pageResult.docs[0]
|
||||||
|
if (!page) throw new Error('Preisträger index page not found. Expected spaPath=/preistraeger or slug=preistraeger.')
|
||||||
|
|
||||||
|
await payload.update({
|
||||||
|
collection: 'pages',
|
||||||
|
id: page.id,
|
||||||
|
overrideAccess: true,
|
||||||
|
context: { disableRevalidate: true },
|
||||||
|
data: {
|
||||||
|
preistraegerIndex: preistraegerIndexContent,
|
||||||
|
} as never,
|
||||||
|
})
|
||||||
|
|
||||||
|
payload.logger.info(`Preloaded Preisträger index CMS fields for page ${page.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Link } from '@/spa/router';
|
import { Link } from '@/spa/router';
|
||||||
import { Trophy, Search, ChevronDown, ChevronRight, X, ArrowRight } from 'lucide-react';
|
import { Trophy, Search, ChevronDown, ChevronRight, X, ArrowRight } from 'lucide-react';
|
||||||
import { WINNERS, CATEGORIES, YEARS } from '@/spa/data/winners';
|
import { WINNERS, type Winner } from '@/spa/data/winners';
|
||||||
import { useCmsCollection } from '@/spa/cmsRoute';
|
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
import { preistraegerIndexContent } from '@/spa/preistraegerIndexContent';
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
@@ -14,47 +15,61 @@ const BORDER = '#D0D5DD';
|
|||||||
const GRAY = '#666666';
|
const GRAY = '#666666';
|
||||||
const BG_ALT = '#E4E2E3';
|
const BG_ALT = '#E4E2E3';
|
||||||
|
|
||||||
|
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
|
||||||
|
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
|
|
||||||
|
const normalizeWinner = (doc: CmsRouteDoc): Winner => ({
|
||||||
|
id: String(doc.id),
|
||||||
|
slug: String(doc.slug || ''),
|
||||||
|
name: String(doc.title || 'Preisträger'),
|
||||||
|
category: String(doc.category || 'Preisträger'),
|
||||||
|
year: Number(doc.year || new Date().getFullYear()),
|
||||||
|
type: String(doc.awardType || 'Auszeichnung'),
|
||||||
|
img: docImageUrl(doc, '/images/gala-saal-overview.jpg'),
|
||||||
|
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
|
||||||
|
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
|
||||||
|
quote: String(doc.quote || ''),
|
||||||
|
quotePerson: String(doc.quotePerson || ''),
|
||||||
|
quoteRole: String(doc.quoteRole || ''),
|
||||||
|
location: String(doc.location || 'Bayern'),
|
||||||
|
industry: String(doc.industry || 'Mittelstand'),
|
||||||
|
website: String(doc.website || '#'),
|
||||||
|
hasMedia: Boolean(doc.hasMedia),
|
||||||
|
});
|
||||||
|
|
||||||
export default function Preistraeger() {
|
export default function Preistraeger() {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [activeYear, setActiveYear] = useState(YEARS[0]); // default: most recent
|
|
||||||
const [filterCategory, setFilterCategory] = useState('');
|
const [filterCategory, setFilterCategory] = useState('');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [mediaOnly, setMediaOnly] = useState(false);
|
const [mediaOnly, setMediaOnly] = useState(false);
|
||||||
|
const cms = (useCmsRoute()?.doc?.preistraegerIndex || {}) as PreistraegerIndexCms;
|
||||||
const cmsPreistraeger = useCmsCollection('preistraeger');
|
const cmsPreistraeger = useCmsCollection('preistraeger');
|
||||||
|
const hero = { ...preistraegerIndexContent.hero, ...(cms.hero || {}) };
|
||||||
|
const breadcrumb = { ...preistraegerIndexContent.breadcrumb, ...(cms.breadcrumb || {}) };
|
||||||
|
const filters = { ...preistraegerIndexContent.filters, ...(cms.filters || {}) };
|
||||||
|
const count = { ...preistraegerIndexContent.count, ...(cms.count || {}) };
|
||||||
|
const empty = { ...preistraegerIndexContent.empty, ...(cms.empty || {}) };
|
||||||
|
const card = { ...preistraegerIndexContent.card, ...(cms.card || {}) };
|
||||||
|
const cta = { ...preistraegerIndexContent.cta, ...(cms.cta || {}) };
|
||||||
const winners = useMemo(() => {
|
const winners = useMemo(() => {
|
||||||
const existingSlugs = new Set(WINNERS.map((winner) => winner.slug));
|
if (cmsPreistraeger.length) return cmsPreistraeger.map(normalizeWinner).filter((winner) => winner.slug);
|
||||||
return [
|
return WINNERS;
|
||||||
...WINNERS,
|
|
||||||
...cmsPreistraeger
|
|
||||||
.filter((doc) => doc.slug && !existingSlugs.has(String(doc.slug)))
|
|
||||||
.map((doc) => ({
|
|
||||||
id: String(doc.id),
|
|
||||||
slug: String(doc.slug),
|
|
||||||
name: String(doc.title || 'Preisträger'),
|
|
||||||
category: String(doc.category || 'Preisträger'),
|
|
||||||
year: Number(doc.year || new Date().getFullYear()),
|
|
||||||
type: String(doc.awardType || 'Auszeichnung'),
|
|
||||||
img: docImageUrl(doc, '/images/gala-saal-overview.jpg'),
|
|
||||||
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
|
|
||||||
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
|
|
||||||
quote: String(doc.quote || ''),
|
|
||||||
quotePerson: String(doc.quotePerson || ''),
|
|
||||||
quoteRole: String(doc.quoteRole || ''),
|
|
||||||
location: String(doc.location || 'Bayern'),
|
|
||||||
industry: String(doc.industry || 'Mittelstand'),
|
|
||||||
website: String(doc.website || '#'),
|
|
||||||
hasMedia: Boolean(doc.hasMedia),
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
}, [cmsPreistraeger]);
|
}, [cmsPreistraeger]);
|
||||||
|
const years = useMemo(() => [...new Set(winners.map((winner) => winner.year))].sort((a, b) => b - a), [winners]);
|
||||||
|
const categories = useMemo(() => [...new Set(winners.map((winner) => winner.category))].sort((a, b) => a.localeCompare(b, 'de')), [winners]);
|
||||||
|
const [activeYear, setActiveYear] = useState<number | undefined>(undefined);
|
||||||
|
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
|
||||||
|
|
||||||
const filtered = useMemo(() => winners.filter(w => {
|
const filtered = useMemo(() => winners.filter(w => {
|
||||||
if (w.year !== activeYear) return false;
|
if (w.year !== selectedYear) return false;
|
||||||
if (filterCategory && w.category !== filterCategory) return false;
|
if (filterCategory && w.category !== filterCategory) return false;
|
||||||
if (search && !w.name.toLowerCase().includes(search.toLowerCase())) return false;
|
if (search && !w.name.toLowerCase().includes(search.toLowerCase())) return false;
|
||||||
if (mediaOnly && !w.hasMedia) return false;
|
if (mediaOnly && !w.hasMedia) return false;
|
||||||
return true;
|
return true;
|
||||||
}), [activeYear, filterCategory, search, mediaOnly, winners]);
|
}), [selectedYear, filterCategory, search, mediaOnly, winners]);
|
||||||
|
|
||||||
const reset = () => { setFilterCategory(''); setSearch(''); setMediaOnly(false); };
|
const reset = () => { setFilterCategory(''); setSearch(''); setMediaOnly(false); };
|
||||||
const hasFilter = !!(filterCategory || search || mediaOnly);
|
const hasFilter = !!(filterCategory || search || mediaOnly);
|
||||||
@@ -64,8 +79,8 @@ export default function Preistraeger() {
|
|||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<div style={{ width: '100%', height: '50vh', overflow: 'hidden', position: 'relative' }}>
|
<div style={{ width: '100%', height: '50vh', overflow: 'hidden', position: 'relative' }}>
|
||||||
<Image unoptimized
|
<Image unoptimized
|
||||||
src="https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=2000"
|
src={mediaUrl(hero.image, fallbackText(hero.imageUrl, preistraegerIndexContent.hero.imageUrl))}
|
||||||
alt="Preisverleihung"
|
alt={mediaAlt(hero.image, fallbackText(hero.imageAlt, preistraegerIndexContent.hero.imageAlt))}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||||
/>
|
/>
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, transparent 40%, rgba(3,9,58,0.82) 100%)' }} />
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, transparent 40%, rgba(3,9,58,0.82) 100%)' }} />
|
||||||
@@ -74,7 +89,7 @@ export default function Preistraeger() {
|
|||||||
|
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div style={{ background: '#fff', borderBottom: `1px solid ${BORDER}`, padding: isMobile ? '18px 24px' : '18px 80px' }}>
|
<div style={{ background: '#fff', borderBottom: `1px solid ${BORDER}`, padding: isMobile ? '18px 24px' : '18px 80px' }}>
|
||||||
<span style={{ fontFamily: FF, fontSize: 16, color: GOLD, fontWeight: 600, letterSpacing: '0.04em' }}>Preisträger:innen</span>
|
<span style={{ fontFamily: FF, fontSize: 16, color: GOLD, fontWeight: 600, letterSpacing: '0.04em' }}>{fallbackText(breadcrumb.label, preistraegerIndexContent.breadcrumb.label)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Section */}
|
{/* Filter Section */}
|
||||||
@@ -83,7 +98,7 @@ export default function Preistraeger() {
|
|||||||
|
|
||||||
{/* Year Tabs */}
|
{/* Year Tabs */}
|
||||||
<div style={{ display: 'flex', flexWrap: isMobile ? 'nowrap' : 'wrap', gap: 0, marginBottom: 32, borderBottom: `2px solid ${BORDER}`, overflowX: isMobile ? 'auto' : 'visible', WebkitOverflowScrolling: 'touch', maxWidth: '100%' }}>
|
<div style={{ display: 'flex', flexWrap: isMobile ? 'nowrap' : 'wrap', gap: 0, marginBottom: 32, borderBottom: `2px solid ${BORDER}`, overflowX: isMobile ? 'auto' : 'visible', WebkitOverflowScrolling: 'touch', maxWidth: '100%' }}>
|
||||||
{YEARS.map(year => (
|
{years.map(year => (
|
||||||
<button
|
<button
|
||||||
key={year}
|
key={year}
|
||||||
onClick={() => { setActiveYear(year); reset(); }}
|
onClick={() => { setActiveYear(year); reset(); }}
|
||||||
@@ -92,8 +107,8 @@ export default function Preistraeger() {
|
|||||||
padding: isMobile ? '12px 20px' : '12px 32px',
|
padding: isMobile ? '12px 20px' : '12px 32px',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
background: 'none', border: 'none', cursor: 'pointer',
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
color: activeYear === year ? NAVY : GRAY,
|
color: selectedYear === year ? NAVY : GRAY,
|
||||||
borderBottom: activeYear === year ? `2px solid ${GOLD}` : '2px solid transparent',
|
borderBottom: selectedYear === year ? `2px solid ${GOLD}` : '2px solid transparent',
|
||||||
marginBottom: -2,
|
marginBottom: -2,
|
||||||
letterSpacing: '0.04em',
|
letterSpacing: '0.04em',
|
||||||
transition: 'color 0.15s, border-color 0.15s',
|
transition: 'color 0.15s, border-color 0.15s',
|
||||||
@@ -109,22 +124,22 @@ export default function Preistraeger() {
|
|||||||
<SelectControl
|
<SelectControl
|
||||||
value={filterCategory}
|
value={filterCategory}
|
||||||
onChange={setFilterCategory}
|
onChange={setFilterCategory}
|
||||||
placeholder="Preiskategorie …"
|
placeholder={fallbackText(filters.categoryPlaceholder, preistraegerIndexContent.filters.categoryPlaceholder)}
|
||||||
options={CATEGORIES.map(c => ({ label: c, value: c }))}
|
options={categories.map(c => ({ label: c, value: c }))}
|
||||||
width={isMobile ? undefined : 280}
|
width={isMobile ? undefined : 280}
|
||||||
/>
|
/>
|
||||||
<SearchControl value={search} onChange={setSearch} />
|
<SearchControl value={search} onChange={setSearch} placeholder={fallbackText(filters.searchPlaceholder, preistraegerIndexContent.filters.searchPlaceholder)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toggle row */}
|
{/* Toggle row */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: '#101828' }}>
|
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: '#101828' }}>
|
||||||
Diese Erfolgsgeschichten müssen erzählt werden.
|
{fallbackText(filters.storyHeading, preistraegerIndexContent.filters.storyHeading)}
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<span style={{ fontSize: 15 }}>💬</span>
|
<span style={{ fontSize: 15 }}>{fallbackText(filters.mediaIcon, preistraegerIndexContent.filters.mediaIcon)}</span>
|
||||||
<span style={{ fontFamily: FF, fontSize: 15, color: GRAY }}>Mit Medienbeiträgen (Videos & Storys)</span>
|
<span style={{ fontFamily: FF, fontSize: 15, color: GRAY }}>{fallbackText(filters.mediaLabel, preistraegerIndexContent.filters.mediaLabel)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setMediaOnly(v => !v)}
|
onClick={() => setMediaOnly(v => !v)}
|
||||||
style={{
|
style={{
|
||||||
@@ -147,7 +162,7 @@ export default function Preistraeger() {
|
|||||||
onMouseEnter={e => (e.currentTarget.style.color = GOLD)}
|
onMouseEnter={e => (e.currentTarget.style.color = GOLD)}
|
||||||
onMouseLeave={e => (e.currentTarget.style.color = GRAY)}
|
onMouseLeave={e => (e.currentTarget.style.color = GRAY)}
|
||||||
>
|
>
|
||||||
<X size={14} /> Filter zurücksetzen
|
<X size={14} /> {fallbackText(filters.resetLabel, preistraegerIndexContent.filters.resetLabel)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -157,14 +172,14 @@ 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' }}>{activeYear}</strong> – {filtered.length} Preisträger:innen
|
<strong style={{ color: '#101828' }}>{selectedYear}</strong> – {filtered.length} {fallbackText(count.label, preistraegerIndexContent.count.label)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
{filtered.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 }}>
|
||||||
Keine Preisträger für diese Auswahl.
|
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -181,16 +196,16 @@ export default function Preistraeger() {
|
|||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
}}>
|
}}>
|
||||||
{filtered.map(w => (
|
{filtered.map(w => (
|
||||||
<WinnerCard key={w.id} winner={w} />
|
<WinnerCard key={w.id} winner={w} hoverLabel={fallbackText(card.hoverLabel, preistraegerIndexContent.card.hoverLabel)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 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' }}>
|
||||||
<p style={{ fontFamily: FF, fontSize: 17, color: GRAY, marginBottom: 20 }}>Werden Sie der nächste Preisträger.</p>
|
<p style={{ fontFamily: FF, fontSize: 17, color: GRAY, marginBottom: 20 }}>{fallbackText(cta.text, preistraegerIndexContent.cta.text)}</p>
|
||||||
<Link
|
<Link
|
||||||
to="/teilnahme"
|
to={fallbackText(cta.primaryCta?.url, preistraegerIndexContent.cta.primaryCta.url)}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: FF, fontSize: 16, fontWeight: 700, textTransform: 'uppercase',
|
fontFamily: FF, fontSize: 16, fontWeight: 700, textTransform: 'uppercase',
|
||||||
letterSpacing: '0.12em', color: '#101828', background: GOLD,
|
letterSpacing: '0.12em', color: '#101828', background: GOLD,
|
||||||
@@ -200,13 +215,13 @@ export default function Preistraeger() {
|
|||||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
|
||||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
||||||
>
|
>
|
||||||
Jetzt kostenlos bewerben <ArrowRight size={14} />
|
{fallbackText(cta.primaryCta?.label, preistraegerIndexContent.cta.primaryCta.label)} <ArrowRight size={14} />
|
||||||
</Link>
|
</Link>
|
||||||
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : undefined, flexWrap: isMobile ? 'wrap' : 'nowrap', gap: 8 }}>
|
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : undefined, flexWrap: isMobile ? 'wrap' : 'nowrap', gap: 8 }}>
|
||||||
<span style={{ width: 20, height: 1, background: 'rgba(239,191,4,0.3)', display: 'inline-block' }} />
|
<span style={{ width: 20, height: 1, background: 'rgba(239,191,4,0.3)', display: 'inline-block' }} />
|
||||||
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 11, color: 'rgba(16,24,40,0.4)' }}>Diese Arbeit unterstützen:</span>
|
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 11, color: 'rgba(16,24,40,0.4)' }}>{fallbackText(cta.secondaryPrefix, preistraegerIndexContent.cta.secondaryPrefix)}</span>
|
||||||
<Link
|
<Link
|
||||||
to="/mitglied-werden"
|
to={fallbackText(cta.secondaryCta?.url, preistraegerIndexContent.cta.secondaryCta.url)}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
@@ -231,7 +246,7 @@ export default function Preistraeger() {
|
|||||||
(e.currentTarget as HTMLElement).style.borderBottomColor = 'rgba(239,191,4,0.3)';
|
(e.currentTarget as HTMLElement).style.borderBottomColor = 'rgba(239,191,4,0.3)';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Vereinsmitglied werden <ChevronRight size={10} />
|
{fallbackText(cta.secondaryCta?.label, preistraegerIndexContent.cta.secondaryCta.label)} <ChevronRight size={10} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +256,7 @@ export default function Preistraeger() {
|
|||||||
|
|
||||||
// ── WinnerCard ───────────────────────────────────────────────────────────────
|
// ── WinnerCard ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function WinnerCard({ winner }: { winner: import('@/spa/data/winners').Winner }) {
|
function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string }) {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
return (
|
return (
|
||||||
@@ -288,7 +303,7 @@ function WinnerCard({ winner }: { winner: import('@/spa/data/winners').Winner })
|
|||||||
transform: hovered ? 'translateY(0)' : 'translateY(6px)',
|
transform: hovered ? 'translateY(0)' : 'translateY(6px)',
|
||||||
transition: 'opacity 0.25s, transform 0.25s',
|
transition: 'opacity 0.25s, transform 0.25s',
|
||||||
}}>
|
}}>
|
||||||
<Trophy size={14} /> Mehr erfahren
|
<Trophy size={14} /> {hoverLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -330,14 +345,14 @@ function SelectControl({ value, onChange, placeholder, options, width }: {
|
|||||||
|
|
||||||
// ── SearchControl ────────────────────────────────────────────────────────────
|
// ── SearchControl ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SearchControl({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
function SearchControl({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder: string }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', flex: 1, minWidth: 200 }}>
|
<div style={{ position: 'relative', flex: 1, minWidth: 200 }}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.value)}
|
onChange={e => onChange(e.target.value)}
|
||||||
placeholder="Stichwortsuche …"
|
placeholder={placeholder}
|
||||||
style={{
|
style={{
|
||||||
width: '100%', height: 44, padding: '0 40px 0 14px',
|
width: '100%', height: 44, padding: '0 40px 0 14px',
|
||||||
fontFamily: FF, fontSize: 16,
|
fontFamily: FF, fontSize: 16,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function PreistraegerDetail() {
|
|||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
const route = useCmsRoute();
|
const route = useCmsRoute();
|
||||||
const cmsWinner = route?.collection === 'preistraeger' ? route.doc : undefined;
|
const cmsWinner = route?.collection === 'preistraeger' ? route.doc : undefined;
|
||||||
const winner = WINNERS.find(w => w.slug === slug) || (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 || 'Preisträger'),
|
name: String(cmsWinner.title || 'Preisträger'),
|
||||||
@@ -36,7 +36,7 @@ export default function PreistraegerDetail() {
|
|||||||
industry: String(cmsWinner.industry || 'Mittelstand'),
|
industry: String(cmsWinner.industry || 'Mittelstand'),
|
||||||
website: String(cmsWinner.website || '#'),
|
website: String(cmsWinner.website || '#'),
|
||||||
hasMedia: Boolean(cmsWinner.hasMedia),
|
hasMedia: Boolean(cmsWinner.hasMedia),
|
||||||
} : undefined);
|
} : WINNERS.find(w => w.slug === slug);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
if (!winner) return <Navigate to="/preistraeger" replace />;
|
if (!winner) return <Navigate to="/preistraeger" replace />;
|
||||||
|
|||||||
38
src/spa/preistraegerIndexContent.ts
Normal file
38
src/spa/preistraegerIndexContent.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export const preistraegerIndexContent = {
|
||||||
|
hero: {
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=2000',
|
||||||
|
imageAlt: 'Preisverleihung',
|
||||||
|
},
|
||||||
|
breadcrumb: {
|
||||||
|
label: 'Preisträger:innen',
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
categoryPlaceholder: 'Preiskategorie …',
|
||||||
|
searchPlaceholder: 'Stichwortsuche …',
|
||||||
|
storyHeading: 'Diese Erfolgsgeschichten müssen erzählt werden.',
|
||||||
|
mediaIcon: '💬',
|
||||||
|
mediaLabel: 'Mit Medienbeiträgen (Videos & Storys)',
|
||||||
|
resetLabel: 'Filter zurücksetzen',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
label: 'Preisträger:innen',
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
message: 'Keine Preisträger für diese Auswahl.',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
hoverLabel: 'Mehr erfahren',
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
text: 'Werden Sie der nächste Preisträger.',
|
||||||
|
primaryCta: {
|
||||||
|
label: 'Jetzt kostenlos bewerben',
|
||||||
|
url: '/teilnahme',
|
||||||
|
},
|
||||||
|
secondaryPrefix: 'Diese Arbeit unterstützen:',
|
||||||
|
secondaryCta: {
|
||||||
|
label: 'Vereinsmitglied werden',
|
||||||
|
url: '/mitglied-werden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user