feat(portal): public-form component architecture

Replace monolithic register/[eventSlug].vue with composable field
renderer, conditional-logic engine, stepper, and per-field components
driven by Form Builder schema. Adds flatpickr for date fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 17:20:59 +02:00
parent 0cbdad70cd
commit 4074dce402
29 changed files with 2622 additions and 1574 deletions

View File

@@ -0,0 +1,149 @@
import { emailValidator, regexValidator, requiredValidator, urlValidator } from '@core/utils/validators'
import { FormFieldType } from '@/types/formBuilder'
import type { PublicFormField } from '@/types/formBuilder'
export type Validator = (value: unknown) => true | string
/**
* Build a list of client-side validators for a public form field.
* Runs in VTextField `:rules` and the stepper "current step valid"
* gate. Mirrors (a subset of) the backend relaxed rule set so the
* submitter gets feedback before the submit round-trip.
*/
export function getValidatorsForField(field: PublicFormField): Validator[] {
const rules: Validator[] = []
const v = field.validation_rules ?? {}
if (field.is_required) {
rules.push(value => {
const ok = requiredValidator(value)
return ok === true ? true : 'Dit veld is verplicht.'
})
}
switch (field.field_type) {
case FormFieldType.EMAIL:
rules.push(value => {
const ok = emailValidator(value)
return ok === true ? true : 'Vul een geldig e-mailadres in.'
})
break
case FormFieldType.URL:
rules.push(value => {
const ok = urlValidator(value)
return ok === true ? true : 'Vul een geldige URL in (beginnend met http:// of https://).'
})
break
case FormFieldType.PHONE:
rules.push(value => {
if (value === null || value === undefined || value === '') return true
const s = String(value).replace(/\s+/g, '')
return /^\+?[\d()-]{6,}$/.test(s) || 'Vul een geldig telefoonnummer in.'
})
break
case FormFieldType.NUMBER:
rules.push(value => {
if (value === null || value === undefined || value === '') return true
const n = Number(value)
return Number.isFinite(n) || 'Vul een geldig getal in.'
})
if (typeof v.min === 'number') {
const min = v.min
rules.push(value => {
if (value === null || value === undefined || value === '') return true
const n = Number(value)
return Number.isFinite(n) && n >= min ? true : `Minimaal ${min}.`
})
}
if (typeof v.max === 'number') {
const max = v.max
rules.push(value => {
if (value === null || value === undefined || value === '') return true
const n = Number(value)
return Number.isFinite(n) && n <= max ? true : `Maximaal ${max}.`
})
}
break
case FormFieldType.TEXT:
case FormFieldType.TEXTAREA:
if (typeof v.min === 'number') {
const min = v.min
rules.push(value => {
if (value === null || value === undefined || value === '') return true
return String(value).length >= min ? true : `Minimaal ${min} tekens.`
})
}
if (typeof v.max === 'number') {
const max = v.max
rules.push(value => {
if (value === null || value === undefined || value === '') return true
return String(value).length <= max ? true : `Maximaal ${max} tekens.`
})
}
if (typeof v.pattern === 'string' && v.pattern.length > 0) {
const pattern = v.pattern
rules.push(value => {
if (value === null || value === undefined || value === '') return true
const ok = regexValidator(value, pattern)
return ok === true ? true : 'Ongeldige invoer.'
})
}
break
case FormFieldType.MULTISELECT:
case FormFieldType.CHECKBOX_LIST:
if (typeof v.min_selections === 'number') {
const min = v.min_selections
rules.push(value => {
const arr = Array.isArray(value) ? value : []
return arr.length >= min ? true : `Kies er minimaal ${min}.`
})
}
if (typeof v.max_selections === 'number') {
const max = v.max_selections
rules.push(value => {
const arr = Array.isArray(value) ? value : []
return arr.length <= max ? true : `Kies er maximaal ${max}.`
})
}
break
default:
break
}
return rules
}
export function runValidators(rules: Validator[], value: unknown): string | true {
for (const rule of rules) {
const r = rule(value)
if (r !== true) return r
}
return true
}
export function isFieldValueEmpty(value: unknown): boolean {
if (value === null || value === undefined) return true
if (typeof value === 'string') return value.trim() === ''
if (Array.isArray(value)) return value.length === 0
return false
}