TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import Link from "next/link";4import { usePathname } from "next/navigation";5import { ArrowLeftRight, FolderKanban, Plus } from "lucide-react";6import { useApp } from "@/components/app/store";7import { useApi } from "@/lib/client/api";8import { Skeleton } from "@/components/ui/misc";9import { Tooltip } from "@/components/ui/tooltip";10import type { PublicProject } from "@/lib/client/types";11import { ProjectIcon } from "./project-icon";12import { ProjectSwitcher } from "./project-switcher";13import { cn } from "@/lib/utils";1415const MAX_ROWS = 8;1617/**18 * Projects list rendered inside the sidebar / mobile drawer.19 * - rows link to the project page; the current page's project is highlighted20 * - the *active* project (used for new chats) shows a coloured dot + "Active" hint21 * - the ⇄ button opens the quick switcher (ActionSheet on phones, dropdown on desktop)22 */23export function ProjectsSidebarSection({ onNavigate }: { onNavigate?: () => void }) {24 const { activeProjectId } = useApp();25 const pathname = usePathname();26 const { data, isLoading } = useApi<{ projects: PublicProject[] }>("/api/projects");27 const all = React.useMemo(() => (data?.projects ?? []).filter((p) => !p.archived), [data]);28 const list = React.useMemo(() => {29 // Keep the active project visible even when it is not among the first rows.30 const head = all.slice(0, MAX_ROWS);31 const active = activeProjectId ? all.find((p) => p.id === activeProjectId) : undefined;32 if (active && !head.some((p) => p.id === active.id)) return [active, ...head.slice(0, MAX_ROWS - 1)];33 return head;34 }, [all, activeProjectId]);35 const currentId = pathname.match(/^\/app\/projects\/([^/]+)/)?.[1];36 const overflow = all.length - list.length;3738 return (39 <div className="pt-2">40 <div className="flex items-center justify-between px-2 pb-0.5">41 <Link href="/app/projects" onClick={onNavigate} className="tap inline-flex items-center gap-1 text-[11px] font-medium uppercase tracking-wide text-fg-subtle hover:text-fg">42 <FolderKanban className="size-3" /> Projects43 {all.length ? <span className="ml-1 font-normal text-fg-subtle/70">{all.length}</span> : null}44 </Link>45 <div className="flex items-center gap-0.5">46 {all.length ? (47 <ProjectSwitcher onNavigate={onNavigate}>48 <button type="button" className={cn("tap rounded p-0.5 hover:text-fg", activeProjectId ? "text-accent" : "text-fg-subtle")} aria-label="Switch active project">49 <ArrowLeftRight className="size-3.5" />50 </button>51 </ProjectSwitcher>52 ) : null}53 <Tooltip content="New project">54 <Link href="/app/projects?new=1" onClick={onNavigate} className="tap rounded p-0.5 text-fg-subtle hover:text-fg" aria-label="New project">55 <Plus className="size-3.5" />56 </Link>57 </Tooltip>58 </div>59 </div>60 {isLoading && !data ? (61 <div className="space-y-1 px-2 pt-1">62 <Skeleton className="h-7" />63 <Skeleton className="h-7" />64 </div>65 ) : list.length === 0 ? (66 <Link href="/app/projects?new=1" onClick={onNavigate} className="mx-2 mt-1 block rounded-md border border-dashed border-border px-2.5 py-2 text-[12px] text-fg-subtle hover:border-border-strong hover:text-fg">67 Group chats, files and instructions into a project.68 </Link>69 ) : (70 <div className="space-y-px">71 {list.map((p) => {72 const isActive = p.id === activeProjectId;73 const isCurrent = p.id === currentId;74 return (75 <Link key={p.id} href={`/app/projects/${p.id}`} onClick={onNavigate} aria-current={isCurrent ? "page" : undefined} className={cn("group flex min-h-[36px] items-center gap-2 rounded-md px-2 text-[13px] md:min-h-[30px]", isCurrent ? "bg-bg-muted text-fg" : "text-fg-muted hover:bg-bg-muted/70 hover:text-fg")}>76 <ProjectIcon icon={p.icon} color={p.color} size="sm" className="size-5 text-[12px]" />77 <span className="min-w-0 flex-1 truncate">{p.name}</span>78 {isActive ? (79 <span className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-accent" title="Active project for new chats">80 <span className="size-1.5 rounded-full bg-accent" aria-hidden />81 Active82 </span>83 ) : null}84 </Link>85 );86 })}87 {overflow > 0 ? (88 <Link href="/app/projects" onClick={onNavigate} className="block rounded-md px-2 py-1 text-[12px] text-fg-subtle hover:bg-bg-muted/70 hover:text-fg">89 +{overflow} more…90 </Link>91 ) : null}92 </div>93 )}94 </div>95 );96}97