Move user menu to sidebar, fixes #35
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user