This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-B2B/src/components/layout/app-sidebar.tsx
kirukib 3778801ef5 Ship Gishen B2B institutional portal with polished layout and mock-backed flows.
Deliver role-aware shell (sidebar, breadcrumbs, quick search, tables, detail/create layouts), locale-ready pages, shared backend/feature docs, and Vercel project config so HR, finance, and members can demo against typed mocks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 21:18:23 +03:00

152 lines
5.1 KiB
TypeScript

"use client";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { Link, usePathname } from "@/i18n/routing";
import { brand } from "@/data/brand";
import { navForRole } from "@/lib/auth/nav";
import { useAuth } from "@/providers/auth-provider";
import { cn } from "@/lib/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ChevronLeft, ChevronRight, X } from "lucide-react";
export function AppSidebar({
collapsed,
onToggle,
onNavigate,
showClose = false,
}: {
collapsed: boolean;
onToggle: () => void;
onNavigate?: () => void;
showClose?: boolean;
}) {
const t = useTranslations();
const pathname = usePathname();
const { user } = useAuth();
const items = user ? navForRole(user.activeRole) : [];
return (
<aside
className={cn(
"group/sidebar flex h-full flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width,padding] duration-300",
collapsed ? "w-[88px] px-2 py-5" : "w-[264px] px-4 py-5",
)}
>
<div
className={cn(
"flex items-center gap-2 px-2",
collapsed ? "justify-center" : "justify-between",
)}
>
<div
className={cn(
"flex min-w-0 items-center gap-3",
collapsed && "justify-center",
)}
>
<span
className={cn(
"relative flex shrink-0 items-center justify-center overflow-hidden rounded-xl bg-secondary",
collapsed ? "size-10" : "size-11",
)}
>
<Image
src={brand.logo}
alt={brand.shortName}
width={44}
height={44}
className="size-full object-contain p-1.5"
priority
/>
</span>
{!collapsed && (
<div className="min-w-0">
<p className="truncate font-display text-sm font-semibold tracking-tight">
{t("common.appName")}
</p>
<p className="truncate text-[11px] leading-snug text-muted-foreground">
{brand.tagline}
</p>
</div>
)}
</div>
{showClose ? (
<button
type="button"
className="grid size-8 shrink-0 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
onClick={onToggle}
aria-label="Close sidebar"
>
<X className="size-5" />
</button>
) : (
<button
type="button"
className={cn(
"hidden size-8 shrink-0 place-items-center rounded-lg text-muted-foreground transition-all hover:bg-secondary hover:text-foreground md:grid",
"md:pointer-events-none md:opacity-0 md:group-hover/sidebar:pointer-events-auto md:group-hover/sidebar:opacity-100",
"focus-visible:pointer-events-auto focus-visible:opacity-100",
collapsed && "translate-x-1",
)}
onClick={onToggle}
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{collapsed ? (
<ChevronRight className="size-5" />
) : (
<ChevronLeft className="size-5" />
)}
</button>
)}
</div>
<ScrollArea className="mt-6 flex-1">
<nav className="space-y-1 pr-1">
{items.map((item) => {
const active =
item.href === "/"
? pathname === "/"
: pathname === item.href || pathname.startsWith(`${item.href}/`);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
onClick={onNavigate}
className={cn(
"group flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground transition-colors",
"hover:bg-secondary hover:text-brand-deep",
active &&
"bg-brand/10 text-brand-deep shadow-[0_1px_0_rgba(0,0,0,0.02)] ring-1 ring-brand/20",
collapsed && "justify-center px-2",
)}
title={t(item.labelKey)}
>
<span
className={cn(
"grid size-8 shrink-0 place-items-center rounded-lg bg-secondary text-muted-foreground transition-colors",
"group-hover:bg-brand/20 group-hover:text-brand-deep",
active && "bg-brand text-forest shadow-sm",
)}
>
<Icon className="size-4" />
</span>
{!collapsed && (
<>
<span className="truncate">{t(item.labelKey)}</span>
{active && (
<span className="ml-auto h-6 w-1 shrink-0 rounded-full bg-brand" />
)}
</>
)}
</Link>
);
})}
</nav>
</ScrollArea>
</aside>
);
}