From f9faeb7ea05663e1921deb5d0b9c64e5c03cf5bd Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Mon, 13 Apr 2026 13:30:20 +0200 Subject: [PATCH] feat(portal): restructure into three-screen architecture with event tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/portal/components.d.ts | 6 +- apps/portal/env.d.ts | 2 + .../src/components/event/ClaimenTab.vue | 349 ++++++++++++++ .../src/components/event/InformatieTab.vue | 93 ++++ .../event/OverzichtTab.vue} | 96 ++-- .../src/components/event/RoosterTab.vue | 436 +++++++++++++++++ .../src/components/portal/EventCard.vue | 136 ++++++ .../src/components/portal/EventSwitcher.vue | 123 ----- .../src/components/portal/StatusCard.vue | 32 +- .../src/components/portal/UserAvatarMenu.vue | 7 + apps/portal/src/layouts/portal.vue | 159 +++---- .../src/pages/dashboard/claim-shifts.vue | 358 -------------- apps/portal/src/pages/dashboard/my-shifts.vue | 444 ------------------ .../src/pages/evenementen/[eventId].vue | 182 +++++++ apps/portal/src/pages/evenementen/index.vue | 105 +++++ apps/portal/src/pages/index.vue | 2 +- apps/portal/src/pages/login.vue | 18 +- apps/portal/src/pages/profiel.vue | 6 +- .../portal/src/pages/register/[eventSlug].vue | 2 +- apps/portal/src/pages/register/success.vue | 8 +- apps/portal/src/plugins/1.router/guards.ts | 15 +- apps/portal/typed-router.d.ts | 5 +- 22 files changed, 1482 insertions(+), 1102 deletions(-) create mode 100644 apps/portal/src/components/event/ClaimenTab.vue create mode 100644 apps/portal/src/components/event/InformatieTab.vue rename apps/portal/src/{pages/dashboard/index.vue => components/event/OverzichtTab.vue} (58%) create mode 100644 apps/portal/src/components/event/RoosterTab.vue create mode 100644 apps/portal/src/components/portal/EventCard.vue delete mode 100644 apps/portal/src/components/portal/EventSwitcher.vue delete mode 100644 apps/portal/src/pages/dashboard/claim-shifts.vue delete mode 100644 apps/portal/src/pages/dashboard/my-shifts.vue create mode 100644 apps/portal/src/pages/evenementen/[eventId].vue create mode 100644 apps/portal/src/pages/evenementen/index.vue diff --git a/apps/portal/components.d.ts b/apps/portal/components.d.ts index 299a945a..8f719db0 100644 --- a/apps/portal/components.d.ts +++ b/apps/portal/components.d.ts @@ -23,6 +23,7 @@ declare module 'vue' { CardStatisticsHorizontal: typeof import('./src/@core/components/cards/CardStatisticsHorizontal.vue')['default'] CardStatisticsVertical: typeof import('./src/@core/components/cards/CardStatisticsVertical.vue')['default'] CardStatisticsVerticalSimple: typeof import('./src/@core/components/CardStatisticsVerticalSimple.vue')['default'] + ClaimenTab: typeof import('./src/components/event/ClaimenTab.vue')['default'] CustomCheckboxes: typeof import('./src/@core/components/app-form-elements/CustomCheckboxes.vue')['default'] CustomCheckboxesWithIcon: typeof import('./src/@core/components/app-form-elements/CustomCheckboxesWithIcon.vue')['default'] CustomCheckboxesWithImage: typeof import('./src/@core/components/app-form-elements/CustomCheckboxesWithImage.vue')['default'] @@ -32,11 +33,14 @@ declare module 'vue' { CustomRadiosWithImage: typeof import('./src/@core/components/app-form-elements/CustomRadiosWithImage.vue')['default'] DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default'] DropZone: typeof import('./src/@core/components/DropZone.vue')['default'] - EventSwitcher: typeof import('./src/components/portal/EventSwitcher.vue')['default'] + EventCard: typeof import('./src/components/portal/EventCard.vue')['default'] I18n: typeof import('./src/@core/components/I18n.vue')['default'] + InformatieTab: typeof import('./src/components/event/InformatieTab.vue')['default'] MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default'] Notifications: typeof import('./src/@core/components/Notifications.vue')['default'] + OverzichtTab: typeof import('./src/components/event/OverzichtTab.vue')['default'] ProductDescriptionEditor: typeof import('./src/@core/components/ProductDescriptionEditor.vue')['default'] + RoosterTab: typeof import('./src/components/event/RoosterTab.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] ScrollToTop: typeof import('./src/@core/components/ScrollToTop.vue')['default'] diff --git a/apps/portal/env.d.ts b/apps/portal/env.d.ts index 69ad44da..4e85470c 100644 --- a/apps/portal/env.d.ts +++ b/apps/portal/env.d.ts @@ -8,5 +8,7 @@ declare module 'vue-router' { requiresToken?: boolean public?: boolean hideEventMenu?: boolean + navMode?: 'platform' | 'event' + navTitle?: string } } diff --git a/apps/portal/src/components/event/ClaimenTab.vue b/apps/portal/src/components/event/ClaimenTab.vue new file mode 100644 index 00000000..6f0cfe2a --- /dev/null +++ b/apps/portal/src/components/event/ClaimenTab.vue @@ -0,0 +1,349 @@ + + + + + diff --git a/apps/portal/src/components/event/InformatieTab.vue b/apps/portal/src/components/event/InformatieTab.vue new file mode 100644 index 00000000..6f71fb28 --- /dev/null +++ b/apps/portal/src/components/event/InformatieTab.vue @@ -0,0 +1,93 @@ + + + diff --git a/apps/portal/src/pages/dashboard/index.vue b/apps/portal/src/components/event/OverzichtTab.vue similarity index 58% rename from apps/portal/src/pages/dashboard/index.vue rename to apps/portal/src/components/event/OverzichtTab.vue index fcd919f8..89f68a1a 100644 --- a/apps/portal/src/pages/dashboard/index.vue +++ b/apps/portal/src/components/event/OverzichtTab.vue @@ -4,19 +4,18 @@ import { usePortalStore } from '@/stores/usePortalStore' import { useMyShifts } from '@/composables/api/usePortalShifts' import type { PortalPersonPayload } from '@/types/portal' -definePage({ - name: 'portal-dashboard', - meta: { - layout: 'portal', - requiresAuth: true, - }, -}) +const props = defineProps<{ + eventId: string +}>() + +const emit = defineEmits<{ + switchTab: [tab: string] +}>() const portal = usePortalStore() -const eventId = computed(() => portal.activeEventId) +const eventIdRef = computed(() => props.eventId as string | null) -// Fetch my shifts to show upcoming count -const { data: shifts } = useMyShifts(eventId) +const { data: shifts } = useMyShifts(eventIdRef) const effectiveStatus = computed(() => { const fromPerson = portal.currentPerson?.status @@ -75,65 +74,50 @@ function formatNextShift(person: PortalPersonPayload | null): string | null { } const nextShiftSummary = computed(() => formatNextShift(portal.currentPerson)) - -// Portal hydration now happens automatically in the router guard diff --git a/apps/portal/src/components/event/RoosterTab.vue b/apps/portal/src/components/event/RoosterTab.vue new file mode 100644 index 00000000..95dcc982 --- /dev/null +++ b/apps/portal/src/components/event/RoosterTab.vue @@ -0,0 +1,436 @@ + + + + + diff --git a/apps/portal/src/components/portal/EventCard.vue b/apps/portal/src/components/portal/EventCard.vue new file mode 100644 index 00000000..275163fc --- /dev/null +++ b/apps/portal/src/components/portal/EventCard.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/apps/portal/src/components/portal/EventSwitcher.vue b/apps/portal/src/components/portal/EventSwitcher.vue deleted file mode 100644 index f5f9114c..00000000 --- a/apps/portal/src/components/portal/EventSwitcher.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - diff --git a/apps/portal/src/components/portal/StatusCard.vue b/apps/portal/src/components/portal/StatusCard.vue index 352156d8..5d9ddd64 100644 --- a/apps/portal/src/components/portal/StatusCard.vue +++ b/apps/portal/src/components/portal/StatusCard.vue @@ -8,6 +8,10 @@ const props = defineProps<{ availableCount?: number | null }>() +const emit = defineEmits<{ + switchTab: [tab: string] +}>() + const registeredLabel = computed(() => { if (!props.registeredAt) return null try { @@ -97,9 +101,9 @@ const registeredLabel = computed(() => { sm="4" > { class="mb-2" />
- Mijn Diensten + Mijn Rooster
Rooster bekijken @@ -122,9 +126,9 @@ const registeredLabel = computed(() => { sm="4" > { sm="4" >
- Mijn Profiel + Informatie
- Gegevens bekijken + Evenement details
@@ -184,12 +188,13 @@ const registeredLabel = computed(() => { class="text-body-2 text-medium-emphasis mb-0" > Nog geen diensten ingepland. - Diensten claimen - +

@@ -209,10 +214,11 @@ const registeredLabel = computed(() => { /> Diensten ingepland: {{ upcomingCount }}
- { class="me-1" /> Beschikbare diensten bekijken - +
diff --git a/apps/portal/src/components/portal/UserAvatarMenu.vue b/apps/portal/src/components/portal/UserAvatarMenu.vue index ae740203..65d58968 100644 --- a/apps/portal/src/components/portal/UserAvatarMenu.vue +++ b/apps/portal/src/components/portal/UserAvatarMenu.vue @@ -61,6 +61,13 @@ async function logout() { title="Mijn Profiel" /> + + + diff --git a/apps/portal/src/layouts/portal.vue b/apps/portal/src/layouts/portal.vue index 4c744198..9d071133 100644 --- a/apps/portal/src/layouts/portal.vue +++ b/apps/portal/src/layouts/portal.vue @@ -1,5 +1,4 @@ - - - - diff --git a/apps/portal/src/pages/dashboard/my-shifts.vue b/apps/portal/src/pages/dashboard/my-shifts.vue deleted file mode 100644 index ee29eca9..00000000 --- a/apps/portal/src/pages/dashboard/my-shifts.vue +++ /dev/null @@ -1,444 +0,0 @@ - - - - - diff --git a/apps/portal/src/pages/evenementen/[eventId].vue b/apps/portal/src/pages/evenementen/[eventId].vue new file mode 100644 index 00000000..1ccb6857 --- /dev/null +++ b/apps/portal/src/pages/evenementen/[eventId].vue @@ -0,0 +1,182 @@ + + + + + diff --git a/apps/portal/src/pages/evenementen/index.vue b/apps/portal/src/pages/evenementen/index.vue new file mode 100644 index 00000000..05836b31 --- /dev/null +++ b/apps/portal/src/pages/evenementen/index.vue @@ -0,0 +1,105 @@ + + + diff --git a/apps/portal/src/pages/index.vue b/apps/portal/src/pages/index.vue index 891817a5..314bdbcf 100644 --- a/apps/portal/src/pages/index.vue +++ b/apps/portal/src/pages/index.vue @@ -18,7 +18,7 @@ onMounted(async () => { } if (authStore.isAuthenticated) { - router.replace('/dashboard') + router.replace('/evenementen') } }) diff --git a/apps/portal/src/pages/login.vue b/apps/portal/src/pages/login.vue index 75db282a..7557b878 100644 --- a/apps/portal/src/pages/login.vue +++ b/apps/portal/src/pages/login.vue @@ -58,10 +58,22 @@ async function onSubmit(): Promise { // Navigate after login — outside try/catch so navigation errors // (e.g. stale dynamic imports) don't mask a successful login. - const redirect = typeof route.query.to === 'string' ? route.query.to : '/dashboard' - router.replace(redirect || '/dashboard').catch(() => { + let redirect = typeof route.query.to === 'string' ? route.query.to : '' + + // Smart redirect based on number of events + if (!redirect) { + const events = portalStore.userEvents + if (events.length === 1) { + redirect = `/evenementen/${events[0]!.event_id}` + } + else { + redirect = '/evenementen' + } + } + + router.replace(redirect).catch(() => { // Dynamic import can fail after Vite HMR; a full reload recovers. - window.location.href = redirect || '/dashboard' + window.location.href = redirect }) } diff --git a/apps/portal/src/pages/profiel.vue b/apps/portal/src/pages/profiel.vue index 46acfd37..77a4d075 100644 --- a/apps/portal/src/pages/profiel.vue +++ b/apps/portal/src/pages/profiel.vue @@ -8,7 +8,8 @@ definePage({ meta: { layout: 'portal', requiresAuth: true, - hideEventMenu: true, + navMode: 'platform', + navTitle: 'Mijn profiel', }, }) @@ -94,8 +95,7 @@ function formatEventDates(startDate: string, endDate: string): string { } function viewEvent(eventId: string) { - portal.setActiveEvent(eventId) - router.push('/dashboard') + router.push(`/evenementen/${eventId}`) } async function saveProfile() { diff --git a/apps/portal/src/pages/register/[eventSlug].vue b/apps/portal/src/pages/register/[eventSlug].vue index 939331a0..59c9a7da 100644 --- a/apps/portal/src/pages/register/[eventSlug].vue +++ b/apps/portal/src/pages/register/[eventSlug].vue @@ -23,7 +23,7 @@ definePage({ meta: { layout: 'portal', requiresAuth: false, - hideEventMenu: true, + navMode: 'platform', }, }) diff --git a/apps/portal/src/pages/register/success.vue b/apps/portal/src/pages/register/success.vue index 5f62db7e..7d62beab 100644 --- a/apps/portal/src/pages/register/success.vue +++ b/apps/portal/src/pages/register/success.vue @@ -6,7 +6,7 @@ definePage({ meta: { layout: 'portal', requiresAuth: false, - hideEventMenu: true, + navMode: 'platform', }, }) @@ -83,11 +83,11 @@ const isAuthenticated = computed(() => route.query.authenticated === '1' || auth
- Ga naar je dashboard + Ga naar je evenementen = { + '/dashboard': '/evenementen', + '/dashboard/my-shifts': '/evenementen', + '/dashboard/claim-shifts': '/evenementen', +} + export function setupGuards(router: Router) { router.beforeEach(async (to) => { const authStore = useAuthStore() @@ -18,12 +25,18 @@ export function setupGuards(router: Router) { await portalStore.hydrateIfNeeded() } + // Backward-compat redirects for old dashboard routes + const redirect = dashboardRedirects[to.path] + if (redirect && authStore.isAuthenticated) { + return { path: redirect } + } + const requiresAuth = to.meta.requiresAuth === true // Public routes — no auth check needed if (!requiresAuth) { if (authStore.isAuthenticated && guestOnlyPaths.some(p => to.path === p || to.path.startsWith(`${p}/`))) { - return { path: '/dashboard' } + return { path: '/evenementen' } } return diff --git a/apps/portal/typed-router.d.ts b/apps/portal/typed-router.d.ts index 97ba4cae..1c9f7328 100644 --- a/apps/portal/typed-router.d.ts +++ b/apps/portal/typed-router.d.ts @@ -21,9 +21,8 @@ declare module 'vue-router/auto-routes' { 'root': RouteRecordInfo<'root', '/', Record, Record>, 'not-found': RouteRecordInfo<'not-found', '/:path(.*)', { path: ParamValue }, { path: ParamValue }>, 'artist-advance': RouteRecordInfo<'artist-advance', '/advance/:token', { token: ParamValue }, { token: ParamValue }>, - 'portal-dashboard': RouteRecordInfo<'portal-dashboard', '/dashboard', Record, Record>, - 'portal-claim-shifts': RouteRecordInfo<'portal-claim-shifts', '/dashboard/claim-shifts', Record, Record>, - 'portal-my-shifts': RouteRecordInfo<'portal-my-shifts', '/dashboard/my-shifts', Record, Record>, + 'portal-evenementen': RouteRecordInfo<'portal-evenementen', '/evenementen', Record, Record>, + 'portal-event-detail': RouteRecordInfo<'portal-event-detail', '/evenementen/:eventId', { eventId: ParamValue }, { eventId: ParamValue }>, 'login': RouteRecordInfo<'login', '/login', Record, Record>, 'portal-profiel': RouteRecordInfo<'portal-profiel', '/profiel', Record, Record>, 'volunteer-register': RouteRecordInfo<'volunteer-register', '/register/:eventSlug', { eventSlug: ParamValue }, { eventSlug: ParamValue }>,