flowstate/app/components/layout/Navbar.tsx
Chneemann 60e2b77324
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 1m3s
feat(layout): add mobile legal menu overlay and unify footer links
2026-08-16 19:09:46 +02:00

142 lines
5.1 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 } 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: "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>
</>
);
}