diff --git a/docs/payload-field-inventory.md b/docs/payload-field-inventory.md new file mode 100644 index 0000000..b57cdb3 --- /dev/null +++ b/docs/payload-field-inventory.md @@ -0,0 +1,50 @@ +# Payload field-to-consumer inventory + +This inventory records the frontend or platform owner for every retained `Pages` field group and relevant Global. Nested scalar, upload, relationship, and array fields belong to the consumer named for their parent group unless an exception is listed. + +## Shared `Pages` fields + +| Field path | Consumer or contract | +| --- | --- | +| `title`, `slug`, `spaPath`, `publishedAt`, `_status` | Route lookup, preview URLs, publication access, sitemap generation, and generic-page headings in `src/app/(frontend)/[[...slug]]/page.tsx`, the sitemap routes, and `src/spa/pages/CmsGenericPage.tsx`. | +| `hero`, `layout` | Non-SPA pages through `src/heros/RenderHero.tsx`, `src/blocks/RenderBlocks.tsx`, and `src/spa/pages/CmsGenericPage.tsx`. | +| `meta.*` | `@payloadcms/plugin-seo`, route metadata generation, and generic-page title fallback. | + +## Page-specific fields + +| Page field path | Retained consumer | +| --- | --- | +| `home.hero.*` | `src/spa/pages/Home.tsx`; partner relationship records remain authoritative and the names array is presentation fallback only. | +| `home.winners.*` | `src/spa/components/HomeWinnersSection.tsx` and `src/spa/homeWinnerSelection.ts`; also reused by `/der-bmp`. | +| `home.quickCheck.*`, `home.intro.*`, `home.benefits.*` | `src/spa/pages/Home.tsx`. | +| `home.testimonials.*` | `src/spa/pages/Home.tsx` through `src/spa/components/TestimonialsSection.tsx`. | +| `home.videoModal.*` | Homepage video action and modal in `src/spa/pages/Home.tsx`. | +| `participation.hero.*`, `participation.eligibility.*`, `participation.applicationWays.*`, `participation.datesBanner.*` | `src/spa/pages/Participation.tsx`. | +| `participation.process.*` | `src/spa/pages/Participation.tsx` through `src/spa/components/WegZurAuszeichnungSection.tsx`. | +| `participation.awardsGrid.*` | `src/spa/pages/Participation.tsx` through `src/spa/components/AwardsGridSection.tsx`. | +| `about.hero.*`, `about.prize.*`, `about.mittelstand.*`, `about.history.*`, `about.goals.*`, `about.values.*`, `about.cta.*` | `src/spa/pages/About.tsx`. | +| `about.testimonials.*` | `src/spa/pages/About.tsx` through `src/spa/components/TestimonialsSection.tsx`. | +| `contact.*` | `src/spa/pages/Contact.tsx` and `src/spa/components/forms/KontaktForm.tsx`. | +| `datenschutz.*`, `impressum.*` | `src/spa/components/legal/LegalPage.tsx` via the respective route component. | +| `formUpload.*` | `src/spa/pages/FormularUpload.tsx`. | +| `mitgliedWerden.*` | `src/spa/pages/MitgliedWerden.tsx` and `src/spa/components/membership/MembershipWizard.tsx`. | +| `preistraegerIndex.*` | `src/spa/pages/Preistraeger.tsx`; winner records remain collection-owned. | +| `pressIndex.hero.*`, `pressIndex.events.*`, `pressIndex.news.*`, `pressIndex.downloads.*` | `src/spa/pages/Press.tsx`; event/article records remain collection-owned, while missing-field labels and images are presentation fallbacks only. | +| `netzwerk.hero.*`, `netzwerk.patronage.*`, `netzwerk.juryIntro.*`, `netzwerk.expertise.*`, `netzwerk.sponsoring.*` | `src/spa/pages/Netzwerk.tsx`. | +| `netzwerk.jury.{eyebrow,heading,description,chairBadge,members}` | Jury heading and authoritative rendered relationship selection in `src/spa/pages/Netzwerk.tsx`. | +| `netzwerk.partners.{eyebrow,heading,description,detailLabel,tiers,modal}` | Partner section, tier labels, and modal copy in `src/spa/pages/Netzwerk.tsx`; partner records remain collection-owned. | +| `netzwerk.sponsoringForm.*` | The two visible form steps, validation, success state, and footer copy in `src/spa/components/forms/SponsoringForm.tsx`. | + +## Global ownership + +| Global | Consumer or contract | +| --- | --- | +| `application-form` | Single source for the application section and form on both `/` and `/teilnahme`, hydrated by the frontend route and consumed by `ApplicationFormSection` / `BewerbungsForm`. | +| `application-phase` | Shared phase state and phase-specific site copy used by Home, shared layout CTAs, the application/newsletter form switch, and sponsoring submissions. | +| `newsletter-form` | Evaluation/completed newsletter variants shared by `/` and `/teilnahme`. | +| `header`, `footer` | Site-wide navigation, branding, links, social entries, and footer partner marks rendered by `src/spa/components/layout/Layout.tsx`. | +| `site-settings` | Shared skyline, Preisträger placeholder, and generic-page fallback copy. | + +## Task 4 removals + +The audit found no frontend, metadata, SEO, seed-only contract, or explicit Global purpose for these fields: `home.application`, `home.form`, `participation.applicationForm`, `participation.form`, the hidden participation CTA controls (`eligibility.secondaryCta`, `eligibility.nudgeCta`, `applicationWays.recommendedLabel`, `applicationWays.fallbackCta`, `applicationWays.ways[].featured`, and `applicationWays.ways[].cta`), `about.highlights`, the five `pressIndex.downloads.kit*` controls, `pressIndex.accreditation`, the old event/news CTA labels, `netzwerk.jury.note`, `netzwerk.partners.premiumLabel`, `netzwerk.partners.defaultLogoColor`, the four non-rendered `netzwerk.sponsoring` footnote/form-eyebrow controls, and the obsolete sponsoring package step (`packages`, `step1Eyebrow`, `step1Heading`, `requiredPackageError`). They are intentionally absent from the retained inventory. diff --git a/src/collections/Pages/aboutFields.ts b/src/collections/Pages/aboutFields.ts index 0860877..614eb61 100644 --- a/src/collections/Pages/aboutFields.ts +++ b/src/collections/Pages/aboutFields.ts @@ -123,52 +123,9 @@ export const aboutFields: Field[] = [ }, ], }, - { - name: 'highlights', - label: '04 · Preisträger highlights', - type: 'group', - admin: { - ...sectionAdmin('Legacy data retained for compatibility. The rendered section now reuses the homepage Preisträger card grid.'), - hidden: true, - }, - fields: [ - uploadField('fallbackImage', 'Fallback winner image', `Current frontend fallback image: /images/${aboutContent.highlights.fallbackImageFilename}`), - text('eyebrow', 'Eyebrow', aboutContent.highlights.eyebrow), - textarea('heading', 'Heading', aboutContent.highlights.heading), - ctaGroup('cta', 'CTA', aboutContent.highlights.cta.label, aboutContent.highlights.cta.url), - text('hoverLabel', 'Card hover label', aboutContent.highlights.hoverLabel), - { - name: 'fallbackWinner', - label: 'Fallback winner text', - type: 'group', - fields: [ - text('name', 'Name fallback', aboutContent.highlights.fallbackWinner.name), - text('category', 'Category fallback', aboutContent.highlights.fallbackWinner.category), - text('awardType', 'Award type fallback', aboutContent.highlights.fallbackWinner.awardType), - ], - }, - { - name: 'year', - label: 'Fallback winner year', - type: 'number', - defaultValue: aboutContent.highlights.year, - }, - { - name: 'selectedWinners', - label: 'Selected winners', - type: 'relationship', - relationTo: 'preistraeger', - hasMany: true, - maxRows: 8, - admin: { - description: 'Choose up to 8 Preisträger to show. If empty, the frontend falls back to winners from the selected year.', - }, - }, - ], - }, { name: 'testimonials', - label: '05 · Testimonials', + label: '04 · Testimonials', type: 'group', admin: sectionAdmin('Testimonial heading and slides. Upload images when available, or keep the migrated URL fallback.'), fields: [ @@ -199,7 +156,7 @@ export const aboutFields: Field[] = [ }, { name: 'history', - label: '06 · Geschichte & Meilensteine', + label: '05 · Geschichte & Meilensteine', type: 'group', admin: sectionAdmin('Navy timeline section.'), fields: [ @@ -219,7 +176,7 @@ export const aboutFields: Field[] = [ }, { name: 'goals', - label: '07 · Zielsetzung', + label: '06 · Zielsetzung', type: 'group', admin: sectionAdmin('Cream values list next to the history timeline.'), fields: [ @@ -237,7 +194,7 @@ export const aboutFields: Field[] = [ }, { name: 'values', - label: '08 · Vorteile & Werte', + label: '07 · Vorteile & Werte', type: 'group', admin: sectionAdmin('Cream editorial rows before the final CTA.'), fields: [ @@ -256,7 +213,7 @@ export const aboutFields: Field[] = [ }, { name: 'cta', - label: '09 · Final CTA', + label: '08 · Final CTA', type: 'group', admin: sectionAdmin('Bottom navy CTA section.'), fields: [ diff --git a/src/collections/Pages/homeFields.ts b/src/collections/Pages/homeFields.ts index ca0de38..a8ad1d5 100644 --- a/src/collections/Pages/homeFields.ts +++ b/src/collections/Pages/homeFields.ts @@ -33,33 +33,6 @@ const sectionAdmin = (description: string) => ({ initCollapsed: true, }) -const iconOptions = [ - { label: 'Alert circle', value: 'alertCircle' }, - { label: 'Arrow right', value: 'arrowRight' }, - { label: 'Building', value: 'building2' }, - { label: 'Check', value: 'check' }, - { label: 'Check circle', value: 'checkCircle2' }, - { label: 'File text', value: 'fileText' }, - { label: 'Map pin', value: 'mapPin' }, - { label: 'Search', value: 'search' }, - { label: 'Send', value: 'send' }, - { label: 'Star', value: 'star' }, - { label: 'Trending up', value: 'trendingUp' }, - { label: 'Trophy', value: 'trophy' }, - { label: 'User', value: 'user' }, - { label: 'User check', value: 'userCheck' }, - { label: 'User plus', value: 'userPlus' }, - { label: 'Users', value: 'users' }, -] - -const iconField = (defaultValue = 'star'): Field => ({ - name: 'icon', - label: 'Icon', - type: 'select', - defaultValue, - options: iconOptions, -}) - export const homeFields: Field[] = [ { name: 'home', @@ -219,144 +192,9 @@ export const homeFields: Field[] = [ }, ], }, - { - name: 'application', - label: '07 · Application form section', - type: 'group', - admin: { ...sectionAdmin('Legacy page-level form section. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true }, - fields: [ - text('eyebrow', 'Eyebrow', homeContent.application.eyebrow), - textarea('heading', 'Heading', homeContent.application.heading), - textarea('body', 'Body copy', homeContent.application.body), - { name: 'benefits', label: 'Benefit rows', type: 'array', defaultValue: homeContent.application.benefits, fields: [text('num', 'Number'), text('label', 'Label'), text('desc', 'Description')] }, - uploadField('awardImage', 'Award image', `Current frontend image: /images/${homeContent.application.awardImageFilename}`), - text('awardImageAlt', 'Award image alt text', homeContent.application.awardImageAlt), - text('awardCaption', 'Award image caption', homeContent.application.awardCaption), - text('formEyebrow', 'Form eyebrow', homeContent.application.formEyebrow), - text('formTitle', 'Form title', homeContent.application.formTitle), - text('footnote', 'Footnote normal text', homeContent.application.footnote), - text('footnoteStrong', 'Footnote highlighted text', homeContent.application.footnoteStrong), - ], - }, - { - name: 'form', - label: '08 · Embedded application form copy and logic labels', - type: 'group', - admin: { ...sectionAdmin('Legacy page-level form fields. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true }, - fields: [ - text('stepCompleteLabel', 'Completed step label', homeContent.form.stepCompleteLabel), - text('backLabel', 'Back button label', homeContent.form.backLabel), - text('nextLabel', 'Next button label', homeContent.form.nextLabel), - text('submitLabel', 'Submit button label', homeContent.form.submitLabel), - text('yesLabel', 'Yes option label', homeContent.form.yesLabel), - text('noLabel', 'No option label', homeContent.form.noLabel), - text('requiredSuffix', 'Required field suffix', homeContent.form.requiredSuffix), - { - name: 'validation', - label: 'Validation messages', - type: 'group', - fields: [ - text('choose', 'Choose message', homeContent.form.validation.choose), - text('required', 'Required message', homeContent.form.validation.required), - text('invalidEmail', 'Invalid email message', homeContent.form.validation.invalidEmail), - ], - }, - { - name: 'typeStep', - label: 'Step 1 · Submission type', - type: 'group', - fields: [ - text('eyebrow', 'Eyebrow', homeContent.form.typeStep.eyebrow), - text('heading', 'Heading', homeContent.form.typeStep.heading), - text('selfTitle', 'Self-application title', homeContent.form.typeStep.selfTitle), - textarea('selfDesc', 'Self-application description', homeContent.form.typeStep.selfDesc), - text('nominationTitle', 'Nomination title', homeContent.form.typeStep.nominationTitle), - textarea('nominationDesc', 'Nomination description', homeContent.form.typeStep.nominationDesc), - ], - }, - { - name: 'selfSteps', - label: 'Self-application step labels', - type: 'array', - dbName: 'home_self_steps', - defaultValue: homeContent.form.selfSteps, - fields: [text('label', 'Label'), iconField('trophy')], - }, - { - name: 'nominationSteps', - label: 'Nomination step labels', - type: 'array', - dbName: 'home_nom_steps', - defaultValue: homeContent.form.nominationSteps, - fields: [text('label', 'Label'), iconField('trophy')], - }, - { - name: 'employeeOptions', - label: 'Employee options', - type: 'array', - dbName: 'home_emp_opts', - defaultValue: homeContent.form.employeeOptions, - fields: [text('value', 'Value'), text('label', 'Label')], - }, - { - name: 'eligibility', - label: 'Self-application eligibility step', - type: 'group', - fields: [ - text('allowedEmployeeValues', 'Allowed employee option values, comma-separated', homeContent.form.eligibility.allowedEmployeeValues), - text('requiredBayernValue', 'Required Bavaria answer value', homeContent.form.eligibility.requiredBayernValue), - text('eyebrow', 'Eyebrow', homeContent.form.eligibility.eyebrow), - text('heading', 'Heading', homeContent.form.eligibility.heading), - text('employeesLabel', 'Employees field label', homeContent.form.eligibility.employeesLabel), - text('bayernLabel', 'Bavaria field label', homeContent.form.eligibility.bayernLabel), - text('ownerLabel', 'Owner field label', homeContent.form.eligibility.ownerLabel), - text('ineligibleHeading', 'Ineligible heading', homeContent.form.eligibility.ineligibleHeading), - textarea('ineligibleBody', 'Ineligible body', homeContent.form.eligibility.ineligibleBody), - text('ineligibleContactPrefix', 'Ineligible contact prefix', homeContent.form.eligibility.ineligibleContactPrefix), - text('ineligibleContactEmail', 'Ineligible contact email', homeContent.form.eligibility.ineligibleContactEmail), - ], - }, - { - name: 'selfContact', - label: 'Self-application contact step', - type: 'group', - fields: Object.entries(homeContent.form.selfContact).map(([name, value]) => text(name, name, value)), - }, - { - name: 'selfSummary', - label: 'Self-application summary step', - type: 'group', - fields: Object.entries(homeContent.form.selfSummary).map(([name, value]) => text(name, name, value)), - }, - { - name: 'nominationCompany', - label: 'Nomination company step', - type: 'group', - fields: Object.entries(homeContent.form.nominationCompany).map(([name, value]) => text(name, name, value)), - }, - { - name: 'nominationContact', - label: 'Nomination contact step', - type: 'group', - fields: Object.entries(homeContent.form.nominationContact).map(([name, value]) => text(name, name, value)), - }, - { - name: 'nominationSummary', - label: 'Nomination summary step', - type: 'group', - fields: Object.entries(homeContent.form.nominationSummary).map(([name, value]) => text(name, name, value)), - }, - { - name: 'success', - label: 'Success state copy', - type: 'group', - fields: Object.entries(homeContent.form.success).map(([name, value]) => text(name, name, value)), - }, - ], - }, { name: 'videoModal', - label: '10 · Video modal', + label: '07 · Video modal', type: 'group', admin: sectionAdmin('Video and fallback copy shown when the hero video button is clicked.'), fields: [ diff --git a/src/collections/Pages/netzwerkFields.ts b/src/collections/Pages/netzwerkFields.ts index 665c5a9..85e12c9 100644 --- a/src/collections/Pages/netzwerkFields.ts +++ b/src/collections/Pages/netzwerkFields.ts @@ -51,7 +51,6 @@ const personFields: Field[] = [ const rowFields: Field[] = [text('num', 'Number'), text('label', 'Label'), textarea('body', 'Body')] const benefitFields: Field[] = [text('num', 'Number'), text('label', 'Label'), text('sub', 'Subline')] -const packageFields: Field[] = [text('value', 'Value'), text('title', 'Title'), textarea('desc', 'Description')] const formStepFields: Field[] = [text('label', 'Label')] export const netzwerkFields: Field[] = [ @@ -134,13 +133,12 @@ export const netzwerkFields: Field[] = [ name: 'jury', label: '04 · Jury members', type: 'group', - admin: sectionAdmin('Jury section heading, chair badge, members, and note.'), + admin: sectionAdmin('Jury section heading, chair badge, and rendered member selection.'), fields: [ text('eyebrow', 'Eyebrow', netzwerkContent.jury.eyebrow), textarea('heading', 'Heading', netzwerkContent.jury.heading), textarea('description', 'Description', netzwerkContent.jury.description), text('chairBadge', 'Chair badge', netzwerkContent.jury.chairBadge), - textarea('note', 'Note', netzwerkContent.jury.note), { name: 'members', label: 'Members', @@ -183,8 +181,6 @@ export const netzwerkFields: Field[] = [ textarea('heading', 'Heading', netzwerkContent.partners.heading), textarea('description', 'Description', netzwerkContent.partners.description), text('detailLabel', 'Detail label', netzwerkContent.partners.detailLabel), - text('premiumLabel', 'Premium label', netzwerkContent.partners.premiumLabel), - text('defaultLogoColor', 'Default logo color', netzwerkContent.partners.defaultLogoColor), { name: 'tiers', label: 'Tiers', @@ -214,7 +210,7 @@ export const netzwerkFields: Field[] = [ name: 'sponsoring', label: '07 · Sponsoring section', type: 'group', - admin: sectionAdmin('Sponsoring pitch, image, benefits, form eyebrow, and membership footnote.'), + admin: sectionAdmin('Sponsoring pitch, image, benefits, and form eyebrow.'), fields: [ text('eyebrow', 'Eyebrow', netzwerkContent.sponsoring.eyebrow), textarea('heading', 'Heading', netzwerkContent.sponsoring.heading), @@ -222,10 +218,6 @@ export const netzwerkFields: Field[] = [ uploadField('image', 'Form panel image', `Current frontend image: /images/${netzwerkContent.sponsoring.imageFilename}`), text('imageAlt', 'Image alt text', netzwerkContent.sponsoring.imageAlt), text('imageLabel', 'Image label', netzwerkContent.sponsoring.imageLabel), - text('formEyebrow', 'Form eyebrow', netzwerkContent.sponsoring.formEyebrow), - text('footnotePrefix', 'Footnote prefix', netzwerkContent.sponsoring.footnotePrefix), - text('footnoteLabel', 'Footnote link label', netzwerkContent.sponsoring.footnoteLabel), - text('footnoteUrl', 'Footnote link URL', netzwerkContent.sponsoring.footnoteUrl), { name: 'benefits', label: 'Benefits', @@ -240,7 +232,7 @@ export const netzwerkFields: Field[] = [ name: 'sponsoringForm', label: '08 · Sponsoring form', type: 'group', - admin: sectionAdmin('Wizard labels, package options, validation messages, success copy, and footer note.'), + admin: sectionAdmin('Two-step wizard labels, field copy, validation messages, success copy, and footer note.'), fields: [ { name: 'steps', @@ -248,26 +240,18 @@ export const netzwerkFields: Field[] = [ type: 'array', dbName: 'network_form_steps', defaultValue: netzwerkContent.sponsoringForm.steps, + minRows: 2, + maxRows: 2, fields: formStepFields, }, - { - name: 'packages', - label: 'Package options', - type: 'array', - dbName: 'network_form_pkgs', - defaultValue: netzwerkContent.sponsoringForm.packages, - fields: packageFields, - }, - text('step1Eyebrow', 'Step 1 eyebrow', netzwerkContent.sponsoringForm.step1Eyebrow), - text('step1Heading', 'Step 1 heading', netzwerkContent.sponsoringForm.step1Heading), - text('step2Eyebrow', 'Step 2 eyebrow', netzwerkContent.sponsoringForm.step2Eyebrow), - text('step2Heading', 'Step 2 heading', netzwerkContent.sponsoringForm.step2Heading), + text('step2Eyebrow', 'Step 1 eyebrow', netzwerkContent.sponsoringForm.step2Eyebrow), + text('step2Heading', 'Step 1 heading', netzwerkContent.sponsoringForm.step2Heading), text('companyLabel', 'Company label', netzwerkContent.sponsoringForm.companyLabel), text('companyPlaceholder', 'Company placeholder', netzwerkContent.sponsoringForm.companyPlaceholder), text('industryLabel', 'Industry label', netzwerkContent.sponsoringForm.industryLabel), text('industryPlaceholder', 'Industry placeholder', netzwerkContent.sponsoringForm.industryPlaceholder), - text('step3Eyebrow', 'Step 3 eyebrow', netzwerkContent.sponsoringForm.step3Eyebrow), - text('step3Heading', 'Step 3 heading', netzwerkContent.sponsoringForm.step3Heading), + text('step3Eyebrow', 'Step 2 eyebrow', netzwerkContent.sponsoringForm.step3Eyebrow), + text('step3Heading', 'Step 2 heading', netzwerkContent.sponsoringForm.step3Heading), text('contactLabel', 'Contact label', netzwerkContent.sponsoringForm.contactLabel), text('contactPlaceholder', 'Contact placeholder', netzwerkContent.sponsoringForm.contactPlaceholder), text('emailLabel', 'Email label', netzwerkContent.sponsoringForm.emailLabel), @@ -275,7 +259,6 @@ export const netzwerkFields: Field[] = [ text('backLabel', 'Back label', netzwerkContent.sponsoringForm.backLabel), text('nextLabel', 'Next label', netzwerkContent.sponsoringForm.nextLabel), text('submitLabel', 'Submit label', netzwerkContent.sponsoringForm.submitLabel), - text('requiredPackageError', 'Required package error', netzwerkContent.sponsoringForm.requiredPackageError), text('requiredFieldError', 'Required field error', netzwerkContent.sponsoringForm.requiredFieldError), text('invalidEmailError', 'Invalid email error', netzwerkContent.sponsoringForm.invalidEmailError), text('doneLabel', 'Done step label', netzwerkContent.sponsoringForm.doneLabel), diff --git a/src/collections/Pages/participationFields.ts b/src/collections/Pages/participationFields.ts index 868dc51..1d9ed35 100644 --- a/src/collections/Pages/participationFields.ts +++ b/src/collections/Pages/participationFields.ts @@ -24,13 +24,6 @@ const textarea = (name: string, label: string, defaultValue?: string): Field => defaultValue, }) -const checkbox = (name: string, label: string, defaultValue = false): Field => ({ - name, - type: 'checkbox', - label, - defaultValue, -}) - const sectionAdmin = (description: string) => ({ description, initCollapsed: true, @@ -133,7 +126,6 @@ export const participationFields: Field[] = [ textarea('heading', 'Heading', participationContent.eligibility.heading), textarea('description', 'Intro paragraph', participationContent.eligibility.description), ctaGroup('primaryCta', 'Primary CTA', participationContent.eligibility.primaryCta.label, participationContent.eligibility.primaryCta.url), - ctaGroup('secondaryCta', 'Secondary CTA', participationContent.eligibility.secondaryCta.label, participationContent.eligibility.secondaryCta.url), { name: 'criteria', label: 'Eligibility rows', @@ -151,7 +143,6 @@ export const participationFields: Field[] = [ fields: [text('title', 'Title'), textarea('body', 'Body'), iconField('building2')], }, text('nudgeText', 'Bottom nudge text', participationContent.eligibility.nudgeText), - ctaGroup('nudgeCta', 'Bottom CTA', participationContent.eligibility.nudgeCta.label, participationContent.eligibility.nudgeCta.url), ], }, { @@ -163,13 +154,6 @@ export const participationFields: Field[] = [ text('eyebrow', 'Eyebrow', participationContent.applicationWays.eyebrow), textarea('heading', 'Heading', participationContent.applicationWays.heading), textarea('description', 'Intro paragraph', participationContent.applicationWays.description), - text('recommendedLabel', 'Featured card label', participationContent.applicationWays.recommendedLabel), - ctaGroup( - 'fallbackCta', - 'Fallback CTA for path cards', - participationContent.applicationWays.fallbackCta.label, - participationContent.applicationWays.fallbackCta.url, - ), { name: 'institutions', label: 'Proposal institutions', @@ -189,7 +173,6 @@ export const participationFields: Field[] = [ textarea('title', 'Title'), textarea('desc', 'Description'), iconField('send', 'ico'), - checkbox('featured', 'Featured card'), { name: 'listType', label: 'Detail list type', @@ -208,7 +191,6 @@ export const participationFields: Field[] = [ dbName: 'way_facts', fields: factFields, }, - ctaGroup('cta', 'CTA', participationContent.applicationWays.fallbackCta.label, participationContent.applicationWays.fallbackCta.url), ], }, ], @@ -268,149 +250,6 @@ export const participationFields: Field[] = [ }, ], }, - { - name: 'applicationForm', - label: '07 · Application form wrapper', - type: 'group', - admin: { ...sectionAdmin('Legacy page-level form section. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true }, - fields: [ - uploadField('image', 'Form image', `Current frontend image: /images/${participationContent.applicationForm.imageFilename}`), - text('imageAlt', 'Image alt text', participationContent.applicationForm.imageAlt), - text('imageCaption', 'Image caption', participationContent.applicationForm.imageCaption), - text('eyebrow', 'Eyebrow', participationContent.applicationForm.eyebrow), - textarea('heading', 'Heading', participationContent.applicationForm.heading), - textarea('description', 'Body copy', participationContent.applicationForm.description), - { - name: 'facts', - label: 'Fact rows', - type: 'array', - dbName: 'form_facts', - defaultValue: participationContent.applicationForm.facts, - fields: [text('num', 'Number'), text('label', 'Label'), textarea('desc', 'Description')], - }, - text('formEyebrow', 'Form eyebrow', participationContent.applicationForm.formEyebrow), - text('formTitle', 'Form title', participationContent.applicationForm.formTitle), - ctaGroup('uploadCta', 'Top upload CTA', participationContent.applicationForm.uploadCta.label, participationContent.applicationForm.uploadCta.url), - textarea('uploadPrompt', 'Bottom upload prompt', participationContent.applicationForm.uploadPrompt), - ctaGroup('uploadFooterCta', 'Bottom upload CTA', participationContent.applicationForm.uploadFooterCta.label, participationContent.applicationForm.uploadFooterCta.url), - ], - }, - { - name: 'form', - label: '08 · Application form copy and logic labels', - type: 'group', - admin: { ...sectionAdmin('Legacy page-level form fields. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true }, - fields: [ - text('stepCompleteLabel', 'Completed step label', participationContent.form.stepCompleteLabel), - text('backLabel', 'Back button label', participationContent.form.backLabel), - text('nextLabel', 'Next button label', participationContent.form.nextLabel), - text('submitLabel', 'Submit button label', participationContent.form.submitLabel), - text('yesLabel', 'Yes option label', participationContent.form.yesLabel), - text('noLabel', 'No option label', participationContent.form.noLabel), - text('requiredSuffix', 'Required field suffix', participationContent.form.requiredSuffix), - { - name: 'validation', - label: 'Validation messages', - type: 'group', - fields: [ - text('choose', 'Choose message', participationContent.form.validation.choose), - text('required', 'Required message', participationContent.form.validation.required), - text('invalidEmail', 'Invalid email message', participationContent.form.validation.invalidEmail), - ], - }, - { - name: 'typeStep', - label: 'Step 1 · Submission type', - type: 'group', - fields: [ - text('eyebrow', 'Eyebrow', participationContent.form.typeStep.eyebrow), - text('heading', 'Heading', participationContent.form.typeStep.heading), - text('selfTitle', 'Self-application title', participationContent.form.typeStep.selfTitle), - textarea('selfDesc', 'Self-application description', participationContent.form.typeStep.selfDesc), - text('nominationTitle', 'Nomination title', participationContent.form.typeStep.nominationTitle), - textarea('nominationDesc', 'Nomination description', participationContent.form.typeStep.nominationDesc), - ], - }, - { - name: 'selfSteps', - label: 'Self-application step labels', - type: 'array', - dbName: 'self_steps', - defaultValue: participationContent.form.selfSteps, - fields: [text('label', 'Label'), iconField('trophy')], - }, - { - name: 'nominationSteps', - label: 'Nomination step labels', - type: 'array', - dbName: 'nom_steps', - defaultValue: participationContent.form.nominationSteps, - fields: [text('label', 'Label'), iconField('trophy')], - }, - { - name: 'employeeOptions', - label: 'Employee options', - type: 'array', - dbName: 'emp_opts', - defaultValue: participationContent.form.employeeOptions, - fields: [text('value', 'Value'), text('label', 'Label')], - }, - { - name: 'eligibility', - label: 'Self-application eligibility step', - type: 'group', - fields: [ - text('allowedEmployeeValues', 'Allowed employee option values, comma-separated', participationContent.form.eligibility.allowedEmployeeValues), - text('requiredBayernValue', 'Required Bavaria answer value', participationContent.form.eligibility.requiredBayernValue), - text('eyebrow', 'Eyebrow', participationContent.form.eligibility.eyebrow), - text('heading', 'Heading', participationContent.form.eligibility.heading), - text('employeesLabel', 'Employees field label', participationContent.form.eligibility.employeesLabel), - text('bayernLabel', 'Bavaria field label', participationContent.form.eligibility.bayernLabel), - text('ownerLabel', 'Owner field label', participationContent.form.eligibility.ownerLabel), - text('ineligibleHeading', 'Ineligible heading', participationContent.form.eligibility.ineligibleHeading), - textarea('ineligibleBody', 'Ineligible body', participationContent.form.eligibility.ineligibleBody), - text('ineligibleContactPrefix', 'Ineligible contact prefix', participationContent.form.eligibility.ineligibleContactPrefix), - text('ineligibleContactEmail', 'Ineligible contact email', participationContent.form.eligibility.ineligibleContactEmail), - ], - }, - { - name: 'selfContact', - label: 'Self-application contact step', - type: 'group', - fields: Object.entries(participationContent.form.selfContact).map(([name, value]) => text(name, name, value)), - }, - { - name: 'selfSummary', - label: 'Self-application summary step', - type: 'group', - fields: Object.entries(participationContent.form.selfSummary).map(([name, value]) => text(name, name, value)), - }, - { - name: 'nominationCompany', - label: 'Nomination company step', - type: 'group', - fields: Object.entries(participationContent.form.nominationCompany).map(([name, value]) => text(name, name, value)), - }, - { - name: 'nominationContact', - label: 'Nomination contact step', - type: 'group', - fields: Object.entries(participationContent.form.nominationContact).map(([name, value]) => text(name, name, value)), - }, - { - name: 'nominationSummary', - label: 'Nomination summary step', - type: 'group', - fields: Object.entries(participationContent.form.nominationSummary).map(([name, value]) => text(name, name, value)), - }, - { - name: 'success', - label: 'Success state copy', - type: 'group', - fields: Object.entries(participationContent.form.success).map(([name, value]) => text(name, name, value)), - }, - ], - }, ], }, ] diff --git a/src/collections/Pages/pressIndexFields.ts b/src/collections/Pages/pressIndexFields.ts index dd9f495..f52d149 100644 --- a/src/collections/Pages/pressIndexFields.ts +++ b/src/collections/Pages/pressIndexFields.ts @@ -66,7 +66,6 @@ export const pressIndexFields: Field[] = [ text('eyebrow', 'Eyebrow', pressIndexContent.events.eyebrow), textarea('heading', 'Heading', pressIndexContent.events.heading), textarea('description', 'Description', pressIndexContent.events.description), - text('detailLabel', 'Detail link label', pressIndexContent.events.detailLabel), text('defaultStatusLabel', 'Missing status fallback label', pressIndexContent.events.defaultStatusLabel), text('openStatusLabel', 'Open status label', pressIndexContent.events.openStatusLabel), text('missingTitleLabel', 'Missing event title label', pressIndexContent.events.missingTitleLabel), @@ -92,7 +91,6 @@ export const pressIndexFields: Field[] = [ text('eyebrow', 'Eyebrow', pressIndexContent.news.eyebrow), textarea('heading', 'Heading', pressIndexContent.news.heading), textarea('description', 'Description', pressIndexContent.news.description), - text('readMoreLabel', 'Read more label', pressIndexContent.news.readMoreLabel), text('missingTitleLabel', 'Missing news title label', pressIndexContent.news.missingTitleLabel), text('missingCategoryLabel', 'Missing news category label', pressIndexContent.news.missingCategoryLabel), uploadField('collectionFallbackImage', 'Missing news image fallback', `Current frontend image: /images/${pressIndexContent.news.collectionFallbackImageFilename}`), @@ -102,38 +100,16 @@ export const pressIndexFields: Field[] = [ name: 'downloads', label: '04 · Press material and contact', type: 'group', - admin: sectionAdmin('Download button and press contact block.'), + admin: sectionAdmin('Press material introduction and contact block.'), fields: [ text('eyebrow', 'Eyebrow', pressIndexContent.downloads.eyebrow), textarea('heading', 'Heading', pressIndexContent.downloads.heading), textarea('description', 'Description', pressIndexContent.downloads.description), - text('kitLabel', 'Kit label', pressIndexContent.downloads.kitLabel), - text('kitMeta', 'Kit metadata', pressIndexContent.downloads.kitMeta), - uploadField('kitFile', 'Press kit file', `Current frontend file: ${pressIndexContent.downloads.kitUrl}`), - text('kitUrl', 'Kit URL', pressIndexContent.downloads.kitUrl), - text('kitDownloadName', 'Kit download filename', pressIndexContent.downloads.kitDownloadName), text('contactEyebrow', 'Contact eyebrow', pressIndexContent.downloads.contactEyebrow), text('contactName', 'Contact name', pressIndexContent.downloads.contactName), textarea('contactLines', 'Contact lines', pressIndexContent.downloads.contactLines), ], }, - { - name: 'accreditation', - label: '05 · Accreditation form copy', - type: 'group', - admin: sectionAdmin('Labels and success state for the press accreditation form.'), - fields: [ - text('eyebrow', 'Eyebrow', pressIndexContent.accreditation.eyebrow), - textarea('heading', 'Heading', pressIndexContent.accreditation.heading), - textarea('description', 'Description', pressIndexContent.accreditation.description), - text('mediumLabel', 'Medium label', pressIndexContent.accreditation.mediumLabel), - text('nameLabel', 'Name label', pressIndexContent.accreditation.nameLabel), - text('emailLabel', 'Email label', pressIndexContent.accreditation.emailLabel), - text('submitLabel', 'Submit label', pressIndexContent.accreditation.submitLabel), - text('successHeading', 'Success heading', pressIndexContent.accreditation.successHeading), - textarea('successMessage', 'Success message', pressIndexContent.accreditation.successMessage), - ], - }, ], }, ] diff --git a/src/migrations/20260731_160000_focused_payload_cleanup.ts b/src/migrations/20260731_160000_focused_payload_cleanup.ts new file mode 100644 index 0000000..d15fc14 --- /dev/null +++ b/src/migrations/20260731_160000_focused_payload_cleanup.ts @@ -0,0 +1,397 @@ +import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite' + +import { homeContent } from '@/spa/homeContent' + +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 obsoleteChildTables = [ + 'pages_home_application_benefits', + '_pages_v_version_home_application_benefits', + 'home_self_steps', + '_home_self_steps_v', + 'home_nom_steps', + '_home_nom_steps_v', + 'home_emp_opts', + '_home_emp_opts_v', + 'form_facts', + '_form_facts_v', + 'self_steps', + '_self_steps_v', + 'nom_steps', + '_nom_steps_v', + 'emp_opts', + '_emp_opts_v', + 'network_form_pkgs', + '_network_form_pkgs_v', +] as const + +const livePrefixes = [ + 'home_application_', + 'home_form_', + 'participation_application_form_', + 'participation_form_', + 'about_highlights_', + 'press_index_downloads_kit_', + 'press_index_accreditation_', +] as const + +const versionPrefixes = livePrefixes.map((prefix) => `version_${prefix}`) + +const standaloneLiveColumns = [ + 'netzwerk_jury_note', + 'netzwerk_partners_premium_label', + 'netzwerk_partners_default_logo_color', + 'netzwerk_sponsoring_footnote_prefix', + 'netzwerk_sponsoring_footnote_label', + 'netzwerk_sponsoring_footnote_url', + 'netzwerk_sponsoring_form_eyebrow', + 'netzwerk_sponsoring_form_step1_eyebrow', + 'netzwerk_sponsoring_form_step1_heading', + 'netzwerk_sponsoring_form_required_package_error', + 'participation_eligibility_secondary_cta_label', + 'participation_eligibility_secondary_cta_url', + 'participation_eligibility_nudge_cta_label', + 'participation_eligibility_nudge_cta_url', + 'participation_application_ways_recommended_label', + 'participation_application_ways_fallback_cta_label', + 'participation_application_ways_fallback_cta_url', + 'press_index_events_detail_label', + 'press_index_news_read_more_label', +] as const + +const obsoleteApplicationWayColumns = ['featured', 'cta_label', 'cta_url'] as const + +const uploadIndexes = [ + 'pages_home_application_home_application_award_image_idx', + '_pages_v_version_home_application_version_home_applicati_idx', + 'pages_participation_application_form_participation_appli_idx', + '_pages_v_version_participation_application_form_version__idx', + 'pages_about_highlights_about_highlights_fallback_image_idx', + '_pages_v_version_about_highlights_version_about_highligh_idx', + 'pages_press_index_downloads_press_index_downloads_kit_fi_idx', + '_pages_v_version_press_index_downloads_version_press_ind_idx', +] as const + +function flattenScalars(value: Record, prefix = ''): Array<[string, string]> { + return Object.entries(value).flatMap(([key, item]) => { + const path = prefix ? `${prefix}_${snake(key)}` : snake(key) + if (Array.isArray(item) || item === undefined || item === null) return [] + if (typeof item === 'object') return flattenScalars(item as Record, path) + return [[path, String(item)]] + }) +} + +const homeApplicationColumns = flattenScalars(homeContent.application as Record) + .filter(([name]) => name !== 'award_image_filename') + .map(([name, value]) => [`home_application_${name}`, `text DEFAULT ${literal(value)}`] as const) + +const sharedFormColumns = flattenScalars(homeContent.form as Record) + +const participationApplicationColumns = [ + 'image_alt', + 'image_caption', + 'eyebrow', + 'heading', + 'description', + 'form_eyebrow', + 'form_title', + 'upload_cta_label', + 'upload_cta_url', + 'upload_prompt', + 'upload_footer_cta_label', + 'upload_footer_cta_url', +].map((suffix) => [`participation_application_form_${suffix}`, 'text'] as const) + +const aboutHighlightColumns = [ + 'eyebrow', + 'heading', + 'cta_label', + 'cta_url', + 'hover_label', + 'fallback_winner_name', + 'fallback_winner_category', + 'fallback_winner_award_type', +].map((suffix) => [`about_highlights_${suffix}`, 'text'] as const) + +const pressColumns = [ + 'press_index_downloads_kit_label', + 'press_index_downloads_kit_meta', + 'press_index_downloads_kit_url', + 'press_index_downloads_kit_download_name', + 'press_index_accreditation_eyebrow', + 'press_index_accreditation_heading', + 'press_index_accreditation_description', + 'press_index_accreditation_medium_label', + 'press_index_accreditation_name_label', + 'press_index_accreditation_email_label', + 'press_index_accreditation_submit_label', + 'press_index_accreditation_success_heading', + 'press_index_accreditation_success_message', +].map((name) => [name, 'text'] as const) + +const restoredLiveColumns = [ + ...homeApplicationColumns, + ['home_application_award_caption', 'text'] as const, + ['home_application_award_image_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const, + ...sharedFormColumns.map(([name, value]) => [`home_form_${name}`, `text DEFAULT ${literal(value)}`] as const), + ...participationApplicationColumns, + ['participation_application_form_image_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const, + ...sharedFormColumns.map(([name, value]) => [`participation_form_${name}`, `text DEFAULT ${literal(value)}`] as const), + ...aboutHighlightColumns, + ['about_highlights_fallback_image_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const, + ['about_highlights_year', 'numeric'] as const, + ...pressColumns, + ['press_index_downloads_kit_file_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const, + ...standaloneLiveColumns.map((name) => [name, 'text'] 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 columnNames(db: MigrationDB, tableName: string) { + if (!(await tableExists(db, tableName))) return [] + const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }> + return columns.map((column) => column.name) +} + +async function columnExists(db: MigrationDB, tableName: string, columnName: string) { + return (await columnNames(db, tableName)).includes(columnName) +} + +async function addColumn(db: MigrationDB, tableName: string, columnName: string, definition: string) { + if (await columnExists(db, tableName, columnName)) return + await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} ADD ${ident(columnName)} ${definition};`)) +} + +function splitDefinitions(value: string) { + const definitions: string[] = [] + let current = '' + let depth = 0 + let quote = '' + + for (let index = 0; index < value.length; index += 1) { + const character = value[index] + const previous = value[index - 1] + + if (quote) { + current += character + if (character === quote && previous !== '\\') quote = '' + continue + } + + if (character === '`' || character === '"' || character === "'") { + quote = character + current += character + continue + } + + if (character === '(') depth += 1 + if (character === ')') depth -= 1 + if (character === ',' && depth === 0) { + definitions.push(current.trim()) + current = '' + continue + } + current += character + } + + if (current.trim()) definitions.push(current.trim()) + return definitions +} + +function definitionColumnName(definition: string) { + const match = definition.trim().match(/^(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|([^\s]+))/) + return match?.[1] || match?.[2] || match?.[3] || match?.[4] || '' +} + +function referencesColumn(definition: string, column: string) { + return [ident(column), `"${column}"`, `[${column}]`].some((value) => definition.includes(value)) +} + +async function rebuildTableWithoutColumns( + db: MigrationDB, + tableName: string, + removedColumns: readonly string[], +) { + if (removedColumns.length === 0) return + + const schemaRows = (await db.all( + sql.raw(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`), + )) as Array<{ sql: string }> + const schema = schemaRows[0]?.sql + if (!schema) return + + const open = schema.indexOf('(') + const close = schema.lastIndexOf(')') + if (open < 0 || close < open) throw new Error(`Could not parse schema for ${tableName}`) + + const definitions = splitDefinitions(schema.slice(open + 1, close)) + const keptDefinitions = definitions.filter((definition) => { + const columnName = definitionColumnName(definition) + const isConstraint = /^(CONSTRAINT|FOREIGN|PRIMARY|UNIQUE|CHECK)\b/i.test(columnName) + if (isConstraint) return !removedColumns.some((column) => referencesColumn(definition, column)) + return !removedColumns.includes(columnName) + }) + const keptColumns = (await columnNames(db, tableName)).filter((column) => !removedColumns.includes(column)) + const indexRows = (await db.all( + sql.raw(`SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ${literal(tableName)} AND sql IS NOT NULL`), + )) as Array<{ sql: string }> + const keptIndexes = indexRows + .map((row) => row.sql) + .filter((indexSql) => !removedColumns.some((column) => referencesColumn(indexSql, column))) + const temporaryTable = `__task4_${tableName}` + const columnList = keptColumns.map(ident).join(', ') + + await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(temporaryTable)};`)) + await db.run(sql.raw(`CREATE TABLE ${ident(temporaryTable)} (${keptDefinitions.join(',\n')})${schema.slice(close + 1)};`)) + await db.run(sql.raw(`INSERT INTO ${ident(temporaryTable)} (${columnList}) SELECT ${columnList} FROM ${ident(tableName)};`)) + await db.run(sql.raw(`DROP TABLE ${ident(tableName)};`)) + await db.run(sql.raw(`ALTER TABLE ${ident(temporaryTable)} RENAME TO ${ident(tableName)};`)) + + for (const indexSql of keptIndexes) await db.run(sql.raw(`${indexSql};`)) +} + +async function trimSponsoringSteps(db: MigrationDB, tableName: string) { + if (!(await tableExists(db, tableName))) return + + await db.run(sql.raw(` + DELETE FROM ${ident(tableName)} + WHERE ${ident('id')} IN ( + SELECT ${ident('id')} + FROM ( + SELECT ${ident('id')}, ROW_NUMBER() OVER ( + PARTITION BY ${ident('_parent_id')} ORDER BY ${ident('_order')} DESC + ) AS row_number + FROM ${ident(tableName)} + ) + WHERE row_number > 2 + ); + `)) + + await db.run(sql.raw(` + UPDATE ${ident(tableName)} AS current + SET ${ident('_order')} = ( + SELECT COUNT(*) + FROM ${ident(tableName)} AS preceding + WHERE preceding.${ident('_parent_id')} = current.${ident('_parent_id')} + AND preceding.${ident('_order')} <= current.${ident('_order')} + ); + `)) +} + +async function createArrayTable( + db: MigrationDB, + tableName: string, + parentTable: 'pages' | '_pages_v', + versioned: boolean, + fields: string, +) { + if (await tableExists(db, tableName)) return + + await db.run(sql.raw(` + CREATE TABLE ${ident(tableName)} ( + ${ident('_order')} integer NOT NULL, + ${ident('_parent_id')} integer NOT NULL, + ${ident('id')} ${versioned ? 'integer' : 'text'} PRIMARY KEY NOT NULL, + ${fields}, + ${versioned ? `${ident('_uuid')} text,` : ''} + FOREIGN KEY (${ident('_parent_id')}) REFERENCES ${ident(parentTable)}(${ident('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)} (${ident('_order')});`)) + await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_parent_id_idx`)} ON ${ident(tableName)} (${ident('_parent_id')});`)) +} + +async function restoreChildTables(db: MigrationDB) { + const textRows = `${ident('num')} text, ${ident('label')} text, ${ident('desc')} text` + const stepRows = `${ident('label')} text, ${ident('icon')} text DEFAULT 'trophy'` + const optionRows = `${ident('value')} text, ${ident('label')} text` + const packageRows = `${ident('value')} text, ${ident('title')} text, ${ident('desc')} text` + + await createArrayTable(db, 'pages_home_application_benefits', 'pages', false, textRows) + await createArrayTable(db, '_pages_v_version_home_application_benefits', '_pages_v', true, textRows) + await createArrayTable(db, 'home_self_steps', 'pages', false, stepRows) + await createArrayTable(db, '_home_self_steps_v', '_pages_v', true, stepRows) + await createArrayTable(db, 'home_nom_steps', 'pages', false, stepRows) + await createArrayTable(db, '_home_nom_steps_v', '_pages_v', true, stepRows) + await createArrayTable(db, 'home_emp_opts', 'pages', false, optionRows) + await createArrayTable(db, '_home_emp_opts_v', '_pages_v', true, optionRows) + await createArrayTable(db, 'form_facts', 'pages', false, textRows) + await createArrayTable(db, '_form_facts_v', '_pages_v', true, textRows) + await createArrayTable(db, 'self_steps', 'pages', false, stepRows) + await createArrayTable(db, '_self_steps_v', '_pages_v', true, stepRows) + await createArrayTable(db, 'nom_steps', 'pages', false, stepRows) + await createArrayTable(db, '_nom_steps_v', '_pages_v', true, stepRows) + await createArrayTable(db, 'emp_opts', 'pages', false, optionRows) + await createArrayTable(db, '_emp_opts_v', '_pages_v', true, optionRows) + await createArrayTable(db, 'network_form_pkgs', 'pages', false, packageRows) + await createArrayTable(db, '_network_form_pkgs_v', '_pages_v', true, packageRows) +} + +export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise { + await db.run(sql.raw('PRAGMA foreign_keys=OFF;')) + + for (const indexName of uploadIndexes) { + await db.run(sql.raw(`DROP INDEX IF EXISTS ${ident(indexName)};`)) + } + + await db.run(sql.raw("DELETE FROM `pages_rels` WHERE `path` = 'about.highlights.selectedWinners';")) + await db.run(sql.raw("DELETE FROM `_pages_v_rels` WHERE `path` = 'version.about.highlights.selectedWinners';")) + + for (const tableName of obsoleteChildTables) { + await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(tableName)};`)) + } + + const pageColumns = await columnNames(db, 'pages') + const removedPageColumns = pageColumns.filter( + (column) => standaloneLiveColumns.includes(column as (typeof standaloneLiveColumns)[number]) || livePrefixes.some((prefix) => column.startsWith(prefix)), + ) + const versionColumns = await columnNames(db, '_pages_v') + const standaloneVersionColumns = standaloneLiveColumns.map((column) => `version_${column}`) + const removedVersionColumns = versionColumns.filter( + (column) => standaloneVersionColumns.includes(column) || versionPrefixes.some((prefix) => column.startsWith(prefix)), + ) + + await rebuildTableWithoutColumns(db, 'pages', removedPageColumns) + await rebuildTableWithoutColumns(db, '_pages_v', removedVersionColumns) + await rebuildTableWithoutColumns(db, 'app_ways', obsoleteApplicationWayColumns) + await rebuildTableWithoutColumns(db, '_app_ways_v', obsoleteApplicationWayColumns) + + await trimSponsoringSteps(db, 'network_form_steps') + await trimSponsoringSteps(db, '_network_form_steps_v') + + await db.run(sql.raw('PRAGMA foreign_keys=ON;')) +} + +export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise { + for (const [column, definition] of restoredLiveColumns) { + await addColumn(db, 'pages', column, definition) + await addColumn(db, '_pages_v', `version_${column}`, definition) + } + + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_home_application_home_application_award_image_idx` ON `pages` (`home_application_award_image_id`);')) + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_home_application_version_home_applicati_idx` ON `_pages_v` (`version_home_application_award_image_id`);')) + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_participation_application_form_participation_appli_idx` ON `pages` (`participation_application_form_image_id`);')) + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_participation_application_form_version__idx` ON `_pages_v` (`version_participation_application_form_image_id`);')) + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_about_highlights_about_highlights_fallback_image_idx` ON `pages` (`about_highlights_fallback_image_id`);')) + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_about_highlights_version_about_highligh_idx` ON `_pages_v` (`version_about_highlights_fallback_image_id`);')) + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_press_index_downloads_press_index_downloads_kit_fi_idx` ON `pages` (`press_index_downloads_kit_file_id`);')) + await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_press_index_downloads_version_press_ind_idx` ON `_pages_v` (`version_press_index_downloads_kit_file_id`);')) + + await restoreChildTables(db) + + await addColumn(db, 'app_ways', 'featured', 'integer DEFAULT false') + await addColumn(db, 'app_ways', 'cta_label', "text DEFAULT 'Zum Formular'") + await addColumn(db, 'app_ways', 'cta_url', "text DEFAULT '#bewerben'") + await addColumn(db, '_app_ways_v', 'featured', 'integer DEFAULT false') + await addColumn(db, '_app_ways_v', 'cta_label', "text DEFAULT 'Zum Formular'") + await addColumn(db, '_app_ways_v', 'cta_url', "text DEFAULT '#bewerben'") +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 15d3b27..610d3e3 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -16,6 +16,7 @@ import * as migration_20260713_194500_contact_submissions from './20260713_19450 import * as migration_20260713_213000_netzwerk_jury_selection from './20260713_213000_netzwerk_jury_selection'; import * as migration_20260713_220000_preistraeger_detail_cleanup from './20260713_220000_preistraeger_detail_cleanup'; import * as migration_20260731_143000_remove_press_record_fallbacks from './20260731_143000_remove_press_record_fallbacks'; +import * as migration_20260731_160000_focused_payload_cleanup from './20260731_160000_focused_payload_cleanup'; export const migrations = [ { @@ -108,4 +109,9 @@ export const migrations = [ down: migration_20260731_143000_remove_press_record_fallbacks.down, name: '20260731_143000_remove_press_record_fallbacks', }, + { + up: migration_20260731_160000_focused_payload_cleanup.up, + down: migration_20260731_160000_focused_payload_cleanup.down, + name: '20260731_160000_focused_payload_cleanup', + }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index 3f5a8fe..2c547fd 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -365,191 +365,6 @@ export interface Page { }[] | null; }; - /** - * Legacy page-level form section. The frontend now uses Formulare > Bewerbungsformular. - */ - application?: { - eyebrow?: string | null; - heading?: string | null; - body?: string | null; - benefits?: - | { - num?: string | null; - label?: string | null; - desc?: string | null; - id?: string | null; - }[] - | null; - /** - * Current frontend image: /images/preisuebergabe.jpg - */ - awardImage?: (number | null) | Media; - awardImageAlt?: string | null; - awardCaption?: string | null; - formEyebrow?: string | null; - formTitle?: string | null; - footnote?: string | null; - footnoteStrong?: string | null; - }; - /** - * Legacy page-level form fields. The frontend now uses Formulare > Bewerbungsformular. - */ - form?: { - stepCompleteLabel?: string | null; - backLabel?: string | null; - nextLabel?: string | null; - submitLabel?: string | null; - yesLabel?: string | null; - noLabel?: string | null; - requiredSuffix?: string | null; - validation?: { - choose?: string | null; - required?: string | null; - invalidEmail?: string | null; - }; - typeStep?: { - eyebrow?: string | null; - heading?: string | null; - selfTitle?: string | null; - selfDesc?: string | null; - nominationTitle?: string | null; - nominationDesc?: string | null; - }; - selfSteps?: - | { - label?: string | null; - icon?: - | ( - | 'alertCircle' - | 'arrowRight' - | 'building2' - | 'check' - | 'checkCircle2' - | 'fileText' - | 'mapPin' - | 'search' - | 'send' - | 'star' - | 'trendingUp' - | 'trophy' - | 'user' - | 'userCheck' - | 'userPlus' - | 'users' - ) - | null; - id?: string | null; - }[] - | null; - nominationSteps?: - | { - label?: string | null; - icon?: - | ( - | 'alertCircle' - | 'arrowRight' - | 'building2' - | 'check' - | 'checkCircle2' - | 'fileText' - | 'mapPin' - | 'search' - | 'send' - | 'star' - | 'trendingUp' - | 'trophy' - | 'user' - | 'userCheck' - | 'userPlus' - | 'users' - ) - | null; - id?: string | null; - }[] - | null; - employeeOptions?: - | { - value?: string | null; - label?: string | null; - id?: string | null; - }[] - | null; - eligibility?: { - allowedEmployeeValues?: string | null; - requiredBayernValue?: string | null; - eyebrow?: string | null; - heading?: string | null; - employeesLabel?: string | null; - bayernLabel?: string | null; - ownerLabel?: string | null; - ineligibleHeading?: string | null; - ineligibleBody?: string | null; - ineligibleContactPrefix?: string | null; - ineligibleContactEmail?: string | null; - }; - selfContact?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - companyPlaceholder?: string | null; - nameLabel?: string | null; - namePlaceholder?: string | null; - emailLabel?: string | null; - emailPlaceholder?: string | null; - phoneLabel?: string | null; - phonePlaceholder?: string | null; - footnote?: string | null; - }; - selfSummary?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - employeesLabel?: string | null; - locationLabel?: string | null; - locationPrefix?: string | null; - contactLabel?: string | null; - }; - nominationCompany?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - companyPlaceholder?: string | null; - industryLabel?: string | null; - industryPlaceholder?: string | null; - locationLabel?: string | null; - locationPlaceholder?: string | null; - }; - nominationContact?: { - eyebrow?: string | null; - heading?: string | null; - nameLabel?: string | null; - namePlaceholder?: string | null; - emailLabel?: string | null; - emailPlaceholder?: string | null; - relationshipLabel?: string | null; - relationshipPlaceholder?: string | null; - footnote?: string | null; - }; - nominationSummary?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - industryLabel?: string | null; - locationLabel?: string | null; - nominatedByLabel?: string | null; - emptyValue?: string | null; - }; - success?: { - selfHeading?: string | null; - nominationHeading?: string | null; - selfPrefix?: string | null; - selfMiddle?: string | null; - selfSuffix?: string | null; - nominationPrefix?: string | null; - nominationMiddle?: string | null; - nominationSuffix?: string | null; - }; - }; /** * Video and fallback copy shown when the hero video button is clicked. */ @@ -752,10 +567,6 @@ export interface Page { label?: string | null; url?: string | null; }; - secondaryCta?: { - label?: string | null; - url?: string | null; - }; criteria?: | { num?: string | null; @@ -812,10 +623,6 @@ export interface Page { }[] | null; nudgeText?: string | null; - nudgeCta?: { - label?: string | null; - url?: string | null; - }; }; /** * Cream section explaining proposal and self-application paths. @@ -824,11 +631,6 @@ export interface Page { eyebrow?: string | null; heading?: string | null; description?: string | null; - recommendedLabel?: string | null; - fallbackCta?: { - label?: string | null; - url?: string | null; - }; institutions?: | { text?: string | null; @@ -860,7 +662,6 @@ export interface Page { | 'users' ) | null; - featured?: boolean | null; listType?: ('institutions' | 'facts') | null; facts?: | { @@ -869,10 +670,6 @@ export interface Page { id?: string | null; }[] | null; - cta?: { - label?: string | null; - url?: string | null; - }; id?: string | null; }[] | null; @@ -926,198 +723,6 @@ export interface Page { }[] | null; }; - /** - * Legacy page-level form section. The frontend now uses Formulare > Bewerbungsformular. - */ - applicationForm?: { - /** - * Current frontend image: /images/preistraeger-jubel.jpg - */ - image?: (number | null) | Media; - imageAlt?: string | null; - imageCaption?: string | null; - eyebrow?: string | null; - heading?: string | null; - description?: string | null; - facts?: - | { - num?: string | null; - label?: string | null; - desc?: string | null; - id?: string | null; - }[] - | null; - formEyebrow?: string | null; - formTitle?: string | null; - uploadCta?: { - label?: string | null; - url?: string | null; - }; - uploadPrompt?: string | null; - uploadFooterCta?: { - label?: string | null; - url?: string | null; - }; - }; - /** - * Legacy page-level form fields. The frontend now uses Formulare > Bewerbungsformular. - */ - form?: { - stepCompleteLabel?: string | null; - backLabel?: string | null; - nextLabel?: string | null; - submitLabel?: string | null; - yesLabel?: string | null; - noLabel?: string | null; - requiredSuffix?: string | null; - validation?: { - choose?: string | null; - required?: string | null; - invalidEmail?: string | null; - }; - typeStep?: { - eyebrow?: string | null; - heading?: string | null; - selfTitle?: string | null; - selfDesc?: string | null; - nominationTitle?: string | null; - nominationDesc?: string | null; - }; - selfSteps?: - | { - label?: string | null; - icon?: - | ( - | 'alertCircle' - | 'arrowRight' - | 'building2' - | 'check' - | 'checkCircle2' - | 'fileText' - | 'mapPin' - | 'search' - | 'send' - | 'star' - | 'trendingUp' - | 'trophy' - | 'user' - | 'userCheck' - | 'userPlus' - | 'users' - ) - | null; - id?: string | null; - }[] - | null; - nominationSteps?: - | { - label?: string | null; - icon?: - | ( - | 'alertCircle' - | 'arrowRight' - | 'building2' - | 'check' - | 'checkCircle2' - | 'fileText' - | 'mapPin' - | 'search' - | 'send' - | 'star' - | 'trendingUp' - | 'trophy' - | 'user' - | 'userCheck' - | 'userPlus' - | 'users' - ) - | null; - id?: string | null; - }[] - | null; - employeeOptions?: - | { - value?: string | null; - label?: string | null; - id?: string | null; - }[] - | null; - eligibility?: { - allowedEmployeeValues?: string | null; - requiredBayernValue?: string | null; - eyebrow?: string | null; - heading?: string | null; - employeesLabel?: string | null; - bayernLabel?: string | null; - ownerLabel?: string | null; - ineligibleHeading?: string | null; - ineligibleBody?: string | null; - ineligibleContactPrefix?: string | null; - ineligibleContactEmail?: string | null; - }; - selfContact?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - companyPlaceholder?: string | null; - nameLabel?: string | null; - namePlaceholder?: string | null; - emailLabel?: string | null; - emailPlaceholder?: string | null; - phoneLabel?: string | null; - phonePlaceholder?: string | null; - footnote?: string | null; - }; - selfSummary?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - employeesLabel?: string | null; - locationLabel?: string | null; - locationPrefix?: string | null; - contactLabel?: string | null; - }; - nominationCompany?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - companyPlaceholder?: string | null; - industryLabel?: string | null; - industryPlaceholder?: string | null; - locationLabel?: string | null; - locationPlaceholder?: string | null; - }; - nominationContact?: { - eyebrow?: string | null; - heading?: string | null; - nameLabel?: string | null; - namePlaceholder?: string | null; - emailLabel?: string | null; - emailPlaceholder?: string | null; - relationshipLabel?: string | null; - relationshipPlaceholder?: string | null; - footnote?: string | null; - }; - nominationSummary?: { - eyebrow?: string | null; - heading?: string | null; - companyLabel?: string | null; - industryLabel?: string | null; - locationLabel?: string | null; - nominatedByLabel?: string | null; - emptyValue?: string | null; - }; - success?: { - selfHeading?: string | null; - nominationHeading?: string | null; - selfPrefix?: string | null; - selfMiddle?: string | null; - selfSuffix?: string | null; - nominationPrefix?: string | null; - nominationMiddle?: string | null; - nominationSuffix?: string | null; - }; - }; }; /** * Edit the Der BMP page in the same order it appears on the frontend. The Preisträger grid reuses the homepage Preisträger card grid and is edited on the Startseite page. The generic Hero and Content tabs are hidden for this SPA-managed route. @@ -1196,32 +801,6 @@ export interface Page { }[] | null; }; - /** - * Legacy data retained for compatibility. The rendered section now reuses the homepage Preisträger card grid. - */ - highlights?: { - /** - * Current frontend fallback image: /images/gala-saal-overview.jpg - */ - fallbackImage?: (number | null) | Media; - eyebrow?: string | null; - heading?: string | null; - cta?: { - label?: string | null; - url?: string | null; - }; - hoverLabel?: string | null; - fallbackWinner?: { - name?: string | null; - category?: string | null; - awardType?: string | null; - }; - year?: number | null; - /** - * Choose up to 8 Preisträger to show. If empty, the frontend falls back to winners from the selected year. - */ - selectedWinners?: (number | Preistraeger)[] | null; - }; /** * Testimonial heading and slides. Upload images when available, or keep the migrated URL fallback. */ @@ -1518,7 +1097,6 @@ export interface Page { eyebrow?: string | null; heading?: string | null; description?: string | null; - detailLabel?: string | null; defaultStatusLabel?: string | null; openStatusLabel?: string | null; missingTitleLabel?: string | null; @@ -1544,7 +1122,6 @@ export interface Page { eyebrow?: string | null; heading?: string | null; description?: string | null; - readMoreLabel?: string | null; missingTitleLabel?: string | null; missingCategoryLabel?: string | null; /** @@ -1553,38 +1130,16 @@ export interface Page { collectionFallbackImage?: (number | null) | Media; }; /** - * Download button and press contact block. + * Press material introduction and contact block. */ downloads?: { eyebrow?: string | null; heading?: string | null; description?: string | null; - kitLabel?: string | null; - kitMeta?: string | null; - /** - * Current frontend file: /Print_BMP.zip - */ - kitFile?: (number | null) | Media; - kitUrl?: string | null; - kitDownloadName?: string | null; contactEyebrow?: string | null; contactName?: string | null; contactLines?: string | null; }; - /** - * Labels and success state for the press accreditation form. - */ - accreditation?: { - eyebrow?: string | null; - heading?: string | null; - description?: string | null; - mediumLabel?: string | null; - nameLabel?: string | null; - emailLabel?: string | null; - submitLabel?: string | null; - successHeading?: string | null; - successMessage?: string | null; - }; }; /** * Edit the Formular hochladen page copy in frontend order. Upload interaction remains handled by the SPA component. @@ -1732,14 +1287,13 @@ export interface Page { | null; }; /** - * Jury section heading, chair badge, members, and note. + * Jury section heading, chair badge, and rendered member selection. */ jury?: { eyebrow?: string | null; heading?: string | null; description?: string | null; chairBadge?: string | null; - note?: string | null; /** * Choose the jury members rendered on this page. Members with a quote are displayed before members without one. */ @@ -1770,8 +1324,6 @@ export interface Page { heading?: string | null; description?: string | null; detailLabel?: string | null; - premiumLabel?: string | null; - defaultLogoColor?: string | null; tiers?: | { tier?: number | null; @@ -1792,7 +1344,7 @@ export interface Page { }; }; /** - * Sponsoring pitch, image, benefits, form eyebrow, and membership footnote. + * Sponsoring pitch, image, benefits, and form eyebrow. */ sponsoring?: { eyebrow?: string | null; @@ -1804,10 +1356,6 @@ export interface Page { image?: (number | null) | Media; imageAlt?: string | null; imageLabel?: string | null; - formEyebrow?: string | null; - footnotePrefix?: string | null; - footnoteLabel?: string | null; - footnoteUrl?: string | null; benefits?: | { num?: string | null; @@ -1818,7 +1366,7 @@ export interface Page { | null; }; /** - * Wizard labels, package options, validation messages, success copy, and footer note. + * Two-step wizard labels, field copy, validation messages, success copy, and footer note. */ sponsoringForm?: { steps?: @@ -1827,16 +1375,6 @@ export interface Page { id?: string | null; }[] | null; - packages?: - | { - value?: string | null; - title?: string | null; - desc?: string | null; - id?: string | null; - }[] - | null; - step1Eyebrow?: string | null; - step1Heading?: string | null; step2Eyebrow?: string | null; step2Heading?: string | null; companyLabel?: string | null; @@ -1852,7 +1390,6 @@ export interface Page { backLabel?: string | null; nextLabel?: string | null; submitLabel?: string | null; - requiredPackageError?: string | null; requiredFieldError?: string | null; invalidEmailError?: string | null; doneLabel?: string | null; @@ -3173,166 +2710,6 @@ export interface PagesSelect { id?: T; }; }; - application?: - | T - | { - eyebrow?: T; - heading?: T; - body?: T; - benefits?: - | T - | { - num?: T; - label?: T; - desc?: T; - id?: T; - }; - awardImage?: T; - awardImageAlt?: T; - awardCaption?: T; - formEyebrow?: T; - formTitle?: T; - footnote?: T; - footnoteStrong?: T; - }; - form?: - | T - | { - stepCompleteLabel?: T; - backLabel?: T; - nextLabel?: T; - submitLabel?: T; - yesLabel?: T; - noLabel?: T; - requiredSuffix?: T; - validation?: - | T - | { - choose?: T; - required?: T; - invalidEmail?: T; - }; - typeStep?: - | T - | { - eyebrow?: T; - heading?: T; - selfTitle?: T; - selfDesc?: T; - nominationTitle?: T; - nominationDesc?: T; - }; - selfSteps?: - | T - | { - label?: T; - icon?: T; - id?: T; - }; - nominationSteps?: - | T - | { - label?: T; - icon?: T; - id?: T; - }; - employeeOptions?: - | T - | { - value?: T; - label?: T; - id?: T; - }; - eligibility?: - | T - | { - allowedEmployeeValues?: T; - requiredBayernValue?: T; - eyebrow?: T; - heading?: T; - employeesLabel?: T; - bayernLabel?: T; - ownerLabel?: T; - ineligibleHeading?: T; - ineligibleBody?: T; - ineligibleContactPrefix?: T; - ineligibleContactEmail?: T; - }; - selfContact?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - companyPlaceholder?: T; - nameLabel?: T; - namePlaceholder?: T; - emailLabel?: T; - emailPlaceholder?: T; - phoneLabel?: T; - phonePlaceholder?: T; - footnote?: T; - }; - selfSummary?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - employeesLabel?: T; - locationLabel?: T; - locationPrefix?: T; - contactLabel?: T; - }; - nominationCompany?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - companyPlaceholder?: T; - industryLabel?: T; - industryPlaceholder?: T; - locationLabel?: T; - locationPlaceholder?: T; - }; - nominationContact?: - | T - | { - eyebrow?: T; - heading?: T; - nameLabel?: T; - namePlaceholder?: T; - emailLabel?: T; - emailPlaceholder?: T; - relationshipLabel?: T; - relationshipPlaceholder?: T; - footnote?: T; - }; - nominationSummary?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - industryLabel?: T; - locationLabel?: T; - nominatedByLabel?: T; - emptyValue?: T; - }; - success?: - | T - | { - selfHeading?: T; - nominationHeading?: T; - selfPrefix?: T; - selfMiddle?: T; - selfSuffix?: T; - nominationPrefix?: T; - nominationMiddle?: T; - nominationSuffix?: T; - }; - }; videoModal?: | T | { @@ -3505,12 +2882,6 @@ export interface PagesSelect { label?: T; url?: T; }; - secondaryCta?: - | T - | { - label?: T; - url?: T; - }; criteria?: | T | { @@ -3529,12 +2900,6 @@ export interface PagesSelect { id?: T; }; nudgeText?: T; - nudgeCta?: - | T - | { - label?: T; - url?: T; - }; }; applicationWays?: | T @@ -3542,13 +2907,6 @@ export interface PagesSelect { eyebrow?: T; heading?: T; description?: T; - recommendedLabel?: T; - fallbackCta?: - | T - | { - label?: T; - url?: T; - }; institutions?: | T | { @@ -3562,7 +2920,6 @@ export interface PagesSelect { title?: T; desc?: T; icon?: T; - featured?: T; listType?: T; facts?: | T @@ -3571,12 +2928,6 @@ export interface PagesSelect { desc?: T; id?: T; }; - cta?: - | T - | { - label?: T; - url?: T; - }; id?: T; }; }; @@ -3631,177 +2982,6 @@ export interface PagesSelect { id?: T; }; }; - applicationForm?: - | T - | { - image?: T; - imageAlt?: T; - imageCaption?: T; - eyebrow?: T; - heading?: T; - description?: T; - facts?: - | T - | { - num?: T; - label?: T; - desc?: T; - id?: T; - }; - formEyebrow?: T; - formTitle?: T; - uploadCta?: - | T - | { - label?: T; - url?: T; - }; - uploadPrompt?: T; - uploadFooterCta?: - | T - | { - label?: T; - url?: T; - }; - }; - form?: - | T - | { - stepCompleteLabel?: T; - backLabel?: T; - nextLabel?: T; - submitLabel?: T; - yesLabel?: T; - noLabel?: T; - requiredSuffix?: T; - validation?: - | T - | { - choose?: T; - required?: T; - invalidEmail?: T; - }; - typeStep?: - | T - | { - eyebrow?: T; - heading?: T; - selfTitle?: T; - selfDesc?: T; - nominationTitle?: T; - nominationDesc?: T; - }; - selfSteps?: - | T - | { - label?: T; - icon?: T; - id?: T; - }; - nominationSteps?: - | T - | { - label?: T; - icon?: T; - id?: T; - }; - employeeOptions?: - | T - | { - value?: T; - label?: T; - id?: T; - }; - eligibility?: - | T - | { - allowedEmployeeValues?: T; - requiredBayernValue?: T; - eyebrow?: T; - heading?: T; - employeesLabel?: T; - bayernLabel?: T; - ownerLabel?: T; - ineligibleHeading?: T; - ineligibleBody?: T; - ineligibleContactPrefix?: T; - ineligibleContactEmail?: T; - }; - selfContact?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - companyPlaceholder?: T; - nameLabel?: T; - namePlaceholder?: T; - emailLabel?: T; - emailPlaceholder?: T; - phoneLabel?: T; - phonePlaceholder?: T; - footnote?: T; - }; - selfSummary?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - employeesLabel?: T; - locationLabel?: T; - locationPrefix?: T; - contactLabel?: T; - }; - nominationCompany?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - companyPlaceholder?: T; - industryLabel?: T; - industryPlaceholder?: T; - locationLabel?: T; - locationPlaceholder?: T; - }; - nominationContact?: - | T - | { - eyebrow?: T; - heading?: T; - nameLabel?: T; - namePlaceholder?: T; - emailLabel?: T; - emailPlaceholder?: T; - relationshipLabel?: T; - relationshipPlaceholder?: T; - footnote?: T; - }; - nominationSummary?: - | T - | { - eyebrow?: T; - heading?: T; - companyLabel?: T; - industryLabel?: T; - locationLabel?: T; - nominatedByLabel?: T; - emptyValue?: T; - }; - success?: - | T - | { - selfHeading?: T; - nominationHeading?: T; - selfPrefix?: T; - selfMiddle?: T; - selfSuffix?: T; - nominationPrefix?: T; - nominationMiddle?: T; - nominationSuffix?: T; - }; - }; }; about?: | T @@ -3871,29 +3051,6 @@ export interface PagesSelect { id?: T; }; }; - highlights?: - | T - | { - fallbackImage?: T; - eyebrow?: T; - heading?: T; - cta?: - | T - | { - label?: T; - url?: T; - }; - hoverLabel?: T; - fallbackWinner?: - | T - | { - name?: T; - category?: T; - awardType?: T; - }; - year?: T; - selectedWinners?: T; - }; testimonials?: | T | { @@ -4136,7 +3293,6 @@ export interface PagesSelect { eyebrow?: T; heading?: T; description?: T; - detailLabel?: T; defaultStatusLabel?: T; openStatusLabel?: T; missingTitleLabel?: T; @@ -4158,7 +3314,6 @@ export interface PagesSelect { eyebrow?: T; heading?: T; description?: T; - readMoreLabel?: T; missingTitleLabel?: T; missingCategoryLabel?: T; collectionFallbackImage?: T; @@ -4169,28 +3324,10 @@ export interface PagesSelect { eyebrow?: T; heading?: T; description?: T; - kitLabel?: T; - kitMeta?: T; - kitFile?: T; - kitUrl?: T; - kitDownloadName?: T; contactEyebrow?: T; contactName?: T; contactLines?: T; }; - accreditation?: - | T - | { - eyebrow?: T; - heading?: T; - description?: T; - mediumLabel?: T; - nameLabel?: T; - emailLabel?: T; - submitLabel?: T; - successHeading?: T; - successMessage?: T; - }; }; formUpload?: | T @@ -4330,7 +3467,6 @@ export interface PagesSelect { heading?: T; description?: T; chairBadge?: T; - note?: T; members?: T; }; expertise?: @@ -4356,8 +3492,6 @@ export interface PagesSelect { heading?: T; description?: T; detailLabel?: T; - premiumLabel?: T; - defaultLogoColor?: T; tiers?: | T | { @@ -4388,10 +3522,6 @@ export interface PagesSelect { image?: T; imageAlt?: T; imageLabel?: T; - formEyebrow?: T; - footnotePrefix?: T; - footnoteLabel?: T; - footnoteUrl?: T; benefits?: | T | { @@ -4410,16 +3540,6 @@ export interface PagesSelect { label?: T; id?: T; }; - packages?: - | T - | { - value?: T; - title?: T; - desc?: T; - id?: T; - }; - step1Eyebrow?: T; - step1Heading?: T; step2Eyebrow?: T; step2Heading?: T; companyLabel?: T; @@ -4435,7 +3555,6 @@ export interface PagesSelect { backLabel?: T; nextLabel?: T; submitLabel?: T; - requiredPackageError?: T; requiredFieldError?: T; invalidEmailError?: T; doneLabel?: T; diff --git a/src/scripts/preload-about-page-cms.ts b/src/scripts/preload-about-page-cms.ts index 28b4b4b..6ee8559 100644 --- a/src/scripts/preload-about-page-cms.ts +++ b/src/scripts/preload-about-page-cms.ts @@ -41,25 +41,15 @@ async function main() { const page = pageResult.docs[0] if (!page) throw new Error('About page not found. Expected spaPath=/der-bmp or slug=der-bmp/about.') - const [heroImage, prizeImage, mittelstandImage, fallbackWinnerImage, winnerResult] = await Promise.all([ + const [heroImage, prizeImage, mittelstandImage] = await Promise.all([ mediaByFilename(payload, aboutContent.hero.backgroundImageFilename), mediaByFilename(payload, aboutContent.prize.imageFilename), mediaByFilename(payload, aboutContent.mittelstand.imageFilename), - mediaByFilename(payload, aboutContent.highlights.fallbackImageFilename), - payload.find({ - collection: 'preistraeger', - limit: 8, - pagination: false, - draft: true, - overrideAccess: true, - where: { year: { equals: aboutContent.highlights.year } }, - }), ]) const { backgroundImageFilename: _heroFilename, ...heroContent } = aboutContent.hero const { imageFilename: _prizeFilename, ...prizeContent } = aboutContent.prize const { imageFilename: _mittelstandFilename, ...mittelstandContent } = aboutContent.mittelstand - const { fallbackImageFilename: _highlightFallbackFilename, ...highlightsContent } = aboutContent.highlights const testimonials = await testimonialsWithImages(payload) await payload.update({ @@ -84,11 +74,6 @@ async function main() { image: mittelstandImage, body: aboutContent.mittelstand.body.map((text) => ({ text })), }, - highlights: { - ...highlightsContent, - fallbackImage: fallbackWinnerImage, - selectedWinners: winnerResult.docs.map((winner) => winner.id), - }, testimonials, }, } as never, diff --git a/src/scripts/preload-home-page-cms.ts b/src/scripts/preload-home-page-cms.ts index 79c8acf..a37cccb 100644 --- a/src/scripts/preload-home-page-cms.ts +++ b/src/scripts/preload-home-page-cms.ts @@ -41,12 +41,11 @@ async function main() { const page = pageResult.docs[0] if (!page) throw new Error('Home page not found. Expected spaPath=/ or slug=startseite/home.') - const [heroImage, quickImage, introImage, benefitsImage, awardImage] = await Promise.all([ + const [heroImage, quickImage, introImage, benefitsImage] = await Promise.all([ mediaByFilename(payload, homeContent.hero.backgroundImageFilename), mediaByFilename(payload, homeContent.quickCheck.imageFilename), mediaByFilename(payload, homeContent.intro.imageFilename), mediaByFilename(payload, homeContent.benefits.imageFilename), - mediaByFilename(payload, homeContent.application.awardImageFilename), ]) const fallbackWinnerImage = await mediaByFilename(payload, homeContent.winners.fallbackImageFilename) const { backgroundImageFilename: _heroFilename, ...heroContent } = homeContent.hero @@ -54,7 +53,6 @@ async function main() { const { imageFilename: _introFilename, ...introContent } = homeContent.intro const { fallbackImageFilename: _winnerFallbackFilename, ...winnersContent } = homeContent.winners const { imageFilename: _benefitsFilename, ...benefitsContent } = homeContent.benefits - const { awardImageFilename: _awardFilename, ...applicationContent } = homeContent.application const testimonials = await testimonialsWithImages(payload) const winners = await payload.find({ @@ -94,11 +92,6 @@ async function main() { ...benefitsContent, image: benefitsImage, }, - application: { - ...applicationContent, - awardImage, - }, - form: homeContent.form, videoModal: homeContent.videoModal, }, } as never, diff --git a/src/scripts/preload-participation-page-cms.ts b/src/scripts/preload-participation-page-cms.ts index c5f11c5..58286eb 100644 --- a/src/scripts/preload-participation-page-cms.ts +++ b/src/scripts/preload-participation-page-cms.ts @@ -23,13 +23,11 @@ async function main() { const page = pageResult.docs[0] if (!page) throw new Error('Participation page not found. Expected spaPath=/teilnahme or slug=teilnahme/participation.') - const [heroImage, formImage, ...awardCardImages] = await Promise.all([ + const [heroImage, ...awardCardImages] = await Promise.all([ mediaByFilename(payload, participationContent.hero.backgroundImageFilename), - mediaByFilename(payload, participationContent.applicationForm.imageFilename), ...participationContent.awardsGrid.cards.map((card) => mediaByFilename(payload, card.imageFilename)), ]) const { backgroundImageFilename: _heroFilename, ...heroContent } = participationContent.hero - const { imageFilename: _formFilename, ...applicationFormContent } = participationContent.applicationForm const { cards: awardCards, ...awardsGridContent } = participationContent.awardsGrid await payload.update({ @@ -54,11 +52,6 @@ async function main() { image: awardCardImages[index], })), }, - applicationForm: { - ...applicationFormContent, - image: formImage, - }, - form: participationContent.form, }, } as never, }) diff --git a/src/scripts/preload-press-index-page-cms.ts b/src/scripts/preload-press-index-page-cms.ts index 0e97dbb..89c8958 100644 --- a/src/scripts/preload-press-index-page-cms.ts +++ b/src/scripts/preload-press-index-page-cms.ts @@ -23,11 +23,10 @@ async function main() { const page = pageResult.docs[0] if (!page) throw new Error('Press index page not found. Expected spaPath=/presse or slug=presse/press.') - const [heroImage, eventFallbackImage, newsFallbackImage, kitFile] = await Promise.all([ + const [heroImage, eventFallbackImage, newsFallbackImage] = await Promise.all([ mediaByFilename(payload, pressIndexContent.hero.imageFilename), mediaByFilename(payload, pressIndexContent.events.collectionFallbackImageFilename), mediaByFilename(payload, pressIndexContent.news.collectionFallbackImageFilename), - mediaByFilename(payload, pressIndexContent.downloads.kitDownloadName), ]) const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events @@ -52,10 +51,7 @@ async function main() { ...newsContent, collectionFallbackImage: newsFallbackImage, }, - downloads: { - ...pressIndexContent.downloads, - kitFile, - }, + downloads: pressIndexContent.downloads, }, } as never, }) diff --git a/src/spa/aboutContent.ts b/src/spa/aboutContent.ts index d1194c7..7c4d7ec 100644 --- a/src/spa/aboutContent.ts +++ b/src/spa/aboutContent.ts @@ -43,20 +43,6 @@ export const aboutContent = { 'In einer globalisierten Welt sind es diese Unternehmen, die für Stabilität, Innovation und soziale Verantwortung stehen. Der BMP macht sie sichtbar.', ], }, - highlights: { - fallbackImageFilename: 'gala-saal-overview.jpg', - eyebrow: 'Preisträger-Highlights', - heading: 'AUSGEZEICHNETE 2025', - cta: { label: 'Alle Preisträger', url: '/preistraeger' }, - hoverLabel: 'Detail →', - year: 2025, - fallbackWinner: { - name: 'Preisträger', - category: 'Preisträger', - awardType: 'Auszeichnung', - }, - selectedWinners: [], - }, testimonials: testimonialsContent, history: { eyebrow: 'Geschichte & Meilensteine', diff --git a/src/spa/components/forms/SponsoringForm.tsx b/src/spa/components/forms/SponsoringForm.tsx index c6c2d95..df5d8b4 100644 --- a/src/spa/components/forms/SponsoringForm.tsx +++ b/src/spa/components/forms/SponsoringForm.tsx @@ -147,8 +147,7 @@ export default function SponsoringForm({ theme = 'dark', content }: { theme?: 'd const isMobile = useIsMobile(); const applicationPhase = useApplicationPhase(); const copy = { ...netzwerkContent.sponsoringForm, ...(content || {}) }; - const configuredSteps = fallbackArray(copy.steps, netzwerkContent.sponsoringForm.steps); - const steps = configuredSteps.slice(-2); + const steps = fallbackArray(copy.steps, netzwerkContent.sponsoringForm.steps); const [step, setStep] = useState(0); const [dir, setDir] = useState(1); const [submitted, setSubmitted] = useState(false); diff --git a/src/spa/homeContent.ts b/src/spa/homeContent.ts index c25d6cf..01bc64a 100644 --- a/src/spa/homeContent.ts +++ b/src/spa/homeContent.ts @@ -106,7 +106,6 @@ export const homeContent = { ], awardImageFilename: 'preisuebergabe.jpg', awardImageAlt: 'Preisverleihung Bayerischer Mittelstandspreis', - awardCaption: 'Preisverleihung 2025', formEyebrow: 'Direktbewerbung 2026', formTitle: 'Kostenlos bewerben', footnote: 'Kostenlos und unverbindlich –', diff --git a/src/spa/netzwerkContent.ts b/src/spa/netzwerkContent.ts index 7a4f033..a02d1aa 100644 --- a/src/spa/netzwerkContent.ts +++ b/src/spa/netzwerkContent.ts @@ -61,7 +61,6 @@ export const netzwerkContent = { description: 'Unabhängige Expertinnen und Experten aus Wirtschaft, Wissenschaft und Verbänden, für einen vollständig unabhängigen Bewertungsprozess.', chairBadge: 'Jury-Vorsitz', - note: 'Hinweis: Einzelne Persönlichkeiten engagieren sich sowohl in der Jury als auch als Partner des BMP – beide Rollen werden auf dieser Seite getrennt ausgewiesen.', members: [ { name: 'Prof. Dr. Klaus Bergmann', @@ -123,8 +122,6 @@ export const netzwerkContent = { heading: 'PARTNER & SPONSOREN', description: 'Ein starkes Netzwerk aus Unternehmen und Medien begleitet den Bayerischen Mittelstandspreis. Klicken für Details.', detailLabel: 'Details', - premiumLabel: '', - defaultLogoColor: '#111D55', tiers: [ { tier: 1, label: 'Große Darstellung', note: '' }, { tier: 2, label: 'Mittlere Darstellung', note: '' }, @@ -287,10 +284,6 @@ export const netzwerkContent = { imageFilename: 'networking-innenhof.jpg', imageAlt: 'BMP Partnernetzwerk', imageLabel: 'BMP Partnernetzwerk', - formEyebrow: 'Sponsoring 2026', - footnotePrefix: 'Oder:', - footnoteLabel: 'Vereinsmitglied werden', - footnoteUrl: '/mitglied-werden', benefits: [ { num: '01', label: 'Markenpräsenz', sub: 'Bühne, Drucksachen, digitale Kanäle' }, { num: '02', label: 'Top-Entscheider', sub: 'Exklusives Netzwerk-Dinner für Partner' }, @@ -302,13 +295,6 @@ export const netzwerkContent = { { label: 'Unternehmen' }, { label: 'Kontakt' }, ], - packages: [ - { value: 'main', title: 'Haupt-Sponsoring', desc: 'Maximale Sichtbarkeit auf allen Kanälen – Bühne, Digital, Print.' }, - { value: 'category', title: 'Kategorie-Partner', desc: 'Namensgebung einer Auszeichnungskategorie – fokussiertes Branding.' }, - { value: 'event', title: 'Event-Partner', desc: 'Präsenz beim exklusiven Gala-Abend in München.' }, - ], - step1Eyebrow: 'Schritt 1 – Paket wählen', - step1Heading: 'Interesse an', step2Eyebrow: 'Schritt 2 – Unternehmen', step2Heading: 'Ihr Unternehmen', companyLabel: 'Unternehmen *', @@ -324,7 +310,6 @@ export const netzwerkContent = { backLabel: 'Zurück', nextLabel: 'Weiter', submitLabel: 'Exposé anfordern', - requiredPackageError: 'Bitte wählen Sie ein Paket.', requiredFieldError: 'Pflichtfeld', invalidEmailError: 'Ungültige E-Mail', doneLabel: '✓', diff --git a/src/spa/pages/Home.tsx b/src/spa/pages/Home.tsx index cde028b..acd39ef 100644 --- a/src/spa/pages/Home.tsx +++ b/src/spa/pages/Home.tsx @@ -34,7 +34,6 @@ type HomeHeroCms = { videoLabel?: string | null; }; type HomeSectionCms = CmsRecord & { - awardCaption?: string | null; awardImage?: unknown; awardImageAlt?: string | null; benefits?: unknown; diff --git a/src/spa/pages/Press.tsx b/src/spa/pages/Press.tsx index 6a86d85..b516801 100644 --- a/src/spa/pages/Press.tsx +++ b/src/spa/pages/Press.tsx @@ -23,7 +23,7 @@ type PressIndexCms = Partial & { hero?: Partial & { image?: unknown } events?: Partial & { collectionFallbackImage?: unknown } news?: Partial & { collectionFallbackImage?: unknown } - downloads?: Partial & { kitFile?: unknown } + downloads?: Partial } const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; diff --git a/src/spa/participationContent.ts b/src/spa/participationContent.ts index 706e037..5265894 100644 --- a/src/spa/participationContent.ts +++ b/src/spa/participationContent.ts @@ -51,7 +51,6 @@ export const participationContent = { eyebrow: 'Berechtigung', heading: 'WER KANN\nTEILNEHMEN?', primaryCta: { label: 'Jetzt bewerben', url: '#bewerben' }, - secondaryCta: { label: 'Mitglied werden', url: '/mitglied-werden' }, description: 'Vier klar definierte Kriterien: Erfüllen Sie alle, bewerben Sie sich kostenlos und ohne bürokratischen Aufwand.', criteria: [ @@ -74,7 +73,6 @@ export const participationContent = { }, ], nudgeText: 'Alle 4 Punkte erfüllt? Dann jetzt kostenlos bewerben.', - nudgeCta: { label: 'Zum Formular', url: '#bewerben' }, }, applicationWays: { eyebrow: 'Bewerbung 2026', @@ -90,15 +88,12 @@ export const participationContent = { 'Mitglieder des Wirtschaftsbeirates Bayern', 'Ehemalige Preisträger des Bayerischen Mittelstandspreises', ].map((text) => ({ text })), - fallbackCta: { label: 'Zum Formular', url: '#bewerben' }, ways: [ { label: 'Weg 01', title: 'Vorschlag\nunterbreiten', desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.', icon: 'send', - featured: false, - cta: { label: 'Vorschlag unterbreiten', url: '#bewerben' }, listType: 'institutions', facts: [], }, @@ -107,8 +102,6 @@ export const participationContent = { title: 'Initiativ­bewerbung', desc: 'Für Unternehmer, die selbst die Initiative ergreifen.', icon: 'userCheck', - featured: false, - cta: { label: 'Jetzt bewerben', url: '#bewerben' }, listType: 'facts', facts: [ { label: '0 EUR', desc: 'Teilnahmegebühr' }, @@ -118,7 +111,6 @@ export const participationContent = { ], }, ], - recommendedLabel: 'Empfohlen', }, datesBanner: { heading: 'Wichtige Termine 2026', @@ -135,25 +127,6 @@ export const participationContent = { ], }, awardsGrid: participationAwardsGrid, - applicationForm: { - imageFilename: 'preistraeger-jubel.jpg', - imageAlt: 'Preisverleihung 2025', - imageCaption: 'Preisverleihung 2025', - eyebrow: 'Direktbewerbung', - heading: 'BEWERBEN ODER\nVORSCHLAGEN.', - description: - 'Sie können sich selbst bewerben oder ein herausragendes Unternehmen vorschlagen. Firmenverbunde können sich ebenfalls bewerben. Die Teilnahme ist vollständig kostenfrei.', - facts: [ - { num: '01', label: '0 EUR', desc: 'Teilnahmegebühr' }, - { num: '02', label: 'Schnell', desc: 'Ca. 30–45 Minuten' }, - { num: '03', label: 'Mehrstufig', desc: 'Transparenter Prozess' }, - ], - formEyebrow: 'Bewerbung 2026', - formTitle: 'Kostenlos bewerben', - 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.', - uploadFooterCta: { label: 'Formular hochladen →', url: '/formular-hochladen' }, - }, form: { stepCompleteLabel: '✓', backLabel: 'Zurück', diff --git a/src/spa/pressIndexContent.ts b/src/spa/pressIndexContent.ts index e8089a7..b215ec8 100644 --- a/src/spa/pressIndexContent.ts +++ b/src/spa/pressIndexContent.ts @@ -10,7 +10,6 @@ export const pressIndexContent = { eyebrow: 'Termine 2026', heading: 'EVENTS RUND UM DEN PREIS.', description: 'Von der Jurysitzung bis zur festlichen Gala – alle Veranstaltungen auf einen Blick.', - detailLabel: 'Details', defaultStatusLabel: 'Geplant', openStatusLabel: 'Anmeldung offen', missingTitleLabel: 'Event', @@ -29,7 +28,6 @@ export const pressIndexContent = { eyebrow: 'Newsroom', heading: 'NACHRICHTEN & EINBLICKE.', description: 'Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.', - readMoreLabel: 'Weiterlesen', missingTitleLabel: 'Presse', missingCategoryLabel: 'Presse', collectionFallbackImageFilename: 'gala-saal-overview.jpg', @@ -38,23 +36,8 @@ export const pressIndexContent = { eyebrow: 'Pressekontakt & Material', heading: 'PRESSE-MATERIAL & KONTAKT.', description: 'Laden Sie unser offizielles Material herunter oder kontaktieren Sie direkt unser Presseteam für individuelle Anfragen.', - kitLabel: 'Download Presse-Kit', - kitMeta: 'Print_BMP.zip – 1,5 MB', - kitUrl: '/Print_BMP.zip', - kitDownloadName: 'Print_BMP.zip', contactEyebrow: 'Pressekontakt', contactName: 'Tanja Meier', contactLines: 'presse@bmp-bayern.de\n+49 89 123 456 99', }, - accreditation: { - eyebrow: 'Akkreditierung', - heading: 'AKKREDITIERUNG ANFRAGEN.', - description: 'Melden Sie sich für unsere Presse-Verteiler an oder fordern Sie eine Akkreditierung für die Gala-Verleihung an.', - mediumLabel: 'Medium / Redaktion *', - nameLabel: 'Ihr Name *', - emailLabel: 'E-Mail-Adresse *', - submitLabel: 'Akkreditierung anfragen', - successHeading: 'Anfrage eingegangen', - successMessage: 'Vielen Dank für Ihre Akkreditierungsanfrage. Unser Presseteam meldet sich zeitnah bei Ihnen.', - }, } diff --git a/tests/int/payload-field-inventory.int.spec.ts b/tests/int/payload-field-inventory.int.spec.ts new file mode 100644 index 0000000..d87d699 --- /dev/null +++ b/tests/int/payload-field-inventory.int.spec.ts @@ -0,0 +1,133 @@ +import type { Field } from 'payload' +import { describe, expect, it } from 'vitest' + +import { ApplicationForm } from '@/ApplicationForm/config' +import { ApplicationPhase } from '@/ApplicationPhase/config' +import { Footer } from '@/Footer/config' +import { Header } from '@/Header/config' +import { NewsletterForm } from '@/NewsletterForm/config' +import { SiteSettings } from '@/SiteSettings/config' +import { aboutFields } from '@/collections/Pages/aboutFields' +import { homeFields } from '@/collections/Pages/homeFields' +import { netzwerkFields } from '@/collections/Pages/netzwerkFields' +import { participationFields } from '@/collections/Pages/participationFields' +import { pressIndexFields } from '@/collections/Pages/pressIndexFields' + +function fieldPaths(fields: Field[], prefix = ''): string[] { + return fields.flatMap((field) => { + if (!('name' in field) || typeof field.name !== 'string') { + return 'fields' in field && Array.isArray(field.fields) ? fieldPaths(field.fields, prefix) : [] + } + + const path = prefix ? `${prefix}.${field.name}` : field.name + const children = 'fields' in field && Array.isArray(field.fields) ? fieldPaths(field.fields, path) : [] + return [path, ...children] + }) +} + +function findField(fields: Field[], path: string): Field | undefined { + const [name, ...rest] = path.split('.') + const field = fields.find((candidate) => 'name' in candidate && candidate.name === name) + if (!field || rest.length === 0) return field + if (!('fields' in field) || !Array.isArray(field.fields)) return undefined + return findField(field.fields, rest.join('.')) +} + +describe('Payload field inventory', () => { + it('keeps obsolete page controls out of the schema', () => { + const paths = [ + ...fieldPaths(homeFields), + ...fieldPaths(participationFields), + ...fieldPaths(aboutFields), + ...fieldPaths(pressIndexFields), + ...fieldPaths(netzwerkFields), + ] + + expect(paths).not.toEqual( + expect.arrayContaining([ + 'home.application', + 'home.form', + 'participation.applicationForm', + 'participation.form', + 'participation.eligibility.secondaryCta', + 'participation.eligibility.nudgeCta', + 'participation.applicationWays.recommendedLabel', + 'participation.applicationWays.fallbackCta', + 'participation.applicationWays.ways.featured', + 'participation.applicationWays.ways.cta', + 'about.highlights', + 'pressIndex.events.detailLabel', + 'pressIndex.news.readMoreLabel', + 'pressIndex.downloads.kitLabel', + 'pressIndex.downloads.kitMeta', + 'pressIndex.downloads.kitFile', + 'pressIndex.downloads.kitUrl', + 'pressIndex.downloads.kitDownloadName', + 'pressIndex.accreditation', + 'netzwerk.jury.note', + 'netzwerk.partners.premiumLabel', + 'netzwerk.partners.defaultLogoColor', + 'netzwerk.sponsoring.footnotePrefix', + 'netzwerk.sponsoring.footnoteLabel', + 'netzwerk.sponsoring.footnoteUrl', + 'netzwerk.sponsoring.formEyebrow', + 'netzwerk.sponsoringForm.packages', + 'netzwerk.sponsoringForm.step1Eyebrow', + 'netzwerk.sponsoringForm.step1Heading', + 'netzwerk.sponsoringForm.requiredPackageError', + ]), + ) + }) + + it('retains rendered page controls and authoritative relationships', () => { + const paths = [ + ...fieldPaths(homeFields), + ...fieldPaths(participationFields), + ...fieldPaths(aboutFields), + ...fieldPaths(pressIndexFields), + ...fieldPaths(netzwerkFields), + ] + + expect(paths).toEqual( + expect.arrayContaining([ + 'home.hero', + 'home.winners.featured', + 'home.videoModal.video', + 'participation.process.steps', + 'participation.awardsGrid.cards', + 'about.testimonials.items', + 'pressIndex.events.statusLabels', + 'pressIndex.downloads.contactName', + 'netzwerk.jury.members', + 'netzwerk.partners.tiers', + 'netzwerk.partners.modal.websiteLabel', + 'netzwerk.sponsoringForm.steps', + 'netzwerk.sponsoringForm.requiredFieldError', + ]), + ) + }) + + it('keeps shared multi-page and site-wide content in Globals', () => { + const globals = [ApplicationForm, ApplicationPhase, NewsletterForm, Header, Footer, SiteSettings] + const slugs = globals.map((global) => global.slug) + + expect(slugs).toEqual([ + 'application-form', + 'application-phase', + 'newsletter-form', + 'header', + 'footer', + 'site-settings', + ]) + expect(fieldPaths(ApplicationForm.fields)).toEqual(expect.arrayContaining(['section', 'form'])) + expect(fieldPaths(ApplicationPhase.fields)).toEqual(expect.arrayContaining(['activePhase', 'phases'])) + expect(fieldPaths(NewsletterForm.fields)).toEqual(expect.arrayContaining(['evaluation', 'completed'])) + }) + + it('exposes exactly two editable sponsoring step labels', () => { + expect(findField(netzwerkFields, 'netzwerk.sponsoringForm.steps')).toMatchObject({ + minRows: 2, + maxRows: 2, + }) + }) +})