echos-ocr/src/components/layout/sidebar.tsx

191 lines
4.9 KiB
TypeScript
Raw Normal View History

"use client";
import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
CreditCard,
CalendarDays,
Users,
BarChart3,
Settings,
ChevronsLeft,
ChevronsRight,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { ScrollArea } from "@/components/ui/scroll-area";
const STORAGE_KEY = "echo-sidebar-collapsed";
const navItems = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
{ href: "/cards", label: "Response Cards", icon: CreditCard },
{ href: "/events", label: "Collection Days", icon: CalendarDays },
{ href: "/people", label: "People", icon: Users },
{ href: "/reports", label: "Reports", icon: BarChart3 },
];
const bottomItems = [
{ href: "/settings", label: "Settings", icon: Settings },
];
function isActive(pathname: string, href: string) {
if (href === "/") return pathname === "/";
return pathname === href || pathname.startsWith(href + "/");
}
type SidebarContextValue = {
collapsed: boolean;
setCollapsed: (v: boolean) => void;
toggle: () => void;
};
const SidebarContext = React.createContext<SidebarContextValue>({
collapsed: false,
setCollapsed: () => {},
toggle: () => {},
});
export function useSidebar() {
return React.useContext(SidebarContext);
}
export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [collapsed, setCollapsedState] = React.useState(false);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "true") setCollapsedState(true);
} catch {}
}, []);
const setCollapsed = React.useCallback((v: boolean) => {
setCollapsedState(v);
try {
localStorage.setItem(STORAGE_KEY, String(v));
} catch {}
}, []);
const toggle = React.useCallback(() => {
setCollapsed(!collapsed);
}, [collapsed, setCollapsed]);
const value = React.useMemo(
() => ({ collapsed: mounted ? collapsed : false, setCollapsed, toggle }),
[mounted, collapsed, setCollapsed, toggle]
);
return (
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
);
}
function NavItem({
href,
label,
icon: Icon,
active,
collapsed,
}: {
href: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
active: boolean;
collapsed: boolean;
}) {
const content = (
<Link
href={href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
active
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
collapsed && "justify-center px-0"
)}
>
<Icon className="size-4 shrink-0" />
{!collapsed && <span className="truncate">{label}</span>}
</Link>
);
if (collapsed) {
return (
<Tooltip>
<TooltipTrigger render={<div />}>{content}</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{label}
</TooltipContent>
</Tooltip>
);
}
return content;
}
export function Sidebar() {
const pathname = usePathname();
const { collapsed, toggle } = useSidebar();
return (
<aside
className={cn(
"fixed left-0 top-16 z-30 flex h-[calc(100vh-4rem)] flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-200 ease-in-out",
collapsed ? "w-16" : "w-60"
)}
>
<ScrollArea className="flex-1">
<nav className={cn("flex flex-col gap-1 p-3", collapsed && "px-2")}>
{navItems.map((item) => (
<NavItem
key={item.href}
{...item}
active={isActive(pathname, item.href)}
collapsed={collapsed}
/>
))}
</nav>
</ScrollArea>
<div
className={cn(
"flex flex-col gap-1 border-t border-sidebar-border p-3",
collapsed && "px-2"
)}
>
{bottomItems.map((item) => (
<NavItem
key={item.href}
{...item}
active={isActive(pathname, item.href)}
collapsed={collapsed}
/>
))}
<button
onClick={toggle}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-sidebar-foreground/50 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
collapsed && "justify-center px-0"
)}
>
{collapsed ? (
<ChevronsRight className="size-4 shrink-0" />
) : (
<ChevronsLeft className="size-4 shrink-0" />
)}
{!collapsed && <span>Collapse</span>}
</button>
</div>
</aside>
);
}