100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
/**
|
|
* @file 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 } from "lucide-react";
|
|
import useSWR from "swr";
|
|
|
|
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?task=new", icon: FilePenLine },
|
|
{ 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 function to render navigation links to avoid code duplication
|
|
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}
|
|
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 Navigation */}
|
|
<nav className="md:hidden h-16 border-t border-border backdrop-blur-xl px-6 flex items-center justify-around fixed bottom-0 left-0 right-0 z-50 bg-background-muted">
|
|
{renderNavLinks(true)}
|
|
</nav>
|
|
</>
|
|
);
|
|
}
|