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

@@ -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>