feat: festival/event model frontend + topbar activeren

- Events lijst: card grid met festival/serie chips
- Festival detail: programmaonderdelen grid
- CreateSubEventDialog voor sub-events binnen festival
- EventTabsNav: breadcrumb terug naar festival
- Sessie A: festival-bewuste EventResource + children endpoint
- Topbar: zoekbalk, theme switcher, shortcuts, notificaties
- Schema v1.7 + BACKLOG.md toegevoegd
- 121 tests groen
This commit is contained in:
2026-04-08 10:06:47 +02:00
parent 6848bc2c49
commit c776331cf8
21 changed files with 1087 additions and 190 deletions

View File

@@ -7,7 +7,6 @@ import { themeConfig } from '@themeConfig'
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import NavBarI18n from '@core/components/I18n.vue'
import { HorizontalNavLayout } from '@layouts'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
</script>
@@ -28,11 +27,6 @@ import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
</RouterLink>
<VSpacer />
<NavBarI18n
v-if="themeConfig.app.i18n.enable && themeConfig.app.i18n.langConfig?.length"
:languages="themeConfig.app.i18n.langConfig"
/>
<NavbarThemeSwitcher class="me-2" />
<UserProfile />
</template>

View File

@@ -1,13 +1,14 @@
<script lang="ts" setup>
import navItems from '@/navigation/vertical'
import { themeConfig } from '@themeConfig'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavBarNotifications from '@/layouts/components/NavBarNotifications.vue'
import NavSearchBar from '@/layouts/components/NavSearchBar.vue'
import NavbarShortcuts from '@/layouts/components/NavbarShortcuts.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import OrganisationSwitcher from '@/components/layout/OrganisationSwitcher.vue'
import NavBarI18n from '@core/components/I18n.vue'
// @layouts plugin
import { VerticalNavLayout } from '@layouts'
@@ -21,12 +22,15 @@ import { VerticalNavLayout } from '@layouts'
<div class="vertical-nav-items-shadow" />
</template>
<!-- 👉 navbar -->
<!-- 👉 navbar (match Vuexy full-version: search + actions; search flex-grows) -->
<template #navbar="{ toggleVerticalOverlayNavActive }">
<div class="d-flex h-100 align-center">
<div
class="d-flex h-100 align-center w-100"
style="min-inline-size: 0;"
>
<IconBtn
id="vertical-nav-toggle-btn"
class="ms-n3 d-lg-none"
class="ms-n3 d-lg-none flex-shrink-0"
@click="toggleVerticalOverlayNavActive(true)"
>
<VIcon
@@ -35,15 +39,12 @@ import { VerticalNavLayout } from '@layouts'
/>
</IconBtn>
<NavbarThemeSwitcher />
<NavSearchBar class="flex-grow-1 ms-lg-n3 min-w-0" />
<VSpacer />
<NavBarI18n
v-if="themeConfig.app.i18n.enable && themeConfig.app.i18n.langConfig?.length"
:languages="themeConfig.app.i18n.langConfig"
/>
<UserProfile />
<NavbarThemeSwitcher class="flex-shrink-0 me-2" />
<NavbarShortcuts class="flex-shrink-0" />
<NavBarNotifications class="flex-shrink-0 me-1" />
<UserProfile class="flex-shrink-0" />
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import avatar1 from '@images/avatars/avatar-1.png'
import { computed } from 'vue'
import { useLogout } from '@/composables/api/useAuth'
import { useAuthStore } from '@/stores/useAuthStore'
@@ -7,6 +7,28 @@ const router = useRouter()
const authStore = useAuthStore()
const { mutate: logout, isPending: isLoggingOut } = useLogout()
const user = computed(() => authStore.user)
const initials = computed(() => {
const name = user.value?.name?.trim() ?? ''
if (!name)
return '?'
const parts = name.split(/\s+/).filter(Boolean)
if (parts.length >= 2)
return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase()
return name.slice(0, 2).toUpperCase()
})
const avatarSrc = computed((): string | null => {
const raw = user.value?.avatar
if (!raw)
return null
if (raw.startsWith('http://') || raw.startsWith('https://'))
return raw
const base = (import.meta.env.VITE_API_URL as string | undefined)?.replace(/\/api\/v1\/?$/, '') ?? ''
return raw.startsWith('/') ? `${base}${raw}` : `${base}/${raw}`
})
function handleLogout() {
logout(undefined, {
onSettled: () => {
@@ -17,105 +39,57 @@ function handleLogout() {
</script>
<template>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
bordered
color="success"
<VAvatar
class="cursor-pointer"
color="primary"
variant="tonal"
size="38"
>
<VAvatar
class="cursor-pointer"
color="primary"
variant="tonal"
<VImg
v-if="avatarSrc"
:src="avatarSrc"
:alt="user?.name ?? ''"
cover
/>
<span
v-else
class="text-caption font-weight-bold"
>
<VImg :src="avatar1" />
{{ initials }}
</span>
<!-- SECTION Menu -->
<VMenu
activator="parent"
width="230"
location="bottom end"
offset="14px"
>
<VList>
<!-- 👉 User Avatar & Name -->
<VListItem>
<template #prepend>
<VListItemAction start>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
color="success"
>
<VAvatar
color="primary"
variant="tonal"
>
<VImg :src="avatar1" />
</VAvatar>
</VBadge>
</VListItemAction>
</template>
<VMenu
activator="parent"
width="260"
location="bottom end"
offset="14px"
>
<VList>
<VListItem class="text-high-emphasis">
<VListItemTitle class="font-weight-semibold">
{{ user?.name ?? '—' }}
</VListItemTitle>
<VListItemSubtitle class="text-wrap">
{{ user?.email ?? '' }}
</VListItemSubtitle>
</VListItem>
<VListItemTitle class="font-weight-semibold">
{{ authStore.user?.name ?? 'User' }}
</VListItemTitle>
<VListItemSubtitle>{{ authStore.currentOrganisation?.role ?? '' }}</VListItemSubtitle>
</VListItem>
<VDivider class="my-2" />
<VDivider class="my-2" />
<!-- 👉 Profile -->
<VListItem link>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-user"
size="22"
/>
</template>
<VListItemTitle>Profile</VListItemTitle>
</VListItem>
<!-- 👉 Settings -->
<VListItem link>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-settings"
size="22"
/>
</template>
<VListItemTitle>Settings</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 Logout -->
<VListItem
:disabled="isLoggingOut"
@click="handleLogout"
>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-logout"
size="22"
/>
</template>
<VListItemTitle>Logout</VListItemTitle>
</VListItem>
</VList>
</VMenu>
<!-- !SECTION -->
</VAvatar>
</VBadge>
<VListItem
:disabled="isLoggingOut"
@click="handleLogout"
>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-logout"
size="22"
/>
</template>
<VListItemTitle>Uitloggen</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VAvatar>
</template>