mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-04-18 22:09:30 -04:00
241 lines
7.8 KiB
TypeScript
241 lines
7.8 KiB
TypeScript
import { Bell, CalendarClock, Database, HardDrive, Settings, ShieldCheck } from "lucide-react";
|
|
import { useState } from "react";
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarSeparator,
|
|
useSidebar,
|
|
} from "~/client/components/ui/sidebar";
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
|
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from "~/client/components/ui/hover-card";
|
|
import { cn } from "~/client/lib/utils";
|
|
import { APP_VERSION, RCLONE_VERSION, RESTIC_VERSION, SHOUTRRR_VERSION } from "~/client/lib/version";
|
|
import { useUpdates } from "~/client/hooks/use-updates";
|
|
import { ReleaseNotesDialog } from "./release-notes-dialog";
|
|
import { OrganizationSwitcher } from "./organization-switcher";
|
|
import { Link } from "@tanstack/react-router";
|
|
|
|
const items = [
|
|
{
|
|
title: "Volumes",
|
|
url: "/volumes",
|
|
icon: HardDrive,
|
|
},
|
|
{
|
|
title: "Repositories",
|
|
url: "/repositories",
|
|
icon: Database,
|
|
},
|
|
{
|
|
title: "Backups",
|
|
url: "/backups",
|
|
icon: CalendarClock,
|
|
},
|
|
{
|
|
title: "Notifications",
|
|
url: "/notifications",
|
|
icon: Bell,
|
|
},
|
|
{
|
|
title: "Settings",
|
|
url: "/settings",
|
|
icon: Settings,
|
|
},
|
|
];
|
|
|
|
type Props = {
|
|
isInstanceAdmin: boolean;
|
|
};
|
|
|
|
export function AppSidebar({ isInstanceAdmin }: Props) {
|
|
const { state, isMobile, setOpenMobile } = useSidebar();
|
|
const { updates, hasUpdate } = useUpdates();
|
|
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
|
|
|
|
const isCollapsed = state === "collapsed";
|
|
|
|
const displayVersion = APP_VERSION.startsWith("v") || APP_VERSION === "dev" ? APP_VERSION : `v${APP_VERSION}`;
|
|
const releaseUrl =
|
|
APP_VERSION === "dev"
|
|
? "https://github.com/nicotsx/zerobyte"
|
|
: `https://github.com/nicotsx/zerobyte/releases/tag/${displayVersion}`;
|
|
|
|
return (
|
|
<Sidebar variant="inset" collapsible="icon" className="p-0">
|
|
<SidebarHeader className="bg-card-header border-b border-border/80 dark:border-border/50 hidden md:flex h-16.25 flex-row items-center p-4">
|
|
<Link to="/volumes" className="flex items-center gap-3 font-semibold pl-2">
|
|
<img src="/images/zerobyte.png" alt="Zerobyte Logo" className={cn("h-8 w-8 shrink-0 object-contain -ml-2")} />
|
|
<span
|
|
className={cn("text-base transition-all duration-200 -ml-1", {
|
|
"opacity-0 w-0 overflow-hidden ": isCollapsed,
|
|
})}
|
|
>
|
|
Zerobyte
|
|
</span>
|
|
</Link>
|
|
</SidebarHeader>
|
|
<SidebarContent className="p-2 border-r">
|
|
<SidebarGroup>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{items.map((item) => (
|
|
<SidebarMenuItem key={item.title}>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<SidebarMenuButton asChild className="relative overflow-hidden">
|
|
<Link
|
|
to={item.url}
|
|
onClick={() => isMobile && setOpenMobile(false)}
|
|
activeProps={{ className: "bg-strong-accent/10" }}
|
|
className="w-full flex items-center gap-2"
|
|
>
|
|
{({ isActive }) => (
|
|
<>
|
|
{isActive && (
|
|
<div
|
|
className={cn("absolute left-0 top-0 h-full w-0.75 bg-strong-accent mr-2", {
|
|
hidden: isCollapsed,
|
|
})}
|
|
/>
|
|
)}
|
|
<item.icon
|
|
className={cn("transition-all duration-200", {
|
|
"text-strong-accent": isActive,
|
|
"ml-1": isActive && !isCollapsed,
|
|
"text-muted-foreground": !isActive,
|
|
})}
|
|
/>
|
|
<span
|
|
className={cn({
|
|
"text-foreground font-medium": isActive,
|
|
"text-muted-foreground": !isActive,
|
|
})}
|
|
>
|
|
{item.title}
|
|
</span>
|
|
</>
|
|
)}
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right" className={cn({ hidden: !isCollapsed })}>
|
|
<p>{item.title}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</SidebarMenuItem>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
{isInstanceAdmin && (
|
|
<>
|
|
<SidebarSeparator />
|
|
<SidebarGroup>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<SidebarMenuButton asChild className="relative overflow-hidden">
|
|
<Link
|
|
to="/admin"
|
|
onClick={() => isMobile && setOpenMobile(false)}
|
|
activeProps={{ className: "bg-strong-accent/10" }}
|
|
className="w-full flex items-center gap-2"
|
|
>
|
|
{({ isActive }) => (
|
|
<>
|
|
{isActive && (
|
|
<div
|
|
className={cn("absolute left-0 top-0 h-full w-0.75 bg-strong-accent mr-2", {
|
|
hidden: isCollapsed,
|
|
})}
|
|
/>
|
|
)}
|
|
<ShieldCheck
|
|
className={cn("transition-all duration-200", {
|
|
"text-strong-accent": isActive,
|
|
"ml-1": isActive && !isCollapsed,
|
|
"text-muted-foreground": !isActive,
|
|
})}
|
|
/>
|
|
<span
|
|
className={cn({
|
|
"text-foreground font-medium": isActive,
|
|
"text-muted-foreground": !isActive,
|
|
})}
|
|
>
|
|
Administration
|
|
</span>
|
|
</>
|
|
)}
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right" className={cn({ hidden: !isCollapsed })}>
|
|
<p>Administration</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</>
|
|
)}
|
|
</SidebarContent>
|
|
<SidebarFooter className="p-4 border-r border-border/80 dark:border-border/50">
|
|
<OrganizationSwitcher />
|
|
<div className="flex items-center justify-between gap-2">
|
|
<HoverCard openDelay={200}>
|
|
<HoverCardTrigger asChild>
|
|
<a
|
|
href={releaseUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={cn("text-xs text-muted-foreground hover:text-foreground", {
|
|
"opacity-0 w-0 overflow-hidden": state === "collapsed",
|
|
})}
|
|
>
|
|
{displayVersion}
|
|
</a>
|
|
</HoverCardTrigger>
|
|
<HoverCardContent side="top" align="start" className="w-fit p-3">
|
|
<div className="flex flex-col gap-2">
|
|
<div className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[11px]">
|
|
<span className="text-muted-foreground">Restic:</span>
|
|
<span className="font-mono">{RESTIC_VERSION}</span>
|
|
<span className="text-muted-foreground">Rclone:</span>
|
|
<span className="font-mono">{RCLONE_VERSION}</span>
|
|
<span className="text-muted-foreground">Shoutrrr:</span>
|
|
<span className="font-mono">{SHOUTRRR_VERSION}</span>
|
|
</div>
|
|
</div>
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
{hasUpdate && state !== "collapsed" && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowReleaseNotes(true)}
|
|
className="text-[10px] font-medium text-destructive hover:underline cursor-pointer"
|
|
>
|
|
Update available
|
|
</button>
|
|
)}
|
|
</div>
|
|
<ReleaseNotesDialog open={showReleaseNotes} onOpenChange={setShowReleaseNotes} updates={updates} />
|
|
</SidebarFooter>
|
|
</Sidebar>
|
|
);
|
|
}
|