Organize/components/nav/projects-nav-section.tsx

113 lines
3.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronDown, ChevronRight, FolderKanban } from "lucide-react";
import { cn } from "@/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useProjects } from "@/components/projects/projects-context";
const STORAGE_KEY = "organize:projects-expanded";
/**
* The "Projects" nav entry: a link to /projects (the list + create page)
* plus, underneath it, a live list of the user's projects that expands
* and contracts independently of the link itself -- clicking the label
* navigates, clicking the chevron just toggles the list.
*/
export function ProjectsNavSection({ collapsed }: { collapsed: boolean }) {
const pathname = usePathname();
const { projects } = useProjects();
// Defaults to expanded on both server and first client render (same
// reasoning as SideNavProvider's `collapsed`) to avoid a hydration
// mismatch; the persisted value applies right after mount.
const [expanded, setExpanded] = useState(true);
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored !== null) setExpanded(stored === "true");
}, []);
function toggleExpanded() {
setExpanded((prev) => {
const next = !prev;
localStorage.setItem(STORAGE_KEY, String(next));
return next;
});
}
const isListActive = pathname === "/projects";
const link = (
<Link
href="/projects"
className={cn(
"flex flex-1 items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
"hover:bg-accent hover:text-accent-foreground",
collapsed && "justify-center px-0",
isListActive && "bg-accent text-accent-foreground"
)}
>
<FolderKanban className="size-5 shrink-0" />
{!collapsed && <span className="truncate">Projects</span>}
</Link>
);
// No room for an expandable sub-list in the icon rail -- just a link,
// matching how every other collapsed nav item behaves.
if (collapsed) {
return (
<Tooltip>
<TooltipTrigger render={link} />
<TooltipContent side="right">Projects</TooltipContent>
</Tooltip>
);
}
return (
<div>
<div className="flex items-center gap-0.5">
{link}
<button
type="button"
onClick={toggleExpanded}
aria-label={expanded ? "Collapse projects list" : "Expand projects list"}
aria-expanded={expanded}
className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
>
{expanded ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</button>
</div>
{expanded && projects.length > 0 && (
<div className="mt-0.5 flex flex-col gap-0.5 border-l border-sidebar-border pl-3">
{projects.map((project) => {
const isActive = pathname === `/projects/${project.id}`;
return (
<Link
key={project.id}
href={`/projects/${project.id}`}
className={cn(
"truncate rounded-lg px-3 py-1.5 text-sm transition-colors",
"hover:bg-accent hover:text-accent-foreground",
isActive
? "bg-accent font-medium text-accent-foreground"
: "text-muted-foreground"
)}
>
{project.title}
</Link>
);
})}
</div>
)}
</div>
);
}