feat: add application form configuration and migration for shared application form data

- Introduced a new configuration file for the application form with various fields and validation messages.
- Created a migration script to set up the database schema for the application form, including necessary tables and default values.
- Added a migration for supporting awards copy to enhance the pages with additional columns.
- Implemented a new SPA component for the application form section, integrating form logic and UI elements.
This commit is contained in:
syntaxbullet
2026-07-13 16:07:58 +02:00
parent ee7e891a7d
commit 9ebe544987
19 changed files with 1026 additions and 259 deletions

View File

@@ -0,0 +1,191 @@
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 sectionScalars = [
['eyebrow', homeContent.application.eyebrow],
['heading', homeContent.application.heading],
['body', homeContent.application.body],
['awardImageAlt', homeContent.application.awardImageAlt],
['formEyebrow', homeContent.application.formEyebrow],
['formTitle', homeContent.application.formTitle],
['footnote', homeContent.application.footnote],
['footnoteStrong', homeContent.application.footnoteStrong],
] as const
const topLevelFormScalars = [
['stepCompleteLabel', homeContent.form.stepCompleteLabel],
['backLabel', homeContent.form.backLabel],
['nextLabel', homeContent.form.nextLabel],
['submitLabel', homeContent.form.submitLabel],
['yesLabel', homeContent.form.yesLabel],
['noLabel', homeContent.form.noLabel],
['requiredSuffix', homeContent.form.requiredSuffix],
] as const
const formGroups = {
validation: homeContent.form.validation,
typeStep: homeContent.form.typeStep,
eligibility: homeContent.form.eligibility,
selfContact: homeContent.form.selfContact,
selfSummary: homeContent.form.selfSummary,
nominationCompany: homeContent.form.nominationCompany,
nominationContact: homeContent.form.nominationContact,
nominationSummary: homeContent.form.nominationSummary,
success: homeContent.form.success,
} as const
const mainScalarDefinitions = [
...sectionScalars.map(([name, value]) => [`section_${snake(name)}`, value] as const),
...topLevelFormScalars.map(([name, value]) => [`form_${snake(name)}`, value] as const),
...Object.entries(formGroups).flatMap(([group, values]) =>
Object.entries(values).map(([name, value]) => [`form_${snake(group)}_${snake(name)}`, value] as const),
),
]
async function tableExists(db: MigrationDB, tableName: string) {
const rows = (await db.all(
sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
)) as Array<{ name: string }>
return rows.length > 0
}
async function columnExists(db: MigrationDB, tableName: string, columnName: string) {
if (!(await tableExists(db, tableName))) return false
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
return columns.some((column) => column.name === columnName)
}
async function createArrayTable(
db: MigrationDB,
tableName: string,
fields: Array<{ name: string; defaultValue?: string }>,
) {
const fieldDefinitions = fields
.map((field) => `${ident(field.name)} text${field.defaultValue ? ` DEFAULT ${literal(field.defaultValue)}` : ''}`)
.join(',\n')
await db.run(sql.raw(`
CREATE TABLE IF NOT EXISTS ${ident(tableName)} (
\`_order\` integer NOT NULL,
\`_parent_id\` integer NOT NULL,
\`id\` text PRIMARY KEY NOT NULL,
${fieldDefinitions},
FOREIGN KEY (\`_parent_id\`) REFERENCES \`application_form\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`))
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_order_idx`)} ON ${ident(tableName)} (\`_order\`);`))
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_parent_id_idx`)} ON ${ident(tableName)} (\`_parent_id\`);`))
}
async function seedArrayFromPage(
db: MigrationDB,
target: string,
source: string,
columns: string[],
defaults: ReadonlyArray<Record<string, unknown>>,
) {
const sourceExists = await tableExists(db, source)
if (sourceExists) {
const columnList = columns.map(ident).join(', ')
await db.run(sql.raw(`
INSERT OR IGNORE INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, ${columnList})
SELECT source.\`_order\`, 1, lower(hex(randomblob(16))), ${columns.map((column) => `source.${ident(column)}`).join(', ')}
FROM ${ident(source)} source
INNER JOIN \`pages\` page ON page.\`id\` = source.\`_parent_id\`
WHERE page.\`spa_path\` = '/' OR page.\`slug\` IN ('startseite', 'home')
ORDER BY source.\`_order\`;
`))
}
const count = (await db.all(sql.raw(`SELECT COUNT(*) AS count FROM ${ident(target)} WHERE \`_parent_id\` = 1;`))) as Array<{ count: number }>
if (Number(count[0]?.count || 0) > 0) return
for (const [index, item] of defaults.entries()) {
const values = columns.map((column) => literal(String(item[column] ?? ''))).join(', ')
await db.run(sql.raw(`
INSERT INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, ${columns.map(ident).join(', ')})
VALUES (${index + 1}, 1, lower(hex(randomblob(16))), ${values});
`))
}
}
export async function up({ db }: MigrateUpArgs): Promise<void> {
const scalarColumns = mainScalarDefinitions
.map(([name, value]) => `${ident(name)} text DEFAULT ${literal(value)}`)
.join(',\n')
await db.run(sql.raw(`
CREATE TABLE IF NOT EXISTS \`application_form\` (
\`id\` integer PRIMARY KEY NOT NULL,
${scalarColumns},
\`section_award_image_id\` integer,
\`updated_at\` text,
\`created_at\` text,
FOREIGN KEY (\`section_award_image_id\`) REFERENCES \`media\`(\`id\`) ON UPDATE no action ON DELETE set null
);
`))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `application_form_section_section_award_image_idx` ON `application_form` (`section_award_image_id`);'))
await createArrayTable(db, 'application_form_benefits', [
{ name: 'num' },
{ name: 'label' },
{ name: 'desc' },
])
await createArrayTable(db, 'application_form_self_steps', [
{ name: 'label' },
{ name: 'icon', defaultValue: 'trophy' },
])
await createArrayTable(db, 'application_form_nom_steps', [
{ name: 'label' },
{ name: 'icon', defaultValue: 'trophy' },
])
await createArrayTable(db, 'application_form_emp_opts', [
{ name: 'value' },
{ name: 'label' },
])
await db.run(sql.raw(`
INSERT OR IGNORE INTO \`application_form\` (\`id\`, \`updated_at\`, \`created_at\`)
VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
`))
if (await columnExists(db, 'pages', 'home_application_eyebrow')) {
const assignments = [
...sectionScalars.map(([name]) => [`section_${snake(name)}`, `home_application_${snake(name)}`] as const),
...topLevelFormScalars.map(([name]) => [`form_${snake(name)}`, `home_form_${snake(name)}`] as const),
...Object.keys(formGroups).flatMap((group) =>
Object.keys(formGroups[group as keyof typeof formGroups]).map((name) => [
`form_${snake(group)}_${snake(name)}`,
`home_form_${snake(group)}_${snake(name)}`,
] as const),
),
]
.filter(([, source]) => source !== 'home_application_award_image')
.map(([target, source]) => `${ident(target)} = COALESCE((SELECT ${ident(source)} FROM \`pages\` WHERE \`spa_path\` = '/' OR \`slug\` IN ('startseite', 'home') ORDER BY CASE WHEN \`spa_path\` = '/' THEN 0 ELSE 1 END LIMIT 1), ${ident(target)})`)
if (await columnExists(db, 'pages', 'home_application_award_image_id')) {
assignments.push("`section_award_image_id` = (SELECT `home_application_award_image_id` FROM `pages` WHERE `spa_path` = '/' OR `slug` IN ('startseite', 'home') ORDER BY CASE WHEN `spa_path` = '/' THEN 0 ELSE 1 END LIMIT 1)")
}
await db.run(sql.raw(`UPDATE \`application_form\` SET ${assignments.join(',\n')} WHERE \`id\` = 1;`))
}
await seedArrayFromPage(db, 'application_form_benefits', 'pages_home_application_benefits', ['num', 'label', 'desc'], homeContent.application.benefits)
await seedArrayFromPage(db, 'application_form_self_steps', 'home_self_steps', ['label', 'icon'], homeContent.form.selfSteps)
await seedArrayFromPage(db, 'application_form_nom_steps', 'home_nom_steps', ['label', 'icon'], homeContent.form.nominationSteps)
await seedArrayFromPage(db, 'application_form_emp_opts', 'home_emp_opts', ['value', 'label'], homeContent.form.employeeOptions)
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
await db.run(sql.raw('DROP TABLE IF EXISTS `application_form_emp_opts`;'))
await db.run(sql.raw('DROP TABLE IF EXISTS `application_form_nom_steps`;'))
await db.run(sql.raw('DROP TABLE IF EXISTS `application_form_self_steps`;'))
await db.run(sql.raw('DROP TABLE IF EXISTS `application_form_benefits`;'))
await db.run(sql.raw('DROP TABLE IF EXISTS `application_form`;'))
}