ubiquitous-invention/apps/web/components/sidebar/sidebar-nav.tsx
Randall Stillwell a508ece6e7 feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:

- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)

Made-with: Cursor
2026-03-26 22:39:16 -05:00

227 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>
);
}