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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user