Move user menu to sidebar, fixes #35

This commit is contained in:
2025-11-14 17:45:57 +01:00
parent 83644a2a3e
commit 71260199a1
13 changed files with 328 additions and 365 deletions
-21
View File
@@ -1,21 +0,0 @@
<script setup lang="ts">
import { SidebarInset } from '@/components/ui/sidebar';
import { computed } from 'vue';
interface Props {
variant?: 'header' | 'sidebar';
class?: string;
}
const props = defineProps<Props>();
const className = computed(() => props.class);
</script>
<template>
<SidebarInset v-if="props.variant === 'sidebar'" :class="className">
<slot />
</SidebarInset>
<main v-else class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl" :class="className">
<slot />
</main>
</template>
+25
View File
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { HTMLAttributes } from 'vue';
const props = defineProps<{
class?: HTMLAttributes;
}>();
</script>
<template>
<div :class="cn('grid grid-cols-[2fr_3fr_2fr] mb-12 gap-4', props.class)">
<div class="place-self-stretch flex row items-center justify-start">
<slot name="left" />
</div>
<div class="relative w-full items-center">
<slot name="middle" />
</div>
<div class="place-self-stretch flex row items-center justify-end">
<slot name="right" />
</div>
</div>
</template>
+8 -13
View File
@@ -5,11 +5,14 @@ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarTrigger }
import { dashboard, crm, offers, invoices, newInvoice, timesheets, customers, leads, achievements } from '@/routes';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { edit } from '@/routes/profile';
import { type NavItem, type NavGroup } from '@/types';
import { Link } from '@inertiajs/vue3';
import { Kanban, Euro, Contact, Trophy, Calculator, Settings, Target, BookUser, Timer, Headset, IdCard, Plus } from 'lucide-vue-next';
import { InertiaLinkProps, Link, usePage } from '@inertiajs/vue3';
import { Kanban, Euro, Trophy, Calculator, BookUser, Timer, Headset, Plus } from 'lucide-vue-next';
import AppLogo from './AppLogo.vue';
import { computed } from 'vue';
const page = usePage();
const auth = computed(() => page.props.auth);
const mainNavGroups: NavGroup[] = [
{
@@ -72,14 +75,6 @@ const mainNavGroups: NavGroup[] = [
}
];
const footerNavItems: NavItem[] = [
{
title: 'Einstellungen',
href: edit(),
icon: Settings,
color: 'text-gray-500',
}
];
</script>
<template>
@@ -107,12 +102,12 @@ const footerNavItems: NavItem[] = [
</TooltipProvider>
</SidebarHeader>
<SidebarContent>
<SidebarContent class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden">
<NavMain :groups="mainNavGroups" />
</SidebarContent>
<SidebarFooter>
<NavFooter :items="footerNavItems" />
<NavFooter :user="auth.user" />
</SidebarFooter>
</Sidebar>
<slot />
+23 -19
View File
@@ -1,32 +1,36 @@
<script setup lang="ts">
import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { toUrl } from '@/lib/utils';
import { type NavItem } from '@/types';
import { Link } from '@inertiajs/vue3';
import { User } from '@/types';
import { defineProps } from 'vue';
import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import UserMenuContent from '@/components/UserMenuContent.vue';
interface Props {
items: NavItem[];
const props = defineProps<{
user: User;
class?: string;
}
}>()
defineProps<Props>();
</script>
<template>
<SidebarGroup :class="`group-data-[collapsible=icon]:p-0 ${$props.class || ''}`">
<SidebarGroup class="p-0">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton
class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100"
as-child>
<Link :href="item.href">
<component :is="item.icon" :class="item.color" stroke-width="1.5" />
<span>{{ item.title }}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuItem>
<SidebarMenuButton
class="pl-0 rounded-[1rem_var(--radius-md)_var(--radius-md)_1rem] group-data-[state=collapsed]:rounded-full">
<UserInfo :user="props.user" />
</SidebarMenuButton>
</SidebarMenuItem>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<UserMenuContent />
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</template>
</template>
+12 -9
View File
@@ -3,6 +3,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useInitials } from '@/composables/useInitials';
import type { User } from '@/types';
import { computed } from 'vue';
import { Kbd, KbdGroup } from '@/components/ui/kbd'
interface Props {
user: User;
@@ -20,15 +21,17 @@ const showAvatar = computed(() => props.user.avatar && props.user.avatar !== '')
</script>
<template>
<Avatar class="h-10 w-10 overflow-hidden rounded-lg">
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
<AvatarFallback class="rounded-full text-black dark:text-white">
{{ getInitials(user.name) }}
</AvatarFallback>
</Avatar>
<div class="flex items-center gap-2">
<Avatar class="size-8 overflow-hidden rounded-lg">
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
<AvatarFallback class="rounded-full bg-primary text-black dark:text-white">
{{ getInitials(user.name) }}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
<span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
<span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
</div>
</div>
</template>
+19 -21
View File
@@ -1,45 +1,43 @@
<script setup lang="ts">
import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import { logout } from '@/routes';
import { edit } from '@/routes/profile';
import type { User } from '@/types';
import { Link, router } from '@inertiajs/vue3';
import { LogOut, Settings } from 'lucide-vue-next';
import { LogOut, Settings, User } from 'lucide-vue-next';
import axios from 'axios';
interface Props {
user: User;
}
import { Kbd, KbdGroup } from '@/components/ui/kbd';
const handleLogout = () => {
router.flushAll();
localStorage.removeItem('sanctum_token');
delete axios.defaults.headers.common['Authorization'];
};
defineProps<Props>();
</script>
<template>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserInfo :user="user" :show-email="true" />
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem :as-child="true">
<Link class="block w-full" :href="edit()" prefetch as="button">
<Settings class="mr-2 h-4 w-4" />
Settings
<DropdownMenuItem as-child>
<Link :href="edit()" prefetch class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Settings stroke-width="1.5" />
<span class="mr-4">Einstellungen</span>
</div>
<KbdGroup>
<Kbd class="visible-mac"></Kbd>
<Kbd class="visible-pc">Ctrl</Kbd>
<Kbd>,</Kbd>
</KbdGroup>
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem :as-child="true">
<DropdownMenuItem as-child>
<Link class="block w-full" :href="logout()" @click="handleLogout" as="button" data-test="logout-button">
<LogOut class="mr-2 h-4 w-4" />
<LogOut stroke-width="1.5" class="mr-2 h-4 w-4" />
Log out
</Link>
</DropdownMenuItem>
@@ -29,7 +29,7 @@ import { alertStore } from "@/stores/alertStore"
import { GrowingTextarea } from '../ui/growing-textarea'
import { toast } from "vue-sonner"
import { SendMailDialog } from "../ui/send-mail-dialog"
import { Kbd, KbdGroup } from "../ui/kbd"
import { Kbd, KbdGroup } from '@/components/ui/kbd';
import DialogClose from "../ui/dialog/DialogClose.vue"
import DialogCloseButton from "../DialogCloseButton/DialogCloseButton.vue"
@@ -385,7 +385,7 @@ const updateTotalAmount = () => {
<!-- Preview -->
<DropdownMenuItem v-if="invoice?.paymentStatus == 'draft'"
class="flex items-center justify-between" @click="preview">
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<Eye :strokeWidth="1.5" class="text-current" />
<span class="mr-4">Vorschau</span>
</div>
@@ -399,7 +399,7 @@ const updateTotalAmount = () => {
<!-- PDF -->
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'" class="flex justify-between"
@click="downloadPdf">
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<FileText stroke-width="1.5" class="text-muted-foreground" />
<div class="mr-4 flex flex-col">
<span>PDF exportieren</span>
@@ -416,7 +416,7 @@ const updateTotalAmount = () => {
<!-- XML -->
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'" class="flex justify-between"
@click="downloadXml">
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<CodeXml stroke-width="1.5" class="text-muted-foreground" />
<div class="mr-4 flex flex-col">
<span>XML exportieren</span>
@@ -431,7 +431,7 @@ const updateTotalAmount = () => {
<DropdownMenuItem
v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)"
class="flex justify-between" @click="deleteInvoice">
<div class="flex items-center gap-2">
<div class="flex items-center gap-3">
<!-- <FileX stroke-width="1.5" class="text-muted-foreground"/> -->
<Ban :strokeWidth="1.5" class="text-current" />
<span class="mr-2">Stornieren</span>
@@ -442,7 +442,7 @@ const updateTotalAmount = () => {
<DropdownMenuItem
class="flex justify-between text-destructive! hover:bg-red-100! dark:hover:bg-red-950!"
@click="deleteInvoice">
<div class="flex items-center gap-2">
<div class="flex items-center gap-3">
<Trash2 :strokeWidth="1.5" class="text-current" />
<span class="mr-2">Löschen</span>
</div>