149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
/**
|
|
* @file components/layout/Navbar.tsx
|
|
* @description Client component providing responsive navigation for desktop and mobile views with active route indicators and trash count.
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import {
|
|
LayoutDashboard,
|
|
FileText,
|
|
Trash2,
|
|
FilePenLine,
|
|
Users,
|
|
} from "lucide-react";
|
|
import useSWR from "swr";
|
|
import MobileLegalMenu from "./MobileLegalMenu";
|
|
|
|
/**
|
|
* Fetcher function for SWR to retrieve JSON data from the API endpoint.
|
|
*
|
|
* @param {string} url - The API endpoint URL to fetch.
|
|
* @returns {Promise<any>} The parsed JSON response data.
|
|
*/
|
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
|
|
|
/**
|
|
* Array of navigation items containing their name, route path, and corresponding icon.
|
|
*/
|
|
const navItems = [
|
|
{ name: "Summary", href: "/summary", icon: FileText },
|
|
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
|
|
{ name: "Add Task", href: "/tasks", icon: FilePenLine },
|
|
{ name: "Member", href: "/member", icon: Users },
|
|
{ name: "Trash", href: "/trash", icon: Trash2 },
|
|
];
|
|
|
|
/**
|
|
* Renders the responsive navigation bar featuring desktop sidebar layout
|
|
* and mobile bottom bar layout with active state styling and cached trash counter.
|
|
*
|
|
* @returns {JSX.Element} The rendered navigation component.
|
|
*/
|
|
export default function Navbar() {
|
|
const pathname = usePathname();
|
|
|
|
const { data } = useSWR("/api/trash/count", fetcher, {
|
|
revalidateOnFocus: false,
|
|
revalidateIfStale: false,
|
|
});
|
|
|
|
const trashCount = data?.count || 0;
|
|
|
|
/**
|
|
* Shared helper function to render navigation links for the desktop sidebar layout.
|
|
*
|
|
* @param {boolean} [isMobile=false] - Flag indicating whether the links are rendered for mobile layout.
|
|
* @returns {JSX.Element[]} An array of rendered navigation link elements.
|
|
*/
|
|
const renderNavLinks = (isMobile = false) =>
|
|
navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const isTrash = item.href === "/trash";
|
|
const isActive =
|
|
pathname === item.href ||
|
|
(item.href !== "/" && pathname.startsWith(item.href));
|
|
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href === "/tasks" ? "/tasks?task=new" : item.href}
|
|
aria-current={isActive ? "page" : undefined}
|
|
className={
|
|
isMobile
|
|
? `flex flex-col items-center justify-center gap-1 transition-colors ${
|
|
isActive
|
|
? "cursor-default pointer-events-none"
|
|
: "text-foreground-muted hover:text-foreground"
|
|
}`
|
|
: `flex items-center gap-3 px-3 py-2 rounded-lg font-medium transition-colors ${
|
|
isActive
|
|
? "bg-foreground/10 cursor-default pointer-events-none"
|
|
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
|
}`
|
|
}
|
|
>
|
|
<div className="relative inline-flex items-center">
|
|
<Icon
|
|
className={`${isMobile ? "w-5 h-5" : "w-4 h-4"} ${isActive ? "text-primary" : ""}`}
|
|
/>
|
|
{isTrash && trashCount > 0 && (
|
|
<span className="absolute -top-1.5 -right-2 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-destructive text-[9px] font-bold text-white shadow-sm ring-1 ring-background">
|
|
{trashCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<span className={isMobile ? "text-xs font-medium" : ""}>
|
|
{item.name}
|
|
</span>
|
|
</Link>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop Navigation */}
|
|
<nav className="hidden md:flex flex-col mt-6 space-y-1">
|
|
{renderNavLinks(false)}
|
|
</nav>
|
|
|
|
{/* Mobile Bottom Navigation */}
|
|
<nav className="md:hidden h-16 border-t border-border backdrop-blur-xl px-4 flex items-center justify-around fixed bottom-0 left-0 right-0 z-50 bg-background-muted">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const isTrash = item.href === "/trash";
|
|
const isActive =
|
|
pathname === item.href ||
|
|
(item.href !== "/" && pathname.startsWith(item.href));
|
|
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href === "/tasks" ? "/tasks?task=new" : item.href}
|
|
className={`flex flex-col items-center justify-center gap-1 transition-colors ${
|
|
isActive
|
|
? "text-primary font-semibold"
|
|
: "text-foreground-muted hover:text-foreground"
|
|
}`}
|
|
>
|
|
<div className="relative inline-flex items-center">
|
|
<Icon className="w-5 h-5" />
|
|
{isTrash && trashCount > 0 && (
|
|
<span className="absolute -top-1.5 -right-2 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-destructive text-[9px] font-bold text-white shadow-sm ring-1 ring-background">
|
|
{trashCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<span className="text-xs font-medium">{item.name}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
|
|
<MobileLegalMenu />
|
|
</nav>
|
|
</>
|
|
);
|
|
}
|