feat(portal): redesign registration wizard, header and login with Vuexy patterns
- Registration page: switch to blank layout with split-screen design (event branding illustration on left, AppStepper wizard on right), replacing generic chip stepper - Portal header: refined to flat surface bar with proper branding and icon-based nav - Login page: upgrade to Vuexy V2 auth pattern with split-screen illustration - Success page: blank layout with centered card, avatar icon, and footer mask Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,55 +36,90 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
|
|||||||
<AppLoadingIndicator ref="refLoadingIndicator" />
|
<AppLoadingIndicator ref="refLoadingIndicator" />
|
||||||
|
|
||||||
<VAppBar
|
<VAppBar
|
||||||
color="primary"
|
flat
|
||||||
|
color="surface"
|
||||||
|
border="b"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
>
|
>
|
||||||
<VAppBarNavIcon
|
<VContainer
|
||||||
class="d-sm-none"
|
fluid
|
||||||
@click="isMobileMenuOpen = !isMobileMenuOpen"
|
class="d-flex align-center py-0"
|
||||||
/>
|
style="max-inline-size: 1440px;"
|
||||||
|
>
|
||||||
|
<!-- Logo & Brand -->
|
||||||
|
<RouterLink
|
||||||
|
to="/"
|
||||||
|
class="d-flex align-center gap-x-2 text-decoration-none"
|
||||||
|
>
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-users-group"
|
||||||
|
size="26"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<span class="text-h6 font-weight-bold text-high-emphasis">
|
||||||
|
Crewli
|
||||||
|
</span>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
<VAppBarTitle class="text-h6">
|
<!-- Mobile nav toggle -->
|
||||||
Crewli Portal
|
<VAppBarNavIcon
|
||||||
</VAppBarTitle>
|
class="d-sm-none ms-auto"
|
||||||
|
@click="isMobileMenuOpen = !isMobileMenuOpen"
|
||||||
|
/>
|
||||||
|
|
||||||
<template #append>
|
<!-- Desktop navigation -->
|
||||||
<div class="d-none d-sm-flex align-center gap-2">
|
<div class="d-none d-sm-flex align-center gap-1 ms-6">
|
||||||
<VBtn
|
<VBtn
|
||||||
v-for="item in navItems"
|
v-for="item in navItems"
|
||||||
:key="item.to"
|
:key="item.to"
|
||||||
:to="item.to"
|
:to="item.to"
|
||||||
variant="text"
|
variant="text"
|
||||||
color="white"
|
color="default"
|
||||||
|
size="small"
|
||||||
|
class="text-medium-emphasis"
|
||||||
>
|
>
|
||||||
<VIcon
|
<VIcon
|
||||||
start
|
start
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
|
size="18"
|
||||||
/>
|
/>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<VSpacer />
|
||||||
|
|
||||||
|
<!-- Auth actions -->
|
||||||
<VBtn
|
<VBtn
|
||||||
v-if="authStore.isAuthenticated"
|
v-if="authStore.isAuthenticated"
|
||||||
variant="outlined"
|
variant="tonal"
|
||||||
color="white"
|
color="primary"
|
||||||
class="ms-2"
|
size="small"
|
||||||
@click="authStore.logout(); $router.push('/login')"
|
@click="authStore.logout(); $router.push('/login')"
|
||||||
>
|
>
|
||||||
|
<VIcon
|
||||||
|
start
|
||||||
|
icon="tabler-logout"
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
Uitloggen
|
Uitloggen
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
|
||||||
<VBtn
|
<VBtn
|
||||||
v-else
|
v-else
|
||||||
variant="outlined"
|
variant="tonal"
|
||||||
color="white"
|
color="primary"
|
||||||
class="ms-2"
|
size="small"
|
||||||
to="/login"
|
to="/login"
|
||||||
>
|
>
|
||||||
|
<VIcon
|
||||||
|
start
|
||||||
|
icon="tabler-login"
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
Inloggen
|
Inloggen
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</template>
|
</VContainer>
|
||||||
</VAppBar>
|
</VAppBar>
|
||||||
|
|
||||||
<!-- Mobile navigation drawer -->
|
<!-- Mobile navigation drawer -->
|
||||||
@@ -126,6 +161,7 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
|
|||||||
<VContainer
|
<VContainer
|
||||||
fluid
|
fluid
|
||||||
class="pa-4 pa-sm-6"
|
class="pa-4 pa-sm-6"
|
||||||
|
style="max-inline-size: 1440px;"
|
||||||
>
|
>
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<Suspense
|
<Suspense
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
|
||||||
|
import authV2LoginIllustrationLight from '@images/pages/auth-v2-login-illustration-light.png'
|
||||||
|
import authV2LoginIllustrationDark from '@images/pages/auth-v2-login-illustration-dark.png'
|
||||||
|
import authV2LoginIllustrationBorderedLight from '@images/pages/auth-v2-login-illustration-bordered-light.png'
|
||||||
|
import authV2LoginIllustrationBorderedDark from '@images/pages/auth-v2-login-illustration-bordered-dark.png'
|
||||||
|
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||||
|
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
name: 'login',
|
name: 'login',
|
||||||
meta: {
|
meta: {
|
||||||
@@ -13,53 +21,132 @@ const form = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isPasswordVisible = ref(false)
|
const isPasswordVisible = ref(false)
|
||||||
|
|
||||||
|
const authThemeImg = useGenerateImageVariant(
|
||||||
|
authV2LoginIllustrationLight,
|
||||||
|
authV2LoginIllustrationDark,
|
||||||
|
authV2LoginIllustrationBorderedLight,
|
||||||
|
authV2LoginIllustrationBorderedDark,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VApp>
|
<!-- Logo -->
|
||||||
<VMain class="d-flex align-center justify-center" style="min-height: 100vh;">
|
<RouterLink to="/">
|
||||||
|
<div class="auth-logo d-flex align-center gap-x-3">
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-users-group"
|
||||||
|
size="28"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<h1 class="auth-title">
|
||||||
|
Crewli
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<VRow
|
||||||
|
no-gutters
|
||||||
|
class="auth-wrapper bg-surface"
|
||||||
|
>
|
||||||
|
<!-- Left: Illustration -->
|
||||||
|
<VCol
|
||||||
|
md="8"
|
||||||
|
class="d-none d-md-flex"
|
||||||
|
>
|
||||||
|
<div class="position-relative bg-background w-100 me-0">
|
||||||
|
<div
|
||||||
|
class="d-flex align-center justify-center w-100 h-100"
|
||||||
|
style="padding-inline: 6.25rem;"
|
||||||
|
>
|
||||||
|
<VImg
|
||||||
|
max-width="613"
|
||||||
|
:src="authThemeImg"
|
||||||
|
class="auth-illustration mt-16 mb-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
class="auth-footer-mask flip-in-rtl"
|
||||||
|
:src="authThemeMask"
|
||||||
|
alt="auth-footer-mask"
|
||||||
|
height="280"
|
||||||
|
width="100"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<!-- Right: Login form -->
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
class="auth-card-v2 d-flex align-center justify-center"
|
||||||
|
>
|
||||||
<VCard
|
<VCard
|
||||||
max-width="450"
|
flat
|
||||||
width="100%"
|
:max-width="500"
|
||||||
class="pa-6 ma-4"
|
class="mt-12 mt-sm-0 pa-6"
|
||||||
>
|
>
|
||||||
<VCardTitle class="text-h5 text-center mb-2">
|
<VCardText>
|
||||||
Inloggen
|
<h4 class="text-h4 mb-1">
|
||||||
</VCardTitle>
|
Welkom terug!
|
||||||
<VCardSubtitle class="text-center mb-6">
|
</h4>
|
||||||
Log in om je rooster en shifts te bekijken
|
<p class="mb-0">
|
||||||
</VCardSubtitle>
|
Log in om je rooster en shifts te bekijken
|
||||||
|
</p>
|
||||||
|
</VCardText>
|
||||||
|
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm @submit.prevent>
|
<VForm @submit.prevent>
|
||||||
<VTextField
|
<VRow>
|
||||||
v-model="form.email"
|
<VCol cols="12">
|
||||||
label="E-mailadres"
|
<VTextField
|
||||||
type="email"
|
v-model="form.email"
|
||||||
placeholder="je@email.nl"
|
autofocus
|
||||||
class="mb-4"
|
label="E-mailadres"
|
||||||
/>
|
type="email"
|
||||||
|
placeholder="je@email.nl"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
<VTextField
|
<VCol cols="12">
|
||||||
v-model="form.password"
|
<VTextField
|
||||||
label="Wachtwoord"
|
v-model="form.password"
|
||||||
placeholder="Je wachtwoord"
|
label="Wachtwoord"
|
||||||
:type="isPasswordVisible ? 'text' : 'password'"
|
placeholder="Je wachtwoord"
|
||||||
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
:type="isPasswordVisible ? 'text' : 'password'"
|
||||||
class="mb-6"
|
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
||||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VBtn
|
<div class="d-flex align-center flex-wrap justify-end my-6">
|
||||||
block
|
<a
|
||||||
type="submit"
|
class="text-primary text-body-2"
|
||||||
color="primary"
|
href="javascript:void(0)"
|
||||||
>
|
>
|
||||||
Inloggen
|
Wachtwoord vergeten?
|
||||||
</VBtn>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VBtn
|
||||||
|
block
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Inloggen
|
||||||
|
</VBtn>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VMain>
|
</VCol>
|
||||||
</VApp>
|
</VRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@core/scss/template/pages/page-auth";
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
import { useForm } from 'vee-validate'
|
import { useForm } from 'vee-validate'
|
||||||
import { toTypedSchema } from '@vee-validate/zod'
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
|
||||||
|
import registerMultiStepIllustrationLight from '@images/illustrations/register-multi-step-illustration-light.png'
|
||||||
|
import registerMultiStepIllustrationDark from '@images/illustrations/register-multi-step-illustration-dark.png'
|
||||||
|
import registerMultiStepBgLight from '@images/pages/register-multi-step-bg-light.png'
|
||||||
|
import registerMultiStepBgDark from '@images/pages/register-multi-step-bg-dark.png'
|
||||||
import { useAuthStore } from '@/stores/useAuthStore'
|
import { useAuthStore } from '@/stores/useAuthStore'
|
||||||
import { useRegistrationData, useSubmitRegistration } from '@/composables/api/useVolunteerRegistration'
|
import { useRegistrationData, useSubmitRegistration } from '@/composables/api/useVolunteerRegistration'
|
||||||
import { fullRegistrationSchema } from '@/schemas/registrationSchema'
|
import { fullRegistrationSchema } from '@/schemas/registrationSchema'
|
||||||
@@ -16,7 +21,7 @@ import type {
|
|||||||
definePage({
|
definePage({
|
||||||
name: 'volunteer-register',
|
name: 'volunteer-register',
|
||||||
meta: {
|
meta: {
|
||||||
layout: 'portal',
|
layout: 'blank',
|
||||||
requiresAuth: false,
|
requiresAuth: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -30,9 +35,12 @@ const eventSlug = computed(() => route.params.eventSlug as string)
|
|||||||
const { data: registrationData, isLoading, isError } = useRegistrationData(eventSlug)
|
const { data: registrationData, isLoading, isError } = useRegistrationData(eventSlug)
|
||||||
const { mutateAsync: submitRegistration, isPending: isSubmitting } = useSubmitRegistration()
|
const { mutateAsync: submitRegistration, isPending: isSubmitting } = useSubmitRegistration()
|
||||||
|
|
||||||
const currentStep = ref(1)
|
const currentStep = ref(0)
|
||||||
const submitError = ref<string | null>(null)
|
const submitError = ref<string | null>(null)
|
||||||
|
|
||||||
|
const registerMultiStepIllustration = useGenerateImageVariant(registerMultiStepIllustrationLight, registerMultiStepIllustrationDark)
|
||||||
|
const registerMultiStepBg = useGenerateImageVariant(registerMultiStepBgLight, registerMultiStepBgDark)
|
||||||
|
|
||||||
// VeeValidate form
|
// VeeValidate form
|
||||||
const { errors, defineField, validateField, setFieldValue } = useForm({
|
const { errors, defineField, validateField, setFieldValue } = useForm({
|
||||||
validationSchema: toTypedSchema(fullRegistrationSchema),
|
validationSchema: toTypedSchema(fullRegistrationSchema),
|
||||||
@@ -62,20 +70,29 @@ const [motivation] = defineField('motivation')
|
|||||||
const [motivationOther] = defineField('motivation_other')
|
const [motivationOther] = defineField('motivation_other')
|
||||||
|
|
||||||
// Pre-fill authenticated user data
|
// Pre-fill authenticated user data
|
||||||
watch(() => authStore.user, (user) => {
|
watch(() => authStore.user, user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
setFieldValue('name', user.name)
|
setFieldValue('name', user.name)
|
||||||
setFieldValue('email', user.email)
|
setFieldValue('email', user.email)
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
// Step 4: Section preferences (by name, not ID)
|
// Step 3: Section preferences (by name, not ID)
|
||||||
const selectedSections = ref<string[]>([])
|
const selectedSections = ref<string[]>([])
|
||||||
|
|
||||||
// Step 5: Availability
|
// Step 4: Availability
|
||||||
const selectedTimeSlotIds = ref<string[]>([])
|
const selectedTimeSlotIds = ref<string[]>([])
|
||||||
const timeSlotPreferences = ref<Record<string, number>>({})
|
const timeSlotPreferences = ref<Record<string, number>>({})
|
||||||
|
|
||||||
|
// Stepper items for AppStepper
|
||||||
|
const stepperItems = [
|
||||||
|
{ title: 'Over jou', subtitle: 'Persoonlijke gegevens', icon: 'tabler-user' },
|
||||||
|
{ title: 'Extra info', subtitle: 'Aanvullende details', icon: 'tabler-list-details' },
|
||||||
|
{ title: 'Motivatie', subtitle: 'Waarom wil je helpen?', icon: 'tabler-heart' },
|
||||||
|
{ title: 'Secties', subtitle: 'Voorkeur werkgebieden', icon: 'tabler-layout-grid' },
|
||||||
|
{ title: 'Beschikbaarheid', subtitle: 'Wanneer ben je er?', icon: 'tabler-calendar-event' },
|
||||||
|
]
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const tshirtSizeItems = [
|
const tshirtSizeItems = [
|
||||||
{ title: 'Geen voorkeur', value: '' },
|
{ title: 'Geen voorkeur', value: '' },
|
||||||
@@ -90,25 +107,25 @@ const motivationItems = [
|
|||||||
{ title: 'Anders', value: 'Anders' },
|
{ title: 'Anders', value: 'Anders' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const stepTitles = ['Over jou', 'Meer over jou', 'Motivatie', 'Secties', 'Beschikbaarheid']
|
|
||||||
|
|
||||||
// Section helpers
|
// Section helpers
|
||||||
const sectionsByCategory = computed(() => {
|
const sectionsByCategory = computed(() => {
|
||||||
if (!registrationData.value?.sections) return {}
|
if (!registrationData.value?.sections) return {}
|
||||||
|
|
||||||
return registrationData.value.sections.reduce((groups, section) => {
|
return registrationData.value.sections.reduce((groups, section) => {
|
||||||
const cat = section.category || 'Overig'
|
const cat = section.category || 'Overig'
|
||||||
if (!groups[cat]) groups[cat] = []
|
if (!groups[cat]) groups[cat] = []
|
||||||
groups[cat].push(section)
|
groups[cat].push(section)
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
}, {} as Record<string, SectionOption[]>)
|
}, {} as Record<string, SectionOption[]>)
|
||||||
})
|
})
|
||||||
|
|
||||||
function isSelected(name: string) {
|
function isSelected(sectionName: string) {
|
||||||
return selectedSections.value.includes(name)
|
return selectedSections.value.includes(sectionName)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectionPriority(name: string) {
|
function getSelectionPriority(sectionName: string) {
|
||||||
return selectedSections.value.indexOf(name) + 1
|
return selectedSections.value.indexOf(sectionName) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCount = computed(() => selectedSections.value.length)
|
const selectedCount = computed(() => selectedSections.value.length)
|
||||||
@@ -117,6 +134,7 @@ const selectedCount = computed(() => selectedSections.value.length)
|
|||||||
const timeSlotsByDate = computed(() => {
|
const timeSlotsByDate = computed(() => {
|
||||||
if (!registrationData.value?.time_slots) return []
|
if (!registrationData.value?.time_slots) return []
|
||||||
const groups = new Map<string, TimeSlotOption[]>()
|
const groups = new Map<string, TimeSlotOption[]>()
|
||||||
|
|
||||||
for (const slot of registrationData.value.time_slots) {
|
for (const slot of registrationData.value.time_slots) {
|
||||||
if (!groups.has(slot.date)) groups.set(slot.date, [])
|
if (!groups.has(slot.date)) groups.set(slot.date, [])
|
||||||
groups.get(slot.date)!.push(slot)
|
groups.get(slot.date)!.push(slot)
|
||||||
@@ -133,13 +151,13 @@ const totalSelectedHours = computed(() => {
|
|||||||
.reduce((sum, s) => sum + s.duration_hours, 0)
|
.reduce((sum, s) => sum + s.duration_hours, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Step field mapping for validation
|
// Step field mapping for validation (0-based)
|
||||||
type FormField = 'name' | 'email' | 'phone' | 'tshirt_size' | 'first_aid' | 'allergies' | 'access_requirements' | 'driving_licence' | 'motivation' | 'motivation_other'
|
type FormField = 'name' | 'email' | 'phone' | 'tshirt_size' | 'first_aid' | 'allergies' | 'access_requirements' | 'driving_licence' | 'motivation' | 'motivation_other'
|
||||||
|
|
||||||
const stepFields: Record<number, FormField[]> = {
|
const stepFields: Record<number, FormField[]> = {
|
||||||
1: ['name', 'email', 'phone'],
|
0: ['name', 'email', 'phone'],
|
||||||
2: ['tshirt_size', 'first_aid', 'allergies', 'access_requirements', 'driving_licence'],
|
1: ['tshirt_size', 'first_aid', 'allergies', 'access_requirements', 'driving_licence'],
|
||||||
3: ['motivation', 'motivation_other'],
|
2: ['motivation', 'motivation_other'],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
@@ -153,22 +171,22 @@ async function validateCurrentStep(): Promise<boolean> {
|
|||||||
|
|
||||||
async function nextStep() {
|
async function nextStep() {
|
||||||
if (await validateCurrentStep()) {
|
if (await validateCurrentStep()) {
|
||||||
if (currentStep.value < 5) currentStep.value++
|
if (currentStep.value < 4) currentStep.value++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prevStep() {
|
function prevStep() {
|
||||||
if (currentStep.value > 1) currentStep.value--
|
if (currentStep.value > 0) currentStep.value--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section toggle
|
// Section toggle
|
||||||
function toggleSection(name: string) {
|
function toggleSection(sectionName: string) {
|
||||||
const idx = selectedSections.value.indexOf(name)
|
const idx = selectedSections.value.indexOf(sectionName)
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
selectedSections.value.splice(idx, 1)
|
selectedSections.value.splice(idx, 1)
|
||||||
}
|
}
|
||||||
else if (selectedSections.value.length < 5) {
|
else if (selectedSections.value.length < 5) {
|
||||||
selectedSections.value.push(name)
|
selectedSections.value.push(sectionName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +212,17 @@ function formatDate(dateStr: string): string {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateRange(start: string, end: string): string {
|
||||||
|
const startDate = new Date(`${start}T00:00:00`)
|
||||||
|
const endDate = new Date(`${end}T00:00:00`)
|
||||||
|
|
||||||
|
if (startDate.getMonth() === endDate.getMonth() && startDate.getFullYear() === endDate.getFullYear()) {
|
||||||
|
return `${startDate.getDate()} – ${endDate.toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' })}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${startDate.toLocaleDateString('nl-NL', { day: 'numeric', month: 'long' })} – ${endDate.toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' })}`
|
||||||
|
}
|
||||||
|
|
||||||
function formatTimeRange(start: string, end: string): string {
|
function formatTimeRange(start: string, end: string): string {
|
||||||
return `${start.slice(0, 5)} – ${end.slice(0, 5)}`
|
return `${start.slice(0, 5)} – ${end.slice(0, 5)}`
|
||||||
}
|
}
|
||||||
@@ -202,11 +231,12 @@ function formatTimeRange(start: string, end: string): string {
|
|||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
submitError.value = null
|
submitError.value = null
|
||||||
|
|
||||||
// Validate steps 1-3
|
// Validate steps 0-2
|
||||||
for (let step = 1; step <= 3; step++) {
|
for (let step = 0; step <= 2; step++) {
|
||||||
const fields = stepFields[step]
|
const fields = stepFields[step]
|
||||||
if (!fields) continue
|
if (!fields) continue
|
||||||
const results = await Promise.all(fields.map(f => validateField(f)))
|
const results = await Promise.all(fields.map(f => validateField(f)))
|
||||||
|
|
||||||
if (!results.every(r => r.valid)) {
|
if (!results.every(r => r.valid)) {
|
||||||
currentStep.value = step
|
currentStep.value = step
|
||||||
|
|
||||||
@@ -216,8 +246,8 @@ async function onSubmit() {
|
|||||||
|
|
||||||
if (!registrationData.value) return
|
if (!registrationData.value) return
|
||||||
|
|
||||||
const sectionPreferences: SectionPreference[] = selectedSections.value.map((name, index) => ({
|
const sectionPreferences: SectionPreference[] = selectedSections.value.map((sectionName, index) => ({
|
||||||
section_name: name,
|
section_name: sectionName,
|
||||||
priority: index + 1,
|
priority: index + 1,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -257,8 +287,10 @@ async function onSubmit() {
|
|||||||
}
|
}
|
||||||
catch (error: unknown) {
|
catch (error: unknown) {
|
||||||
const axiosError = error as { response?: { status?: number; data?: { errors?: Record<string, string[]> } } }
|
const axiosError = error as { response?: { status?: number; data?: { errors?: Record<string, string[]> } } }
|
||||||
|
|
||||||
if (axiosError.response?.status === 422) {
|
if (axiosError.response?.status === 422) {
|
||||||
const serverErrors = axiosError.response.data?.errors
|
const serverErrors = axiosError.response.data?.errors
|
||||||
|
|
||||||
if (serverErrors) {
|
if (serverErrors) {
|
||||||
for (const field of Object.keys(serverErrors)) {
|
for (const field of Object.keys(serverErrors)) {
|
||||||
for (const [step, fields] of Object.entries(stepFields)) {
|
for (const [step, fields] of Object.entries(stepFields)) {
|
||||||
@@ -280,26 +312,44 @@ async function onSubmit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Loading -->
|
<!-- Logo -->
|
||||||
|
<RouterLink to="/">
|
||||||
|
<div class="auth-logo d-flex align-center gap-x-3">
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-users-group"
|
||||||
|
size="28"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<h1 class="auth-title">
|
||||||
|
Crewli
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<!-- Loading state -->
|
||||||
<VRow
|
<VRow
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
justify="center"
|
no-gutters
|
||||||
|
class="auth-wrapper bg-surface"
|
||||||
>
|
>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
md="10"
|
class="d-flex align-center justify-center"
|
||||||
lg="8"
|
|
||||||
>
|
>
|
||||||
<VCard class="pa-6">
|
<VCard
|
||||||
<VSkeletonLoader type="heading" />
|
flat
|
||||||
<VSkeletonLoader
|
:max-width="500"
|
||||||
type="text@3"
|
class="pa-12 text-center"
|
||||||
class="mt-4"
|
>
|
||||||
/>
|
<VProgressCircular
|
||||||
<VSkeletonLoader
|
indeterminate
|
||||||
type="button"
|
color="primary"
|
||||||
class="mt-4"
|
size="48"
|
||||||
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
|
<p class="text-body-1 text-medium-emphasis mb-0">
|
||||||
|
Registratieformulier laden...
|
||||||
|
</p>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
@@ -307,34 +357,36 @@ async function onSubmit() {
|
|||||||
<!-- Error / Not available -->
|
<!-- Error / Not available -->
|
||||||
<VRow
|
<VRow
|
||||||
v-else-if="isError || !registrationData"
|
v-else-if="isError || !registrationData"
|
||||||
justify="center"
|
no-gutters
|
||||||
|
class="auth-wrapper bg-surface"
|
||||||
>
|
>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
md="8"
|
class="d-flex align-center justify-center"
|
||||||
lg="6"
|
|
||||||
>
|
>
|
||||||
<VCard class="text-center pa-8">
|
<VCard
|
||||||
|
flat
|
||||||
|
:max-width="500"
|
||||||
|
class="text-center pa-8"
|
||||||
|
>
|
||||||
<VIcon
|
<VIcon
|
||||||
icon="tabler-calendar-off"
|
icon="tabler-calendar-off"
|
||||||
size="64"
|
size="64"
|
||||||
color="warning"
|
color="warning"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
<VCardTitle class="text-h5">
|
<h4 class="text-h5 mb-2">
|
||||||
Niet beschikbaar
|
Niet beschikbaar
|
||||||
</VCardTitle>
|
</h4>
|
||||||
<VCardText class="text-body-1">
|
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||||
Dit evenement accepteert momenteel geen aanmeldingen.
|
Dit evenement accepteert momenteel geen aanmeldingen.
|
||||||
</VCardText>
|
</p>
|
||||||
<VCardActions class="justify-center">
|
<VBtn
|
||||||
<VBtn
|
to="/"
|
||||||
to="/"
|
variant="outlined"
|
||||||
variant="outlined"
|
>
|
||||||
>
|
Terug naar startpagina
|
||||||
Terug naar startpagina
|
</VBtn>
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
@@ -342,180 +394,304 @@ async function onSubmit() {
|
|||||||
<!-- Registration Form -->
|
<!-- Registration Form -->
|
||||||
<VRow
|
<VRow
|
||||||
v-else
|
v-else
|
||||||
justify="center"
|
no-gutters
|
||||||
|
class="auth-wrapper"
|
||||||
>
|
>
|
||||||
|
<!-- Left: Event branding & illustration -->
|
||||||
|
<VCol
|
||||||
|
md="4"
|
||||||
|
class="d-none d-md-flex"
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-column align-center justify-center w-100 position-relative">
|
||||||
|
<!-- Event info -->
|
||||||
|
<div
|
||||||
|
class="text-center px-8 mb-6"
|
||||||
|
style="z-index: 1;"
|
||||||
|
>
|
||||||
|
<VChip
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
size="small"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
Aanmelding vrijwilliger
|
||||||
|
</VChip>
|
||||||
|
|
||||||
|
<h2 class="text-h4 font-weight-bold mb-2">
|
||||||
|
{{ registrationData.event.name }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="d-flex align-center justify-center gap-2 text-body-1 text-medium-emphasis">
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-calendar"
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
|
{{ formatDateRange(registrationData.event.start_date, registrationData.event.end_date) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Illustration -->
|
||||||
|
<VImg
|
||||||
|
:src="registerMultiStepIllustration"
|
||||||
|
class="illustration-image flip-in-rtl"
|
||||||
|
style="z-index: 1;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
class="bg-image position-absolute w-100 flip-in-rtl"
|
||||||
|
:src="registerMultiStepBg"
|
||||||
|
alt="register-bg"
|
||||||
|
height="340"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<!-- Right: Registration form -->
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
md="10"
|
md="8"
|
||||||
lg="8"
|
class="auth-card-v2 d-flex align-center justify-center pa-4 pa-sm-10"
|
||||||
|
style="background-color: rgb(var(--v-theme-surface));"
|
||||||
>
|
>
|
||||||
<VCard>
|
<VCard
|
||||||
<VCardTitle class="text-h5 pa-4 pa-sm-6 pb-2">
|
flat
|
||||||
Aanmelden als vrijwilliger
|
class="mt-12 mt-sm-0 w-100"
|
||||||
</VCardTitle>
|
:max-width="750"
|
||||||
<VCardSubtitle class="px-4 px-sm-6 pb-4">
|
>
|
||||||
{{ registrationData.event.name }}
|
<!-- Mobile: Event info (hidden on desktop where it shows in left panel) -->
|
||||||
</VCardSubtitle>
|
<div class="d-md-none text-center mb-6">
|
||||||
|
<VChip
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
size="small"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
Aanmelding vrijwilliger
|
||||||
|
</VChip>
|
||||||
|
|
||||||
|
<h3 class="text-h5 font-weight-bold mb-1">
|
||||||
|
{{ registrationData.event.name }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="d-flex align-center justify-center gap-2 text-body-2 text-medium-emphasis">
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-calendar"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
{{ formatDateRange(registrationData.event.start_date, registrationData.event.end_date) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth info / Login hint -->
|
||||||
<VAlert
|
<VAlert
|
||||||
v-if="authStore.isAuthenticated"
|
v-if="authStore.isAuthenticated"
|
||||||
type="info"
|
type="info"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
class="mx-4 mx-sm-6"
|
class="mb-6"
|
||||||
>
|
>
|
||||||
Je bent ingelogd als {{ authStore.user?.name }}. Je gegevens zijn automatisch ingevuld.
|
Je bent ingelogd als {{ authStore.user?.name }}. Je gegevens zijn automatisch ingevuld.
|
||||||
</VAlert>
|
</VAlert>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-body-2 text-medium-emphasis mb-6"
|
||||||
|
>
|
||||||
|
Al een account?
|
||||||
|
<RouterLink
|
||||||
|
to="/login"
|
||||||
|
class="text-primary font-weight-medium"
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</RouterLink>
|
||||||
|
om je gegevens automatisch in te vullen.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit error -->
|
||||||
<VAlert
|
<VAlert
|
||||||
v-if="submitError"
|
v-if="submitError"
|
||||||
type="error"
|
type="error"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
class="mx-4 mx-sm-6 mt-4"
|
class="mb-6"
|
||||||
closable
|
closable
|
||||||
@click:close="submitError = null"
|
@click:close="submitError = null"
|
||||||
>
|
>
|
||||||
{{ submitError }}
|
{{ submitError }}
|
||||||
</VAlert>
|
</VAlert>
|
||||||
|
|
||||||
<!-- Step indicator -->
|
<!-- Stepper -->
|
||||||
<div class="d-flex flex-wrap align-center justify-center ga-1 px-4 px-sm-6 pt-6">
|
<AppStepper
|
||||||
<template
|
v-model:current-step="currentStep"
|
||||||
v-for="(title, i) in stepTitles"
|
:items="stepperItems"
|
||||||
:key="i"
|
:direction="smAndDown ? 'vertical' : 'horizontal'"
|
||||||
>
|
icon-size="22"
|
||||||
<VChip
|
class="stepper-icon-step-bg mb-6"
|
||||||
:color="currentStep === i + 1 ? 'primary' : currentStep > i + 1 ? 'success' : undefined"
|
/>
|
||||||
:variant="currentStep === i + 1 ? 'elevated' : currentStep > i + 1 ? 'tonal' : 'outlined'"
|
|
||||||
size="small"
|
<VDivider class="mb-2" />
|
||||||
class="step-chip"
|
|
||||||
@click="i + 1 < currentStep ? currentStep = i + 1 : undefined"
|
|
||||||
>
|
|
||||||
<VIcon
|
|
||||||
v-if="currentStep > i + 1"
|
|
||||||
icon="tabler-check"
|
|
||||||
size="14"
|
|
||||||
start
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="text-caption font-weight-bold me-1"
|
|
||||||
>{{ i + 1 }}</span>
|
|
||||||
<span v-if="!smAndDown || currentStep === i + 1">{{ title }}</span>
|
|
||||||
</VChip>
|
|
||||||
<VIcon
|
|
||||||
v-if="i < 4"
|
|
||||||
icon="tabler-chevron-right"
|
|
||||||
size="14"
|
|
||||||
class="text-disabled d-none d-sm-inline-flex"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step content -->
|
<!-- Step content -->
|
||||||
<VWindow v-model="currentStep">
|
<VWindow
|
||||||
<!-- Step 1: Over jou -->
|
v-model="currentStep"
|
||||||
<VWindowItem :value="1">
|
class="disable-tab-transition"
|
||||||
<div class="pa-4 pa-sm-6">
|
>
|
||||||
<VTextField
|
<!-- Step 0: Over jou -->
|
||||||
v-model="name"
|
<VWindowItem>
|
||||||
label="Naam *"
|
<div class="pt-6">
|
||||||
:error-messages="errors.name"
|
<h4 class="text-h4 mb-1">
|
||||||
:disabled="authStore.isAuthenticated"
|
Over jou
|
||||||
autofocus
|
</h4>
|
||||||
class="mb-4"
|
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||||
/>
|
Vul je persoonlijke gegevens in
|
||||||
<VTextField
|
</p>
|
||||||
v-model="email"
|
|
||||||
label="E-mailadres *"
|
<VRow>
|
||||||
type="email"
|
<VCol cols="12">
|
||||||
:error-messages="errors.email"
|
<VTextField
|
||||||
:disabled="authStore.isAuthenticated"
|
v-model="name"
|
||||||
class="mb-4"
|
label="Naam *"
|
||||||
/>
|
:error-messages="errors.name"
|
||||||
<VTextField
|
:disabled="authStore.isAuthenticated"
|
||||||
v-model="phone"
|
autofocus
|
||||||
label="Telefoonnummer"
|
/>
|
||||||
:error-messages="errors.phone"
|
</VCol>
|
||||||
/>
|
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="email"
|
||||||
|
label="E-mailadres *"
|
||||||
|
type="email"
|
||||||
|
:error-messages="errors.email"
|
||||||
|
:disabled="authStore.isAuthenticated"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="phone"
|
||||||
|
label="Telefoonnummer"
|
||||||
|
:error-messages="errors.phone"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
</div>
|
</div>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- Step 2: Meer over jou -->
|
<!-- Step 1: Extra info -->
|
||||||
<VWindowItem :value="2">
|
<VWindowItem>
|
||||||
<div class="pa-4 pa-sm-6">
|
<div class="pt-6">
|
||||||
<VSelect
|
<h4 class="text-h4 mb-1">
|
||||||
v-model="tshirtSize"
|
Extra informatie
|
||||||
:items="tshirtSizeItems"
|
</h4>
|
||||||
label="Shirtmaat"
|
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||||
:error-messages="errors.tshirt_size"
|
Vertel ons meer over jezelf
|
||||||
class="mb-4"
|
</p>
|
||||||
/>
|
|
||||||
<VSwitch
|
<VRow>
|
||||||
v-model="firstAid"
|
<VCol
|
||||||
label="Ik heb een EHBO-diploma"
|
cols="12"
|
||||||
color="primary"
|
sm="6"
|
||||||
hide-details
|
>
|
||||||
class="mb-4"
|
<VSelect
|
||||||
/>
|
v-model="tshirtSize"
|
||||||
<VTextarea
|
:items="tshirtSizeItems"
|
||||||
v-model="allergies"
|
label="Shirtmaat"
|
||||||
label="Allergieën"
|
:error-messages="errors.tshirt_size"
|
||||||
:error-messages="errors.allergies"
|
/>
|
||||||
:counter="500"
|
</VCol>
|
||||||
rows="2"
|
|
||||||
auto-grow
|
<VCol
|
||||||
class="mb-4"
|
cols="12"
|
||||||
/>
|
sm="6"
|
||||||
<VTextarea
|
>
|
||||||
v-model="accessRequirements"
|
<div class="d-flex flex-column gap-4 pt-2">
|
||||||
label="Toegangsbehoeften"
|
<VSwitch
|
||||||
:error-messages="errors.access_requirements"
|
v-model="firstAid"
|
||||||
hint="Bijv. rolstoeltoegankelijk, rustige werkplek, etc."
|
label="Ik heb een EHBO-diploma"
|
||||||
persistent-hint
|
color="primary"
|
||||||
:counter="500"
|
hide-details
|
||||||
rows="2"
|
/>
|
||||||
auto-grow
|
|
||||||
class="mb-4"
|
<VSwitch
|
||||||
/>
|
v-model="drivingLicence"
|
||||||
<VSwitch
|
label="Ik heb een rijbewijs B"
|
||||||
v-model="drivingLicence"
|
color="primary"
|
||||||
label="Ik heb een rijbewijs B"
|
hide-details
|
||||||
color="primary"
|
/>
|
||||||
hide-details
|
</div>
|
||||||
/>
|
</VCol>
|
||||||
|
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextarea
|
||||||
|
v-model="allergies"
|
||||||
|
label="Allergieën"
|
||||||
|
:error-messages="errors.allergies"
|
||||||
|
:counter="500"
|
||||||
|
rows="2"
|
||||||
|
auto-grow
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextarea
|
||||||
|
v-model="accessRequirements"
|
||||||
|
label="Toegangsbehoeften"
|
||||||
|
:error-messages="errors.access_requirements"
|
||||||
|
hint="Bijv. rolstoeltoegankelijk, rustige werkplek, etc."
|
||||||
|
persistent-hint
|
||||||
|
:counter="500"
|
||||||
|
rows="2"
|
||||||
|
auto-grow
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
</div>
|
</div>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- Step 3: Motivatie -->
|
<!-- Step 2: Motivatie -->
|
||||||
<VWindowItem :value="3">
|
<VWindowItem>
|
||||||
<div class="pa-4 pa-sm-6">
|
<div class="pt-6">
|
||||||
<VSelect
|
<h4 class="text-h4 mb-1">
|
||||||
v-model="motivation"
|
Motivatie
|
||||||
:items="motivationItems"
|
</h4>
|
||||||
label="Wat is je motivatie?"
|
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||||
:error-messages="errors.motivation"
|
Waarom wil je vrijwilliger worden?
|
||||||
clearable
|
</p>
|
||||||
class="mb-4"
|
|
||||||
/>
|
<VRow>
|
||||||
<VTextarea
|
<VCol cols="12">
|
||||||
v-if="motivation"
|
<VSelect
|
||||||
v-model="motivationOther"
|
v-model="motivation"
|
||||||
label="Toelichting"
|
:items="motivationItems"
|
||||||
:error-messages="errors.motivation_other"
|
label="Wat is je motivatie?"
|
||||||
:counter="500"
|
:error-messages="errors.motivation"
|
||||||
rows="3"
|
clearable
|
||||||
auto-grow
|
/>
|
||||||
/>
|
</VCol>
|
||||||
|
|
||||||
|
<VCol
|
||||||
|
v-if="motivation"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<VTextarea
|
||||||
|
v-model="motivationOther"
|
||||||
|
label="Toelichting"
|
||||||
|
:error-messages="errors.motivation_other"
|
||||||
|
:counter="500"
|
||||||
|
rows="3"
|
||||||
|
auto-grow
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
</div>
|
</div>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- Step 4: Voorkeurssecties -->
|
<!-- Step 3: Voorkeurssecties -->
|
||||||
<VWindowItem :value="4">
|
<VWindowItem>
|
||||||
<div class="pa-4 pa-sm-6">
|
<div class="pt-6">
|
||||||
<h3 class="text-h6 mb-1">
|
<h4 class="text-h4 mb-1">
|
||||||
Bij welke onderdelen wil je het liefst helpen?
|
Bij welke onderdelen wil je helpen?
|
||||||
</h3>
|
</h4>
|
||||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||||
Selecteer maximaal 5 onderdelen. Je eerste keuze heeft de hoogste prioriteit.
|
Selecteer maximaal 5 onderdelen. Je eerste keuze heeft de hoogste prioriteit.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -526,6 +702,7 @@ async function onSubmit() {
|
|||||||
<div class="text-subtitle-2 text-medium-emphasis mt-4 mb-2">
|
<div class="text-subtitle-2 text-medium-emphasis mt-4 mb-2">
|
||||||
{{ category }}
|
{{ category }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VRow dense>
|
<VRow dense>
|
||||||
<VCol
|
<VCol
|
||||||
v-for="section in sections"
|
v-for="section in sections"
|
||||||
@@ -547,12 +724,14 @@ async function onSubmit() {
|
|||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VIcon
|
<VIcon
|
||||||
v-if="section.icon"
|
v-if="section.icon"
|
||||||
size="20"
|
size="20"
|
||||||
>
|
>
|
||||||
{{ section.icon }}
|
{{ section.icon }}
|
||||||
</VIcon>
|
</VIcon>
|
||||||
|
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<div class="text-body-1 font-weight-medium">
|
<div class="text-body-1 font-weight-medium">
|
||||||
{{ section.name }}
|
{{ section.name }}
|
||||||
@@ -564,6 +743,7 @@ async function onSubmit() {
|
|||||||
{{ section.registration_description }}
|
{{ section.registration_description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VChip
|
<VChip
|
||||||
v-if="isSelected(section.name)"
|
v-if="isSelected(section.name)"
|
||||||
size="x-small"
|
size="x-small"
|
||||||
@@ -596,19 +776,24 @@ async function onSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- Step 5: Beschikbaarheid -->
|
<!-- Step 4: Beschikbaarheid -->
|
||||||
<VWindowItem :value="5">
|
<VWindowItem>
|
||||||
<div class="pa-4 pa-sm-6">
|
<div class="pt-6">
|
||||||
|
<h4 class="text-h4 mb-1">
|
||||||
|
Beschikbaarheid
|
||||||
|
</h4>
|
||||||
|
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||||
|
Selecteer de tijdsloten waarop je beschikbaar bent en geef je voorkeur aan met sterren.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="registrationData.time_slots.length === 0"
|
v-if="registrationData.time_slots.length === 0"
|
||||||
class="text-body-1 text-medium-emphasis"
|
class="text-body-1 text-medium-emphasis"
|
||||||
>
|
>
|
||||||
Er zijn geen tijdsloten beschikbaar voor dit evenement.
|
Er zijn geen tijdsloten beschikbaar voor dit evenement.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
|
||||||
Selecteer de tijdsloten waarop je beschikbaar bent en geef je voorkeur aan met sterren.
|
|
||||||
</p>
|
|
||||||
<div
|
<div
|
||||||
v-for="[date, slots] in timeSlotsByDate"
|
v-for="[date, slots] in timeSlotsByDate"
|
||||||
:key="date"
|
:key="date"
|
||||||
@@ -621,7 +806,6 @@ async function onSubmit() {
|
|||||||
<VListItem
|
<VListItem
|
||||||
v-for="slot in slots"
|
v-for="slot in slots"
|
||||||
:key="slot.id"
|
:key="slot.id"
|
||||||
class="timeslot-item"
|
|
||||||
@click="toggleTimeSlot(slot.id)"
|
@click="toggleTimeSlot(slot.id)"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -630,10 +814,12 @@ async function onSubmit() {
|
|||||||
@click.stop="toggleTimeSlot(slot.id)"
|
@click.stop="toggleTimeSlot(slot.id)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<VListItemTitle>{{ slot.name }}</VListItemTitle>
|
<VListItemTitle>{{ slot.name }}</VListItemTitle>
|
||||||
<VListItemSubtitle>
|
<VListItemSubtitle>
|
||||||
{{ formatTimeRange(slot.start_time, slot.end_time) }} · {{ slot.duration_hours }}u
|
{{ formatTimeRange(slot.start_time, slot.end_time) }} · {{ slot.duration_hours }}u
|
||||||
</VListItemSubtitle>
|
</VListItemSubtitle>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-if="selectedTimeSlotIds.includes(slot.id)"
|
v-if="selectedTimeSlotIds.includes(slot.id)"
|
||||||
#append
|
#append
|
||||||
@@ -673,35 +859,46 @@ async function onSubmit() {
|
|||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
</VWindow>
|
</VWindow>
|
||||||
|
|
||||||
<VDivider />
|
<!-- Navigation buttons -->
|
||||||
|
<div class="d-flex flex-wrap justify-space-between gap-x-4 mt-6">
|
||||||
<!-- Navigation -->
|
|
||||||
<div class="d-flex align-center pa-4 pa-sm-6">
|
|
||||||
<VBtn
|
<VBtn
|
||||||
v-if="currentStep > 1"
|
color="secondary"
|
||||||
variant="text"
|
:disabled="currentStep === 0"
|
||||||
prepend-icon="tabler-arrow-left"
|
variant="tonal"
|
||||||
@click="prevStep"
|
@click="prevStep"
|
||||||
>
|
>
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-arrow-left"
|
||||||
|
start
|
||||||
|
class="flip-in-rtl"
|
||||||
|
/>
|
||||||
Vorige
|
Vorige
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
<VBtn
|
||||||
v-if="currentStep < 5"
|
v-if="currentStep === 4"
|
||||||
color="primary"
|
color="success"
|
||||||
append-icon="tabler-arrow-right"
|
|
||||||
@click="nextStep"
|
|
||||||
>
|
|
||||||
Volgende
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
v-else
|
|
||||||
color="primary"
|
|
||||||
:loading="isSubmitting"
|
:loading="isSubmitting"
|
||||||
prepend-icon="tabler-send"
|
|
||||||
@click="onSubmit"
|
@click="onSubmit"
|
||||||
>
|
>
|
||||||
Aanmelding versturen
|
Aanmelding versturen
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-send"
|
||||||
|
end
|
||||||
|
/>
|
||||||
|
</VBtn>
|
||||||
|
|
||||||
|
<VBtn
|
||||||
|
v-else
|
||||||
|
color="primary"
|
||||||
|
@click="nextStep"
|
||||||
|
>
|
||||||
|
Volgende
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-arrow-right"
|
||||||
|
end
|
||||||
|
class="flip-in-rtl"
|
||||||
|
/>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
@@ -709,14 +906,15 @@ async function onSubmit() {
|
|||||||
</VRow>
|
</VRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss">
|
||||||
.step-chip {
|
@use "@core/scss/template/pages/page-auth.scss";
|
||||||
cursor: pointer;
|
|
||||||
min-block-size: 32px;
|
.illustration-image {
|
||||||
|
block-size: 550px;
|
||||||
|
inline-size: 248px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-item,
|
.bg-image {
|
||||||
.timeslot-item {
|
inset-block-end: 0;
|
||||||
min-block-size: 48px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
|
||||||
|
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||||
|
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||||
import { useAuthStore } from '@/stores/useAuthStore'
|
import { useAuthStore } from '@/stores/useAuthStore'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
name: 'register-success',
|
name: 'register-success',
|
||||||
meta: {
|
meta: {
|
||||||
layout: 'portal',
|
layout: 'blank',
|
||||||
requiresAuth: false,
|
requiresAuth: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -14,55 +17,102 @@ const authStore = useAuthStore()
|
|||||||
|
|
||||||
const eventName = computed(() => (route.query.event as string) || 'het evenement')
|
const eventName = computed(() => (route.query.event as string) || 'het evenement')
|
||||||
const isAuthenticated = computed(() => route.query.authenticated === '1' || authStore.isAuthenticated)
|
const isAuthenticated = computed(() => route.query.authenticated === '1' || authStore.isAuthenticated)
|
||||||
|
|
||||||
|
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VRow justify="center">
|
<!-- Logo -->
|
||||||
|
<RouterLink to="/">
|
||||||
|
<div class="auth-logo d-flex align-center gap-x-3">
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-users-group"
|
||||||
|
size="28"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<h1 class="auth-title">
|
||||||
|
Crewli
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<VRow
|
||||||
|
no-gutters
|
||||||
|
class="auth-wrapper bg-surface"
|
||||||
|
>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
md="8"
|
class="d-flex align-center justify-center"
|
||||||
lg="6"
|
|
||||||
>
|
>
|
||||||
<VCard class="text-center pa-8">
|
<div class="position-relative w-100 d-flex align-center justify-center" style="min-block-size: 100dvh;">
|
||||||
<VIcon
|
<VCard
|
||||||
icon="tabler-circle-check"
|
flat
|
||||||
size="80"
|
:max-width="550"
|
||||||
color="success"
|
class="text-center pa-8 pa-sm-12"
|
||||||
class="mb-4"
|
style="z-index: 1;"
|
||||||
/>
|
>
|
||||||
<VCardTitle class="text-h5 mb-2">
|
<VAvatar
|
||||||
Bedankt voor je aanmelding!
|
size="100"
|
||||||
</VCardTitle>
|
color="success"
|
||||||
<VCardText class="text-body-1">
|
variant="tonal"
|
||||||
<p class="mb-4">
|
class="mb-6"
|
||||||
Bedankt voor je aanmelding bij <strong>{{ eventName }}</strong>!
|
>
|
||||||
|
<VIcon
|
||||||
|
icon="tabler-circle-check"
|
||||||
|
size="60"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
|
||||||
|
<h4 class="text-h4 mb-2">
|
||||||
|
Bedankt voor je aanmelding!
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p class="text-body-1 text-medium-emphasis mb-2">
|
||||||
|
Je aanmelding bij <strong>{{ eventName }}</strong> is succesvol ontvangen.
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-4">
|
|
||||||
Je aanmelding wordt beoordeeld door het organisatieteam.
|
<p class="text-body-1 text-medium-emphasis mb-2">
|
||||||
|
Het organisatieteam beoordeelt je aanmelding zo snel mogelijk.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-medium-emphasis">
|
|
||||||
|
<p class="text-body-2 text-disabled mb-8">
|
||||||
Je ontvangt een e-mail zodra je aanmelding is goedgekeurd.
|
Je ontvangt een e-mail zodra je aanmelding is goedgekeurd.
|
||||||
</p>
|
</p>
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="justify-center pt-4">
|
<div class="d-flex flex-wrap justify-center gap-4">
|
||||||
<VBtn
|
<VBtn
|
||||||
v-if="isAuthenticated"
|
v-if="isAuthenticated"
|
||||||
to="/dashboard"
|
to="/dashboard"
|
||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="tabler-dashboard"
|
prepend-icon="tabler-dashboard"
|
||||||
>
|
>
|
||||||
Ga naar je dashboard
|
Ga naar je dashboard
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn
|
|
||||||
v-else
|
<VBtn
|
||||||
to="/login"
|
v-else
|
||||||
variant="outlined"
|
to="/login"
|
||||||
prepend-icon="tabler-login"
|
color="primary"
|
||||||
>
|
variant="tonal"
|
||||||
Heb je al een account? Log in
|
prepend-icon="tabler-login"
|
||||||
</VBtn>
|
>
|
||||||
</VCardActions>
|
Heb je al een account? Log in
|
||||||
</VCard>
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
|
<img
|
||||||
|
class="auth-footer-mask flip-in-rtl"
|
||||||
|
:src="authThemeMask"
|
||||||
|
alt="footer-mask"
|
||||||
|
height="280"
|
||||||
|
width="100"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@core/scss/template/pages/page-auth.scss";
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user