228 lines
6.6 KiB
TypeScript
228 lines
6.6 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useMemo, useState } from "react";
|
||
|
|
import {
|
||
|
|
ChevronDown,
|
||
|
|
FileText,
|
||
|
|
FolderKanban,
|
||
|
|
Home,
|
||
|
|
LayoutGrid,
|
||
|
|
Presentation,
|
||
|
|
Search,
|
||
|
|
Settings,
|
||
|
|
Star,
|
||
|
|
} from "lucide-react";
|
||
|
|
import { useParams, usePathname } from "next/navigation";
|
||
|
|
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||
|
|
import { Separator } from "@/components/ui/separator";
|
||
|
|
import { useWorkspaceStore } from "@/lib/stores/workspace-store";
|
||
|
|
|
||
|
|
import { SidebarItem } from "./sidebar-item";
|
||
|
|
|
||
|
|
const PROJECT_DOTS = [
|
||
|
|
"hsl(var(--primary))",
|
||
|
|
"hsl(var(--teal))",
|
||
|
|
"hsl(38 92% 50%)",
|
||
|
|
"hsl(199 89% 48%)",
|
||
|
|
];
|
||
|
|
|
||
|
|
function NavSectionLabel({
|
||
|
|
children,
|
||
|
|
collapsed,
|
||
|
|
}: {
|
||
|
|
children: React.ReactNode;
|
||
|
|
collapsed?: boolean;
|
||
|
|
}) {
|
||
|
|
if (collapsed) return null;
|
||
|
|
return (
|
||
|
|
<div className="px-2 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground first:pt-1">
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SidebarNav({
|
||
|
|
collapsed,
|
||
|
|
onOpenSearch,
|
||
|
|
}: {
|
||
|
|
collapsed: boolean;
|
||
|
|
onOpenSearch?: () => void;
|
||
|
|
}) {
|
||
|
|
const pathname = usePathname();
|
||
|
|
const params = useParams();
|
||
|
|
const workspace = useWorkspaceStore((s) => s.currentWorkspace);
|
||
|
|
const slugParam =
|
||
|
|
typeof params?.workspaceSlug === "string" ? params.workspaceSlug : "";
|
||
|
|
const base = workspace?.slug
|
||
|
|
? `/${workspace.slug}`
|
||
|
|
: slugParam
|
||
|
|
? `/${slugParam}`
|
||
|
|
: "";
|
||
|
|
|
||
|
|
const [favoritesOpen, setFavoritesOpen] = useState(true);
|
||
|
|
const [projectsOpen, setProjectsOpen] = useState(true);
|
||
|
|
|
||
|
|
const homeActive = pathname === base || pathname === `${base}/`;
|
||
|
|
|
||
|
|
const favoriteItems = useMemo(
|
||
|
|
() => [
|
||
|
|
{ label: "Q1 Launch", href: `${base}/favorites/q1` },
|
||
|
|
{ label: "Design system", href: `${base}/favorites/design` },
|
||
|
|
],
|
||
|
|
[base],
|
||
|
|
);
|
||
|
|
|
||
|
|
const projectItems = useMemo(
|
||
|
|
() => [
|
||
|
|
{ label: "Product roadmap", slug: "product-roadmap" },
|
||
|
|
{ label: "Marketing", slug: "marketing" },
|
||
|
|
{ label: "Engineering", slug: "engineering" },
|
||
|
|
{ label: "Operations", slug: "operations" },
|
||
|
|
],
|
||
|
|
[base],
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ScrollArea className="flex-1">
|
||
|
|
<nav className="flex flex-col gap-0.5 px-2 pb-4 pt-1">
|
||
|
|
<SidebarItem
|
||
|
|
href={onOpenSearch ? "#" : `${base}/search`}
|
||
|
|
icon={<Search />}
|
||
|
|
label="Search"
|
||
|
|
collapsed={collapsed}
|
||
|
|
active={pathname.startsWith(`${base}/search`)}
|
||
|
|
onClick={
|
||
|
|
onOpenSearch
|
||
|
|
? (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
onOpenSearch();
|
||
|
|
}
|
||
|
|
: undefined
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Separator className="my-2 bg-sidebar-border" />
|
||
|
|
|
||
|
|
<SidebarItem
|
||
|
|
href={base || "/"}
|
||
|
|
icon={<Home />}
|
||
|
|
label="Home"
|
||
|
|
collapsed={collapsed}
|
||
|
|
active={homeActive}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="ghost"
|
||
|
|
className={cn(
|
||
|
|
"mb-0.5 flex h-8 w-full items-center justify-between rounded-md px-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||
|
|
collapsed && "justify-center px-0",
|
||
|
|
)}
|
||
|
|
onClick={() => setFavoritesOpen((o) => !o)}
|
||
|
|
>
|
||
|
|
{!collapsed ? (
|
||
|
|
<>
|
||
|
|
<span className="flex items-center gap-1.5">
|
||
|
|
<Star className="size-3.5" />
|
||
|
|
Favorites
|
||
|
|
</span>
|
||
|
|
<ChevronDown
|
||
|
|
className={cn(
|
||
|
|
"size-3.5 shrink-0 transition-transform duration-200",
|
||
|
|
!favoritesOpen && "-rotate-90",
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<Star className="size-4 text-muted-foreground" />
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
{favoritesOpen || collapsed
|
||
|
|
? favoriteItems.map((fav) => (
|
||
|
|
<SidebarItem
|
||
|
|
key={fav.href}
|
||
|
|
href={fav.href}
|
||
|
|
icon={<Star className="size-[15px]" />}
|
||
|
|
label={fav.label}
|
||
|
|
collapsed={collapsed}
|
||
|
|
active={pathname === fav.href}
|
||
|
|
/>
|
||
|
|
))
|
||
|
|
: null}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="ghost"
|
||
|
|
className={cn(
|
||
|
|
"mb-0.5 flex h-8 w-full items-center justify-between rounded-md px-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||
|
|
collapsed && "justify-center px-0",
|
||
|
|
)}
|
||
|
|
onClick={() => setProjectsOpen((o) => !o)}
|
||
|
|
>
|
||
|
|
{!collapsed ? (
|
||
|
|
<>
|
||
|
|
<span className="flex items-center gap-1.5">
|
||
|
|
<FolderKanban className="size-3.5" />
|
||
|
|
Projects
|
||
|
|
</span>
|
||
|
|
<ChevronDown
|
||
|
|
className={cn(
|
||
|
|
"size-3.5 shrink-0 transition-transform duration-200",
|
||
|
|
!projectsOpen && "-rotate-90",
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<FolderKanban className="size-4 text-muted-foreground" />
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
{projectsOpen || collapsed
|
||
|
|
? projectItems.map((p, i) => (
|
||
|
|
<SidebarItem
|
||
|
|
key={p.slug}
|
||
|
|
href={`${base}/projects/${p.slug}`}
|
||
|
|
icon={<LayoutGrid className="size-[15px]" />}
|
||
|
|
label={p.label}
|
||
|
|
collapsed={collapsed}
|
||
|
|
active={pathname === `${base}/projects/${p.slug}`}
|
||
|
|
dotColor={PROJECT_DOTS[i % PROJECT_DOTS.length]}
|
||
|
|
/>
|
||
|
|
))
|
||
|
|
: null}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<NavSectionLabel collapsed={collapsed}>Content</NavSectionLabel>
|
||
|
|
<SidebarItem
|
||
|
|
href={`${base}/documents`}
|
||
|
|
icon={<FileText />}
|
||
|
|
label="Documents"
|
||
|
|
collapsed={collapsed}
|
||
|
|
active={pathname.startsWith(`${base}/documents`)}
|
||
|
|
/>
|
||
|
|
<SidebarItem
|
||
|
|
href={`${base}/whiteboards`}
|
||
|
|
icon={<Presentation />}
|
||
|
|
label="Whiteboards"
|
||
|
|
collapsed={collapsed}
|
||
|
|
active={pathname.startsWith(`${base}/whiteboards`)}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<NavSectionLabel collapsed={collapsed}>Workspace</NavSectionLabel>
|
||
|
|
<SidebarItem
|
||
|
|
href={`${base}/settings`}
|
||
|
|
icon={<Settings />}
|
||
|
|
label="Settings"
|
||
|
|
collapsed={collapsed}
|
||
|
|
active={pathname.startsWith(`${base}/settings`)}
|
||
|
|
/>
|
||
|
|
</nav>
|
||
|
|
</ScrollArea>
|
||
|
|
);
|
||
|
|
}
|