feat(portal): horizontal navbar layout with avatar menu and profile restructuring

Replace the simple inline header with a proper Vuexy-style horizontal
navbar featuring left (logo + event switcher), center (conditional menu
items based on approval status), and right (avatar dropdown with profile
link and logout) sections. Move profile page from /profile to /profiel
as a platform-level page with "Mijn evenementen" section, removing the
event-scoped status card and remarks field. Registration and success
pages now use the portal layout with hideEventMenu meta so they get the
navbar when logged in but no event menu items.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 12:44:21 +02:00
parent 59ad09fad2
commit 2d7464e05b
10 changed files with 628 additions and 464 deletions

View File

@@ -46,5 +46,6 @@ declare module 'vue' {
TheCustomizer: typeof import('./src/@core/components/TheCustomizer.vue')['default']
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
TiptapEditor: typeof import('./src/@core/components/TiptapEditor.vue')['default']
UserAvatarMenu: typeof import('./src/components/portal/UserAvatarMenu.vue')['default']
}
}

View File

@@ -7,5 +7,6 @@ declare module 'vue-router' {
requiresAuth?: boolean
requiresToken?: boolean
public?: boolean
hideEventMenu?: boolean
}
}

View File

@@ -147,7 +147,7 @@ const registeredLabel = computed(() => {
sm="4"
>
<VCard
:to="{ name: 'portal-profile' }"
:to="{ name: 'portal-profiel' }"
class="h-100 text-decoration-none portal-action-card"
elevation="1"
>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import { useAuthStore } from '@/stores/useAuthStore'
const authStore = useAuthStore()
const router = useRouter()
const userInitials = computed(() => {
const user = authStore.user
if (!user) return '?'
const first = user.first_name?.charAt(0) ?? ''
const last = user.last_name?.charAt(0) ?? ''
return (first + last).toUpperCase() || '?'
})
const userFullName = computed(() => authStore.user?.full_name ?? '')
const userEmail = computed(() => authStore.user?.email ?? '')
async function logout() {
await authStore.logout()
await router.push('/login')
}
</script>
<template>
<VMenu
location="bottom end"
:close-on-content-click="false"
min-width="220"
>
<template #activator="{ props: menuProps }">
<VAvatar
v-bind="menuProps"
size="36"
color="primary"
class="cursor-pointer"
>
<span class="text-body-2 font-weight-medium text-white">
{{ userInitials }}
</span>
</VAvatar>
</template>
<VList density="compact">
<!-- User info -->
<VListItem class="pb-0">
<VListItemTitle class="font-weight-bold text-body-1">
{{ userFullName }}
</VListItemTitle>
<VListItemSubtitle class="text-caption text-medium-emphasis">
{{ userEmail }}
</VListItemSubtitle>
</VListItem>
<VDivider class="my-2" />
<!-- Profile link -->
<VListItem
to="/profiel"
prepend-icon="tabler-user"
title="Mijn Profiel"
/>
<VDivider class="my-2" />
<!-- Logout -->
<VListItem
prepend-icon="tabler-logout"
title="Uitloggen"
class="text-error"
@click="logout"
/>
</VList>
</VMenu>
</template>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import EventSwitcher from '@/components/portal/EventSwitcher.vue'
import UserAvatarMenu from '@/components/portal/UserAvatarMenu.vue'
import { useAuthStore } from '@/stores/useAuthStore'
import { usePortalStore } from '@/stores/usePortalStore'
@@ -9,17 +10,28 @@ injectSkinClasses()
const authStore = useAuthStore()
const portal = usePortalStore()
const router = useRouter()
const route = useRoute()
const isMobileMenuOpen = ref(false)
const hideEventMenu = computed(() => route.meta.hideEventMenu === true)
const isApproved = computed(() => portal.currentPerson?.status === 'approved')
const navItems = computed(() => {
if (!authStore.isAuthenticated) return []
const hasActiveEvent = computed(() => !!portal.activeEventId)
const showEventMenu = computed(() => {
if (hideEventMenu.value) return false
if (!hasActiveEvent.value) return false
return true
})
const menuItems = computed(() => {
if (!showEventMenu.value) return []
const items = [
{ title: 'Dashboard', to: '/dashboard', icon: 'tabler-dashboard' },
{ title: 'Dashboard', to: '/dashboard', icon: 'tabler-layout-dashboard' },
]
if (isApproved.value) {
@@ -29,7 +41,14 @@ const navItems = computed(() => {
)
}
items.push({ title: 'Mijn Profiel', to: '/profile', icon: 'tabler-user' })
return items
})
// Mobile drawer items include profile + logout
const mobileNavItems = computed(() => {
const items = [...menuItems.value]
items.push({ title: 'Mijn Profiel', to: '/profiel', icon: 'tabler-user' })
return items
})
@@ -45,14 +64,11 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
refLoadingIndicator.value.resolveHandle()
}, { immediate: true })
async function logoutAndRedirect(): Promise<void> {
await authStore.logout()
await router.push('/login')
}
async function logoutFromDrawer(): Promise<void> {
async function logout() {
isMobileMenuOpen.value = false
await logoutAndRedirect()
await authStore.logout()
const router = useRouter()
await router.push('/login')
}
</script>
@@ -60,20 +76,22 @@ async function logoutFromDrawer(): Promise<void> {
<VApp>
<AppLoadingIndicator ref="refLoadingIndicator" />
<!-- Navbar: only shown when authenticated -->
<VAppBar
v-if="authStore.isAuthenticated"
flat
color="surface"
border="b"
density="comfortable"
height="64"
>
<VContainer
fluid
class="d-flex align-center py-0"
style="max-inline-size: 1440px;"
>
<!-- Logo & Brand -->
<!-- Left section: Logo + Event Switcher -->
<RouterLink
to="/"
to="/dashboard"
class="d-flex align-center gap-x-2 text-decoration-none flex-shrink-0"
>
<VIcon
@@ -81,32 +99,27 @@ async function logoutFromDrawer(): Promise<void> {
size="26"
color="primary"
/>
<span class="text-h6 font-weight-bold text-high-emphasis">
<span class="text-h6 font-weight-bold text-high-emphasis d-none d-sm-inline">
Crewli
</span>
</RouterLink>
<EventSwitcher
v-if="authStore.isAuthenticated"
class="min-w-0 flex-grow-1 flex-sm-grow-0"
/>
<EventSwitcher class="min-w-0 flex-grow-1 flex-sm-grow-0" />
<!-- Mobile nav toggle -->
<VAppBarNavIcon
class="d-sm-none ms-auto"
@click="isMobileMenuOpen = !isMobileMenuOpen"
/>
<!-- Desktop navigation -->
<div class="d-none d-sm-flex align-center gap-1 ms-6">
<!-- Center section: Desktop menu items -->
<div
v-if="menuItems.length > 0"
class="d-none d-md-flex align-center gap-1 ms-4"
>
<VBtn
v-for="item in navItems"
v-for="item in menuItems"
:key="item.to"
:to="item.to"
variant="text"
color="default"
size="small"
class="text-medium-emphasis"
exact
class="portal-nav-btn"
>
<VIcon
start
@@ -119,48 +132,54 @@ async function logoutFromDrawer(): Promise<void> {
<VSpacer />
<!-- Auth actions -->
<VBtn
v-if="authStore.isAuthenticated"
variant="tonal"
color="primary"
size="small"
@click="logoutAndRedirect"
>
<VIcon
start
icon="tabler-logout"
size="18"
/>
Uitloggen
</VBtn>
<!-- Right section: Avatar menu (desktop) -->
<div class="d-none d-md-flex align-center">
<UserAvatarMenu />
</div>
<VBtn
v-else
variant="tonal"
color="primary"
size="small"
to="/login"
>
<VIcon
start
icon="tabler-login"
size="18"
/>
Inloggen
</VBtn>
<!-- Mobile nav toggle -->
<VAppBarNavIcon
class="d-md-none"
@click="isMobileMenuOpen = !isMobileMenuOpen"
/>
</VContainer>
</VAppBar>
<!-- Mobile navigation drawer -->
<VNavigationDrawer
v-if="authStore.isAuthenticated"
v-model="isMobileMenuOpen"
temporary
class="d-sm-none"
location="end"
class="d-md-none"
>
<!-- User info header -->
<div class="pa-4 pb-2">
<div class="d-flex align-center gap-3">
<VAvatar
size="40"
color="primary"
>
<span class="text-body-2 font-weight-medium text-white">
{{ (authStore.user?.first_name?.charAt(0) ?? '') + (authStore.user?.last_name?.charAt(0) ?? '') }}
</span>
</VAvatar>
<div class="min-w-0">
<div class="text-body-1 font-weight-bold text-truncate">
{{ authStore.user?.full_name }}
</div>
<div class="text-caption text-medium-emphasis text-truncate">
{{ authStore.user?.email }}
</div>
</div>
</div>
</div>
<VDivider />
<VList nav>
<VListItem
v-for="item in navItems"
v-for="item in mobileNavItems"
:key="item.to"
:to="item.to"
:prepend-icon="item.icon"
@@ -171,18 +190,10 @@ async function logoutFromDrawer(): Promise<void> {
<VDivider class="my-2" />
<VListItem
v-if="authStore.isAuthenticated"
prepend-icon="tabler-logout"
title="Uitloggen"
@click="logoutFromDrawer"
/>
<VListItem
v-else
to="/login"
prepend-icon="tabler-login"
title="Inloggen"
@click="isMobileMenuOpen = false"
class="text-error"
@click="logout"
/>
</VList>
</VNavigationDrawer>
@@ -204,5 +215,42 @@ async function logoutFromDrawer(): Promise<void> {
</RouterView>
</VContainer>
</VMain>
<!-- Footer -->
<VFooter
app
color="transparent"
class="justify-center text-caption text-medium-emphasis py-3"
>
Powered by Crewli
</VFooter>
</VApp>
</template>
<style scoped>
.portal-nav-btn {
position: relative;
font-weight: 500;
letter-spacing: 0.01em;
}
.portal-nav-btn::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background: rgb(var(--v-theme-primary));
transition: width 0.2s ease, left 0.2s ease;
}
.portal-nav-btn.router-link-active {
color: rgb(var(--v-theme-primary)) !important;
}
.portal-nav-btn.router-link-active::after {
width: 60%;
left: 20%;
}
</style>

View File

@@ -0,0 +1,426 @@
<script setup lang="ts">
import { useAuthStore } from '@/stores/useAuthStore'
import { usePortalStore } from '@/stores/usePortalStore'
import { useUpdateProfile, useUpdatePassword } from '@/composables/api/usePortalProfile'
definePage({
name: 'portal-profiel',
meta: {
layout: 'portal',
requiresAuth: true,
hideEventMenu: true,
},
})
const authStore = useAuthStore()
const portal = usePortalStore()
const router = useRouter()
const updateProfileMutation = useUpdateProfile()
const updatePasswordMutation = useUpdatePassword()
const snackbar = ref(false)
const snackbarMessage = ref('')
const snackbarColor = ref('success')
// Profile form
const profileForm = ref({
first_name: '',
last_name: '',
phone: '',
date_of_birth: '',
})
const profileError = ref<string | null>(null)
// Password form
const passwordForm = ref({
current_password: '',
password: '',
password_confirmation: '',
})
const passwordError = ref<string | null>(null)
const passwordFieldErrors = ref<Record<string, string>>({})
const showCurrentPassword = ref(false)
const showNewPassword = ref(false)
const showConfirmPassword = ref(false)
// Populate profile form from auth user / current person data
watch(
[() => authStore.user, () => portal.currentPerson],
([user, person]) => {
profileForm.value = {
first_name: person?.first_name ?? user?.first_name ?? '',
last_name: person?.last_name ?? user?.last_name ?? '',
phone: person?.phone ?? '',
date_of_birth: person?.date_of_birth ?? '',
}
},
{ immediate: true },
)
// Status helpers for event list
function statusColor(status: string): string {
if (status === 'approved') return 'success'
if (status === 'pending' || status === 'applied' || status === 'invited') return 'warning'
if (status === 'rejected') return 'error'
return 'secondary'
}
function statusLabel(status: string): string {
const map: Record<string, string> = {
pending: 'In behandeling',
applied: 'In behandeling',
invited: 'Uitgenodigd',
approved: 'Goedgekeurd',
rejected: 'Afgewezen',
no_show: 'Niet verschenen',
}
return map[status] ?? status
}
function formatEventDates(startDate: string, endDate: string): string {
try {
const start = new Date(`${startDate}T12:00:00`)
const end = new Date(`${endDate}T12:00:00`)
const opts: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' }
return `${start.toLocaleDateString('nl-NL', opts)} ${end.toLocaleDateString('nl-NL', opts)}`
}
catch {
return `${startDate} ${endDate}`
}
}
function viewEvent(eventId: string) {
portal.setActiveEvent(eventId)
router.push('/dashboard')
}
async function saveProfile() {
profileError.value = null
if (!portal.activeEventId) return
try {
const result = await updateProfileMutation.mutateAsync({
event_id: portal.activeEventId,
...profileForm.value,
phone: profileForm.value.phone || null,
date_of_birth: profileForm.value.date_of_birth || null,
})
// Refresh person data and auth user
await Promise.all([
portal.fetchCurrentPerson(),
authStore.fetchUser(),
])
snackbarMessage.value = result.message
snackbarColor.value = 'success'
snackbar.value = true
}
catch (err: any) {
const data = err?.response?.data
if (data?.errors) {
const firstError = Object.values(data.errors).flat()[0] as string
profileError.value = firstError
}
else {
profileError.value = data?.message ?? 'Er is een fout opgetreden.'
}
}
}
async function savePassword() {
passwordError.value = null
passwordFieldErrors.value = {}
try {
const result = await updatePasswordMutation.mutateAsync(passwordForm.value)
passwordForm.value = {
current_password: '',
password: '',
password_confirmation: '',
}
snackbarMessage.value = result.message
snackbarColor.value = 'success'
snackbar.value = true
}
catch (err: any) {
const data = err?.response?.data
if (data?.errors) {
passwordFieldErrors.value = {}
for (const [key, messages] of Object.entries(data.errors)) {
passwordFieldErrors.value[key] = (messages as string[])[0]
}
}
else {
passwordError.value = data?.message ?? 'Er is een fout opgetreden.'
}
}
}
</script>
<template>
<VRow justify="center">
<VCol
cols="12"
md="8"
lg="6"
>
<h4 class="text-h4 mb-4">
Mijn Profiel
</h4>
<!-- Profile form -->
<VCard class="mb-4">
<VCardTitle>Persoonlijke gegevens</VCardTitle>
<VCardText>
<VAlert
v-if="profileError"
type="error"
variant="tonal"
density="compact"
class="mb-4"
>
{{ profileError }}
</VAlert>
<VForm @submit.prevent="saveProfile">
<VRow>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.first_name"
label="Voornaam"
variant="outlined"
density="comfortable"
hide-details="auto"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.last_name"
label="Achternaam"
variant="outlined"
density="comfortable"
hide-details="auto"
/>
</VCol>
<VCol cols="12">
<VTextField
:model-value="authStore.user?.email"
label="E-mailadres"
variant="outlined"
density="comfortable"
hide-details="auto"
readonly
prepend-inner-icon="tabler-lock"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
Je e-mailadres kan niet worden gewijzigd.
</p>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.phone"
label="Telefoonnummer"
variant="outlined"
density="comfortable"
hide-details="auto"
prepend-inner-icon="tabler-phone"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.date_of_birth"
label="Geboortedatum"
variant="outlined"
density="comfortable"
hide-details="auto"
type="date"
>
<template #prepend-inner>
<VIcon
icon="tabler-calendar"
size="20"
/>
</template>
</VTextField>
</VCol>
</VRow>
<div class="d-flex justify-end mt-4">
<VBtn
type="submit"
color="primary"
:loading="updateProfileMutation.isPending.value"
>
Opslaan
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
<!-- Password change -->
<VCard class="mb-4">
<VCardTitle>Wachtwoord wijzigen</VCardTitle>
<VCardText>
<VAlert
v-if="passwordError"
type="error"
variant="tonal"
density="compact"
class="mb-4"
>
{{ passwordError }}
</VAlert>
<VForm @submit.prevent="savePassword">
<VRow>
<VCol cols="12">
<VTextField
v-model="passwordForm.current_password"
label="Huidig wachtwoord"
variant="outlined"
density="comfortable"
hide-details="auto"
:type="showCurrentPassword ? 'text' : 'password'"
:append-inner-icon="showCurrentPassword ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="passwordFieldErrors.current_password"
@click:append-inner="showCurrentPassword = !showCurrentPassword"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="passwordForm.password"
label="Nieuw wachtwoord"
variant="outlined"
density="comfortable"
hide-details="auto"
:type="showNewPassword ? 'text' : 'password'"
:append-inner-icon="showNewPassword ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="passwordFieldErrors.password"
@click:append-inner="showNewPassword = !showNewPassword"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="passwordForm.password_confirmation"
label="Bevestig nieuw wachtwoord"
variant="outlined"
density="comfortable"
hide-details="auto"
:type="showConfirmPassword ? 'text' : 'password'"
:append-inner-icon="showConfirmPassword ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="passwordFieldErrors.password_confirmation"
@click:append-inner="showConfirmPassword = !showConfirmPassword"
/>
</VCol>
</VRow>
<div class="d-flex justify-end mt-4">
<VBtn
type="submit"
color="primary"
:loading="updatePasswordMutation.isPending.value"
>
Wachtwoord wijzigen
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
<!-- My events -->
<VCard v-if="portal.userEvents.length > 0">
<VCardTitle>Mijn evenementen</VCardTitle>
<VCardText class="pa-0">
<VList>
<VListItem
v-for="ev in portal.userEvents"
:key="ev.event_id"
class="py-3"
>
<template #prepend>
<VIcon
icon="tabler-calendar-event"
size="24"
color="primary"
class="me-1"
/>
</template>
<VListItemTitle class="font-weight-medium">
{{ ev.event_name }}
</VListItemTitle>
<VListItemSubtitle class="text-caption mt-1">
{{ formatEventDates(ev.start_date, ev.end_date) }}
</VListItemSubtitle>
<template #append>
<div class="d-flex align-center gap-2">
<VChip
:color="statusColor(ev.person_status)"
size="small"
label
>
{{ statusLabel(ev.person_status) }}
</VChip>
<VBtn
variant="text"
color="primary"
size="small"
@click="viewEvent(ev.event_id)"
>
Bekijk
</VBtn>
</div>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
<!-- No events -->
<VAlert
v-else
type="info"
variant="tonal"
>
Je bent nog niet aangemeld voor een evenement.
</VAlert>
<!-- Snackbar -->
<VSnackbar
v-model="snackbar"
:color="snackbarColor"
:timeout="4000"
>
{{ snackbarMessage }}
</VSnackbar>
</VCol>
</VRow>
</template>

View File

@@ -1,389 +0,0 @@
<script setup lang="ts">
import { useAuthStore } from '@/stores/useAuthStore'
import { usePortalStore } from '@/stores/usePortalStore'
import { useUpdateProfile, useUpdatePassword } from '@/composables/api/usePortalProfile'
definePage({
name: 'portal-profile',
meta: {
layout: 'portal',
requiresAuth: true,
},
})
const authStore = useAuthStore()
const portal = usePortalStore()
const updateProfileMutation = useUpdateProfile()
const updatePasswordMutation = useUpdatePassword()
const snackbar = ref(false)
const snackbarMessage = ref('')
const snackbarColor = ref('success')
// Profile form
const profileForm = ref({
first_name: '',
last_name: '',
phone: '',
date_of_birth: '',
remarks: '',
})
const profileError = ref<string | null>(null)
// Password form
const passwordForm = ref({
current_password: '',
password: '',
password_confirmation: '',
})
const passwordError = ref<string | null>(null)
const passwordFieldErrors = ref<Record<string, string>>({})
const showCurrentPassword = ref(false)
const showNewPassword = ref(false)
const showConfirmPassword = ref(false)
// Populate profile form from current person data
watch(() => portal.currentPerson, (person) => {
if (person) {
profileForm.value = {
first_name: person.first_name ?? authStore.user?.first_name ?? '',
last_name: person.last_name ?? authStore.user?.last_name ?? '',
phone: person.phone ?? '',
date_of_birth: person.date_of_birth ?? '',
remarks: person.remarks ?? '',
}
}
}, { immediate: true })
const statusConfig: Record<string, { label: string; color: string }> = {
pending: { label: 'In afwachting', color: 'warning' },
approved: { label: 'Goedgekeurd', color: 'success' },
rejected: { label: 'Afgewezen', color: 'error' },
}
const effectiveStatus = computed(() => {
const s = portal.currentPerson?.status ?? portal.activeEvent?.person_status ?? 'pending'
return statusConfig[s] ?? statusConfig.pending
})
async function saveProfile() {
profileError.value = null
if (!portal.activeEventId) return
try {
const result = await updateProfileMutation.mutateAsync({
event_id: portal.activeEventId,
...profileForm.value,
phone: profileForm.value.phone || null,
date_of_birth: profileForm.value.date_of_birth || null,
remarks: profileForm.value.remarks || null,
})
// Refresh person data and auth user
await Promise.all([
portal.fetchCurrentPerson(),
authStore.fetchUser(),
])
snackbarMessage.value = result.message
snackbarColor.value = 'success'
snackbar.value = true
}
catch (err: any) {
const data = err?.response?.data
if (data?.errors) {
const firstError = Object.values(data.errors).flat()[0] as string
profileError.value = firstError
}
else {
profileError.value = data?.message ?? 'Er is een fout opgetreden.'
}
}
}
async function savePassword() {
passwordError.value = null
passwordFieldErrors.value = {}
try {
const result = await updatePasswordMutation.mutateAsync(passwordForm.value)
passwordForm.value = {
current_password: '',
password: '',
password_confirmation: '',
}
snackbarMessage.value = result.message
snackbarColor.value = 'success'
snackbar.value = true
}
catch (err: any) {
const data = err?.response?.data
if (data?.errors) {
passwordFieldErrors.value = {}
for (const [key, messages] of Object.entries(data.errors)) {
passwordFieldErrors.value[key] = (messages as string[])[0]
}
}
else {
passwordError.value = data?.message ?? 'Er is een fout opgetreden.'
}
}
}
</script>
<template>
<VRow justify="center">
<VCol
cols="12"
md="8"
lg="6"
>
<h4 class="text-h4 mb-4">
Mijn Profiel
</h4>
<!-- Loading state -->
<VSkeletonLoader
v-if="portal.isLoadingPerson && !portal.currentPerson"
type="card"
class="mb-4"
/>
<template v-else-if="portal.currentPerson">
<!-- Status & Event info -->
<VCard class="mb-4">
<VCardText>
<div class="d-flex align-center justify-space-between flex-wrap gap-2">
<div>
<div class="text-body-2 text-medium-emphasis">
Evenement
</div>
<div class="text-body-1 font-weight-medium">
{{ portal.activeEvent?.event_name }}
</div>
</div>
<VChip
:color="effectiveStatus.color"
variant="tonal"
size="small"
>
{{ effectiveStatus.label }}
</VChip>
</div>
</VCardText>
</VCard>
<!-- Profile form -->
<VCard class="mb-4">
<VCardTitle>Persoonlijke gegevens</VCardTitle>
<VCardText>
<VAlert
v-if="profileError"
type="error"
variant="tonal"
density="compact"
class="mb-4"
>
{{ profileError }}
</VAlert>
<VForm @submit.prevent="saveProfile">
<VRow>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.first_name"
label="Voornaam"
variant="outlined"
density="comfortable"
hide-details="auto"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.last_name"
label="Achternaam"
variant="outlined"
density="comfortable"
hide-details="auto"
/>
</VCol>
<VCol cols="12">
<VTextField
:model-value="portal.currentPerson.email || authStore.user?.email"
label="E-mailadres"
variant="outlined"
density="comfortable"
hide-details="auto"
readonly
prepend-inner-icon="tabler-lock"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
Je e-mailadres kan niet worden gewijzigd.
</p>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.phone"
label="Telefoonnummer"
variant="outlined"
density="comfortable"
hide-details="auto"
prepend-inner-icon="tabler-phone"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="profileForm.date_of_birth"
label="Geboortedatum"
variant="outlined"
density="comfortable"
hide-details="auto"
type="date"
>
<template #prepend-inner>
<VIcon
icon="tabler-calendar"
size="20"
/>
</template>
</VTextField>
</VCol>
<VCol cols="12">
<VTextarea
v-model="profileForm.remarks"
label="Opmerkingen"
variant="outlined"
density="comfortable"
hide-details="auto"
rows="3"
placeholder="Allergieën, dieetwensen, overige opmerkingen..."
/>
</VCol>
</VRow>
<div class="d-flex justify-end mt-4">
<VBtn
type="submit"
color="primary"
:loading="updateProfileMutation.isPending.value"
>
Opslaan
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
<!-- Password change -->
<VCard>
<VCardTitle>Wachtwoord wijzigen</VCardTitle>
<VCardText>
<VAlert
v-if="passwordError"
type="error"
variant="tonal"
density="compact"
class="mb-4"
>
{{ passwordError }}
</VAlert>
<VForm @submit.prevent="savePassword">
<VRow>
<VCol cols="12">
<VTextField
v-model="passwordForm.current_password"
label="Huidig wachtwoord"
variant="outlined"
density="comfortable"
hide-details="auto"
:type="showCurrentPassword ? 'text' : 'password'"
:append-inner-icon="showCurrentPassword ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="passwordFieldErrors.current_password"
@click:append-inner="showCurrentPassword = !showCurrentPassword"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="passwordForm.password"
label="Nieuw wachtwoord"
variant="outlined"
density="comfortable"
hide-details="auto"
:type="showNewPassword ? 'text' : 'password'"
:append-inner-icon="showNewPassword ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="passwordFieldErrors.password"
@click:append-inner="showNewPassword = !showNewPassword"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<VTextField
v-model="passwordForm.password_confirmation"
label="Bevestig nieuw wachtwoord"
variant="outlined"
density="comfortable"
hide-details="auto"
:type="showConfirmPassword ? 'text' : 'password'"
:append-inner-icon="showConfirmPassword ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="passwordFieldErrors.password_confirmation"
@click:append-inner="showConfirmPassword = !showConfirmPassword"
/>
</VCol>
</VRow>
<div class="d-flex justify-end mt-4">
<VBtn
type="submit"
color="primary"
:loading="updatePasswordMutation.isPending.value"
>
Wachtwoord wijzigen
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</template>
<VAlert
v-else
type="warning"
variant="tonal"
>
We konden je profiel niet laden. Probeer het later opnieuw.
</VAlert>
<!-- Snackbar -->
<VSnackbar
v-model="snackbar"
:color="snackbarColor"
:timeout="4000"
>
{{ snackbarMessage }}
</VSnackbar>
</VCol>
</VRow>
</template>

View File

@@ -21,8 +21,9 @@ import type {
definePage({
name: 'volunteer-register',
meta: {
layout: 'blank',
layout: 'portal',
requiresAuth: false,
hideEventMenu: true,
},
})

View File

@@ -4,8 +4,9 @@ import { useAuthStore } from '@/stores/useAuthStore'
definePage({
name: 'register-success',
meta: {
layout: 'blank',
layout: 'portal',
requiresAuth: false,
hideEventMenu: true,
},
})

View File

@@ -25,7 +25,7 @@ declare module 'vue-router/auto-routes' {
'portal-claim-shifts': RouteRecordInfo<'portal-claim-shifts', '/dashboard/claim-shifts', Record<never, never>, Record<never, never>>,
'portal-my-shifts': RouteRecordInfo<'portal-my-shifts', '/dashboard/my-shifts', Record<never, never>, Record<never, never>>,
'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>,
'portal-profile': RouteRecordInfo<'portal-profile', '/profile', Record<never, never>, Record<never, never>>,
'portal-profiel': RouteRecordInfo<'portal-profiel', '/profiel', Record<never, never>, Record<never, never>>,
'volunteer-register': RouteRecordInfo<'volunteer-register', '/register/:eventSlug', { eventSlug: ParamValue<true> }, { eventSlug: ParamValue<false> }>,
'register-success': RouteRecordInfo<'register-success', '/register/success', Record<never, never>, Record<never, never>>,
'volunteer-register-info': RouteRecordInfo<'volunteer-register-info', '/registreren', Record<never, never>, Record<never, never>>,