feat: implement newsletter subscription feature with new forms and database structure
- Added new newsletter subscription functionality to the participation page. - Created a new `NewsletterForm` component to handle user subscriptions. - Introduced `NewsletterSubscriptions` collection to manage subscriptions in the database. - Developed migration script to set up necessary database tables and fields for the newsletter feature. - Refactored participation content to remove deprecated newsletter phase data and integrate new structure. - Updated the `Participation` page to utilize the new newsletter form and handle different application phases.
This commit is contained in:
78
src/NewsletterForm/config.ts
Normal file
78
src/NewsletterForm/config.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import type { Field, GlobalConfig } from 'payload'
|
||||||
|
|
||||||
|
import { authenticated } from '@/access/authenticated'
|
||||||
|
import { defaultNewsletterFormData, type NewsletterInterestCopy } from '@/spa/newsletterForm'
|
||||||
|
|
||||||
|
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 variantFields = (defaults: NewsletterInterestCopy, benefitsDbName: string): Field[] => [
|
||||||
|
text('eyebrow', 'Linke Eyebrow', defaults.eyebrow),
|
||||||
|
textarea('heading', 'Linke Überschrift', defaults.heading),
|
||||||
|
textarea('body', 'Linker Beschreibungstext', defaults.body),
|
||||||
|
{
|
||||||
|
name: 'benefits',
|
||||||
|
label: 'Vorteile',
|
||||||
|
type: 'array',
|
||||||
|
dbName: benefitsDbName,
|
||||||
|
defaultValue: defaults.benefits,
|
||||||
|
fields: [text('num', 'Nummer', ''), text('label', 'Titel', ''), text('desc', 'Beschreibung', '')],
|
||||||
|
},
|
||||||
|
text('formEyebrow', 'Formular-Eyebrow', defaults.formEyebrow),
|
||||||
|
text('formTitle', 'Formular-Überschrift', defaults.formTitle),
|
||||||
|
textarea('formBody', 'Formular-Beschreibung', defaults.formBody),
|
||||||
|
text('emailLabel', 'E-Mail-Feldbezeichnung', defaults.emailLabel),
|
||||||
|
text('emailPlaceholder', 'E-Mail-Platzhalter', defaults.emailPlaceholder),
|
||||||
|
text('submitLabel', 'Button-Beschriftung', defaults.submitLabel),
|
||||||
|
textarea('privacy', 'Datenschutz-Hinweis', defaults.privacy),
|
||||||
|
text('validationRequired', 'Fehler: E-Mail fehlt', defaults.validationRequired),
|
||||||
|
text('validationInvalid', 'Fehler: E-Mail ungültig', defaults.validationInvalid),
|
||||||
|
textarea('submissionError', 'Fehler: Speichern fehlgeschlagen', defaults.submissionError),
|
||||||
|
text('successHeading', 'Erfolgsüberschrift', defaults.successHeading),
|
||||||
|
textarea('successBody', 'Erfolgstext', defaults.successBody),
|
||||||
|
text('footnote', 'Fußzeile normal', defaults.footnote),
|
||||||
|
text('footnoteStrong', 'Fußzeile hervorgehoben', defaults.footnoteStrong),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const NewsletterForm: GlobalConfig = {
|
||||||
|
slug: 'newsletter-form',
|
||||||
|
label: 'Newsletter-Formular',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
update: authenticated,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: 'Formulare',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'evaluation',
|
||||||
|
label: 'Phase 02 · Jury / Auswertung',
|
||||||
|
type: 'group',
|
||||||
|
admin: {
|
||||||
|
description: 'Gemeinsamer Newsletter-Inhalt für Startseite und Teilnahme-Seite während der Auswertungsphase.',
|
||||||
|
},
|
||||||
|
fields: variantFields(defaultNewsletterFormData.evaluation, 'newsletter_eval_bens'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'completed',
|
||||||
|
label: 'Phase 03 · Preisverleihung abgeschlossen',
|
||||||
|
type: 'group',
|
||||||
|
admin: {
|
||||||
|
description: 'Gemeinsamer Newsletter-Inhalt für Startseite und Teilnahme-Seite nach Abschluss des Preisjahrgangs.',
|
||||||
|
},
|
||||||
|
fields: variantFields(defaultNewsletterFormData.completed, 'newsletter_done_bens'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import NextApp from '@/spa/NextApp'
|
|||||||
import { normalizeApplicationPhaseData, type SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
import { normalizeApplicationPhaseData, type SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
||||||
import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation'
|
import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation'
|
||||||
import type { CmsRouteCollection, CmsRouteDoc } from '@/spa/cmsRoute'
|
import type { CmsRouteCollection, CmsRouteDoc } from '@/spa/cmsRoute'
|
||||||
|
import { normalizeNewsletterFormData, type SpaNewsletterFormData } from '@/spa/newsletterForm'
|
||||||
import { normalizeSiteSettings, type SpaSiteSettingsData } from '@/spa/siteSettings'
|
import { normalizeSiteSettings, type SpaSiteSettingsData } from '@/spa/siteSettings'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -104,6 +105,7 @@ export default async function Page({ params }: Args) {
|
|||||||
footer,
|
footer,
|
||||||
siteSettings,
|
siteSettings,
|
||||||
applicationPhase,
|
applicationPhase,
|
||||||
|
newsletterForm,
|
||||||
route,
|
route,
|
||||||
supportPages,
|
supportPages,
|
||||||
preistraeger,
|
preistraeger,
|
||||||
@@ -116,6 +118,7 @@ export default async function Page({ params }: Args) {
|
|||||||
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),
|
||||||
payload.findGlobal({ slug: 'application-phase', depth: 0 }).catch(() => undefined),
|
payload.findGlobal({ slug: 'application-phase', depth: 0 }).catch(() => undefined),
|
||||||
|
payload.findGlobal({ slug: 'newsletter-form', depth: 0 }).catch(() => undefined),
|
||||||
findRouteDoc(payload, pathname, draft),
|
findRouteDoc(payload, pathname, draft),
|
||||||
findSupportPages(payload, draft).catch(() => []),
|
findSupportPages(payload, draft).catch(() => []),
|
||||||
findList(payload, 'preistraeger', draft).catch(() => []),
|
findList(payload, 'preistraeger', draft).catch(() => []),
|
||||||
@@ -134,6 +137,7 @@ export default async function Page({ params }: Args) {
|
|||||||
footer={normalizeFooterData(footer)}
|
footer={normalizeFooterData(footer)}
|
||||||
siteSettings={normalizeSiteSettings(siteSettings as Partial<SpaSiteSettingsData> | undefined)}
|
siteSettings={normalizeSiteSettings(siteSettings as Partial<SpaSiteSettingsData> | undefined)}
|
||||||
applicationPhase={normalizeApplicationPhaseData(applicationPhase as Partial<SpaApplicationPhaseData> | undefined)}
|
applicationPhase={normalizeApplicationPhaseData(applicationPhase as Partial<SpaApplicationPhaseData> | undefined)}
|
||||||
|
newsletterForm={normalizeNewsletterFormData(newsletterForm as Partial<SpaNewsletterFormData> | undefined)}
|
||||||
cmsRoute={{
|
cmsRoute={{
|
||||||
collection: route.collection,
|
collection: route.collection,
|
||||||
doc: route.doc as unknown as CmsRouteDoc,
|
doc: route.doc as unknown as CmsRouteDoc,
|
||||||
|
|||||||
145
src/collections/NewsletterSubscriptions.ts
Normal file
145
src/collections/NewsletterSubscriptions.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import type { CollectionConfig, PayloadRequest } from 'payload'
|
||||||
|
|
||||||
|
import { authenticated } from '@/access/authenticated'
|
||||||
|
import type { NewsletterPhase, NewsletterSource } from '@/spa/newsletterForm'
|
||||||
|
|
||||||
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
const sources = new Set<NewsletterSource>(['homepage', 'participation'])
|
||||||
|
const phases = new Set<NewsletterPhase>(['evaluation', 'completed'])
|
||||||
|
|
||||||
|
type SubscribeBody = {
|
||||||
|
email?: unknown
|
||||||
|
phase?: unknown
|
||||||
|
source?: unknown
|
||||||
|
website?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribe(req: PayloadRequest) {
|
||||||
|
let body: SubscribeBody
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!req.json) throw new Error('Request body is unavailable.')
|
||||||
|
body = (await req.json()) as SubscribeBody
|
||||||
|
} catch {
|
||||||
|
return Response.json({ success: false }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof body.website === 'string' && body.website.length > 0) {
|
||||||
|
return Response.json({ success: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : ''
|
||||||
|
const source = typeof body.source === 'string' ? body.source : ''
|
||||||
|
const phase = typeof body.phase === 'string' ? body.phase : ''
|
||||||
|
|
||||||
|
if (
|
||||||
|
email.length === 0 ||
|
||||||
|
email.length > 254 ||
|
||||||
|
!emailPattern.test(email) ||
|
||||||
|
!sources.has(source as NewsletterSource) ||
|
||||||
|
!phases.has(phase as NewsletterPhase)
|
||||||
|
) {
|
||||||
|
return Response.json({ success: false }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await req.payload.find({
|
||||||
|
collection: 'newsletter-subscriptions',
|
||||||
|
depth: 0,
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
where: { email: { equals: email } },
|
||||||
|
})
|
||||||
|
const lastSubmittedAt = new Date().toISOString()
|
||||||
|
|
||||||
|
if (existing.docs[0]) {
|
||||||
|
await req.payload.update({
|
||||||
|
collection: 'newsletter-subscriptions',
|
||||||
|
id: existing.docs[0].id,
|
||||||
|
data: { lastSubmittedAt, phase: phase as NewsletterPhase, source: source as NewsletterSource },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await req.payload.create({
|
||||||
|
collection: 'newsletter-subscriptions',
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
lastSubmittedAt,
|
||||||
|
phase: phase as NewsletterPhase,
|
||||||
|
source: source as NewsletterSource,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
req.payload.logger.error({ err: error, msg: 'Newsletter subscription could not be stored.' })
|
||||||
|
return Response.json({ success: false }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewsletterSubscriptions: CollectionConfig<'newsletter-subscriptions'> = {
|
||||||
|
slug: 'newsletter-subscriptions',
|
||||||
|
labels: {
|
||||||
|
singular: 'Newsletter-Anmeldung',
|
||||||
|
plural: 'Newsletter-Anmeldungen',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
create: authenticated,
|
||||||
|
delete: authenticated,
|
||||||
|
read: authenticated,
|
||||||
|
update: authenticated,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
defaultColumns: ['email', 'source', 'phase', 'lastSubmittedAt'],
|
||||||
|
group: 'Formulare',
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
endpoints: [
|
||||||
|
{
|
||||||
|
path: '/subscribe',
|
||||||
|
method: 'post',
|
||||||
|
handler: subscribe,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
label: 'E-Mail-Adresse',
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'source',
|
||||||
|
label: 'Quelle',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: 'Startseite', value: 'homepage' },
|
||||||
|
{ label: 'Teilnahme-Seite', value: 'participation' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'phase',
|
||||||
|
label: 'Bewerbungsphase',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: 'Jury / Auswertung', value: 'evaluation' },
|
||||||
|
{ label: 'Preisverleihung abgeschlossen', value: 'completed' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastSubmittedAt',
|
||||||
|
label: 'Zuletzt angemeldet',
|
||||||
|
type: 'date',
|
||||||
|
required: true,
|
||||||
|
defaultValue: () => new Date().toISOString(),
|
||||||
|
admin: {
|
||||||
|
date: { displayFormat: 'dd.MM.yyyy HH:mm' },
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -60,36 +60,6 @@ const iconField = (defaultValue = 'star'): Field => ({
|
|||||||
options: iconOptions,
|
options: iconOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const newsletterVariantFields = (
|
|
||||||
defaults: typeof homeContent.application.newsletterPhaseEvaluation,
|
|
||||||
benefitsDbName: string,
|
|
||||||
): Field[] => [
|
|
||||||
text('eyebrow', 'Left eyebrow', defaults.eyebrow),
|
|
||||||
textarea('heading', 'Left heading', defaults.heading),
|
|
||||||
textarea('body', 'Left body copy', defaults.body),
|
|
||||||
{
|
|
||||||
name: 'benefits',
|
|
||||||
label: 'Benefit rows',
|
|
||||||
type: 'array',
|
|
||||||
dbName: benefitsDbName,
|
|
||||||
defaultValue: defaults.benefits,
|
|
||||||
fields: [text('num', 'Number'), text('label', 'Label'), text('desc', 'Description')],
|
|
||||||
},
|
|
||||||
text('formEyebrow', 'Form eyebrow', defaults.formEyebrow),
|
|
||||||
text('formTitle', 'Form title', defaults.formTitle),
|
|
||||||
textarea('formBody', 'Form body copy', defaults.formBody),
|
|
||||||
text('emailLabel', 'Email label', defaults.emailLabel),
|
|
||||||
text('emailPlaceholder', 'Email placeholder', defaults.emailPlaceholder),
|
|
||||||
text('submitLabel', 'Submit button label', defaults.submitLabel),
|
|
||||||
textarea('privacy', 'Privacy note', defaults.privacy),
|
|
||||||
text('validationRequired', 'Required email validation message', defaults.validationRequired),
|
|
||||||
text('validationInvalid', 'Invalid email validation message', defaults.validationInvalid),
|
|
||||||
text('successHeading', 'Success heading', defaults.successHeading),
|
|
||||||
textarea('successBody', 'Success body copy', defaults.successBody),
|
|
||||||
text('footnote', 'Footnote normal text', defaults.footnote),
|
|
||||||
text('footnoteStrong', 'Footnote highlighted text', defaults.footnoteStrong),
|
|
||||||
]
|
|
||||||
|
|
||||||
export const homeFields: Field[] = [
|
export const homeFields: Field[] = [
|
||||||
{
|
{
|
||||||
name: 'home',
|
name: 'home',
|
||||||
@@ -266,20 +236,6 @@ export const homeFields: Field[] = [
|
|||||||
text('formTitle', 'Form title', homeContent.application.formTitle),
|
text('formTitle', 'Form title', homeContent.application.formTitle),
|
||||||
text('footnote', 'Footnote normal text', homeContent.application.footnote),
|
text('footnote', 'Footnote normal text', homeContent.application.footnote),
|
||||||
text('footnoteStrong', 'Footnote highlighted text', homeContent.application.footnoteStrong),
|
text('footnoteStrong', 'Footnote highlighted text', homeContent.application.footnoteStrong),
|
||||||
{
|
|
||||||
name: 'newsletterPhaseEvaluation',
|
|
||||||
label: 'Phase 02 · Newsletter form',
|
|
||||||
type: 'group',
|
|
||||||
admin: sectionAdmin('Homepage newsletter copy used when the active application phase is Auswertung / Jury bewertet.'),
|
|
||||||
fields: newsletterVariantFields(homeContent.application.newsletterPhaseEvaluation, 'home_news_eval_bens'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'newsletterPhaseCompleted',
|
|
||||||
label: 'Phase 03 · Newsletter form',
|
|
||||||
type: 'group',
|
|
||||||
admin: sectionAdmin('Homepage newsletter copy used when the active application phase is Preisverleihung abgeschlossen.'),
|
|
||||||
fields: newsletterVariantFields(homeContent.application.newsletterPhaseCompleted, 'home_news_done_bens'),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,35 +76,6 @@ const ctaGroup = (name: string, label: string, labelDefault: string, urlDefault:
|
|||||||
type: 'group',
|
type: 'group',
|
||||||
fields: linkGroup(labelDefault, urlDefault),
|
fields: linkGroup(labelDefault, urlDefault),
|
||||||
})
|
})
|
||||||
const newsletterVariantFields = (
|
|
||||||
defaults: typeof participationContent.applicationForm.newsletterPhaseEvaluation,
|
|
||||||
benefitsDbName: string,
|
|
||||||
): Field[] => [
|
|
||||||
text('eyebrow', 'Left eyebrow', defaults.eyebrow),
|
|
||||||
textarea('heading', 'Left heading', defaults.heading),
|
|
||||||
textarea('body', 'Left body copy', defaults.body),
|
|
||||||
{
|
|
||||||
name: 'benefits',
|
|
||||||
label: 'Benefit rows',
|
|
||||||
type: 'array',
|
|
||||||
dbName: benefitsDbName,
|
|
||||||
defaultValue: defaults.benefits,
|
|
||||||
fields: [text('num', 'Number'), text('label', 'Label'), text('desc', 'Description')],
|
|
||||||
},
|
|
||||||
text('formEyebrow', 'Form eyebrow', defaults.formEyebrow),
|
|
||||||
text('formTitle', 'Form title', defaults.formTitle),
|
|
||||||
textarea('formBody', 'Form body copy', defaults.formBody),
|
|
||||||
text('emailLabel', 'Email label', defaults.emailLabel),
|
|
||||||
text('emailPlaceholder', 'Email placeholder', defaults.emailPlaceholder),
|
|
||||||
text('submitLabel', 'Submit button label', defaults.submitLabel),
|
|
||||||
textarea('privacy', 'Privacy note', defaults.privacy),
|
|
||||||
text('validationRequired', 'Required email validation message', defaults.validationRequired),
|
|
||||||
text('validationInvalid', 'Invalid email validation message', defaults.validationInvalid),
|
|
||||||
text('successHeading', 'Success heading', defaults.successHeading),
|
|
||||||
textarea('successBody', 'Success body copy', defaults.successBody),
|
|
||||||
text('footnote', 'Footnote normal text', defaults.footnote),
|
|
||||||
text('footnoteStrong', 'Footnote highlighted text', defaults.footnoteStrong),
|
|
||||||
]
|
|
||||||
const awardCardDefaults = participationContent.awardsGrid.cards.map(({ imageFilename: _imageFilename, ...card }) => card)
|
const awardCardDefaults = participationContent.awardsGrid.cards.map(({ imageFilename: _imageFilename, ...card }) => card)
|
||||||
|
|
||||||
export const participationFields: Field[] = [
|
export const participationFields: Field[] = [
|
||||||
@@ -320,20 +291,6 @@ export const participationFields: Field[] = [
|
|||||||
ctaGroup('uploadCta', 'Top upload CTA', participationContent.applicationForm.uploadCta.label, participationContent.applicationForm.uploadCta.url),
|
ctaGroup('uploadCta', 'Top upload CTA', participationContent.applicationForm.uploadCta.label, participationContent.applicationForm.uploadCta.url),
|
||||||
textarea('uploadPrompt', 'Bottom upload prompt', participationContent.applicationForm.uploadPrompt),
|
textarea('uploadPrompt', 'Bottom upload prompt', participationContent.applicationForm.uploadPrompt),
|
||||||
ctaGroup('uploadFooterCta', 'Bottom upload CTA', participationContent.applicationForm.uploadFooterCta.label, participationContent.applicationForm.uploadFooterCta.url),
|
ctaGroup('uploadFooterCta', 'Bottom upload CTA', participationContent.applicationForm.uploadFooterCta.label, participationContent.applicationForm.uploadFooterCta.url),
|
||||||
{
|
|
||||||
name: 'newsletterPhaseEvaluation',
|
|
||||||
label: 'Phase 02 · Newsletter form',
|
|
||||||
type: 'group',
|
|
||||||
admin: sectionAdmin('Participation page newsletter copy used when the active application phase is Auswertung / Jury bewertet.'),
|
|
||||||
fields: newsletterVariantFields(participationContent.applicationForm.newsletterPhaseEvaluation, 'part_news_eval_bens'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'newsletterPhaseCompleted',
|
|
||||||
label: 'Phase 03 · Newsletter form',
|
|
||||||
type: 'group',
|
|
||||||
admin: sectionAdmin('Participation page newsletter copy used when the active application phase is Preisverleihung abgeschlossen.'),
|
|
||||||
fields: newsletterVariantFields(participationContent.applicationForm.newsletterPhaseCompleted, 'part_news_done_bens'),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
|
||||||
|
|
||||||
|
import { defaultNewsletterFormData, type NewsletterInterestCopy } from '@/spa/newsletterForm'
|
||||||
|
|
||||||
|
type MigrationDB = MigrateUpArgs['db']
|
||||||
|
|
||||||
|
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
|
||||||
|
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
|
||||||
|
const snake = (value: string) => value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
||||||
|
|
||||||
|
const scalarFields = [
|
||||||
|
'eyebrow',
|
||||||
|
'heading',
|
||||||
|
'body',
|
||||||
|
'formEyebrow',
|
||||||
|
'formTitle',
|
||||||
|
'formBody',
|
||||||
|
'emailLabel',
|
||||||
|
'emailPlaceholder',
|
||||||
|
'submitLabel',
|
||||||
|
'privacy',
|
||||||
|
'validationRequired',
|
||||||
|
'validationInvalid',
|
||||||
|
'successHeading',
|
||||||
|
'successBody',
|
||||||
|
'footnote',
|
||||||
|
'footnoteStrong',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const sharedScalarFields = [...scalarFields, 'submissionError'] as const
|
||||||
|
const pagePrefixes = [
|
||||||
|
'home_application_newsletter_phase_evaluation',
|
||||||
|
'home_application_newsletter_phase_completed',
|
||||||
|
'participation_application_form_newsletter_phase_evaluation',
|
||||||
|
'participation_application_form_newsletter_phase_completed',
|
||||||
|
] as const
|
||||||
|
const legacyBenefitTables = [
|
||||||
|
'home_news_eval_bens',
|
||||||
|
'home_news_done_bens',
|
||||||
|
'part_news_eval_bens',
|
||||||
|
'part_news_done_bens',
|
||||||
|
'_home_news_eval_bens_v',
|
||||||
|
'_home_news_done_bens_v',
|
||||||
|
'_part_news_eval_bens_v',
|
||||||
|
'_part_news_done_bens_v',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
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 = 'text') {
|
||||||
|
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)};`))
|
||||||
|
}
|
||||||
|
|
||||||
|
function globalColumn(phase: 'evaluation' | 'completed', field: (typeof sharedScalarFields)[number]) {
|
||||||
|
return `${phase}_${snake(field)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageColumn(prefix: (typeof pagePrefixes)[number], field: (typeof scalarFields)[number]) {
|
||||||
|
return `${prefix}_${snake(field)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function globalTableColumns() {
|
||||||
|
const definitions: string[] = ['`id` integer PRIMARY KEY NOT NULL']
|
||||||
|
|
||||||
|
for (const phase of ['evaluation', 'completed'] as const) {
|
||||||
|
const defaults = defaultNewsletterFormData[phase]
|
||||||
|
for (const field of sharedScalarFields) {
|
||||||
|
definitions.push(`${ident(globalColumn(phase, field))} text DEFAULT ${literal(defaults[field])}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
definitions.push('`updated_at` text', '`created_at` text')
|
||||||
|
return definitions.join(',\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createBenefitTable(db: MigrationDB, tableName: string, parentTable: string, idType: 'text' | 'integer') {
|
||||||
|
const versionUUID = parentTable === '_pages_v' ? ',\n `_uuid` text' : ''
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${ident(tableName)} (
|
||||||
|
\`_order\` integer NOT NULL,
|
||||||
|
\`_parent_id\` integer NOT NULL,
|
||||||
|
\`id\` ${idType} PRIMARY KEY NOT NULL,
|
||||||
|
\`num\` text,
|
||||||
|
\`label\` text,
|
||||||
|
\`desc\` text${versionUUID},
|
||||||
|
FOREIGN KEY (\`_parent_id\`) REFERENCES ${ident(parentTable)}(\`id\`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_order_idx`)} ON ${ident(tableName)} (\`_order\`);`))
|
||||||
|
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_parent_id_idx`)} ON ${ident(tableName)} (\`_parent_id\`);`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedGlobalScalarsFromHome(db: MigrationDB) {
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
INSERT OR IGNORE INTO \`newsletter_form\` (\`id\`, \`updated_at\`, \`created_at\`)
|
||||||
|
VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
||||||
|
`))
|
||||||
|
|
||||||
|
if (!(await columnExists(db, 'pages', pageColumn(pagePrefixes[0], 'eyebrow')))) return
|
||||||
|
|
||||||
|
const assignments: string[] = []
|
||||||
|
for (const phase of ['evaluation', 'completed'] as const) {
|
||||||
|
const prefix = phase === 'evaluation' ? pagePrefixes[0] : pagePrefixes[1]
|
||||||
|
for (const field of scalarFields) {
|
||||||
|
const target = globalColumn(phase, field)
|
||||||
|
const source = pageColumn(prefix, field)
|
||||||
|
assignments.push(
|
||||||
|
`${ident(target)} = COALESCE((SELECT ${ident(source)} FROM \`pages\` WHERE \`spa_path\` = '/' OR \`slug\` IN ('startseite', 'home') ORDER BY CASE WHEN \`spa_path\` = '/' THEN 0 ELSE 1 END LIMIT 1), ${ident(target)})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.run(sql.raw(`UPDATE \`newsletter_form\` SET ${assignments.join(',\n')} WHERE \`id\` = 1;`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedBenefits(db: MigrationDB, phase: 'evaluation' | 'completed') {
|
||||||
|
const target = phase === 'evaluation' ? 'newsletter_eval_bens' : 'newsletter_done_bens'
|
||||||
|
const source = phase === 'evaluation' ? 'home_news_eval_bens' : 'home_news_done_bens'
|
||||||
|
const existingCount = (await db.all(sql.raw(`SELECT COUNT(*) AS count FROM ${ident(target)} WHERE \`_parent_id\` = 1;`))) as Array<{
|
||||||
|
count: number
|
||||||
|
}>
|
||||||
|
if (Number(existingCount[0]?.count || 0) > 0) return
|
||||||
|
|
||||||
|
if (await tableExists(db, source)) {
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
INSERT OR IGNORE INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, \`num\`, \`label\`, \`desc\`)
|
||||||
|
SELECT benefits.\`_order\`, 1, lower(hex(randomblob(16))), benefits.\`num\`, benefits.\`label\`, benefits.\`desc\`
|
||||||
|
FROM ${ident(source)} benefits
|
||||||
|
INNER JOIN \`pages\` page ON page.\`id\` = benefits.\`_parent_id\`
|
||||||
|
WHERE page.\`spa_path\` = '/' OR page.\`slug\` IN ('startseite', 'home')
|
||||||
|
ORDER BY benefits.\`_order\`;
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = (await db.all(sql.raw(`SELECT COUNT(*) AS count FROM ${ident(target)} WHERE \`_parent_id\` = 1;`))) as Array<{
|
||||||
|
count: number
|
||||||
|
}>
|
||||||
|
if (Number(count[0]?.count || 0) > 0) return
|
||||||
|
|
||||||
|
for (const [index, benefit] of defaultNewsletterFormData[phase].benefits.entries()) {
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
INSERT INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, \`num\`, \`label\`, \`desc\`)
|
||||||
|
VALUES (${index + 1}, 1, lower(hex(randomblob(16))), ${literal(benefit.num)}, ${literal(benefit.label)}, ${literal(benefit.desc)});
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeLegacyPageFields(db: MigrationDB) {
|
||||||
|
for (const prefix of pagePrefixes) {
|
||||||
|
for (const field of scalarFields) {
|
||||||
|
await dropColumn(db, 'pages', pageColumn(prefix, field))
|
||||||
|
await dropColumn(db, '_pages_v', `version_${pageColumn(prefix, field)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tableName of legacyBenefitTables) {
|
||||||
|
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(tableName)};`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreLegacyPageFields(db: MigrationDB) {
|
||||||
|
for (const prefix of pagePrefixes) {
|
||||||
|
for (const field of scalarFields) {
|
||||||
|
await addColumn(db, 'pages', pageColumn(prefix, field))
|
||||||
|
await addColumn(db, '_pages_v', `version_${pageColumn(prefix, field)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await createBenefitTable(db, 'home_news_eval_bens', 'pages', 'text')
|
||||||
|
await createBenefitTable(db, 'home_news_done_bens', 'pages', 'text')
|
||||||
|
await createBenefitTable(db, 'part_news_eval_bens', 'pages', 'text')
|
||||||
|
await createBenefitTable(db, 'part_news_done_bens', 'pages', 'text')
|
||||||
|
await createBenefitTable(db, '_home_news_eval_bens_v', '_pages_v', 'integer')
|
||||||
|
await createBenefitTable(db, '_home_news_done_bens_v', '_pages_v', 'integer')
|
||||||
|
await createBenefitTable(db, '_part_news_eval_bens_v', '_pages_v', 'integer')
|
||||||
|
await createBenefitTable(db, '_part_news_done_bens_v', '_pages_v', 'integer')
|
||||||
|
|
||||||
|
if (!(await tableExists(db, 'newsletter_form'))) return
|
||||||
|
|
||||||
|
for (const [pageKind, pageFilter] of [
|
||||||
|
['home', "`spa_path` = '/' OR `slug` IN ('startseite', 'home')"],
|
||||||
|
['participation', "`spa_path` = '/teilnahme' OR `slug` IN ('teilnahme', 'participation')"],
|
||||||
|
] as const) {
|
||||||
|
const assignments: string[] = []
|
||||||
|
for (const phase of ['evaluation', 'completed'] as const) {
|
||||||
|
const prefix = pageKind === 'home'
|
||||||
|
? phase === 'evaluation' ? pagePrefixes[0] : pagePrefixes[1]
|
||||||
|
: phase === 'evaluation' ? pagePrefixes[2] : pagePrefixes[3]
|
||||||
|
for (const field of scalarFields) {
|
||||||
|
assignments.push(`${ident(pageColumn(prefix, field))} = (SELECT ${ident(globalColumn(phase, field))} FROM \`newsletter_form\` WHERE \`id\` = 1)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.run(sql.raw(`UPDATE \`pages\` SET ${assignments.join(',\n')} WHERE ${pageFilter};`))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [target, source, filter] of [
|
||||||
|
['home_news_eval_bens', 'newsletter_eval_bens', "page.`spa_path` = '/' OR page.`slug` IN ('startseite', 'home')"],
|
||||||
|
['home_news_done_bens', 'newsletter_done_bens', "page.`spa_path` = '/' OR page.`slug` IN ('startseite', 'home')"],
|
||||||
|
['part_news_eval_bens', 'newsletter_eval_bens', "page.`spa_path` = '/teilnahme' OR page.`slug` IN ('teilnahme', 'participation')"],
|
||||||
|
['part_news_done_bens', 'newsletter_done_bens', "page.`spa_path` = '/teilnahme' OR page.`slug` IN ('teilnahme', 'participation')"],
|
||||||
|
] as const) {
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
INSERT INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, \`num\`, \`label\`, \`desc\`)
|
||||||
|
SELECT benefit.\`_order\`, page.\`id\`, lower(hex(randomblob(16))), benefit.\`num\`, benefit.\`label\`, benefit.\`desc\`
|
||||||
|
FROM ${ident(source)} benefit CROSS JOIN \`pages\` page
|
||||||
|
WHERE ${filter};
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rebuildLockedDocumentRelations(db: MigrationDB, includeNewsletter: boolean) {
|
||||||
|
const tableName = 'payload_locked_documents_rels'
|
||||||
|
if (!(await tableExists(db, tableName))) return
|
||||||
|
|
||||||
|
const newsletterColumn = includeNewsletter ? ',\n `newsletter_subscriptions_id` integer' : ''
|
||||||
|
const newsletterForeignKey = includeNewsletter
|
||||||
|
? ',\n FOREIGN KEY (`newsletter_subscriptions_id`) REFERENCES `newsletter_subscriptions`(`id`) ON UPDATE no action ON DELETE cascade'
|
||||||
|
: ''
|
||||||
|
const existingNewsletterColumn = await columnExists(db, tableName, 'newsletter_subscriptions_id')
|
||||||
|
const commonColumns = [
|
||||||
|
'id',
|
||||||
|
'order',
|
||||||
|
'parent_id',
|
||||||
|
'path',
|
||||||
|
'pages_id',
|
||||||
|
'preistraeger_id',
|
||||||
|
'jury_mitglieder_id',
|
||||||
|
'partners_id',
|
||||||
|
'posts_id',
|
||||||
|
'events_id',
|
||||||
|
...(includeNewsletter && existingNewsletterColumn ? ['newsletter_subscriptions_id'] : []),
|
||||||
|
'media_id',
|
||||||
|
'users_id',
|
||||||
|
'payload_folders_id',
|
||||||
|
]
|
||||||
|
|
||||||
|
await db.run(sql.raw('DROP TABLE IF EXISTS `__new_payload_locked_documents_rels`;'))
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
CREATE TABLE \`__new_payload_locked_documents_rels\` (
|
||||||
|
\`id\` integer PRIMARY KEY NOT NULL,
|
||||||
|
\`order\` integer,
|
||||||
|
\`parent_id\` integer NOT NULL,
|
||||||
|
\`path\` text NOT NULL,
|
||||||
|
\`pages_id\` integer,
|
||||||
|
\`preistraeger_id\` integer,
|
||||||
|
\`jury_mitglieder_id\` integer,
|
||||||
|
\`partners_id\` integer,
|
||||||
|
\`posts_id\` integer,
|
||||||
|
\`events_id\` integer${newsletterColumn},
|
||||||
|
\`media_id\` integer,
|
||||||
|
\`users_id\` integer,
|
||||||
|
\`payload_folders_id\` integer,
|
||||||
|
FOREIGN KEY (\`parent_id\`) REFERENCES \`payload_locked_documents\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`pages_id\`) REFERENCES \`pages\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`preistraeger_id\`) REFERENCES \`preistraeger\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`jury_mitglieder_id\`) REFERENCES \`jury_mitglieder\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`partners_id\`) REFERENCES \`partners\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`posts_id\`) REFERENCES \`posts\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`events_id\`) REFERENCES \`events\`(\`id\`) ON UPDATE no action ON DELETE cascade${newsletterForeignKey},
|
||||||
|
FOREIGN KEY (\`media_id\`) REFERENCES \`media\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`users_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (\`payload_folders_id\`) REFERENCES \`payload_folders\`(\`id\`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`))
|
||||||
|
|
||||||
|
const copiedColumns = commonColumns.map(ident).join(', ')
|
||||||
|
await db.run(sql.raw(`INSERT INTO \`__new_payload_locked_documents_rels\` (${copiedColumns}) SELECT ${copiedColumns} FROM ${ident(tableName)};`))
|
||||||
|
await db.run(sql.raw(`DROP TABLE ${ident(tableName)};`))
|
||||||
|
await db.run(sql.raw(`ALTER TABLE \`__new_payload_locked_documents_rels\` RENAME TO ${ident(tableName)};`))
|
||||||
|
|
||||||
|
for (const [suffix, column] of [
|
||||||
|
['order', 'order'],
|
||||||
|
['parent', 'parent_id'],
|
||||||
|
['path', 'path'],
|
||||||
|
['pages_id', 'pages_id'],
|
||||||
|
['preistraeger_id', 'preistraeger_id'],
|
||||||
|
['jury_mitglieder_id', 'jury_mitglieder_id'],
|
||||||
|
['partners_id', 'partners_id'],
|
||||||
|
['posts_id', 'posts_id'],
|
||||||
|
['events_id', 'events_id'],
|
||||||
|
...(includeNewsletter ? [['newsletter_subscriptions_i', 'newsletter_subscriptions_id']] : []),
|
||||||
|
['media_id', 'media_id'],
|
||||||
|
['users_id', 'users_id'],
|
||||||
|
['payload_folders_id', 'payload_folders_id'],
|
||||||
|
] as string[][]) {
|
||||||
|
await db.run(sql.raw(`CREATE INDEX ${ident(`${tableName}_${suffix}_idx`)} ON ${ident(tableName)} (${ident(column)});`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.run(sql.raw(`CREATE TABLE IF NOT EXISTS \`newsletter_form\` (${globalTableColumns()});`))
|
||||||
|
await createBenefitTable(db, 'newsletter_eval_bens', 'newsletter_form', 'text')
|
||||||
|
await createBenefitTable(db, 'newsletter_done_bens', 'newsletter_form', 'text')
|
||||||
|
await seedGlobalScalarsFromHome(db)
|
||||||
|
await seedBenefits(db, 'evaluation')
|
||||||
|
await seedBenefits(db, 'completed')
|
||||||
|
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
CREATE TABLE IF NOT EXISTS \`newsletter_subscriptions\` (
|
||||||
|
\`id\` integer PRIMARY KEY NOT NULL,
|
||||||
|
\`email\` text NOT NULL,
|
||||||
|
\`source\` text NOT NULL,
|
||||||
|
\`phase\` text NOT NULL,
|
||||||
|
\`last_submitted_at\` text NOT NULL,
|
||||||
|
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||||
|
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL
|
||||||
|
);
|
||||||
|
`))
|
||||||
|
await db.run(sql.raw('CREATE UNIQUE INDEX IF NOT EXISTS `newsletter_subscriptions_email_idx` ON `newsletter_subscriptions` (`email`);'))
|
||||||
|
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `newsletter_subscriptions_updated_at_idx` ON `newsletter_subscriptions` (`updated_at`);'))
|
||||||
|
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `newsletter_subscriptions_created_at_idx` ON `newsletter_subscriptions` (`created_at`);'))
|
||||||
|
|
||||||
|
await rebuildLockedDocumentRelations(db, true)
|
||||||
|
|
||||||
|
await removeLegacyPageFields(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
|
||||||
|
await restoreLegacyPageFields(db)
|
||||||
|
await rebuildLockedDocumentRelations(db, false)
|
||||||
|
await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_subscriptions`;'))
|
||||||
|
await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_eval_bens`;'))
|
||||||
|
await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_done_bens`;'))
|
||||||
|
await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_form`;'))
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import * as migration_20260707_123500_preistraeger_gallery from './20260707_1235
|
|||||||
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';
|
import * as migration_20260707_130500_preistraeger_awards_jury_members from './20260707_130500_preistraeger_awards_jury_members';
|
||||||
import * as migration_20260712_120000_partner_media_only from './20260712_120000_partner_media_only';
|
import * as migration_20260712_120000_partner_media_only from './20260712_120000_partner_media_only';
|
||||||
|
import * as migration_20260713_133000_newsletter_form_and_subscriptions from './20260713_133000_newsletter_form_and_subscriptions';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -66,4 +67,9 @@ export const migrations = [
|
|||||||
down: migration_20260712_120000_partner_media_only.down,
|
down: migration_20260712_120000_partner_media_only.down,
|
||||||
name: '20260712_120000_partner_media_only',
|
name: '20260712_120000_partner_media_only',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260713_133000_newsletter_form_and_subscriptions.up,
|
||||||
|
down: migration_20260713_133000_newsletter_form_and_subscriptions.down,
|
||||||
|
name: '20260713_133000_newsletter_form_and_subscriptions',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export interface Config {
|
|||||||
partners: Partner;
|
partners: Partner;
|
||||||
posts: Post;
|
posts: Post;
|
||||||
events: Event;
|
events: Event;
|
||||||
|
'newsletter-subscriptions': NewsletterSubscription;
|
||||||
media: Media;
|
media: Media;
|
||||||
users: User;
|
users: User;
|
||||||
'payload-kv': PayloadKv;
|
'payload-kv': PayloadKv;
|
||||||
@@ -94,6 +95,7 @@ export interface Config {
|
|||||||
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>;
|
||||||
|
'newsletter-subscriptions': NewsletterSubscriptionsSelect<false> | NewsletterSubscriptionsSelect<true>;
|
||||||
media: MediaSelect<false> | MediaSelect<true>;
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
@@ -112,12 +114,14 @@ export interface Config {
|
|||||||
footer: Footer;
|
footer: Footer;
|
||||||
'site-settings': SiteSetting;
|
'site-settings': SiteSetting;
|
||||||
'application-phase': ApplicationPhase;
|
'application-phase': ApplicationPhase;
|
||||||
|
'newsletter-form': NewsletterForm;
|
||||||
};
|
};
|
||||||
globalsSelect: {
|
globalsSelect: {
|
||||||
header: HeaderSelect<false> | HeaderSelect<true>;
|
header: HeaderSelect<false> | HeaderSelect<true>;
|
||||||
footer: FooterSelect<false> | FooterSelect<true>;
|
footer: FooterSelect<false> | FooterSelect<true>;
|
||||||
'site-settings': SiteSettingsSelect<false> | SiteSettingsSelect<true>;
|
'site-settings': SiteSettingsSelect<false> | SiteSettingsSelect<true>;
|
||||||
'application-phase': ApplicationPhaseSelect<false> | ApplicationPhaseSelect<true>;
|
'application-phase': ApplicationPhaseSelect<false> | ApplicationPhaseSelect<true>;
|
||||||
|
'newsletter-form': NewsletterFormSelect<false> | NewsletterFormSelect<true>;
|
||||||
};
|
};
|
||||||
locale: null;
|
locale: null;
|
||||||
widgets: {
|
widgets: {
|
||||||
@@ -382,64 +386,6 @@ export interface Page {
|
|||||||
formTitle?: string | null;
|
formTitle?: string | null;
|
||||||
footnote?: string | null;
|
footnote?: string | null;
|
||||||
footnoteStrong?: string | null;
|
footnoteStrong?: string | null;
|
||||||
/**
|
|
||||||
* Homepage newsletter copy used when the active application phase is Auswertung / Jury bewertet.
|
|
||||||
*/
|
|
||||||
newsletterPhaseEvaluation?: {
|
|
||||||
eyebrow?: string | null;
|
|
||||||
heading?: string | null;
|
|
||||||
body?: string | null;
|
|
||||||
benefits?:
|
|
||||||
| {
|
|
||||||
num?: string | null;
|
|
||||||
label?: string | null;
|
|
||||||
desc?: string | null;
|
|
||||||
id?: string | null;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
formEyebrow?: string | null;
|
|
||||||
formTitle?: string | null;
|
|
||||||
formBody?: string | null;
|
|
||||||
emailLabel?: string | null;
|
|
||||||
emailPlaceholder?: string | null;
|
|
||||||
submitLabel?: string | null;
|
|
||||||
privacy?: string | null;
|
|
||||||
validationRequired?: string | null;
|
|
||||||
validationInvalid?: string | null;
|
|
||||||
successHeading?: string | null;
|
|
||||||
successBody?: string | null;
|
|
||||||
footnote?: string | null;
|
|
||||||
footnoteStrong?: string | null;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Homepage newsletter copy used when the active application phase is Preisverleihung abgeschlossen.
|
|
||||||
*/
|
|
||||||
newsletterPhaseCompleted?: {
|
|
||||||
eyebrow?: string | null;
|
|
||||||
heading?: string | null;
|
|
||||||
body?: string | null;
|
|
||||||
benefits?:
|
|
||||||
| {
|
|
||||||
num?: string | null;
|
|
||||||
label?: string | null;
|
|
||||||
desc?: string | null;
|
|
||||||
id?: string | null;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
formEyebrow?: string | null;
|
|
||||||
formTitle?: string | null;
|
|
||||||
formBody?: string | null;
|
|
||||||
emailLabel?: string | null;
|
|
||||||
emailPlaceholder?: string | null;
|
|
||||||
submitLabel?: string | null;
|
|
||||||
privacy?: string | null;
|
|
||||||
validationRequired?: string | null;
|
|
||||||
validationInvalid?: string | null;
|
|
||||||
successHeading?: string | null;
|
|
||||||
successBody?: string | null;
|
|
||||||
footnote?: string | null;
|
|
||||||
footnoteStrong?: string | null;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Text, options, and eligibility thresholds used inside the home page multi-step application form.
|
* Text, options, and eligibility thresholds used inside the home page multi-step application form.
|
||||||
@@ -1006,64 +952,6 @@ export interface Page {
|
|||||||
label?: string | null;
|
label?: string | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Participation page newsletter copy used when the active application phase is Auswertung / Jury bewertet.
|
|
||||||
*/
|
|
||||||
newsletterPhaseEvaluation?: {
|
|
||||||
eyebrow?: string | null;
|
|
||||||
heading?: string | null;
|
|
||||||
body?: string | null;
|
|
||||||
benefits?:
|
|
||||||
| {
|
|
||||||
num?: string | null;
|
|
||||||
label?: string | null;
|
|
||||||
desc?: string | null;
|
|
||||||
id?: string | null;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
formEyebrow?: string | null;
|
|
||||||
formTitle?: string | null;
|
|
||||||
formBody?: string | null;
|
|
||||||
emailLabel?: string | null;
|
|
||||||
emailPlaceholder?: string | null;
|
|
||||||
submitLabel?: string | null;
|
|
||||||
privacy?: string | null;
|
|
||||||
validationRequired?: string | null;
|
|
||||||
validationInvalid?: string | null;
|
|
||||||
successHeading?: string | null;
|
|
||||||
successBody?: string | null;
|
|
||||||
footnote?: string | null;
|
|
||||||
footnoteStrong?: string | null;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Participation page newsletter copy used when the active application phase is Preisverleihung abgeschlossen.
|
|
||||||
*/
|
|
||||||
newsletterPhaseCompleted?: {
|
|
||||||
eyebrow?: string | null;
|
|
||||||
heading?: string | null;
|
|
||||||
body?: string | null;
|
|
||||||
benefits?:
|
|
||||||
| {
|
|
||||||
num?: string | null;
|
|
||||||
label?: string | null;
|
|
||||||
desc?: string | null;
|
|
||||||
id?: string | null;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
formEyebrow?: string | null;
|
|
||||||
formTitle?: string | null;
|
|
||||||
formBody?: string | null;
|
|
||||||
emailLabel?: string | null;
|
|
||||||
emailPlaceholder?: string | null;
|
|
||||||
submitLabel?: string | null;
|
|
||||||
privacy?: string | null;
|
|
||||||
validationRequired?: string | null;
|
|
||||||
validationInvalid?: string | null;
|
|
||||||
successHeading?: string | null;
|
|
||||||
successBody?: string | null;
|
|
||||||
footnote?: string | null;
|
|
||||||
footnoteStrong?: string | null;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Text, options, and eligibility thresholds used inside the multi-step application form.
|
* Text, options, and eligibility thresholds used inside the multi-step application form.
|
||||||
@@ -2919,6 +2807,19 @@ export interface Event {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "newsletter-subscriptions".
|
||||||
|
*/
|
||||||
|
export interface NewsletterSubscription {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
source: 'homepage' | 'participation';
|
||||||
|
phase: 'evaluation' | 'completed';
|
||||||
|
lastSubmittedAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-kv".
|
* via the `definition` "payload-kv".
|
||||||
@@ -3059,6 +2960,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'events';
|
relationTo: 'events';
|
||||||
value: number | Event;
|
value: number | Event;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'newsletter-subscriptions';
|
||||||
|
value: number | NewsletterSubscription;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'media';
|
relationTo: 'media';
|
||||||
value: number | Media;
|
value: number | Media;
|
||||||
@@ -3304,62 +3209,6 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
formTitle?: T;
|
formTitle?: T;
|
||||||
footnote?: T;
|
footnote?: T;
|
||||||
footnoteStrong?: T;
|
footnoteStrong?: T;
|
||||||
newsletterPhaseEvaluation?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
eyebrow?: T;
|
|
||||||
heading?: T;
|
|
||||||
body?: T;
|
|
||||||
benefits?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
num?: T;
|
|
||||||
label?: T;
|
|
||||||
desc?: T;
|
|
||||||
id?: T;
|
|
||||||
};
|
|
||||||
formEyebrow?: T;
|
|
||||||
formTitle?: T;
|
|
||||||
formBody?: T;
|
|
||||||
emailLabel?: T;
|
|
||||||
emailPlaceholder?: T;
|
|
||||||
submitLabel?: T;
|
|
||||||
privacy?: T;
|
|
||||||
validationRequired?: T;
|
|
||||||
validationInvalid?: T;
|
|
||||||
successHeading?: T;
|
|
||||||
successBody?: T;
|
|
||||||
footnote?: T;
|
|
||||||
footnoteStrong?: T;
|
|
||||||
};
|
|
||||||
newsletterPhaseCompleted?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
eyebrow?: T;
|
|
||||||
heading?: T;
|
|
||||||
body?: T;
|
|
||||||
benefits?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
num?: T;
|
|
||||||
label?: T;
|
|
||||||
desc?: T;
|
|
||||||
id?: T;
|
|
||||||
};
|
|
||||||
formEyebrow?: T;
|
|
||||||
formTitle?: T;
|
|
||||||
formBody?: T;
|
|
||||||
emailLabel?: T;
|
|
||||||
emailPlaceholder?: T;
|
|
||||||
submitLabel?: T;
|
|
||||||
privacy?: T;
|
|
||||||
validationRequired?: T;
|
|
||||||
validationInvalid?: T;
|
|
||||||
successHeading?: T;
|
|
||||||
successBody?: T;
|
|
||||||
footnote?: T;
|
|
||||||
footnoteStrong?: T;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
form?:
|
form?:
|
||||||
| T
|
| T
|
||||||
@@ -3827,62 +3676,6 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
label?: T;
|
label?: T;
|
||||||
url?: T;
|
url?: T;
|
||||||
};
|
};
|
||||||
newsletterPhaseEvaluation?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
eyebrow?: T;
|
|
||||||
heading?: T;
|
|
||||||
body?: T;
|
|
||||||
benefits?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
num?: T;
|
|
||||||
label?: T;
|
|
||||||
desc?: T;
|
|
||||||
id?: T;
|
|
||||||
};
|
|
||||||
formEyebrow?: T;
|
|
||||||
formTitle?: T;
|
|
||||||
formBody?: T;
|
|
||||||
emailLabel?: T;
|
|
||||||
emailPlaceholder?: T;
|
|
||||||
submitLabel?: T;
|
|
||||||
privacy?: T;
|
|
||||||
validationRequired?: T;
|
|
||||||
validationInvalid?: T;
|
|
||||||
successHeading?: T;
|
|
||||||
successBody?: T;
|
|
||||||
footnote?: T;
|
|
||||||
footnoteStrong?: T;
|
|
||||||
};
|
|
||||||
newsletterPhaseCompleted?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
eyebrow?: T;
|
|
||||||
heading?: T;
|
|
||||||
body?: T;
|
|
||||||
benefits?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
num?: T;
|
|
||||||
label?: T;
|
|
||||||
desc?: T;
|
|
||||||
id?: T;
|
|
||||||
};
|
|
||||||
formEyebrow?: T;
|
|
||||||
formTitle?: T;
|
|
||||||
formBody?: T;
|
|
||||||
emailLabel?: T;
|
|
||||||
emailPlaceholder?: T;
|
|
||||||
submitLabel?: T;
|
|
||||||
privacy?: T;
|
|
||||||
validationRequired?: T;
|
|
||||||
validationInvalid?: T;
|
|
||||||
successHeading?: T;
|
|
||||||
successBody?: T;
|
|
||||||
footnote?: T;
|
|
||||||
footnoteStrong?: T;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
form?:
|
form?:
|
||||||
| T
|
| T
|
||||||
@@ -5318,6 +5111,18 @@ export interface EventsSelect<T extends boolean = true> {
|
|||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
_status?: T;
|
_status?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "newsletter-subscriptions_select".
|
||||||
|
*/
|
||||||
|
export interface NewsletterSubscriptionsSelect<T extends boolean = true> {
|
||||||
|
email?: T;
|
||||||
|
source?: T;
|
||||||
|
phase?: T;
|
||||||
|
lastSubmittedAt?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "media_select".
|
* via the `definition` "media_select".
|
||||||
@@ -5699,6 +5504,75 @@ export interface ApplicationPhase {
|
|||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "newsletter-form".
|
||||||
|
*/
|
||||||
|
export interface NewsletterForm {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Gemeinsamer Newsletter-Inhalt für Startseite und Teilnahme-Seite während der Auswertungsphase.
|
||||||
|
*/
|
||||||
|
evaluation?: {
|
||||||
|
eyebrow?: string | null;
|
||||||
|
heading?: string | null;
|
||||||
|
body?: string | null;
|
||||||
|
benefits?:
|
||||||
|
| {
|
||||||
|
num?: string | null;
|
||||||
|
label?: string | null;
|
||||||
|
desc?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
formEyebrow?: string | null;
|
||||||
|
formTitle?: string | null;
|
||||||
|
formBody?: string | null;
|
||||||
|
emailLabel?: string | null;
|
||||||
|
emailPlaceholder?: string | null;
|
||||||
|
submitLabel?: string | null;
|
||||||
|
privacy?: string | null;
|
||||||
|
validationRequired?: string | null;
|
||||||
|
validationInvalid?: string | null;
|
||||||
|
submissionError?: string | null;
|
||||||
|
successHeading?: string | null;
|
||||||
|
successBody?: string | null;
|
||||||
|
footnote?: string | null;
|
||||||
|
footnoteStrong?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Gemeinsamer Newsletter-Inhalt für Startseite und Teilnahme-Seite nach Abschluss des Preisjahrgangs.
|
||||||
|
*/
|
||||||
|
completed?: {
|
||||||
|
eyebrow?: string | null;
|
||||||
|
heading?: string | null;
|
||||||
|
body?: string | null;
|
||||||
|
benefits?:
|
||||||
|
| {
|
||||||
|
num?: string | null;
|
||||||
|
label?: string | null;
|
||||||
|
desc?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
formEyebrow?: string | null;
|
||||||
|
formTitle?: string | null;
|
||||||
|
formBody?: string | null;
|
||||||
|
emailLabel?: string | null;
|
||||||
|
emailPlaceholder?: string | null;
|
||||||
|
submitLabel?: string | null;
|
||||||
|
privacy?: string | null;
|
||||||
|
validationRequired?: string | null;
|
||||||
|
validationInvalid?: string | null;
|
||||||
|
submissionError?: string | null;
|
||||||
|
successHeading?: string | null;
|
||||||
|
successBody?: string | null;
|
||||||
|
footnote?: string | null;
|
||||||
|
footnoteStrong?: string | null;
|
||||||
|
};
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "header_select".
|
* via the `definition` "header_select".
|
||||||
@@ -5868,6 +5742,73 @@ export interface ApplicationPhaseSelect<T extends boolean = true> {
|
|||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
globalType?: T;
|
globalType?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "newsletter-form_select".
|
||||||
|
*/
|
||||||
|
export interface NewsletterFormSelect<T extends boolean = true> {
|
||||||
|
evaluation?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
eyebrow?: T;
|
||||||
|
heading?: T;
|
||||||
|
body?: T;
|
||||||
|
benefits?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
num?: T;
|
||||||
|
label?: T;
|
||||||
|
desc?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
formEyebrow?: T;
|
||||||
|
formTitle?: T;
|
||||||
|
formBody?: T;
|
||||||
|
emailLabel?: T;
|
||||||
|
emailPlaceholder?: T;
|
||||||
|
submitLabel?: T;
|
||||||
|
privacy?: T;
|
||||||
|
validationRequired?: T;
|
||||||
|
validationInvalid?: T;
|
||||||
|
submissionError?: T;
|
||||||
|
successHeading?: T;
|
||||||
|
successBody?: T;
|
||||||
|
footnote?: T;
|
||||||
|
footnoteStrong?: T;
|
||||||
|
};
|
||||||
|
completed?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
eyebrow?: T;
|
||||||
|
heading?: T;
|
||||||
|
body?: T;
|
||||||
|
benefits?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
num?: T;
|
||||||
|
label?: T;
|
||||||
|
desc?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
formEyebrow?: T;
|
||||||
|
formTitle?: T;
|
||||||
|
formBody?: T;
|
||||||
|
emailLabel?: T;
|
||||||
|
emailPlaceholder?: T;
|
||||||
|
submitLabel?: T;
|
||||||
|
privacy?: T;
|
||||||
|
validationRequired?: T;
|
||||||
|
validationInvalid?: T;
|
||||||
|
submissionError?: T;
|
||||||
|
successHeading?: T;
|
||||||
|
successBody?: T;
|
||||||
|
footnote?: T;
|
||||||
|
footnoteStrong?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "collections_widget".
|
* via the `definition` "collections_widget".
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url'
|
|||||||
import { Events } from './collections/Events'
|
import { Events } from './collections/Events'
|
||||||
import { JuryMitglieder } from './collections/JuryMitglieder'
|
import { JuryMitglieder } from './collections/JuryMitglieder'
|
||||||
import { Media } from './collections/Media'
|
import { Media } from './collections/Media'
|
||||||
|
import { NewsletterSubscriptions } from './collections/NewsletterSubscriptions'
|
||||||
import { Pages } from './collections/Pages'
|
import { Pages } from './collections/Pages'
|
||||||
import { Partners } from './collections/Partners'
|
import { Partners } from './collections/Partners'
|
||||||
import { Posts } from './collections/Posts'
|
import { Posts } from './collections/Posts'
|
||||||
@@ -15,6 +16,7 @@ import { Users } from './collections/Users'
|
|||||||
import { ApplicationPhase } from './ApplicationPhase/config'
|
import { ApplicationPhase } from './ApplicationPhase/config'
|
||||||
import { Footer } from './Footer/config'
|
import { Footer } from './Footer/config'
|
||||||
import { Header } from './Header/config'
|
import { Header } from './Header/config'
|
||||||
|
import { NewsletterForm } from './NewsletterForm/config'
|
||||||
import { plugins } from './plugins'
|
import { plugins } from './plugins'
|
||||||
import { SiteSettings } from './SiteSettings/config'
|
import { SiteSettings } from './SiteSettings/config'
|
||||||
import { defaultLexical } from '@/fields/defaultLexical'
|
import { defaultLexical } from '@/fields/defaultLexical'
|
||||||
@@ -67,9 +69,9 @@ export default buildConfig({
|
|||||||
url: process.env.DATABASE_URL || '',
|
url: process.env.DATABASE_URL || '',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
collections: [Pages, Preistraeger, JuryMitglieder, Partners, Posts, Events, Media, Users],
|
collections: [Pages, Preistraeger, JuryMitglieder, Partners, Posts, Events, NewsletterSubscriptions, Media, Users],
|
||||||
cors: [getServerSideURL()].filter(Boolean),
|
cors: [getServerSideURL()].filter(Boolean),
|
||||||
globals: [Header, Footer, SiteSettings, ApplicationPhase],
|
globals: [Header, Footer, SiteSettings, ApplicationPhase, NewsletterForm],
|
||||||
plugins,
|
plugins,
|
||||||
secret: process.env.PAYLOAD_SECRET,
|
secret: process.env.PAYLOAD_SECRET,
|
||||||
sharp,
|
sharp,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { mediaByFilename } from './mediaByFilename'
|
|||||||
|
|
||||||
import { defaultApplicationPhaseData } from '@/spa/applicationPhase'
|
import { defaultApplicationPhaseData } from '@/spa/applicationPhase'
|
||||||
import { defaultFooterData, defaultHeaderData } from '@/spa/cmsNavigation'
|
import { defaultFooterData, defaultHeaderData } from '@/spa/cmsNavigation'
|
||||||
|
import { defaultNewsletterFormData } from '@/spa/newsletterForm'
|
||||||
import { defaultSiteSettings } from '@/spa/siteSettings'
|
import { defaultSiteSettings } from '@/spa/siteSettings'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -59,6 +60,11 @@ async function main() {
|
|||||||
context: { disableRevalidate: true },
|
context: { disableRevalidate: true },
|
||||||
data: defaultApplicationPhaseData as never,
|
data: defaultApplicationPhaseData as never,
|
||||||
}),
|
}),
|
||||||
|
payload.updateGlobal({
|
||||||
|
slug: 'newsletter-form',
|
||||||
|
context: { disableRevalidate: true },
|
||||||
|
data: defaultNewsletterFormData as never,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
payload.logger.info('Preloaded global CMS settings.')
|
payload.logger.info('Preloaded global CMS settings.')
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const orderedScripts = [
|
|||||||
path: 'src/scripts/migrate-spa-pages.ts',
|
path: 'src/scripts/migrate-spa-pages.ts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Store header, footer, site settings, and application phase defaults',
|
label: 'Store header, footer, site settings, application phase, and newsletter form defaults',
|
||||||
path: 'src/scripts/preload-globals-cms.ts',
|
path: 'src/scripts/preload-globals-cms.ts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ScrollToTop from '@/spa/components/layout/ScrollToTop'
|
|||||||
import type { SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
import type { SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
||||||
import { type SpaFooterData, type SpaHeaderData } from '@/spa/cmsNavigation'
|
import { type SpaFooterData, type SpaHeaderData } from '@/spa/cmsNavigation'
|
||||||
import { CmsRouteProvider, type CmsRouteData } from '@/spa/cmsRoute'
|
import { CmsRouteProvider, type CmsRouteData } from '@/spa/cmsRoute'
|
||||||
|
import type { SpaNewsletterFormData } from '@/spa/newsletterForm'
|
||||||
import type { SpaSiteSettingsData } from '@/spa/siteSettings'
|
import type { SpaSiteSettingsData } from '@/spa/siteSettings'
|
||||||
import About from '@/spa/pages/About'
|
import About from '@/spa/pages/About'
|
||||||
import BlogDetail from '@/spa/pages/BlogDetail'
|
import BlogDetail from '@/spa/pages/BlogDetail'
|
||||||
@@ -62,6 +63,7 @@ export default function NextApp({
|
|||||||
cmsRoute,
|
cmsRoute,
|
||||||
siteSettings,
|
siteSettings,
|
||||||
applicationPhase,
|
applicationPhase,
|
||||||
|
newsletterForm,
|
||||||
}: {
|
}: {
|
||||||
pathname: string
|
pathname: string
|
||||||
header?: SpaHeaderData
|
header?: SpaHeaderData
|
||||||
@@ -69,12 +71,13 @@ export default function NextApp({
|
|||||||
cmsRoute?: CmsRouteData
|
cmsRoute?: CmsRouteData
|
||||||
siteSettings?: SpaSiteSettingsData
|
siteSettings?: SpaSiteSettingsData
|
||||||
applicationPhase?: SpaApplicationPhaseData
|
applicationPhase?: SpaApplicationPhaseData
|
||||||
|
newsletterForm?: SpaNewsletterFormData
|
||||||
}) {
|
}) {
|
||||||
const page = React.createElement(resolvePage(pathname))
|
const page = React.createElement(resolvePage(pathname))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CmsRouteProvider value={cmsRoute ? { ...cmsRoute, siteSettings, applicationPhase } : undefined}>
|
<CmsRouteProvider value={cmsRoute ? { ...cmsRoute, siteSettings, applicationPhase, newsletterForm } : undefined}>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Layout header={header} footer={footer}>
|
<Layout header={header} footer={footer}>
|
||||||
{page}
|
{page}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const defaultApplicationPhaseData: SpaApplicationPhaseData = {
|
|||||||
'Die Bewerbungsphase 2026 ist eröffnet – es ist noch nicht zu spät. Reichen Sie Ihre Unterlagen bis zum 30. Juni kostenlos ein und zeigen Sie, was den bayerischen Mittelstand ausmacht.',
|
'Die Bewerbungsphase 2026 ist eröffnet – es ist noch nicht zu spät. Reichen Sie Ihre Unterlagen bis zum 30. Juni kostenlos ein und zeigen Sie, was den bayerischen Mittelstand ausmacht.',
|
||||||
cta: { label: 'Jetzt kostenlos bewerben', url: '/teilnahme' },
|
cta: { label: 'Jetzt kostenlos bewerben', url: '/teilnahme' },
|
||||||
accent: '#EFBF04',
|
accent: '#EFBF04',
|
||||||
bg: '#020A1E',
|
bg: '#111D55',
|
||||||
glow: 'rgba(239,191,4,0.14)',
|
glow: 'rgba(239,191,4,0.14)',
|
||||||
meta: 'Deadline: 30. Juni 2026',
|
meta: 'Deadline: 30. Juni 2026',
|
||||||
membershipCta: { label: 'Mitglied werden', url: '/mitglied-werden' },
|
membershipCta: { label: 'Mitglied werden', url: '/mitglied-werden' },
|
||||||
@@ -60,7 +60,7 @@ export const defaultApplicationPhaseData: SpaApplicationPhaseData = {
|
|||||||
'Die Anmeldephase 2026 ist geschlossen. Unsere unabhängige Expertenjury aus Wissenschaft und Praxis sichtet und bewertet derzeit alle Einreichungen.',
|
'Die Anmeldephase 2026 ist geschlossen. Unsere unabhängige Expertenjury aus Wissenschaft und Praxis sichtet und bewertet derzeit alle Einreichungen.',
|
||||||
cta: {},
|
cta: {},
|
||||||
accent: '#4A8FC9',
|
accent: '#4A8FC9',
|
||||||
bg: '#060D1C',
|
bg: '#111D55',
|
||||||
glow: 'rgba(74,143,201,0.12)',
|
glow: 'rgba(74,143,201,0.12)',
|
||||||
meta: 'Ergebnis: August 2026',
|
meta: 'Ergebnis: August 2026',
|
||||||
membershipCta: { label: 'Mitglied werden', url: '/mitglied-werden' },
|
membershipCta: { label: 'Mitglied werden', url: '/mitglied-werden' },
|
||||||
@@ -75,7 +75,7 @@ export const defaultApplicationPhaseData: SpaApplicationPhaseData = {
|
|||||||
'Wir danken allen 247 Bewerberinnen und Bewerbern für Mut und Exzellenz. Drei außergewöhnliche Unternehmen wurden in München ausgezeichnet – ein unvergesslicher Abend.',
|
'Wir danken allen 247 Bewerberinnen und Bewerbern für Mut und Exzellenz. Drei außergewöhnliche Unternehmen wurden in München ausgezeichnet – ein unvergesslicher Abend.',
|
||||||
cta: { label: 'Alle Preisträger ansehen', url: '/preistraeger' },
|
cta: { label: 'Alle Preisträger ansehen', url: '/preistraeger' },
|
||||||
accent: '#EFBF04',
|
accent: '#EFBF04',
|
||||||
bg: '#06090F',
|
bg: '#111D55',
|
||||||
glow: 'rgba(239,191,4,0.08)',
|
glow: 'rgba(239,191,4,0.08)',
|
||||||
meta: 'Gala · Oktober 2026 · München',
|
meta: 'Gala · Oktober 2026 · München',
|
||||||
successApplications: '247',
|
successApplications: '247',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import React, { createContext, useContext } from 'react'
|
import React, { createContext, useContext } from 'react'
|
||||||
|
|
||||||
import type { SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
import type { SpaApplicationPhaseData } from '@/spa/applicationPhase'
|
||||||
|
import type { SpaNewsletterFormData } from '@/spa/newsletterForm'
|
||||||
import type { SpaSiteSettingsData } from '@/spa/siteSettings'
|
import type { SpaSiteSettingsData } from '@/spa/siteSettings'
|
||||||
|
|
||||||
export type CmsRouteCollection = 'pages' | 'posts' | 'events' | 'preistraeger' | 'partners' | 'jury-mitglieder'
|
export type CmsRouteCollection = 'pages' | 'posts' | 'events' | 'preistraeger' | 'partners' | 'jury-mitglieder'
|
||||||
@@ -26,6 +27,7 @@ export type CmsRouteData = {
|
|||||||
lists?: Partial<Record<CmsRouteCollection, CmsRouteDoc[]>>
|
lists?: Partial<Record<CmsRouteCollection, CmsRouteDoc[]>>
|
||||||
siteSettings?: SpaSiteSettingsData
|
siteSettings?: SpaSiteSettingsData
|
||||||
applicationPhase?: SpaApplicationPhaseData
|
applicationPhase?: SpaApplicationPhaseData
|
||||||
|
newsletterForm?: SpaNewsletterFormData
|
||||||
}
|
}
|
||||||
|
|
||||||
const CmsRouteContext = createContext<CmsRouteData | undefined>(undefined)
|
const CmsRouteContext = createContext<CmsRouteData | undefined>(undefined)
|
||||||
@@ -53,3 +55,7 @@ export function resolveCmsRelationship(entry: unknown, docs: CmsRouteDoc[]) {
|
|||||||
export function useApplicationPhase() {
|
export function useApplicationPhase() {
|
||||||
return useCmsRoute()?.applicationPhase
|
return useCmsRoute()?.applicationPhase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useNewsletterForm() {
|
||||||
|
return useCmsRoute()?.newsletterForm
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,42 +2,27 @@ import React, { useState } from 'react';
|
|||||||
import { Check, Mail } from 'lucide-react';
|
import { Check, Mail } from 'lucide-react';
|
||||||
|
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import type { NewsletterInterestCopy, NewsletterPhase, NewsletterSource } from '@/spa/newsletterForm';
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
|
|
||||||
export type NewsletterBenefit = { num: string; label: string; desc: string };
|
export function NewsletterInterestForm({
|
||||||
|
copy,
|
||||||
export type NewsletterInterestCopy = {
|
phase,
|
||||||
eyebrow: string;
|
source,
|
||||||
heading: string;
|
}: {
|
||||||
body: string;
|
copy: NewsletterInterestCopy;
|
||||||
benefits: readonly NewsletterBenefit[];
|
phase: NewsletterPhase;
|
||||||
formEyebrow: string;
|
source: NewsletterSource;
|
||||||
formTitle: string;
|
}) {
|
||||||
formBody: string;
|
|
||||||
emailLabel: string;
|
|
||||||
emailPlaceholder: string;
|
|
||||||
submitLabel: string;
|
|
||||||
privacy: string;
|
|
||||||
validationRequired: string;
|
|
||||||
validationInvalid: string;
|
|
||||||
successHeading: string;
|
|
||||||
successBody: string;
|
|
||||||
footnote: string;
|
|
||||||
footnoteStrong: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NewsletterInterestCms = Partial<Omit<NewsletterInterestCopy, 'benefits'>> & {
|
|
||||||
benefits?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function NewsletterInterestForm({ copy }: { copy: NewsletterInterestCopy }) {
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
const [website, setWebsite] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const trimmedEmail = email.trim();
|
const trimmedEmail = email.trim();
|
||||||
|
|
||||||
@@ -52,7 +37,22 @@ export function NewsletterInterestForm({ copy }: { copy: NewsletterInterestCopy
|
|||||||
}
|
}
|
||||||
|
|
||||||
setError('');
|
setError('');
|
||||||
setSubmitted(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/newsletter-subscriptions/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: trimmedEmail, phase, source, website }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`Newsletter subscription failed with ${response.status}`);
|
||||||
|
setSubmitted(true);
|
||||||
|
} catch {
|
||||||
|
setError(copy.submissionError);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
@@ -98,12 +98,26 @@ export function NewsletterInterestForm({ copy }: { copy: NewsletterInterestCopy
|
|||||||
<p style={{ fontFamily: FF, fontSize: 16, color: 'rgba(17,29,85,0.68)', lineHeight: 1.75, margin: '0 0 28px', maxWidth: 520 }}>
|
<p style={{ fontFamily: FF, fontSize: 16, color: 'rgba(17,29,85,0.68)', lineHeight: 1.75, margin: '0 0 28px', maxWidth: 520 }}>
|
||||||
{copy.formBody}
|
{copy.formBody}
|
||||||
</p>
|
</p>
|
||||||
<label style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: 'rgba(17,29,85,0.5)', marginBottom: 8 }}>
|
<div aria-hidden="true" style={{ position: 'absolute', left: '-10000px', width: 1, height: 1, overflow: 'hidden' }}>
|
||||||
|
<label htmlFor={`newsletter-website-${source}`}>Website</label>
|
||||||
|
<input
|
||||||
|
id={`newsletter-website-${source}`}
|
||||||
|
name="website"
|
||||||
|
type="text"
|
||||||
|
tabIndex={-1}
|
||||||
|
autoComplete="off"
|
||||||
|
value={website}
|
||||||
|
onChange={(event) => setWebsite(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label htmlFor={`newsletter-email-${source}`} style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: 'rgba(17,29,85,0.5)', marginBottom: 8 }}>
|
||||||
{copy.emailLabel}
|
{copy.emailLabel}
|
||||||
</label>
|
</label>
|
||||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, width: '100%', maxWidth: 560 }}>
|
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, width: '100%', maxWidth: 560 }}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<input
|
<input
|
||||||
|
id={`newsletter-email-${source}`}
|
||||||
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -125,10 +139,12 @@ export function NewsletterInterestForm({ copy }: { copy: NewsletterInterestCopy
|
|||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{error && <div style={{ fontFamily: FF, fontSize: 12, color: '#b91c1c', marginTop: 7 }}>{error}</div>}
|
{error && <div role="alert" style={{ fontFamily: FF, fontSize: 12, color: '#b91c1c', marginTop: 7 }}>{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={submitting}
|
||||||
|
aria-busy={submitting}
|
||||||
style={{
|
style={{
|
||||||
height: 50,
|
height: 50,
|
||||||
padding: '0 24px',
|
padding: '0 24px',
|
||||||
@@ -144,7 +160,8 @@ export function NewsletterInterestForm({ copy }: { copy: NewsletterInterestCopy
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
cursor: 'pointer',
|
cursor: submitting ? 'wait' : 'pointer',
|
||||||
|
opacity: submitting ? 0.72 : 1,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -111,56 +111,6 @@ export const homeContent = {
|
|||||||
formTitle: 'Kostenlos bewerben',
|
formTitle: 'Kostenlos bewerben',
|
||||||
footnote: 'Kostenlos und unverbindlich –',
|
footnote: 'Kostenlos und unverbindlich –',
|
||||||
footnoteStrong: 'keine Teilnahmegebühr',
|
footnoteStrong: 'keine Teilnahmegebühr',
|
||||||
newsletterPhaseEvaluation: {
|
|
||||||
eyebrow: 'Updates erhalten',
|
|
||||||
heading: 'BLEIBEN\nSIE AUF\nDEM LAUFENDEN.',
|
|
||||||
body:
|
|
||||||
'Die Bewerbungsphase 2026 ist geschlossen. Hinterlassen Sie Ihre E-Mail und erhalten Sie Updates zur Juryphase, zur Preisverleihung und zum Start der nächsten Runde.',
|
|
||||||
benefits: [
|
|
||||||
{ num: '01', label: 'Juryphase', desc: 'Updates zum Verlauf' },
|
|
||||||
{ num: '02', label: 'Preisverleihung', desc: 'Einladungen und Termine' },
|
|
||||||
{ num: '03', label: 'Nächste Runde', desc: 'Start nicht verpassen' },
|
|
||||||
],
|
|
||||||
formEyebrow: 'Newsletter abonnieren',
|
|
||||||
formTitle: 'Updates erhalten',
|
|
||||||
formBody:
|
|
||||||
'Tragen Sie Ihre E-Mail ein und erhalten Sie Updates zu Terminen, Preisträgern und zur nächsten Bewerbungsphase.',
|
|
||||||
emailLabel: 'E-Mail-Adresse',
|
|
||||||
emailPlaceholder: 'name@unternehmen.de',
|
|
||||||
submitLabel: 'Eintragen',
|
|
||||||
privacy: 'Kein Spam. Nur relevante Informationen rund um den Bayerischen Mittelstandspreis.',
|
|
||||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
|
||||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
|
||||||
successHeading: 'Danke',
|
|
||||||
successBody: 'Ihre E-Mail wurde für BMP-Updates eingetragen.',
|
|
||||||
footnote: 'BMP-Updates -',
|
|
||||||
footnoteStrong: 'Termine und nächste Runde',
|
|
||||||
},
|
|
||||||
newsletterPhaseCompleted: {
|
|
||||||
eyebrow: 'Nächste Runde',
|
|
||||||
heading: 'DEN START\nNICHT\nVERPASSEN.',
|
|
||||||
body:
|
|
||||||
'Der aktuelle Preisjahrgang ist abgeschlossen. Hinterlassen Sie Ihre E-Mail und wir informieren Sie, sobald es Neuigkeiten zur nächsten Bewerbungsphase gibt.',
|
|
||||||
benefits: [
|
|
||||||
{ num: '01', label: 'Startsignal', desc: 'Neue Bewerbungsphase' },
|
|
||||||
{ num: '02', label: 'BMP-News', desc: 'Preisträger und Termine' },
|
|
||||||
{ num: '03', label: 'Erinnerung', desc: 'Rechtzeitig vorbereitet' },
|
|
||||||
],
|
|
||||||
formEyebrow: 'Newsletter abonnieren',
|
|
||||||
formTitle: 'Informiert bleiben',
|
|
||||||
formBody:
|
|
||||||
'Tragen Sie sich ein und erhalten Sie Hinweise zum Bewerbungsstart, zu Veranstaltungen und zu ausgezeichneten Unternehmen.',
|
|
||||||
emailLabel: 'E-Mail-Adresse',
|
|
||||||
emailPlaceholder: 'name@unternehmen.de',
|
|
||||||
submitLabel: 'Abonnieren',
|
|
||||||
privacy: 'Sie erhalten nur BMP-relevante Informationen. Eine Abmeldung ist jederzeit möglich.',
|
|
||||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
|
||||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
|
||||||
successHeading: 'Angemeldet',
|
|
||||||
successBody: 'Danke. Sie erhalten BMP-Updates an die angegebene E-Mail-Adresse.',
|
|
||||||
footnote: 'Bewerbungsstart im Blick -',
|
|
||||||
footnoteStrong: 'BMP-Updates erhalten',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
form: participationContent.form,
|
form: participationContent.form,
|
||||||
videoModal: {
|
videoModal: {
|
||||||
|
|||||||
146
src/spa/newsletterForm.ts
Normal file
146
src/spa/newsletterForm.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
export type NewsletterBenefit = {
|
||||||
|
num: string
|
||||||
|
label: string
|
||||||
|
desc: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewsletterInterestCopy = {
|
||||||
|
eyebrow: string
|
||||||
|
heading: string
|
||||||
|
body: string
|
||||||
|
benefits: readonly NewsletterBenefit[]
|
||||||
|
formEyebrow: string
|
||||||
|
formTitle: string
|
||||||
|
formBody: string
|
||||||
|
emailLabel: string
|
||||||
|
emailPlaceholder: string
|
||||||
|
submitLabel: string
|
||||||
|
privacy: string
|
||||||
|
validationRequired: string
|
||||||
|
validationInvalid: string
|
||||||
|
submissionError: string
|
||||||
|
successHeading: string
|
||||||
|
successBody: string
|
||||||
|
footnote: string
|
||||||
|
footnoteStrong: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewsletterInterestCms = Partial<Omit<NewsletterInterestCopy, 'benefits'>> & {
|
||||||
|
benefits?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SpaNewsletterFormData = {
|
||||||
|
evaluation: NewsletterInterestCopy
|
||||||
|
completed: NewsletterInterestCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewsletterSource = 'homepage' | 'participation'
|
||||||
|
export type NewsletterPhase = 'evaluation' | 'completed'
|
||||||
|
|
||||||
|
export const defaultNewsletterFormData: SpaNewsletterFormData = {
|
||||||
|
evaluation: {
|
||||||
|
eyebrow: 'Updates erhalten',
|
||||||
|
heading: 'BLEIBEN\nSIE AUF\nDEM LAUFENDEN.',
|
||||||
|
body:
|
||||||
|
'Die Bewerbungsphase 2026 ist geschlossen. Hinterlassen Sie Ihre E-Mail und erhalten Sie Updates zur Juryphase, zur Preisverleihung und zum Start der nächsten Runde.',
|
||||||
|
benefits: [
|
||||||
|
{ num: '01', label: 'Juryphase', desc: 'Updates zum Verlauf' },
|
||||||
|
{ num: '02', label: 'Preisverleihung', desc: 'Einladungen und Termine' },
|
||||||
|
{ num: '03', label: 'Nächste Runde', desc: 'Start nicht verpassen' },
|
||||||
|
],
|
||||||
|
formEyebrow: 'Newsletter abonnieren',
|
||||||
|
formTitle: 'Updates erhalten',
|
||||||
|
formBody:
|
||||||
|
'Tragen Sie Ihre E-Mail ein und erhalten Sie Updates zu Terminen, Preisträgern und zur nächsten Bewerbungsphase.',
|
||||||
|
emailLabel: 'E-Mail-Adresse',
|
||||||
|
emailPlaceholder: 'name@unternehmen.de',
|
||||||
|
submitLabel: 'Eintragen',
|
||||||
|
privacy: 'Kein Spam. Nur relevante Informationen rund um den Bayerischen Mittelstandspreis.',
|
||||||
|
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||||
|
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||||
|
submissionError: 'Die Anmeldung konnte nicht gespeichert werden. Bitte versuchen Sie es später erneut.',
|
||||||
|
successHeading: 'Danke',
|
||||||
|
successBody: 'Ihre E-Mail wurde für BMP-Updates eingetragen.',
|
||||||
|
footnote: 'BMP-Updates -',
|
||||||
|
footnoteStrong: 'Termine und nächste Runde',
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
eyebrow: 'Nächste Runde',
|
||||||
|
heading: 'DEN START\nNICHT\nVERPASSEN.',
|
||||||
|
body:
|
||||||
|
'Der aktuelle Preisjahrgang ist abgeschlossen. Hinterlassen Sie Ihre E-Mail und wir informieren Sie, sobald es Neuigkeiten zur nächsten Bewerbungsphase gibt.',
|
||||||
|
benefits: [
|
||||||
|
{ num: '01', label: 'Startsignal', desc: 'Neue Bewerbungsphase' },
|
||||||
|
{ num: '02', label: 'BMP-News', desc: 'Preisträger und Termine' },
|
||||||
|
{ num: '03', label: 'Erinnerung', desc: 'Rechtzeitig vorbereitet' },
|
||||||
|
],
|
||||||
|
formEyebrow: 'Newsletter abonnieren',
|
||||||
|
formTitle: 'Informiert bleiben',
|
||||||
|
formBody:
|
||||||
|
'Tragen Sie sich ein und erhalten Sie Hinweise zum Bewerbungsstart, zu Veranstaltungen und zu ausgezeichneten Unternehmen.',
|
||||||
|
emailLabel: 'E-Mail-Adresse',
|
||||||
|
emailPlaceholder: 'name@unternehmen.de',
|
||||||
|
submitLabel: 'Abonnieren',
|
||||||
|
privacy: 'Sie erhalten nur BMP-relevante Informationen. Eine Abmeldung ist jederzeit möglich.',
|
||||||
|
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
||||||
|
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
||||||
|
submissionError: 'Die Anmeldung konnte nicht gespeichert werden. Bitte versuchen Sie es später erneut.',
|
||||||
|
successHeading: 'Angemeldet',
|
||||||
|
successBody: 'Danke. Sie erhalten BMP-Updates an die angegebene E-Mail-Adresse.',
|
||||||
|
footnote: 'Bewerbungsstart im Blick -',
|
||||||
|
footnoteStrong: 'BMP-Updates erhalten',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = (value: unknown, fallback: string) =>
|
||||||
|
typeof value === 'string' && value.trim().length > 0 ? value : fallback
|
||||||
|
|
||||||
|
function normalizeVariant(value: NewsletterInterestCms | null | undefined, fallback: NewsletterInterestCopy) {
|
||||||
|
const benefits = Array.isArray(value?.benefits)
|
||||||
|
? value.benefits.filter(
|
||||||
|
(benefit): benefit is NewsletterBenefit =>
|
||||||
|
Boolean(benefit) &&
|
||||||
|
typeof benefit === 'object' &&
|
||||||
|
typeof benefit.num === 'string' &&
|
||||||
|
typeof benefit.label === 'string' &&
|
||||||
|
typeof benefit.desc === 'string',
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return {
|
||||||
|
eyebrow: text(value?.eyebrow, fallback.eyebrow),
|
||||||
|
heading: text(value?.heading, fallback.heading),
|
||||||
|
body: text(value?.body, fallback.body),
|
||||||
|
benefits: benefits.length > 0 ? benefits : fallback.benefits,
|
||||||
|
formEyebrow: text(value?.formEyebrow, fallback.formEyebrow),
|
||||||
|
formTitle: text(value?.formTitle, fallback.formTitle),
|
||||||
|
formBody: text(value?.formBody, fallback.formBody),
|
||||||
|
emailLabel: text(value?.emailLabel, fallback.emailLabel),
|
||||||
|
emailPlaceholder: text(value?.emailPlaceholder, fallback.emailPlaceholder),
|
||||||
|
submitLabel: text(value?.submitLabel, fallback.submitLabel),
|
||||||
|
privacy: text(value?.privacy, fallback.privacy),
|
||||||
|
validationRequired: text(value?.validationRequired, fallback.validationRequired),
|
||||||
|
validationInvalid: text(value?.validationInvalid, fallback.validationInvalid),
|
||||||
|
submissionError: text(value?.submissionError, fallback.submissionError),
|
||||||
|
successHeading: text(value?.successHeading, fallback.successHeading),
|
||||||
|
successBody: text(value?.successBody, fallback.successBody),
|
||||||
|
footnote: text(value?.footnote, fallback.footnote),
|
||||||
|
footnoteStrong: text(value?.footnoteStrong, fallback.footnoteStrong),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeNewsletterFormData(value?: Partial<SpaNewsletterFormData> | null): SpaNewsletterFormData {
|
||||||
|
return {
|
||||||
|
evaluation: normalizeVariant(value?.evaluation, defaultNewsletterFormData.evaluation),
|
||||||
|
completed: normalizeVariant(value?.completed, defaultNewsletterFormData.completed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newsletterFormCopyForPhase(data: SpaNewsletterFormData | undefined, activePhase?: string) {
|
||||||
|
const normalized = data || defaultNewsletterFormData
|
||||||
|
return activePhase === '1' ? normalized.evaluation : normalized.completed
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newsletterPhaseForApplicationPhase(activePhase?: string): NewsletterPhase {
|
||||||
|
return activePhase === '1' ? 'evaluation' : 'completed'
|
||||||
|
}
|
||||||
@@ -5,18 +5,19 @@ import { Link } from '@/spa/router';
|
|||||||
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
|
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
|
||||||
import { WINNERS } from '@/spa/data/winners';
|
import { WINNERS } from '@/spa/data/winners';
|
||||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
||||||
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
|
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
|
||||||
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
import type { SpaApplicationPhaseData, SpaApplicationPhaseItem } from '@/spa/applicationPhase'
|
import type { SpaApplicationPhaseData, SpaApplicationPhaseItem } from '@/spa/applicationPhase'
|
||||||
import { resolveCmsRelationship, useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRoute'
|
import { resolveCmsRelationship, useApplicationPhase, useCmsCollection, useCmsRoute, useNewsletterForm } 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'
|
||||||
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
|
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
|
||||||
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
|
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
|
||||||
|
import { newsletterFormCopyForPhase, newsletterPhaseForApplicationPhase } from '@/spa/newsletterForm'
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
|
|
||||||
@@ -57,8 +58,6 @@ type HomeSectionCms = CmsRecord & {
|
|||||||
imageAlt?: string | null;
|
imageAlt?: string | null;
|
||||||
imageLabel?: string | null;
|
imageLabel?: string | null;
|
||||||
items?: unknown;
|
items?: unknown;
|
||||||
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
|
||||||
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
quote?: string | null;
|
quote?: string | null;
|
||||||
stats?: unknown;
|
stats?: unknown;
|
||||||
@@ -118,39 +117,17 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
|
|||||||
|
|
||||||
const HIDE_MEMBERSHIP_HOME_HERO_CTA = true;
|
const HIDE_MEMBERSHIP_HOME_HERO_CTA = true;
|
||||||
const APPLICATION_PHASE_OPEN = '0';
|
const APPLICATION_PHASE_OPEN = '0';
|
||||||
|
const STATUS_SECTION_BACKGROUND = '#111D55';
|
||||||
|
const LEGACY_STATUS_SECTION_BACKGROUNDS = new Set(['#020A1E', '#060D1C', '#06090F']);
|
||||||
|
|
||||||
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
|
const statusSectionBackground = (value?: string | null) => {
|
||||||
'1': homeContent.application.newsletterPhaseEvaluation,
|
const background = value?.trim();
|
||||||
'2': homeContent.application.newsletterPhaseCompleted,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeNewsletterInterestCopy = (
|
if (!background || LEGACY_STATUS_SECTION_BACKGROUNDS.has(background.toUpperCase())) {
|
||||||
value: NewsletterInterestCms | null | undefined,
|
return STATUS_SECTION_BACKGROUND;
|
||||||
fallback: NewsletterInterestCopy,
|
}
|
||||||
): NewsletterInterestCopy => ({
|
|
||||||
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
|
|
||||||
heading: fallbackText(value?.heading, fallback.heading),
|
|
||||||
body: fallbackText(value?.body, fallback.body),
|
|
||||||
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
|
|
||||||
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
|
|
||||||
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
|
|
||||||
formBody: fallbackText(value?.formBody, fallback.formBody),
|
|
||||||
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
|
|
||||||
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
|
|
||||||
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
|
|
||||||
privacy: fallbackText(value?.privacy, fallback.privacy),
|
|
||||||
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
|
|
||||||
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
|
|
||||||
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
|
|
||||||
successBody: fallbackText(value?.successBody, fallback.successBody),
|
|
||||||
footnote: fallbackText(value?.footnote, fallback.footnote),
|
|
||||||
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
|
|
||||||
});
|
|
||||||
|
|
||||||
const getNewsletterInterestCopy = (phase: string | undefined, application: HomeSectionCms) => {
|
return background;
|
||||||
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
|
|
||||||
const cmsCopy = phase === '1' ? application.newsletterPhaseEvaluation : application.newsletterPhaseCompleted;
|
|
||||||
return mergeNewsletterInterestCopy(cmsCopy, fallback);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | null) =>
|
const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | null) =>
|
||||||
@@ -199,11 +176,13 @@ function HomeWinnerCard({
|
|||||||
fallbackTitle,
|
fallbackTitle,
|
||||||
fallbackImage,
|
fallbackImage,
|
||||||
hoverLabel,
|
hoverLabel,
|
||||||
|
horizontalScroll = false,
|
||||||
}: {
|
}: {
|
||||||
winner: WinnerCardData;
|
winner: WinnerCardData;
|
||||||
fallbackTitle: string;
|
fallbackTitle: string;
|
||||||
fallbackImage: string;
|
fallbackImage: string;
|
||||||
hoverLabel: string;
|
hoverLabel: string;
|
||||||
|
horizontalScroll?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@@ -220,7 +199,7 @@ function HomeWinnerCard({
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={href}
|
to={href}
|
||||||
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile ? 'start' : undefined }}
|
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile || horizontalScroll ? 'start' : undefined }}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
@@ -265,9 +244,11 @@ const Home: React.FC = () => {
|
|||||||
const form = home.form || {};
|
const form = home.form || {};
|
||||||
const videoModal = home.videoModal || {};
|
const videoModal = home.videoModal || {};
|
||||||
const applicationPhase = useApplicationPhase();
|
const applicationPhase = useApplicationPhase();
|
||||||
|
const newsletterForm = useNewsletterForm();
|
||||||
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
||||||
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
||||||
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, application);
|
const newsletterCopy = newsletterFormCopyForPhase(newsletterForm, activeApplicationPhase);
|
||||||
|
const newsletterPhase = newsletterPhaseForApplicationPhase(activeApplicationPhase);
|
||||||
const cmsPreistraeger = useCmsCollection('preistraeger');
|
const cmsPreistraeger = useCmsCollection('preistraeger');
|
||||||
const selectedWinners = fallbackArray(winnersSection.featured, []);
|
const selectedWinners = fallbackArray(winnersSection.featured, []);
|
||||||
const heroSecondaryCta = {
|
const heroSecondaryCta = {
|
||||||
@@ -281,6 +262,8 @@ const Home: React.FC = () => {
|
|||||||
const homeWinners = selectedWinnerDocs.length
|
const homeWinners = selectedWinnerDocs.length
|
||||||
? selectedWinnerDocs
|
? selectedWinnerDocs
|
||||||
: (cmsPreistraeger.length ? cmsPreistraeger : WINNERS).filter((w) => Number((w as { year?: string | number }).year) === 2025).slice(0, 8);
|
: (cmsPreistraeger.length ? cmsPreistraeger : WINNERS).filter((w) => Number((w as { year?: string | number }).year) === 2025).slice(0, 8);
|
||||||
|
const scrollDesktopWinners = !isMobile && homeWinners.length > 8;
|
||||||
|
const scrollWinnerGrid = isMobile || scrollDesktopWinners;
|
||||||
const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria);
|
const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria);
|
||||||
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
|
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
|
||||||
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
|
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
|
||||||
@@ -313,53 +296,58 @@ const Home: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container mx-auto relative z-10" style={{ padding: isMobile ? '0 24px' : '0 24px' }}>
|
<div className="container mx-auto relative z-10" style={{ padding: isMobile ? '0 24px' : '0 24px' }}>
|
||||||
<div style={{ maxWidth: isMobile ? '100%' : '48rem' }}>
|
<div style={{ maxWidth: isMobile ? '100%' : '62rem' }}>
|
||||||
<div className="inline-flex items-center gap-3 bg-accent/20 border border-accent/30 rounded-full px-4 py-1.5 mb-8 backdrop-blur-sm animate-fade-in" style={{ marginBottom: isMobile ? 20 : undefined }}>
|
<div className="inline-flex items-center gap-3 bg-accent/20 border border-accent/30 rounded-full px-4 py-1.5 mb-8 backdrop-blur-sm animate-fade-in" style={{ marginBottom: isMobile ? 20 : undefined }}>
|
||||||
<span className="flex h-2 w-2 rounded-full bg-accent"></span>
|
<span className="flex h-2 w-2 rounded-full bg-accent"></span>
|
||||||
<span className="text-accent text-[10px] uppercase tracking-[0.2em] font-bold">{fallbackText(hero.badge, homeContent.hero.badge)}</span>
|
<span className="text-accent text-[10px] uppercase tracking-[0.2em] font-bold">{fallbackText(hero.badge, homeContent.hero.badge)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="font-display font-bold text-white mb-6 leading-[0.9] tracking-tight" style={{ fontSize: isMobile ? 'clamp(1.8rem, 10vw, 3rem)' : 'clamp(3.5rem, 8vw, 6rem)' }}>
|
<h1
|
||||||
{fallbackText(hero.headingLine1, homeContent.hero.headingLine1)} <br />
|
className="font-display font-bold text-white mb-6 leading-[0.9] tracking-tight"
|
||||||
<span className="text-accent">{fallbackText(hero.headingAccent, homeContent.hero.headingAccent)}</span>
|
style={{ fontSize: isMobile ? 'clamp(1.75rem, 8vw, 3rem)' : 'clamp(3.25rem, 6.5vw, 6rem)' }}
|
||||||
|
>
|
||||||
|
<span style={{ display: 'block', whiteSpace: 'nowrap' }}>
|
||||||
|
{fallbackText(hero.headingLine1, homeContent.hero.headingLine1)}
|
||||||
|
</span>
|
||||||
|
<span className="text-accent" style={{ display: 'block', whiteSpace: 'nowrap' }}>
|
||||||
|
{fallbackText(hero.headingAccent, homeContent.hero.headingAccent)}
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-white/70 mb-12 font-light leading-relaxed" style={{ fontSize: isMobile ? '1.0625rem' : '1.375rem', maxWidth: isMobile ? '100%' : '36rem', marginBottom: isMobile ? 28 : undefined }}>
|
<p className="text-white/70 mb-12 font-light leading-relaxed" style={{ fontSize: isMobile ? '1.0625rem' : '1.375rem', maxWidth: isMobile ? '100%' : '36rem', marginBottom: isMobile ? 28 : undefined }}>
|
||||||
{fallbackText(hero.description, homeContent.hero.description)}
|
{fallbackText(hero.description, homeContent.hero.description)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', flexWrap: isMobile ? 'nowrap' : 'wrap', gap: 16, alignItems: isMobile ? 'stretch' : 'center' }}>
|
||||||
<Link
|
<Link
|
||||||
to={fallbackText(hero.primaryCta?.url, homeContent.hero.primaryCta.url)}
|
to={fallbackText(hero.primaryCta?.url, homeContent.hero.primaryCta.url)}
|
||||||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'#101828', background:'#EFBF04', padding:'16px 36px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'background 0.15s, box-shadow 0.2s', width: isMobile ? '100%' : 'auto' }}
|
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'#101828', background:'#EFBF04', padding:'16px 24px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:10, transition:'background 0.15s, box-shadow 0.2s', width: isMobile ? '100%' : 'auto' }}
|
||||||
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 = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
||||||
>
|
>
|
||||||
{fallbackText(hero.primaryCta?.label, homeContent.hero.primaryCta.label)} <ChevronRight size={16} />
|
{fallbackText(hero.primaryCta?.label, homeContent.hero.primaryCta.label)} <ChevronRight size={16} />
|
||||||
</Link>
|
</Link>
|
||||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 16, alignItems: isMobile ? 'stretch' : 'center' }}>
|
<button
|
||||||
{showHeroSecondaryCta && (
|
type="button"
|
||||||
<Link
|
onClick={() => setShowVideo(true)}
|
||||||
to={heroSecondaryCta.url}
|
className="flex items-center gap-4 text-white hover:text-accent transition-colors group"
|
||||||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'rgba(255,255,255,0.85)', border:'1.5px solid rgba(255,255,255,0.35)', padding:'14px 32px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'border-color 0.15s, color 0.15s', background:'transparent', width: isMobile ? '100%' : 'auto' }}
|
style={{ justifyContent: isMobile ? 'center' : 'flex-start' }}
|
||||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#EFBF04'; }}
|
>
|
||||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.35)'; (e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.85)'; }}
|
<div className="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-all">
|
||||||
>
|
<Play size={20} fill="currentColor" />
|
||||||
{heroSecondaryCta.label}
|
</div>
|
||||||
</Link>
|
<span className="uppercase text-xs tracking-widest font-bold">{fallbackText(hero.videoLabel, homeContent.hero.videoLabel)}</span>
|
||||||
)}
|
</button>
|
||||||
<button
|
{showHeroSecondaryCta && (
|
||||||
type="button"
|
<Link
|
||||||
onClick={() => setShowVideo(true)}
|
to={heroSecondaryCta.url}
|
||||||
className="flex items-center gap-4 text-white hover:text-accent transition-colors group"
|
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'rgba(255,255,255,0.85)', border:'1.5px solid rgba(255,255,255,0.35)', padding:'14px 32px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:10, transition:'border-color 0.15s, color 0.15s', background:'transparent', width: isMobile ? '100%' : 'auto' }}
|
||||||
style={{ justifyContent: isMobile ? 'center' : 'flex-start' }}
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#EFBF04'; }}
|
||||||
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.35)'; (e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.85)'; }}
|
||||||
>
|
>
|
||||||
<div className="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-all">
|
{heroSecondaryCta.label}
|
||||||
<Play size={20} fill="currentColor" />
|
</Link>
|
||||||
</div>
|
)}
|
||||||
<span className="uppercase text-xs tracking-widest font-bold">{fallbackText(hero.videoLabel, homeContent.hero.videoLabel)}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,12 +390,13 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)',
|
gridTemplateColumns: scrollWinnerGrid ? undefined : 'repeat(4, 1fr)',
|
||||||
gridAutoFlow: isMobile ? 'column' : undefined,
|
gridAutoFlow: scrollWinnerGrid ? 'column' : undefined,
|
||||||
gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined,
|
gridTemplateRows: scrollWinnerGrid ? `repeat(2, ${isMobile ? '1fr' : '280px'})` : undefined,
|
||||||
gridAutoColumns: isMobile ? '66vw' : undefined,
|
gridAutoColumns: isMobile ? '66vw' : scrollDesktopWinners ? '25vw' : undefined,
|
||||||
overflowX: isMobile ? 'auto' : undefined,
|
overflowX: scrollWinnerGrid ? 'auto' : undefined,
|
||||||
scrollSnapType: isMobile ? 'x mandatory' : undefined,
|
scrollSnapType: scrollWinnerGrid ? 'x mandatory' : undefined,
|
||||||
|
overscrollBehaviorX: scrollWinnerGrid ? 'contain' : undefined,
|
||||||
gap: isMobile ? 8 : 0,
|
gap: isMobile ? 8 : 0,
|
||||||
padding: isMobile ? '0 16px 8px' : 0,
|
padding: isMobile ? '0 16px 8px' : 0,
|
||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
@@ -420,6 +409,7 @@ const Home: React.FC = () => {
|
|||||||
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
|
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
|
||||||
fallbackImage={winnerFallbackImage}
|
fallbackImage={winnerFallbackImage}
|
||||||
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
|
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
|
||||||
|
horizontalScroll={scrollDesktopWinners}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -716,7 +706,7 @@ const Home: React.FC = () => {
|
|||||||
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile && showApplicationForm ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
|
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile && showApplicationForm ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
|
||||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{showApplicationForm ? fallbackText(application.formEyebrow, homeContent.application.formEyebrow) : newsletterCopy.formEyebrow}</span>
|
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{showApplicationForm ? fallbackText(application.formEyebrow, homeContent.application.formEyebrow) : newsletterCopy.formEyebrow}</span>
|
||||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{showApplicationForm ? fallbackText(application.formTitle, homeContent.application.formTitle) : newsletterCopy.formTitle}</h3>
|
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{showApplicationForm ? fallbackText(application.formTitle, homeContent.application.formTitle) : newsletterCopy.formTitle}</h3>
|
||||||
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} />}
|
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} phase={newsletterPhase} source="homepage" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -784,7 +774,7 @@ function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
|
|||||||
return {
|
return {
|
||||||
...phase,
|
...phase,
|
||||||
accent: phase.accent || '#EFBF04',
|
accent: phase.accent || '#EFBF04',
|
||||||
bg: phase.bg || '#020A1E',
|
bg: statusSectionBackground(phase.bg),
|
||||||
cta,
|
cta,
|
||||||
membershipCta,
|
membershipCta,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import React, { useRef } from 'react';
|
|||||||
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react';
|
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react';
|
||||||
import { Link } from '@/spa/router';
|
import { Link } from '@/spa/router';
|
||||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
||||||
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
|
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
|
||||||
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
|
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
|
||||||
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
|
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
|
||||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
import { useApplicationPhase, useCmsRoute } from '@/spa/cmsRoute';
|
import { useApplicationPhase, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute';
|
||||||
import { participationContent } from '@/spa/participationContent';
|
import { participationContent } from '@/spa/participationContent';
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
import { newsletterFormCopyForPhase, newsletterPhaseForApplicationPhase } from '@/spa/newsletterForm';
|
||||||
|
|
||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const FB = '"Inter", sans-serif';
|
const FB = '"Inter", sans-serif';
|
||||||
@@ -26,8 +27,6 @@ type ParticipationCms = Partial<typeof participationContent> & {
|
|||||||
|
|
||||||
type ParticipationApplicationFormCms = Partial<typeof participationContent.applicationForm> & {
|
type ParticipationApplicationFormCms = Partial<typeof participationContent.applicationForm> & {
|
||||||
image?: unknown;
|
image?: unknown;
|
||||||
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
|
||||||
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -48,40 +47,6 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
|
|||||||
|
|
||||||
const APPLICATION_PHASE_OPEN = '0';
|
const APPLICATION_PHASE_OPEN = '0';
|
||||||
|
|
||||||
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
|
|
||||||
'1': participationContent.applicationForm.newsletterPhaseEvaluation,
|
|
||||||
'2': participationContent.applicationForm.newsletterPhaseCompleted,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeNewsletterInterestCopy = (
|
|
||||||
value: NewsletterInterestCms | null | undefined,
|
|
||||||
fallback: NewsletterInterestCopy,
|
|
||||||
): NewsletterInterestCopy => ({
|
|
||||||
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
|
|
||||||
heading: fallbackText(value?.heading, fallback.heading),
|
|
||||||
body: fallbackText(value?.body, fallback.body),
|
|
||||||
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
|
|
||||||
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
|
|
||||||
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
|
|
||||||
formBody: fallbackText(value?.formBody, fallback.formBody),
|
|
||||||
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
|
|
||||||
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
|
|
||||||
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
|
|
||||||
privacy: fallbackText(value?.privacy, fallback.privacy),
|
|
||||||
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
|
|
||||||
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
|
|
||||||
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
|
|
||||||
successBody: fallbackText(value?.successBody, fallback.successBody),
|
|
||||||
footnote: fallbackText(value?.footnote, fallback.footnote),
|
|
||||||
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
|
|
||||||
});
|
|
||||||
|
|
||||||
const getNewsletterInterestCopy = (phase: string | undefined, applicationForm: ParticipationApplicationFormCms) => {
|
|
||||||
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
|
|
||||||
const cmsCopy = phase === '1' ? applicationForm.newsletterPhaseEvaluation : applicationForm.newsletterPhaseCompleted;
|
|
||||||
return mergeNewsletterInterestCopy(cmsCopy, fallback);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFutureAwardApplicationWay = (value: { label?: unknown; title?: unknown }) => {
|
const isFutureAwardApplicationWay = (value: { label?: unknown; title?: unknown }) => {
|
||||||
const label = typeof value.label === 'string' ? value.label.toLowerCase() : '';
|
const label = typeof value.label === 'string' ? value.label.toLowerCase() : '';
|
||||||
const title = typeof value.title === 'string' ? value.title.toLowerCase() : '';
|
const title = typeof value.title === 'string' ? value.title.toLowerCase() : '';
|
||||||
@@ -98,6 +63,7 @@ const Participation: React.FC = () => {
|
|||||||
const qualSectionRef = useRef<HTMLElement>(null);
|
const qualSectionRef = useRef<HTMLElement>(null);
|
||||||
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
|
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
|
||||||
const applicationPhase = useApplicationPhase();
|
const applicationPhase = useApplicationPhase();
|
||||||
|
const newsletterForm = useNewsletterForm();
|
||||||
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
|
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
|
||||||
const process = { ...participationContent.process, ...(cms.process || {}) };
|
const process = { ...participationContent.process, ...(cms.process || {}) };
|
||||||
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
|
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
|
||||||
@@ -108,7 +74,8 @@ const Participation: React.FC = () => {
|
|||||||
const form = { ...participationContent.form, ...(cms.form || {}) };
|
const form = { ...participationContent.form, ...(cms.form || {}) };
|
||||||
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
||||||
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
||||||
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, applicationForm);
|
const newsletterCopy = newsletterFormCopyForPhase(newsletterForm, activeApplicationPhase);
|
||||||
|
const newsletterPhase = newsletterPhaseForApplicationPhase(activeApplicationPhase);
|
||||||
const eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
|
const eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
|
||||||
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
|
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
|
||||||
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
|
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
|
||||||
@@ -324,7 +291,7 @@ const Participation: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{newsletterCopy.formEyebrow}</span>
|
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{newsletterCopy.formEyebrow}</span>
|
||||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{newsletterCopy.formTitle}</h3>
|
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{newsletterCopy.formTitle}</h3>
|
||||||
<NewsletterInterestForm copy={newsletterCopy} />
|
<NewsletterInterestForm copy={newsletterCopy} phase={newsletterPhase} source="participation" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -153,56 +153,6 @@ export const participationContent = {
|
|||||||
uploadCta: { label: 'PDF hochladen', url: '/formular-hochladen' },
|
uploadCta: { label: 'PDF hochladen', url: '/formular-hochladen' },
|
||||||
uploadPrompt: 'Sie haben Ihre Unterlagen bereits als PDF vorbereitet? Laden Sie sie direkt hoch, statt das Formular auszufüllen.',
|
uploadPrompt: 'Sie haben Ihre Unterlagen bereits als PDF vorbereitet? Laden Sie sie direkt hoch, statt das Formular auszufüllen.',
|
||||||
uploadFooterCta: { label: 'Formular hochladen →', url: '/formular-hochladen' },
|
uploadFooterCta: { label: 'Formular hochladen →', url: '/formular-hochladen' },
|
||||||
newsletterPhaseEvaluation: {
|
|
||||||
eyebrow: 'Updates erhalten',
|
|
||||||
heading: 'DIE BEWERBUNG\nIST GESCHLOSSEN.',
|
|
||||||
body:
|
|
||||||
'Die Bewerbungsphase 2026 ist geschlossen. Hinterlassen Sie Ihre E-Mail und erhalten Sie Updates zur Juryphase, zur Preisverleihung und zur nächsten Runde.',
|
|
||||||
benefits: [
|
|
||||||
{ num: '01', label: 'Juryphase', desc: 'Updates zum Verlauf' },
|
|
||||||
{ num: '02', label: 'Preisverleihung', desc: 'Termine und Einblicke' },
|
|
||||||
{ num: '03', label: 'Nächste Runde', desc: 'Start nicht verpassen' },
|
|
||||||
],
|
|
||||||
formEyebrow: 'Newsletter abonnieren',
|
|
||||||
formTitle: 'Updates erhalten',
|
|
||||||
formBody:
|
|
||||||
'Tragen Sie Ihre E-Mail ein und erhalten Sie relevante Informationen zum weiteren Verlauf des Bayerischen Mittelstandspreises.',
|
|
||||||
emailLabel: 'E-Mail-Adresse',
|
|
||||||
emailPlaceholder: 'name@unternehmen.de',
|
|
||||||
submitLabel: 'Eintragen',
|
|
||||||
privacy: 'Kein Spam. Nur relevante Informationen rund um den Bayerischen Mittelstandspreis.',
|
|
||||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
|
||||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
|
||||||
successHeading: 'Danke',
|
|
||||||
successBody: 'Ihre E-Mail wurde für BMP-Updates eingetragen.',
|
|
||||||
footnote: 'BMP-Updates -',
|
|
||||||
footnoteStrong: 'Juryphase und Termine',
|
|
||||||
},
|
|
||||||
newsletterPhaseCompleted: {
|
|
||||||
eyebrow: 'Nächste Runde',
|
|
||||||
heading: 'DEN START\nNICHT\nVERPASSEN.',
|
|
||||||
body:
|
|
||||||
'Der aktuelle Preisjahrgang ist abgeschlossen. Hinterlassen Sie Ihre E-Mail und wir informieren Sie, sobald es Neuigkeiten zur nächsten Bewerbungsphase gibt.',
|
|
||||||
benefits: [
|
|
||||||
{ num: '01', label: 'Startsignal', desc: 'Neue Bewerbungsphase' },
|
|
||||||
{ num: '02', label: 'BMP-News', desc: 'Preisträger und Termine' },
|
|
||||||
{ num: '03', label: 'Erinnerung', desc: 'Rechtzeitig vorbereitet' },
|
|
||||||
],
|
|
||||||
formEyebrow: 'Newsletter abonnieren',
|
|
||||||
formTitle: 'Informiert bleiben',
|
|
||||||
formBody:
|
|
||||||
'Tragen Sie sich ein und erhalten Sie Hinweise zum Bewerbungsstart, zu Veranstaltungen und zu ausgezeichneten Unternehmen.',
|
|
||||||
emailLabel: 'E-Mail-Adresse',
|
|
||||||
emailPlaceholder: 'name@unternehmen.de',
|
|
||||||
submitLabel: 'Abonnieren',
|
|
||||||
privacy: 'Sie erhalten nur BMP-relevante Informationen. Eine Abmeldung ist jederzeit möglich.',
|
|
||||||
validationRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein.',
|
|
||||||
validationInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
|
||||||
successHeading: 'Angemeldet',
|
|
||||||
successBody: 'Danke. Sie erhalten BMP-Updates an die angegebene E-Mail-Adresse.',
|
|
||||||
footnote: 'Bewerbungsstart im Blick -',
|
|
||||||
footnoteStrong: 'BMP-Updates erhalten',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
stepCompleteLabel: '✓',
|
stepCompleteLabel: '✓',
|
||||||
|
|||||||
Reference in New Issue
Block a user