feat(layouts): rewrite layout shells with PrimeVue Drawer + Menubar + Avatar
Layout-shell rewrite per RFC AD-3, B7-option-B. R-10 isolation invariant
honored — this single commit is revertible to roll back the layout
change without losing B1–B6 progress.
New component (PrimeVue-only, no Vuetify imports per F3 hard constraint):
- apps/app/src/layouts/components/AppShell.vue (~210 lines)
- Desktop sidebar (Tailwind grid, lg+ breakpoint) renders nav items
as PrimeVue Buttons + Icons. Mobile (<lg) hides sidebar; PrimeVue
Drawer slides in on hamburger toggle.
- Top bar (Tailwind) has hamburger + title (mobile) and an Avatar +
Menu (PrimeVue) for the user dropdown with "Mijn Profiel" and
"Uitloggen" actions.
- Nav items accept the existing { title, to: { name }, icon: { icon } }
shape from src/navigation/vertical so call-sites stay terse.
Five top-level layouts delegate to AppShell (filename preserved per
AD-3 so vite-plugin-vue-meta-layouts continues to resolve routes
unchanged):
- default.vue — org + (super-admin) platform nav
- OrganizerLayout — same nav as default; matches authenticated org UX
- PortalLayout — portal-specific 2-item nav ("Mijn evenementen",
"Mijn Profiel")
- blank.vue — minimal chrome-less wrapper for login etc.
- PublicLayout — minimal wrapper for public form-fill routes;
uses <main> for semantic structure
F3 functional regressions (intentional — F4 sub-packages reintroduce
each item through PrimeVue):
- NavSearchBar (Vuetify-heavy combobox/overlay) — absent from top bar
- ContextSwitcher (Vuetify VBtn + VMenu) — absent
- NavbarThemeSwitcher (Vuetify IconBtn) — absent; dark mode driven by
PrimeVue's darkModeSelector: '.dark' continues to work via the
existing @core skin classes until F6 cleanup
- NavbarShortcuts (Vuetify-heavy) — absent
- NavBarNotifications (Vuetify-heavy) — absent
- UserProfile from @/layouts/components/ (Vuetify-heavy menu) — replaced
with the minimal Avatar + Menu dropdown described above; rich profile
panel returns in F4
- ImpersonationBanner — absent; super-admin impersonation UX is F4 work
- PortalLayout event-mode vs platform-mode topbar (route.meta.navMode
driven) — absent; F4 reintroduces via AppShell prop or slot
- Suspense + AppLoadingIndicator wrapping pages — dropped; pages handle
their own loading via PrimeVue ProgressSpinner
VApp at App.vue level still wraps everything, so Vuetify components
inside still-Vuetify pages continue to render correctly during the
parallel-mode window.
Test updates (no Vuetify in layout structure to assert against anymore):
- OrganizerLayout.spec.ts — mocks AppShell instead of the deleted
DefaultLayoutWithVerticalNav reference; provides Pinia.
- PortalLayout.spec.ts — same mock pattern; new structural assertions
go through AppShell stub; the new third test verifies
PortalLayout forwards portal nav items + title to AppShell.
- PublicLayout.vue — uses <main> for semantics; PublicLayout.spec.ts
still passes unchanged.
Auto-generated component/auto-import dts files refreshed for the new
AppShell component (committed for stable dev workflow).
Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass (test count unchanged after spec rewrites).
- pnpm build — succeeds in 14.05s; AppShell chunk is ~57 KB raw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,234 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
// Portal layout — content migrated from apps/portal/src/layouts/portal.vue
|
||||
// during WS-3 PR-B1 (single-SPA consolidation). Used by every page
|
||||
// under /portal/** plus selected non-auth portal entry points
|
||||
// (registreren, wachtwoord-instellen). Authenticated portal users see
|
||||
// the navbar + mobile drawer; unauthenticated visits render only the
|
||||
// main content + footer.
|
||||
// PortalLayout — F3 rewrite. Delegates to AppShell with portal nav
|
||||
// items, replacing the previous Vuexy-based 234-line layout.
|
||||
//
|
||||
// Per RFC AD-3 + R-10, filename preserved, contents rewritten. The
|
||||
// previous PortalLayout's event-mode/platform-mode topbar variant
|
||||
// (driven by route.meta.navMode), the embedded UserAvatarMenu, and
|
||||
// the ContextSwitcher widget are absent during F3 — they re-enter in
|
||||
// F4 when each is rewritten to PrimeVue. See the B7 commit body for
|
||||
// the regression list.
|
||||
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type AppLoadingIndicator from '@/components/AppLoadingIndicator.vue'
|
||||
import ContextSwitcher from '@/components/shared/ContextSwitcher.vue'
|
||||
import UserAvatarMenu from '@/components/portal/UserAvatarMenu.vue'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { usePortalStore } from '@/stores/portal/usePortalStore'
|
||||
import AppShell from '@/layouts/components/AppShell.vue'
|
||||
|
||||
const { injectSkinClasses } = useSkins()
|
||||
|
||||
injectSkinClasses()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const portal = usePortalStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
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 ?? '')
|
||||
|
||||
const mobileNavItems = computed(() => [
|
||||
{ title: 'Mijn evenementen', to: '/portal/evenementen', icon: 'tabler-calendar-event' },
|
||||
{ title: 'Mijn Profiel', to: '/portal/profiel', icon: 'tabler-user' },
|
||||
])
|
||||
|
||||
const isFallbackStateActive = ref(false)
|
||||
const refLoadingIndicator = ref<InstanceType<typeof AppLoadingIndicator> | null>(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()
|
||||
await router.push('/login')
|
||||
}
|
||||
const portalNavItems = [
|
||||
{
|
||||
title: 'Mijn evenementen',
|
||||
to: { name: 'portal-evenementen' },
|
||||
icon: { icon: 'tabler-calendar-event' },
|
||||
},
|
||||
{
|
||||
title: 'Mijn Profiel',
|
||||
to: { name: 'portal-profiel' },
|
||||
icon: { icon: 'tabler-user' },
|
||||
},
|
||||
]
|
||||
</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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="default"
|
||||
class="text-medium-emphasis ms-2 d-none d-md-flex"
|
||||
to="/portal/evenementen"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
icon="tabler-arrow-left"
|
||||
size="16"
|
||||
/>
|
||||
Evenementen
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<!-- Platform mode: Crewli logo + optional page title -->
|
||||
<template v-else>
|
||||
<RouterLink
|
||||
to="/portal/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: context switcher + avatar menu (desktop) -->
|
||||
<div class="d-none d-md-flex align-center">
|
||||
<ContextSwitcher class="me-2" />
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<VFooter
|
||||
app
|
||||
color="transparent"
|
||||
class="justify-center text-caption text-medium-emphasis py-3"
|
||||
>
|
||||
Powered by Crewli
|
||||
</VFooter>
|
||||
</VApp>
|
||||
<AppShell
|
||||
:nav-items="portalNavItems"
|
||||
title="Crewli Portal"
|
||||
>
|
||||
<RouterView />
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user