Files
keystone/resources/js/components/AppHeader.vue
Harry Bayliss 5b977c1f41
Some checks failed
CI / Lint (push) Failing after 22s
CI / Tests (push) Failing after 33s
wowowowowo
2026-05-28 15:15:41 +01:00

323 lines
13 KiB
Vue

<script setup lang="ts">
import AppLogo from "@/components/AppLogo.vue";
import AppLogoIcon from "@/components/AppLogoIcon.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import UserMenuContent from "@/components/UserMenuContent.vue";
import { getInitials } from "@/composables/useInitials";
import type { BreadcrumbItem, NavItem } from "@/types";
import { Link, usePage } from "@inertiajs/vue3";
import {
AppWindowIcon,
BoltIcon,
BoxesIcon,
ClipboardListIcon,
Menu,
Search,
ServerIcon,
WorkflowIcon,
} from "lucide-vue-next";
import { computed } from "vue";
interface Props {
breadcrumbs?: BreadcrumbItem[];
}
const props = withDefaults(defineProps<Props>(), {
breadcrumbs: () => [],
});
const page = usePage();
const auth = computed(() => page.props.auth);
const isCurrentRoute = computed(() => (url: string) => page.url === url || page.url.startsWith(`${url}/`));
const activeItemStyles = computed(
() => (url: string) =>
isCurrentRoute.value(url)
? "text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100"
: "",
);
const mainNavItems: NavItem[] = [
// {
// title: 'Dashboard',
// href: new URL(route('dashboard')).pathname,
// icon: LayoutGrid,
// },
];
if (page.props.organisation) {
const organisationId = page.props?.organisation?.id;
mainNavItems.push({
title: page.props.organisation.name,
href: new URL(
route("organisations.show", {
organisation: organisationId,
}),
).pathname,
icon: BoltIcon,
});
mainNavItems.push({
title: "Environments",
href: new URL(
route("environments.index", {
organisation: organisationId,
}),
).pathname,
icon: BoxesIcon,
});
mainNavItems.push({
title: "Applications",
href: new URL(
route("applications.index", {
organisation: organisationId,
}),
).pathname,
icon: AppWindowIcon,
});
mainNavItems.push({
title: "Servers",
href: new URL(
route("servers.index", {
organisation: organisationId,
}),
).pathname,
icon: ServerIcon,
});
mainNavItems.push({
title: "Operations",
href: new URL(
route("operations.index", {
organisation: organisationId,
}),
).pathname,
icon: WorkflowIcon,
});
if (
page.props.organisation.providers_count === 0 ||
page.props.organisation.source_providers_count === 0 ||
page.props.organisation.registries_count === 0 ||
page.props.organisation.servers_count === 0 ||
page.props.organisation.applications_count === 0
) {
mainNavItems.push({
title: "Onboarding",
href: new URL(
route("onboarding.show", {
organisation: organisationId,
}),
).pathname,
icon: ClipboardListIcon,
});
}
}
const rightNavItems: NavItem[] = [
// {
// title: 'Repository',
// href: 'https://github.com/laravel/vue-starter-kit',
// icon: Folder,
// },
// {
// title: 'Documentation',
// href: 'https://laravel.com/docs/starter-kits',
// icon: BookOpen,
// },
];
</script>
<template>
<div>
<div class="border-b border-sidebar-border/80">
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
<!-- Mobile Menu -->
<div class="lg:hidden">
<Sheet>
<SheetTrigger :as-child="true">
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
<Menu class="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" class="w-[300px] p-6">
<SheetTitle class="sr-only">Navigation Menu</SheetTitle>
<SheetHeader class="flex justify-start text-left">
<AppLogoIcon
class="size-6 fill-current text-black dark:text-white"
/>
</SheetHeader>
<div class="flex h-full flex-1 flex-col justify-between space-y-4 py-6">
<nav class="-mx-3 space-y-1">
<Link
v-for="item in mainNavItems"
:key="item.title"
:href="item.href"
class="flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent"
:class="activeItemStyles(item.href)"
>
<component
v-if="item.icon"
:is="item.icon"
class="h-5 w-5"
/>
{{ item.title }}
</Link>
</nav>
<div class="flex flex-col space-y-4">
<a
v-for="item in rightNavItems"
:key="item.title"
:href="item.href"
target="_blank"
rel="noopener noreferrer"
class="flex items-center space-x-2 text-sm font-medium"
>
<component
v-if="item.icon"
:is="item.icon"
class="h-5 w-5"
/>
<span>{{ item.title }}</span>
</a>
</div>
</div>
</SheetContent>
</Sheet>
</div>
<Link :href="route('dashboard')" class="flex items-center gap-x-2">
<AppLogo />
</Link>
<!-- Desktop Menu -->
<div class="hidden h-full lg:flex lg:flex-1">
<NavigationMenu class="ml-10 flex h-full items-stretch">
<NavigationMenuList class="flex h-full items-stretch space-x-2">
<NavigationMenuItem
v-for="(item, index) in mainNavItems"
:key="index"
class="relative flex h-full items-center"
>
<Link :href="item.href">
<NavigationMenuLink
:class="[
navigationMenuTriggerStyle(),
activeItemStyles(item.href),
'h-9 cursor-pointer px-3',
]"
>
<component
v-if="item.icon"
:is="item.icon"
class="mr-2 h-4 w-4"
/>
{{ item.title }}
</NavigationMenuLink>
</Link>
<div
v-if="isCurrentRoute(item.href)"
class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"
></div>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
<div class="ml-auto flex items-center space-x-2">
<div class="relative flex items-center space-x-1">
<Button variant="ghost" size="icon" class="group h-9 w-9 cursor-pointer">
<Search class="size-5 opacity-80 group-hover:opacity-100" />
</Button>
<div class="hidden space-x-1 lg:flex">
<template v-for="item in rightNavItems" :key="item.title">
<TooltipProvider :delay-duration="0">
<Tooltip>
<TooltipTrigger>
<Button
variant="ghost"
size="icon"
as-child
class="group h-9 w-9 cursor-pointer"
>
<a
:href="item.href"
target="_blank"
rel="noopener noreferrer"
>
<span class="sr-only">{{ item.title }}</span>
<component
:is="item.icon"
class="size-5 opacity-80 group-hover:opacity-100"
/>
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ item.title }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger :as-child="true">
<Button
variant="ghost"
size="icon"
class="relative size-10 w-auto rounded-full p-1 focus-within:ring-2 focus-within:ring-primary"
>
<Avatar class="size-8 overflow-hidden rounded-full">
<AvatarImage
v-if="auth.user.avatar"
:src="auth.user.avatar"
:alt="auth.user.name"
/>
<AvatarFallback
class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white"
>
{{ getInitials(auth.user?.name) }}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-56">
<UserMenuContent :user="auth.user" />
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<div
v-if="props.breadcrumbs.length > 1"
class="flex w-full border-b border-sidebar-border/70"
>
<div
class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl"
>
<Breadcrumbs :breadcrumbs="breadcrumbs" />
</div>
</div>
</div>
</template>