feat(portal): restructure into three-screen architecture with event tabs

Replace scattered dashboard pages with a three-screen volunteer portal:

1. Mijn evenementen (/evenementen) - landing page with visual event cards
   in a responsive grid, sorted upcoming-first
2. Event-pagina (/evenementen/:eventId) - single page with hash-based tabs
   (Overzicht, Mijn rooster, Diensten claimen, Informatie) replacing the
   old separate dashboard/my-shifts/claim-shifts pages
3. Mijn profiel (/profiel) - unchanged, platform-level settings

Key changes:
- Extract page content into tab components (RoosterTab, ClaimenTab,
  OverzichtTab, InformatieTab) that receive eventId as prop
- Dual-mode navbar: platform mode (Crewli logo) vs event mode (org name
  + event name + back link)
- StatusCard now emits switchTab events instead of route navigation
- Smart login redirect: 1 event → direct to event, 2+ → overview
- Backward-compat redirects for /dashboard/* → /evenementen
- Delete EventSwitcher (replaced by events overview page)
- Update UserAvatarMenu with "Mijn evenementen" link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 13:30:20 +02:00
parent 2d7464e05b
commit f9faeb7ea0
22 changed files with 1482 additions and 1102 deletions

View File

@@ -1,5 +1,4 @@
<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'
@@ -14,41 +13,20 @@ const route = useRoute()
const isMobileMenuOpen = ref(false)
const hideEventMenu = computed(() => route.meta.hideEventMenu === true)
// 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 as any).navTitle as string | undefined)
const isApproved = computed(() => portal.currentPerson?.status === 'approved')
const eventName = computed(() => portal.activeEvent?.event_name ?? '')
const orgName = computed(() => portal.activeEvent?.organisation_name ?? '')
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-layout-dashboard' },
]
if (isApproved.value) {
items.push(
{ title: 'Mijn Diensten', to: '/dashboard/my-shifts', icon: 'tabler-calendar-check' },
{ title: 'Diensten Claimen', to: '/dashboard/claim-shifts', icon: 'tabler-calendar-plus' },
)
}
return items
})
// Mobile drawer items include profile + logout
// Mobile nav items
const mobileNavItems = computed(() => {
const items = [...menuItems.value]
items.push({ title: 'Mijn Profiel', to: '/profiel', icon: 'tabler-user' })
const items = [
{ title: 'Mijn evenementen', to: '/evenementen', icon: 'tabler-calendar-event' },
{ title: 'Mijn Profiel', to: '/profiel', icon: 'tabler-user' },
]
return items
})
@@ -89,46 +67,73 @@ async function logout() {
class="d-flex align-center py-0"
style="max-inline-size: 1440px;"
>
<!-- Left section: Logo + Event Switcher -->
<RouterLink
to="/dashboard"
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
<!-- 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>
</RouterLink>
<EventSwitcher class="min-w-0 flex-grow-1 flex-sm-grow-0" />
<!-- Center section: Desktop menu items -->
<div
v-if="menuItems.length > 0"
class="d-none d-md-flex align-center gap-1 ms-4"
>
<!-- Back link -->
<VBtn
v-for="item in menuItems"
:key="item.to"
:to="item.to"
variant="text"
color="default"
size="small"
exact
class="portal-nav-btn"
color="default"
class="text-medium-emphasis ms-2 d-none d-md-flex"
to="/evenementen"
>
<VIcon
start
:icon="item.icon"
size="18"
icon="tabler-arrow-left"
size="16"
/>
{{ item.title }}
Evenementen
</VBtn>
</div>
</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 />
@@ -226,31 +231,3 @@ async function logout() {
</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>