zerobyte/app/client/components/app-sidebar.tsx
Nico d7bcf4c9e2
feat: display updates and release notes (#290)
* feat: display notification when new release is available

* refactor: standalone re-usable cache util

* refactor: clear cache on server startup

* refactor: add timeout to gh release fetch

* fix: run with app version env variable
2026-01-04 10:19:23 +01:00

132 lines
3.9 KiB
TypeScript

import { Bell, CalendarClock, Database, HardDrive, Settings } from "lucide-react";
import { Link, NavLink } from "react-router";
import { useState } from "react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "~/client/components/ui/sidebar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
import { cn } from "~/client/lib/utils";
import { APP_VERSION } from "~/client/lib/version";
import { useUpdates } from "~/client/hooks/use-updates";
import { ReleaseNotesDialog } from "./release-notes-dialog";
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,
},
];
export function AppSidebar() {
const { state } = useSidebar();
const { updates, hasUpdate } = useUpdates();
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
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/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 ": state === "collapsed",
})}
>
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>
<NavLink to={item.url}>
{({ isActive }) => (
<>
<item.icon className={cn({ "text-strong-accent": isActive })} />
<span className={cn({ "text-strong-accent": isActive })}>{item.title}</span>
</>
)}
</NavLink>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right" className={cn({ hidden: state !== "collapsed" })}>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="p-4 border-r border-t border-border/50">
<div className="flex items-center justify-between gap-2">
<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>
{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>
);
}