Import source: src/components/shell/header
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,215 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
CircleHelp,
|
||||||
|
Command,
|
||||||
|
LogOut,
|
||||||
|
Menu,
|
||||||
|
MonitorCog,
|
||||||
|
Moon,
|
||||||
|
Search,
|
||||||
|
Sun,
|
||||||
|
UserCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { panelService } from "@/api/services/panel-service";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Logo } from "@/components/common/logo";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useUiStore } from "@/stores/ui-store";
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["dashboard-summary"],
|
||||||
|
queryFn: () => panelService.dashboard.summary(),
|
||||||
|
});
|
||||||
|
const setCommandOpen = useUiStore((state) => state.setCommandOpen);
|
||||||
|
const setTheme = useUiStore((state) => state.setTheme);
|
||||||
|
const setDensity = useUiStore((state) => state.setDensity);
|
||||||
|
const setMobileSidebarOpen = useUiStore((state) => state.setMobileSidebarOpen);
|
||||||
|
const collapsed = useUiStore((state) => state.sidebarCollapsed);
|
||||||
|
const summary = data?.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={cn(
|
||||||
|
"glass-bar sticky top-2 z-20 mx-2 flex h-[var(--header-height)] items-center gap-2 rounded-xl border border-border/60 px-3 transition-[margin] sm:gap-3 lg:mr-3",
|
||||||
|
collapsed ? "lg:ml-[96px]" : "lg:ml-[calc(var(--sidebar-width)+24px)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="lg:hidden"
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setMobileSidebarOpen(true)}
|
||||||
|
aria-label="Open navigation"
|
||||||
|
>
|
||||||
|
<Menu className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Link href="/dashboard" className="lg:hidden">
|
||||||
|
<Logo compact />
|
||||||
|
</Link>
|
||||||
|
<div className="relative hidden min-w-44 flex-1 md:block">
|
||||||
|
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
className="pl-8"
|
||||||
|
placeholder="Search tools, domains, files, users"
|
||||||
|
onFocus={() => setCommandOpen(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="ml-auto md:ml-0"
|
||||||
|
onClick={() => setCommandOpen(true)}
|
||||||
|
>
|
||||||
|
<Command className="h-4 w-4" />
|
||||||
|
<kbd className="hidden border-border/70 sm:inline-flex">Ctrl K</kbd>
|
||||||
|
</Button>
|
||||||
|
<Select defaultValue={summary?.account.id ?? "acct-main"}>
|
||||||
|
<SelectTrigger className="hidden w-44 xl:flex">
|
||||||
|
<SelectValue placeholder="Account" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{summary?.accounts.map((account) => (
|
||||||
|
<SelectItem key={account.id} value={account.id}>
|
||||||
|
{account.username}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select defaultValue={summary?.server.id ?? "srv-dal-01"}>
|
||||||
|
<SelectTrigger className="hidden w-48 xl:flex">
|
||||||
|
<SelectValue placeholder="Server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{summary?.servers.map((server) => (
|
||||||
|
<SelectItem key={server.id} value={server.id}>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<HeaderMenu
|
||||||
|
icon={<Bell className="h-4 w-4" />}
|
||||||
|
label="Notifications"
|
||||||
|
items={[
|
||||||
|
"Email queue has deferred messages",
|
||||||
|
"Backup verified",
|
||||||
|
"Certificate renewal queued",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<HeaderMenu
|
||||||
|
icon={<CircleHelp className="h-4 w-4" />}
|
||||||
|
label="Help"
|
||||||
|
items={["Documentation", "Keyboard shortcuts", "Support tickets"]}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<Button size="icon" variant="ghost" aria-label="Theme selector">
|
||||||
|
<Sun className="h-4 w-4 dark:hidden" />
|
||||||
|
<Moon className="hidden h-4 w-4 dark:block" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
align="end"
|
||||||
|
className="glass-modal z-50 w-44 rounded-lg p-1 data-[state=open]:animate-scale-in"
|
||||||
|
>
|
||||||
|
{(["light", "dark", "system"] as const).map((theme) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={theme}
|
||||||
|
className="rounded-md px-2 py-1.5 text-sm capitalize outline-none transition-colors hover:bg-primary/10 hover:text-primary"
|
||||||
|
onSelect={() => setTheme(theme)}
|
||||||
|
>
|
||||||
|
{theme}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
<DropdownMenu.Separator className="my-1 h-px bg-border/70" />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-primary/10 hover:text-primary"
|
||||||
|
onSelect={() => setDensity("compact")}
|
||||||
|
>
|
||||||
|
Compact density
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-primary/10 hover:text-primary"
|
||||||
|
onSelect={() => setDensity("comfortable")}
|
||||||
|
>
|
||||||
|
Comfortable density
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<UserCircle className="h-4 w-4" />
|
||||||
|
<span className="hidden md:inline">{summary?.user.name ?? "Account"}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
align="end"
|
||||||
|
className="glass-modal z-50 w-56 rounded-lg p-1 data-[state=open]:animate-scale-in"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||||
|
{summary?.user.email}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item className="rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-primary/10 hover:text-primary">
|
||||||
|
<MonitorCog className="mr-2 inline h-4 w-4" />
|
||||||
|
Profile settings
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item className="rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-primary/10 hover:text-primary">
|
||||||
|
<LogOut className="mr-2 inline h-4 w-4" />
|
||||||
|
<Link href="/logout">Logout</Link>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderMenu({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
items: string[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<Button size="icon" variant="ghost" aria-label={label}>
|
||||||
|
{icon}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
align="end"
|
||||||
|
className="glass-modal z-50 w-72 rounded-lg p-1 data-[state=open]:animate-scale-in"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
{items.map((item) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={item}
|
||||||
|
className="rounded-md px-2 py-2 text-sm outline-none transition-colors hover:bg-primary/10 hover:text-primary"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user