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
84 lines
2.1 KiB
TypeScript
84 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
import type { MouseEventHandler, ReactNode } from "react";
|
|
import Link from "next/link";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
|
|
export interface SidebarItemProps {
|
|
href: string;
|
|
icon: ReactNode;
|
|
label: string;
|
|
active?: boolean;
|
|
collapsed?: boolean;
|
|
badge?: number;
|
|
dotColor?: string;
|
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
|
}
|
|
|
|
export function SidebarItem({
|
|
href,
|
|
icon,
|
|
label,
|
|
active,
|
|
collapsed,
|
|
badge,
|
|
dotColor,
|
|
onClick,
|
|
}: SidebarItemProps) {
|
|
const inner = (
|
|
<Link
|
|
href={href}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"group flex min-h-8 w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-sidebar-foreground transition-colors duration-150",
|
|
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
collapsed && "justify-center gap-0 px-0",
|
|
active &&
|
|
"bg-sidebar-accent text-sidebar-accent-foreground shadow-[inset_3px_0_0_0_hsl(var(--primary))]",
|
|
)}
|
|
>
|
|
<span className="flex shrink-0 items-center justify-center text-muted-foreground group-hover:text-sidebar-accent-foreground [&_svg]:size-[18px]">
|
|
{icon}
|
|
</span>
|
|
{dotColor && !collapsed ? (
|
|
<span
|
|
className="size-2 shrink-0 rounded-full"
|
|
style={{ backgroundColor: dotColor }}
|
|
aria-hidden
|
|
/>
|
|
) : null}
|
|
<span
|
|
className={cn(
|
|
"min-w-0 flex-1 truncate text-left font-medium",
|
|
collapsed && "sr-only",
|
|
)}
|
|
>
|
|
{label}
|
|
</span>
|
|
{badge != null && badge > 0 && !collapsed ? (
|
|
<span className="rounded-full bg-primary/15 px-1.5 py-0 text-[10px] font-semibold tabular-nums text-primary">
|
|
{badge > 99 ? "99+" : badge}
|
|
</span>
|
|
) : null}
|
|
</Link>
|
|
);
|
|
|
|
if (collapsed) {
|
|
return (
|
|
<Tooltip delayDuration={0}>
|
|
<TooltipTrigger asChild>{inner}</TooltipTrigger>
|
|
<TooltipContent side="right" className="font-medium">
|
|
{label}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
return inner;
|
|
}
|