Files
Vector/apps/web/src/components/layout/Sidebar.tsx
T
josh db8e86b749
CI / Lint · Typecheck · Test · Build (push) Failing after 36s
CI / Playwright (smoke) (push) Has been skipped
CI / Build & push images (push) Has been skipped
feat: remove FM feature from Vector
FMs move to a separate application. Drops Fm/FmPart tables + Repair.fmId
column, deletes FM_OPENED/FM_CLOSED PartEvent rows, strips FM enums +
webhook events + shared contracts, removes FM routes/services/pages/UI,
and collapses dashboard admin ops to Repairs 7d/30d + trend + custody
backlog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 18:46:40 -04:00

112 lines
3.5 KiB
TypeScript

import { NavLink } from 'react-router-dom';
import {
ArrowRightLeft,
Boxes,
ChevronsLeft,
ChevronsRight,
Hand,
LayoutDashboard,
Layers,
type LucideIcon,
MapPinned,
Package,
Server,
Users as UsersIcon,
Webhook,
} from 'lucide-react';
import { cn, Button, Tooltip, TooltipContent, TooltipTrigger } from '@vector/ui';
import { useAuth } from '../../contexts/AuthContext.js';
interface NavItem {
to: string;
label: string;
icon: LucideIcon;
adminOnly?: boolean;
}
const NAV_ITEMS: NavItem[] = [
{ to: '/', label: 'Dashboard', icon: LayoutDashboard },
{ to: '/parts', label: 'Parts', icon: Package },
{ to: '/part-models', label: 'Part models', icon: Layers },
{ to: '/locations', label: 'Locations', icon: MapPinned },
{ to: '/manufacturers', label: 'Manufacturers', icon: Boxes },
{ to: '/repairs', label: 'Repairs', icon: ArrowRightLeft },
{ to: '/custody', label: 'My Custody', icon: Hand },
{ to: '/hosts', label: 'Hosts', icon: Server },
{ to: '/admin/users', label: 'Users', icon: UsersIcon, adminOnly: true },
{ to: '/admin/webhooks', label: 'Webhooks', icon: Webhook, adminOnly: true },
];
export interface SidebarProps {
collapsed: boolean;
onToggle: () => void;
}
export function Sidebar({ collapsed, onToggle }: SidebarProps) {
const { user } = useAuth();
const items = NAV_ITEMS.filter((i) => !i.adminOnly || user?.role === 'ADMIN');
return (
<aside
className={cn(
'fixed inset-y-0 left-0 z-40 flex flex-col border-r border-border bg-card transition-[width] duration-200',
collapsed ? 'w-14' : 'w-64',
)}
>
<div className="flex h-13 items-center gap-2 border-b border-border px-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-brand text-brand-foreground font-semibold">
V
</div>
{!collapsed && <span className="truncate text-sm font-semibold">Vector</span>}
</div>
<nav className="flex-1 space-y-0.5 p-2">
{items.map((item) => (
<NavItemLink key={item.to} item={item} collapsed={collapsed} />
))}
</nav>
<div className="border-t border-border p-2">
<Button
variant="ghost"
size="icon"
className={cn('w-full', !collapsed && 'justify-start gap-2 px-2')}
onClick={onToggle}
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{collapsed ? <ChevronsRight className="h-4 w-4" /> : <ChevronsLeft className="h-4 w-4" />}
{!collapsed && <span className="text-xs text-muted-foreground">Collapse</span>}
</Button>
</div>
</aside>
);
}
function NavItemLink({ item, collapsed }: { item: NavItem; collapsed: boolean }) {
const content = (
<NavLink
to={item.to}
end={item.to === '/'}
className={({ isActive }) =>
cn(
'flex items-center gap-3 rounded-md px-2.5 py-2 text-sm transition-colors',
isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
collapsed && 'justify-center px-0',
)
}
>
<item.icon className="h-4 w-4 shrink-0" />
{!collapsed && <span className="truncate">{item.label}</span>}
</NavLink>
);
if (!collapsed) return content;
return (
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
</Tooltip>
);
}