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