Files
boocode/apps/web/src/components/ProjectSidebar.tsx
2026-05-14 19:24:50 +00:00

98 lines
3.3 KiB
TypeScript

import { useState } from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
import { Plus, Folder } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { AddProjectModal } from './AddProjectModal';
import { useProjects } from '@/hooks/useProjects';
export function ProjectSidebar() {
const { projects, refresh, remove } = useProjects();
const [addOpen, setAddOpen] = useState(false);
const navigate = useNavigate();
async function handleRemove(id: string) {
try {
await remove(id);
navigate('/');
} catch (err) {
toast.error(err instanceof Error ? err.message : 'failed to remove project');
}
}
return (
<aside className="w-60 shrink-0 border-r bg-sidebar text-sidebar-foreground flex flex-col h-screen">
<div className="px-4 py-3 border-b flex items-center justify-between">
<NavLink to="/" className="font-semibold tracking-tight text-base">
BooCode
</NavLink>
<Button
size="icon-sm"
variant="ghost"
onClick={() => setAddOpen(true)}
aria-label="Add project"
>
<Plus />
</Button>
</div>
<nav className="flex-1 overflow-y-auto py-2">
{projects === null && (
<div className="px-4 py-2 text-xs text-muted-foreground">Loading</div>
)}
{projects && projects.length === 0 && (
<div className="px-4 py-2 text-xs text-muted-foreground">No projects yet.</div>
)}
{projects?.map((p) => (
<div key={p.id} className="px-2">
<DropdownMenu>
<NavLink
to={`/project/${p.id}`}
className={({ isActive }) =>
`group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm ${
isActive
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'hover:bg-sidebar-accent/60'
}`
}
onContextMenu={(e) => {
e.preventDefault();
(
e.currentTarget.parentElement?.querySelector(
'[data-ctxtrigger]'
) as HTMLElement | null
)?.click();
}}
>
<Folder className="size-3.5 shrink-0 opacity-70" />
<span className="truncate" title={p.path}>
{p.name}
</span>
</NavLink>
<DropdownMenuTrigger asChild>
<button data-ctxtrigger className="hidden" aria-hidden />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
variant="destructive"
onClick={() => void handleRemove(p.id)}
>
Remove from sidebar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</nav>
<AddProjectModal open={addOpen} onOpenChange={setAddOpen} onAdded={refresh} />
</aside>
);
}