Files
catalyst/ui/src/components/layout/SideBar.vue
2025-09-21 14:50:28 +02:00

219 lines
7.1 KiB
Vue

<script setup lang="ts">
import Icon from '@/components/Icon.vue'
import CatalystLogo from '@/components/common/CatalystLogo.vue'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail
} from '@/components/ui/sidebar'
import { ChevronsUpDown, LogOut, Settings, Tag, User, Users, Zap } from 'lucide-vue-next'
import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAPI } from '@/api.ts'
import type { Sidebar as SidebarModel } from '@/client/models'
import { useAuthStore } from '@/store/auth'
const authStore = useAuthStore()
const links = [
{
name: 'Reactions',
url: '/reactions',
icon: Zap,
permission: 'reaction:write'
},
{
name: 'Users',
url: '/users',
icon: User,
permission: 'group:write'
},
{
name: 'Groups',
url: '/groups',
icon: Users,
permission: 'group:write'
},
{
name: 'Types',
url: '/types',
icon: Tag,
permission: 'type:write'
},
{
name: 'Settings',
url: '/settings',
icon: Settings,
permission: 'settings:write'
}
]
const userLinks = computed(() => {
return links.filter((link) => authStore.hasPermission(link.permission))
})
const api = useAPI()
const router = useRouter()
const { data: sidebar } = useQuery({
queryKey: ['sidebar'],
queryFn: (): Promise<Array<SidebarModel>> => api.getSidebar()
})
const logout = () => {
authStore.setToken('')
router.push({ name: 'login' })
}
const initials = (user: { name?: string } | undefined) => {
if (!user || !user.name) return ''
const names = user.name.split(' ')
return names.length > 1 ? `${names[0][0]}${names[1][0]}` : names[0][0]
}
</script>
<template>
<SidebarProvider class="h-full w-full" style="--sidebar-width: 12rem">
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem class="flex items-center gap-2 p-2">
<CatalystLogo :size="5" />
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">Catalyst</span>
<span class="truncate text-xs">Incident Management</span>
</div>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Overview</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton :tooltip="'Dashboard'" as-child>
<RouterLink to="/dashboard">
<Icon name="LayoutDashboard" class="size-4" />
<span>Dashboard</span>
</RouterLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Tickets</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem v-for="(typ, index) of sidebar" :key="index">
<SidebarMenuButton :tooltip="typ.plural" as-child>
<RouterLink :to="`/tickets/${typ.id}`">
<Icon :name="typ.icon" class="size-4" />
<span>{{ typ.plural }}</span>
<span class="ml-auto">{{ typ.count }}</span>
</RouterLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
<div class="flex-1" />
<SidebarGroup v-if="userLinks.length > 0" class="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Administration</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem v-for="item in userLinks" :key="item.name">
<SidebarMenuButton as-child>
<RouterLink :to="item.url">
<component :is="item.icon" />
<span>{{ item.name }}</span>
</RouterLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage
:src="authStore.user?.avatar ? authStore.user.avatar : ''"
:alt="authStore.user?.name"
/>
<AvatarFallback class="rounded-lg"
>{{ initials(authStore.user) }}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ authStore.user?.name }}</span>
<span class="truncate text-xs">{{ authStore.user?.email }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side="bottom"
align="end"
:side-offset="4"
>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage
:src="authStore.user?.avatar ? authStore.user.avatar : ''"
:alt="authStore.user?.name"
/>
<AvatarFallback class="rounded-lg"
>{{ initials(authStore.user) }}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ authStore.user?.name }}</span>
<span class="truncate text-xs">{{ authStore.user?.email }}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem @click="logout">
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset class="flex h-full w-full">
<slot />
</SidebarInset>
</SidebarProvider>
</template>