Files
crewli/apps/portal/src/layouts/portal.vue
bert.hausmans cd2c775692 fix: eliminate all TypeScript any usage across Vue components
Replace 24 `err: any` error handler types with proper `AxiosError<ApiErrorResponse>`
typing. Fix additional `as any` casts and `Record<string, any>` patterns in registration
field components, event settings, and portal layout. Create shared `ApiErrorResponse`
type for portal app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:54:01 +02:00

234 lines
6.3 KiB
Vue

<script lang="ts" setup>
import UserAvatarMenu from '@/components/portal/UserAvatarMenu.vue'
import { useAuthStore } from '@/stores/useAuthStore'
import { usePortalStore } from '@/stores/usePortalStore'
const { injectSkinClasses } = useSkins()
injectSkinClasses()
const authStore = useAuthStore()
const portal = usePortalStore()
const route = useRoute()
const isMobileMenuOpen = ref(false)
// Navbar mode: 'event' shows org name + event name + back link
// Default ('platform') shows Crewli logo + page title
const isEventMode = computed(() => route.meta.navMode === 'event')
const navTitle = computed(() => route.meta.navTitle)
const eventName = computed(() => portal.activeEvent?.event_name ?? '')
const orgName = computed(() => portal.activeEvent?.organisation_name ?? '')
// Mobile nav items
const mobileNavItems = computed(() => {
const items = [
{ title: 'Mijn evenementen', to: '/evenementen', icon: 'tabler-calendar-event' },
{ title: 'Mijn Profiel', to: '/profiel', icon: 'tabler-user' },
]
return items
})
const isFallbackStateActive = ref(false)
const refLoadingIndicator = ref<any>(null)
watch([isFallbackStateActive, refLoadingIndicator], () => {
if (isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.fallbackHandle()
if (!isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.resolveHandle()
}, { immediate: true })
async function logout() {
isMobileMenuOpen.value = false
await authStore.logout()
const router = useRouter()
await router.push('/login')
}
</script>
<template>
<VApp>
<AppLoadingIndicator ref="refLoadingIndicator" />
<!-- Navbar: only shown when authenticated -->
<VAppBar
v-if="authStore.isAuthenticated"
flat
color="surface"
border="b"
height="64"
>
<VContainer
fluid
class="d-flex align-center py-0"
style="max-inline-size: 1440px;"
>
<!-- Event mode: Org name + Event name + Back link -->
<template v-if="isEventMode">
<!-- Org name / logo placeholder -->
<div class="d-flex align-center gap-x-2 flex-shrink-0">
<VIcon
icon="tabler-building"
size="24"
color="primary"
/>
<span
v-if="orgName"
class="text-subtitle-1 font-weight-medium text-high-emphasis d-none d-sm-inline text-truncate"
style="max-width: 200px;"
>
{{ orgName }}
</span>
</div>
<!-- Event name -->
<span
v-if="eventName"
class="text-body-1 text-medium-emphasis ms-2 text-truncate d-none d-sm-inline"
style="max-width: 250px;"
>
{{ eventName }}
</span>
<!-- Back link -->
<VBtn
variant="text"
size="small"
color="default"
class="text-medium-emphasis ms-2 d-none d-md-flex"
to="/evenementen"
>
<VIcon
start
icon="tabler-arrow-left"
size="16"
/>
Evenementen
</VBtn>
</template>
<!-- Platform mode: Crewli logo + optional page title -->
<template v-else>
<RouterLink
to="/evenementen"
class="d-flex align-center gap-x-2 text-decoration-none flex-shrink-0"
>
<VIcon
icon="tabler-users-group"
size="26"
color="primary"
/>
<span class="text-h6 font-weight-bold text-high-emphasis d-none d-sm-inline">
Crewli
</span>
</RouterLink>
<span
v-if="navTitle"
class="text-body-1 text-medium-emphasis ms-4 d-none d-md-inline"
>
{{ navTitle }}
</span>
</template>
<VSpacer />
<!-- Right section: Avatar menu (desktop) -->
<div class="d-none d-md-flex align-center">
<UserAvatarMenu />
</div>
<!-- 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
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 mobileNavItems"
:key="item.to"
:to="item.to"
:prepend-icon="item.icon"
:title="item.title"
@click="isMobileMenuOpen = false"
/>
<VDivider class="my-2" />
<VListItem
prepend-icon="tabler-logout"
title="Uitloggen"
class="text-error"
@click="logout"
/>
</VList>
</VNavigationDrawer>
<VMain>
<VContainer
fluid
class="pa-4 pa-sm-6"
style="max-inline-size: 1440px;"
>
<RouterView v-slot="{ Component }">
<Suspense
:timeout="0"
@fallback="isFallbackStateActive = true"
@resolve="isFallbackStateActive = false"
>
<Component :is="Component" />
</Suspense>
</RouterView>
</VContainer>
</VMain>
<!-- Footer -->
<VFooter
app
color="transparent"
class="justify-center text-caption text-medium-emphasis py-3"
>
Powered by Crewli
</VFooter>
</VApp>
</template>