diff --git a/apps/portal/src/components/registration/DynamicRegistrationFields.vue b/apps/portal/src/components/registration/DynamicRegistrationFields.vue deleted file mode 100644 index f0209c8b..00000000 --- a/apps/portal/src/components/registration/DynamicRegistrationFields.vue +++ /dev/null @@ -1,274 +0,0 @@ - - - - - diff --git a/apps/portal/src/pages/register/[eventSlug].vue b/apps/portal/src/pages/register/[eventSlug].vue index 93a8f2de..3b7a444c 100644 --- a/apps/portal/src/pages/register/[eventSlug].vue +++ b/apps/portal/src/pages/register/[eventSlug].vue @@ -2,15 +2,14 @@ import { useForm } from 'vee-validate' import { toTypedSchema } from '@vee-validate/zod' import { useDisplay } from 'vuetify' -import DynamicRegistrationFields from '@/components/registration/DynamicRegistrationFields.vue' import { useAuthStore } from '@/stores/useAuthStore' import { useRegistrationData, useSubmitRegistration } from '@/composables/api/useVolunteerRegistration' import { fullRegistrationSchema } from '@/schemas/registrationSchema' import type { - FieldValuePayload, - RegistrationFieldType, SectionOption, + SectionPreference, TimeSlotOption, + VolunteerAvailability, VolunteerRegistrationForm, } from '@/types/registration' @@ -22,8 +21,6 @@ definePage({ }, }) -type StepKind = 'personal' | 'dynamic' | 'sections' | 'availability' - const route = useRoute('volunteer-register') const router = useRouter() const authStore = useAuthStore() @@ -35,38 +32,9 @@ const { mutateAsync: submitRegistration, isPending: isSubmitting } = useSubmitRe const currentStep = ref(0) const submitError = ref(null) -const fieldValues = ref>({}) -const fieldErrors = ref>({}) -const stepMeta: Record = { - personal: { title: 'Over jou', subtitle: 'Vul je persoonlijke gegevens in' }, - dynamic: { title: 'Extra informatie', subtitle: 'Antwoord op de vragen van de organisatie' }, - sections: { title: 'Voorkeurssecties', subtitle: 'Waar wil je het liefst werken?' }, - availability: { title: 'Beschikbaarheid', subtitle: 'Wanneer kun je helpen?' }, -} - -const stepKinds = computed(() => { - if (!registrationData.value) return ['personal', 'dynamic'] - const kinds: StepKind[] = ['personal', 'dynamic'] - if (registrationData.value.event.registration_show_section_preferences) - kinds.push('sections') - if (registrationData.value.event.registration_show_availability) - kinds.push('availability') - - return kinds -}) - -const steps = computed(() => stepKinds.value.map(k => stepMeta[k])) - -watch(stepKinds, kinds => { - if (currentStep.value >= kinds.length) - currentStep.value = Math.max(0, kinds.length - 1) -}) - -const currentKind = computed(() => stepKinds.value[currentStep.value] ?? 'personal') - -// VeeValidate — fixed personal fields only -const { errors, defineField, validateField, setFieldValue, setFieldError } = useForm({ +// VeeValidate form +const { errors, defineField, validateField, setFieldValue } = useForm({ validationSchema: toTypedSchema(fullRegistrationSchema), initialValues: { first_name: '', @@ -74,6 +42,12 @@ const { errors, defineField, validateField, setFieldValue, setFieldError } = use email: '', date_of_birth: '', phone: '', + tshirt_size: '', + first_aid: false, + allergies: '', + driving_licence: false, + motivation: '', + motivation_other: '', }, }) @@ -82,7 +56,14 @@ const [lastName] = defineField('last_name') const [dateOfBirth] = defineField('date_of_birth') const [email] = defineField('email') const [phone] = defineField('phone') +const [tshirtSize] = defineField('tshirt_size') +const [firstAid] = defineField('first_aid') +const [allergies] = defineField('allergies') +const [drivingLicence] = defineField('driving_licence') +const [motivation] = defineField('motivation') +const [motivationOther] = defineField('motivation_other') +// Pre-fill authenticated user data watch(() => authStore.user, user => { if (user) { setFieldValue('first_name', user.first_name) @@ -91,50 +72,38 @@ watch(() => authStore.user, user => { } }, { immediate: true }) -const registrationFieldsList = computed(() => registrationData.value?.registration_fields ?? []) +// Step 3: Section preferences (by name, not ID) +const selectedSections = ref([]) -const groupedRegistrationFields = computed(() => { - const fields = registrationFieldsList.value - const groups: { section: string | null; fields: typeof fields }[] = [] - for (const f of fields) { - const last = groups[groups.length - 1] - if (last && last.section === f.section) - last.fields.push(f) - else - groups.push({ section: f.section, fields: [f] }) - } - - return groups -}) - -function defaultFieldValue(type: RegistrationFieldType): unknown { - switch (type) { - case 'multiselect': - case 'checkbox': - case 'tag_picker': - return [] - case 'boolean': - return false - case 'number': - return null - default: - return '' - } -} - -watch(registrationFieldsList, fields => { - for (const f of fields) { - if (!(f.slug in fieldValues.value)) - fieldValues.value[f.slug] = defaultFieldValue(f.field_type) - } -}, { immediate: true }) - -// Section preferences (by festival_section_id, max 5, order = priority) -const selectedSectionIds = ref([]) - -// Availability (time slots) +// Step 4: Availability const selectedTimeSlotIds = ref([]) +const timeSlotPreferences = ref>({}) +// Steps definition +const steps = [ + { title: 'Over jou', subtitle: 'Vul je persoonlijke gegevens in' }, + { title: 'Meer over jou', subtitle: 'Praktische informatie' }, + { title: 'Motivatie', subtitle: 'Waarom wil je meehelpen?' }, + { title: 'Secties', subtitle: 'Waar wil je het liefst werken?' }, + { title: 'Beschikbaarheid', subtitle: 'Wanneer kun je helpen?' }, +] + +// Constants +const tshirtSizeItems = [ + { title: 'Geen voorkeur', value: '' }, + ...['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'].map(s => ({ title: s, value: s })), +] + +const motivationItems = [ + { title: 'Gratis festivalpas', value: 'Gratis festivalpas' }, + { title: 'Ervaring opdoen', value: 'Ervaring opdoen' }, + { title: 'Vrienden helpen', value: 'Vrienden helpen' }, + { title: 'CV opbouwen', value: 'CV opbouwen' }, + { title: 'Ik ben gevraagd', value: 'Ik ben gevraagd' }, + { title: 'Anders', value: 'Anders' }, +] + +// Section helpers const sectionsByCategory = computed(() => { if (!registrationData.value?.sections) return {} @@ -147,16 +116,17 @@ const sectionsByCategory = computed(() => { }, {} as Record) }) -function isSelected(sectionId: string) { - return selectedSectionIds.value.includes(sectionId) +function isSelected(sectionName: string) { + return selectedSections.value.includes(sectionName) } -function getSelectionPriority(sectionId: string) { - return selectedSectionIds.value.indexOf(sectionId) + 1 +function getSelectionPriority(sectionName: string) { + return selectedSections.value.indexOf(sectionName) + 1 } -const selectedCount = computed(() => selectedSectionIds.value.length) +const selectedCount = computed(() => selectedSections.value.length) +// Computed const timeSlotsByDate = computed(() => { if (!registrationData.value?.time_slots) return [] const groups = new Map() @@ -188,97 +158,27 @@ const formattedDates = computed(() => { ) }) -const personalFields = ['first_name', 'last_name', 'date_of_birth', 'email', 'phone'] as const +// Step field mapping for validation (0-based) +type FormField = 'first_name' | 'last_name' | 'date_of_birth' | 'email' | 'phone' | 'tshirt_size' | 'first_aid' | 'allergies' | 'driving_licence' | 'motivation' | 'motivation_other' +const stepFields: Record = { + 0: ['first_name', 'last_name', 'date_of_birth', 'email', 'phone'], + 1: ['tshirt_size', 'first_aid', 'allergies', 'driving_licence'], + 2: ['motivation', 'motivation_other'], +} + +// Navigation async function validateCurrentStep(): Promise { - const kind = currentKind.value - if (kind === 'personal') { - const results = await Promise.all(personalFields.map(f => validateField(f))) + const fields = stepFields[currentStep.value] + if (!fields) return true + const results = await Promise.all(fields.map(f => validateField(f))) - return results.every(r => r.valid) - } - if (kind === 'dynamic') - return validateDynamicFields() - - return true -} - -function isMultiValueType(type: RegistrationFieldType): boolean { - return type === 'multiselect' || type === 'checkbox' || type === 'tag_picker' -} - -function valueEmpty(value: unknown, type: RegistrationFieldType): boolean { - if (value === undefined || value === null) return true - if (value === '') return true - if (type === 'number' && (typeof value !== 'number' || Number.isNaN(value))) return true - if (isMultiValueType(type)) { - if (!Array.isArray(value)) return true - - return value.length === 0 - } - - return false -} - -function validateDynamicFields(): boolean { - fieldErrors.value = {} - let firstSlug: string | null = null - - for (const field of registrationFieldsList.value) { - if (!field.is_required) - continue - - const value = fieldValues.value[field.slug] - - if (valueEmpty(value, field.field_type)) { - fieldErrors.value[field.slug] = 'Dit veld is verplicht.' - firstSlug ??= field.slug - continue - } - - if (field.field_type === 'select' || field.field_type === 'radio') { - const opts = field.options ?? [] - if (typeof value === 'string' && !opts.includes(value)) { - fieldErrors.value[field.slug] = 'Maak een geldige keuze.' - firstSlug ??= field.slug - } - } - - if (field.field_type === 'multiselect' || field.field_type === 'checkbox') { - const opts = field.options ?? [] - const arr = Array.isArray(value) ? value : [] - if (arr.some(x => !opts.includes(String(x)))) { - fieldErrors.value[field.slug] = 'Ongeldige optie geselecteerd.' - firstSlug ??= field.slug - } - } - - if (field.field_type === 'tag_picker') { - const tags = field.available_tags ?? [] - const ids = new Set(tags.map(t => t.id)) - const arr = Array.isArray(value) ? value : [] - if (arr.some(id => !ids.has(String(id)))) { - fieldErrors.value[field.slug] = 'Ongeldige tag geselecteerd.' - firstSlug ??= field.slug - } - } - } - - if (firstSlug) { - nextTick(() => { - document.getElementById(`dyn-field-${firstSlug}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) - }) - - return false - } - - return true + return results.every(r => r.valid) } async function nextStep() { if (await validateCurrentStep()) { - if (currentStep.value < steps.value.length - 1) - currentStep.value++ + if (currentStep.value < 4) currentStep.value++ } } @@ -287,47 +187,41 @@ function prevStep() { } function goToStep(index: number) { - if (index < currentStep.value) + if (index < currentStep.value) { currentStep.value = index + } } -function toggleSection(sectionId: string) { - const idx = selectedSectionIds.value.indexOf(sectionId) +// Section toggle +function toggleSection(sectionName: string) { + const idx = selectedSections.value.indexOf(sectionName) if (idx !== -1) { - selectedSectionIds.value.splice(idx, 1) + selectedSections.value.splice(idx, 1) } - else if (selectedSectionIds.value.length < 5) { - selectedSectionIds.value.push(sectionId) + else if (selectedSections.value.length < 5) { + selectedSections.value.push(sectionName) } } -function moveSection(sectionId: string, direction: -1 | 1) { - const i = selectedSectionIds.value.indexOf(sectionId) - const j = i + direction - if (i < 0 || j < 0 || j >= selectedSectionIds.value.length) - return - const next = [...selectedSectionIds.value] - const a = next[i]! - const b = next[j]! - next[i] = b - next[j] = a - selectedSectionIds.value = next -} - +// Time slot toggle function toggleTimeSlot(slotId: string) { const idx = selectedTimeSlotIds.value.indexOf(slotId) - if (idx >= 0) + if (idx >= 0) { selectedTimeSlotIds.value.splice(idx, 1) - else + delete timeSlotPreferences.value[slotId] + } + else { selectedTimeSlotIds.value.push(slotId) + timeSlotPreferences.value[slotId] = 3 + } } +// Helpers function formatDate(dateStr: string): string { return new Date(`${dateStr}T00:00:00`).toLocaleDateString('nl-NL', { weekday: 'long', day: 'numeric', month: 'long', - year: 'numeric', }) } @@ -346,95 +240,49 @@ function formatTimeRange(start: string, end: string): string { return `${start.slice(0, 5)} – ${end.slice(0, 5)}` } -function buildFieldValues(): Record | undefined { - const out: Record = {} - - for (const f of registrationFieldsList.value) { - const raw = fieldValues.value[f.slug] - - if (f.field_type === 'boolean') { - if (typeof raw === 'boolean') - out[f.slug] = raw - continue - } - - if (raw === undefined || raw === null) continue - if (raw === '') continue - if (Array.isArray(raw) && raw.length === 0) continue - out[f.slug] = raw as FieldValuePayload - } - - if (Object.keys(out).length === 0) - return undefined - - return out -} - -function isPersonalField(key: string): key is typeof personalFields[number] { - return (personalFields as readonly string[]).includes(key) -} - -function applyServerValidationErrors(serverErrors: Record) { - fieldErrors.value = {} - for (const [key, msgs] of Object.entries(serverErrors)) { - const m = /^field_values\.(.+)$/.exec(key) - if (m?.[1]) { - fieldErrors.value[m[1]] = msgs[0] ?? 'Ongeldige waarde.' - - continue - } - - if (isPersonalField(key)) - setFieldError(key, msgs[0] ?? 'Ongeldige waarde.') - } -} - +// Submit async function onSubmit() { submitError.value = null - fieldErrors.value = {} - for (const f of personalFields) { - const r = await validateField(f) - if (!r.valid) { - currentStep.value = stepKinds.value.indexOf('personal') + // Validate steps 0-2 + for (let step = 0; step <= 2; step++) { + const fields = stepFields[step] + if (!fields) continue + const results = await Promise.all(fields.map(f => validateField(f))) + + if (!results.every(r => r.valid)) { + currentStep.value = step return } } - if (!validateDynamicFields()) { - currentStep.value = stepKinds.value.indexOf('dynamic') - - return - } - if (!registrationData.value) return + const sectionPreferences: SectionPreference[] = selectedSections.value.map((sectionName, index) => ({ + section_name: sectionName, + priority: index + 1, + })) + + const availabilities: VolunteerAvailability[] = selectedTimeSlotIds.value.map(id => ({ + time_slot_id: id, + preference_level: timeSlotPreferences.value[id] ?? 3, + })) + const payload: VolunteerRegistrationForm = { first_name: firstName.value ?? '', last_name: lastName.value ?? '', + date_of_birth: dateOfBirth.value ?? '', email: email.value ?? '', - phone: (phone.value && String(phone.value).trim() !== '') ? String(phone.value) : undefined, - date_of_birth: (dateOfBirth.value && String(dateOfBirth.value).trim() !== '') - ? String(dateOfBirth.value) - : undefined, - } - - const fv = buildFieldValues() - if (fv) payload.field_values = fv - - if (registrationData.value.event.registration_show_section_preferences && selectedSectionIds.value.length > 0) { - payload.section_preferences = selectedSectionIds.value.map((id, index) => ({ - festival_section_id: id, - priority: index + 1, - })) - } - - if (registrationData.value.event.registration_show_availability && selectedTimeSlotIds.value.length > 0) { - payload.availabilities = selectedTimeSlotIds.value.map(id => ({ - time_slot_id: id, - preference_level: 3, - })) + phone: phone.value ?? '', + tshirt_size: tshirtSize.value ?? '', + first_aid: firstAid.value ?? false, + allergies: allergies.value ?? '', + driving_licence: drivingLicence.value ?? false, + motivation: motivation.value ?? '', + motivation_other: motivationOther.value ?? '', + section_preferences: sectionPreferences, + availabilities, } try { @@ -453,35 +301,20 @@ async function onSubmit() { }) } catch (error: unknown) { - const axiosError = error as { response?: { status?: number; data?: { errors?: Record; message?: string } } } + const axiosError = error as { response?: { status?: number; data?: { errors?: Record } } } if (axiosError.response?.status === 422) { const serverErrors = axiosError.response.data?.errors + if (serverErrors) { - applyServerValidationErrors(serverErrors) + for (const field of Object.keys(serverErrors)) { + for (const [step, fields] of Object.entries(stepFields)) { + if (fields.includes(field as FormField)) { + currentStep.value = Number(step) - const keys = Object.keys(serverErrors) - if (keys.some(k => isPersonalField(k))) { - currentStep.value = stepKinds.value.indexOf('personal') - - return - } - if (keys.some(k => k.startsWith('field_values.'))) { - currentStep.value = stepKinds.value.indexOf('dynamic') - - return - } - if (keys.some(k => k.startsWith('section_preferences'))) { - const idx = stepKinds.value.indexOf('sections') - if (idx !== -1) currentStep.value = idx - - return - } - if (keys.some(k => k.startsWith('availabilities'))) { - const idx = stepKinds.value.indexOf('availability') - if (idx !== -1) currentStep.value = idx - - return + return + } + } } } submitError.value = 'Er zijn validatiefouten gevonden. Controleer je invoer.' @@ -497,12 +330,11 @@ async function onSubmit() {
@@ -521,12 +353,11 @@ async function onSubmit() {
@@ -552,10 +383,7 @@ async function onSubmit() {
-
+
-

+

{{ registrationData.event.name }} -

-

+ +

{{ formattedDates }}

@@ -589,90 +415,90 @@ async function onSubmit() {
-

+

{{ registrationData.event.name }} -

-

+ +

{{ formattedDates }}

+ + + +

+ {{ registrationData.event.registration_welcome_text }} +

+
+ + + + Je bent ingelogd als {{ authStore.user?.full_name }}. Je gegevens zijn automatisch ingevuld. + +
Al een account? Log in om je gegevens automatisch in te vullen.
- -
- - - - Je bent ingelogd als {{ authStore.user?.full_name }}. Je gegevens zijn automatisch ingevuld. - - + {{ submitError }} + + - +
+
+ > + tabler-check + {{ index + 1 }}
+
{{ step.title }}
{{ step.subtitle }}
- +
+ @@ -745,39 +592,38 @@ async function onSubmit() { v-if="index < currentStep" start size="14" - icon="tabler-check" - /> + > + tabler-check + {{ index + 1 }}. {{ step.title }}
+ - + +
-

- Deel {{ currentStep + 1 }} van {{ steps.length }} -

-

- {{ steps[currentStep]?.title }} -

+
+ {{ steps[currentStep].title }} +

- {{ steps[currentStep]?.subtitle }} + {{ steps[currentStep].subtitle }}

- -
+ +
-
+
-

+

Contactgegevens -
+

Vul deze gegevens zorgvuldig in: we gebruiken ze om alle informatie over het evenement naar je te versturen. Je e-mailadres is ook je gebruikersnaam voor Crewli, het systeem waar je straks alle informatie terugvindt. @@ -906,110 +752,190 @@ async function onSubmit() {

- -
- + +
+ + + + + + + +
+ + + +
+
+ + + + + + +
- -
-

- We proberen hier zoveel mogelijk rekening mee te houden, - maar de uiteindelijke indeling wordt bepaald door de organisatie. -

+ +
+ + + + + + + + + + + + +
+ + +
+ -
+
Volgende @@ -1137,9 +1067,7 @@ async function onSubmit() { @@ -1155,7 +1083,8 @@ async function onSubmit() { -
+ +
Powered by Crewli
@@ -1166,79 +1095,14 @@ async function onSubmit() { .registration-container { position: relative; z-index: 1; - max-inline-size: 1040px; -} - -.registration-header-fallback { - background: rgb(var(--v-theme-surface)); - border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important; -} - -.registration-welcome-html :deep(p) { - margin-block-end: 0.5rem; -} - -.registration-welcome-html :deep(p:last-child) { - margin-block-end: 0; -} - -.step-indicator { - inline-size: 48px; - block-size: 48px; - border-radius: 10px; - font-size: 16px; - font-weight: 700; - transition: background-color 0.2s ease, color 0.2s ease; -} - -.step-indicator--active { - background-color: rgb(var(--v-theme-primary)); - color: rgb(var(--v-theme-on-primary)); -} - -.step-indicator--done { - background-color: rgba(var(--v-theme-primary), 0.12); - color: rgb(var(--v-theme-primary)); -} - -.step-indicator--todo { - background-color: rgba(var(--v-theme-on-surface), 0.06); - color: rgba(var(--v-theme-on-surface), 0.35); -} - -.section-category-heading { - padding-block-end: 8px; - border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); -} - -.registration-section-grid { - align-items: stretch; -} - -.section-pref-card { - border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important; - transition: border-color 0.15s ease, background-color 0.15s ease; -} - -.section-pref-card--selected { - border: 2px solid rgb(var(--v-theme-primary)) !important; - background-color: rgba(var(--v-theme-primary), 0.06); -} - -.priority-pill { - position: absolute; - inset-block-start: 10px; - inset-inline-end: 10px; - z-index: 1; - font-size: 11px; - font-weight: 600; - padding-block: 4px; - padding-inline: 10px; - border-radius: 999px; - background-color: rgba(var(--v-theme-on-surface), 0.75); - color: rgb(var(--v-theme-surface)); + max-inline-size: 1000px; } +/* + Native date inputs have a large intrinsic min-width; without min-width: 0 the + grid cell grows and the prepend-inner icon no longer sits inside the outline + next to the value (Vuetify v-field grid: prepend-inner | field). +*/ .volunteer-reg-dob-field :deep(.v-field__field) { min-inline-size: 0; } @@ -1249,6 +1113,10 @@ async function onSubmit() { min-inline-size: 0; } +/* + Vuetify’s .v-list uses overflow: auto, which shows scrollbars on this step when + list rows are slightly wider than the card (e.g. checkbox + text + rating). +*/ .registration-availability-list { overflow: visible !important; } diff --git a/apps/portal/src/schemas/registrationSchema.ts b/apps/portal/src/schemas/registrationSchema.ts index 3f228c49..34f30601 100644 --- a/apps/portal/src/schemas/registrationSchema.ts +++ b/apps/portal/src/schemas/registrationSchema.ts @@ -1,7 +1,6 @@ import { z } from 'zod' -/** Fixed fields for step "Over jou" (portal volunteer registration). */ -export const personalStepSchema = z.object({ +export const step1Schema = z.object({ first_name: z.string().min(1, 'Voornaam is verplicht').max(255), last_name: z.string().min(1, 'Achternaam is verplicht').max(255), email: z.string().min(1, 'E-mailadres is verplicht').email('Ongeldig e-mailadres').max(255), @@ -9,4 +8,18 @@ export const personalStepSchema = z.object({ phone: z.string().max(50).optional().or(z.literal('')), }) -export const fullRegistrationSchema = personalStepSchema +export const step2Schema = z.object({ + tshirt_size: z.enum(['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL']).optional().or(z.literal('')), + first_aid: z.boolean().default(false), + allergies: z.string().max(500).optional().or(z.literal('')), + driving_licence: z.boolean().default(false), +}) + +export const step3Schema = z.object({ + motivation: z.string().max(1000).optional().or(z.literal('')), + motivation_other: z.string().max(500).optional().or(z.literal('')), +}) + +export const fullRegistrationSchema = step1Schema + .merge(step2Schema) + .merge(step3Schema) diff --git a/apps/portal/src/types/registration.ts b/apps/portal/src/types/registration.ts index f4d52da9..9a7b1a14 100644 --- a/apps/portal/src/types/registration.ts +++ b/apps/portal/src/types/registration.ts @@ -1,33 +1,3 @@ -export type RegistrationFieldType = - | 'text' - | 'textarea' - | 'select' - | 'multiselect' - | 'checkbox' - | 'radio' - | 'boolean' - | 'number' - | 'tag_picker' - -export interface RegistrationTagOption { - id: string - name: string - category: string | null -} - -export interface RegistrationField { - id: string - label: string - slug: string - field_type: RegistrationFieldType - options: string[] | null - tag_category: string | null - is_required: boolean - section: string | null - help_text: string | null - available_tags?: RegistrationTagOption[] -} - export interface EventRegistrationData { event: { id: string @@ -38,12 +8,9 @@ export interface EventRegistrationData { registration_banner_url: string | null registration_welcome_text: string | null registration_logo_url: string | null - registration_show_section_preferences: boolean - registration_show_availability: boolean } sections: SectionOption[] time_slots: TimeSlotOption[] - registration_fields: RegistrationField[] } export interface SectionOption { @@ -63,25 +30,33 @@ export interface TimeSlotOption { duration_hours: number } -export interface SectionPreferencePayload { - festival_section_id: string +export interface SectionPreference { + section_name: string priority: number } -export interface VolunteerAvailabilityPayload { +export interface VolunteerAvailability { time_slot_id: string - preference_level?: number + preference_level: number } -export type FieldValuePayload = string | number | boolean | string[] | null - export interface VolunteerRegistrationForm { + // Step 1 first_name: string last_name: string - date_of_birth?: string + date_of_birth: string email: string - phone?: string - field_values?: Record - section_preferences?: SectionPreferencePayload[] - availabilities?: VolunteerAvailabilityPayload[] + phone: string + // Step 2 + tshirt_size: string + first_aid: boolean + allergies: string + driving_licence: boolean + // Step 3 + motivation: string + motivation_other: string + // Step 4 + section_preferences: SectionPreference[] + // Step 5 + availabilities: VolunteerAvailability[] }