inogen/app/components/dashboard/sidebar.tsx

337 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from "react";
import { Link, useLocation } from "react-router";
import { cn } from "~/lib/utils";
import { InogenLogo } from "~/components/ui/brand-logo";
import { useAuth } from "~/contexts/auth-context";
import {
GalleryVerticalEnd,
LayoutDashboard,
FolderOpen,
Users,
BarChart3,
Settings,
ChevronLeft,
ChevronDown,
FileText,
Calendar,
Bell,
User,
Database,
Shield,
HelpCircle,
LogOut,
ChevronRight,
Refrigerator,
} from "lucide-react";
import {
FolderKanban,
Box,
Package,
Workflow,
MonitorSmartphone,
Leaf,
Building2,
Globe,
Lightbulb,
Star,
} from "lucide-react";
interface SidebarProps {
isCollapsed?: boolean;
onToggleCollapse?: () => void;
className?: string;
}
interface MenuItem {
id: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
href?: string;
children?: MenuItem[];
badge?: string | number;
}
const menuItems: MenuItem[] = [
{
id: "dashboard",
label: "صفحه اصلی",
icon: LayoutDashboard,
href: "/dashboard",
},
{
id: "project-management",
label: "مدیریت اجرای پروژه‌ها",
icon: FolderKanban,
href: "/dashboard/project-management",
},
{
id: "innovation-basket",
label: "سبد فناوری و نوآوری",
icon: Box,
children: [
{
id: "product-innovation",
label: "نوآوری در محصول",
icon: Package,
href: "/dashboard/innovation-basket/product-innovation",
},
{
id: "process-innovation",
label: "نوآوری در فرآیند",
icon: Workflow,
href: "/dashboard/innovation-basket/process-innovation",
},
{
id: "digital-innovation",
label: "نوآوری دیجیتال",
icon: MonitorSmartphone,
href: "/dashboard/innovation-basket/digital-innovation",
},
{
id: "green-innovation",
label: "نوآوری سبز",
icon: Leaf,
href: "/dashboard/innovation-basket/green-innovation",
},
{
id: "internal-innovation",
label: "نوآوری ساخت داخل",
icon: Building2,
href: "/dashboard/innovation-basket/internal-innovation",
},
],
},
{
id: "ecosystem",
label: "زیست بوم فناوری و نوآوری",
icon: Globe,
href: "/dashboard/ecosystem",
},
{
id: "ideas",
label: "ایده‌های فناوری و نوآوری",
icon: Lightbulb,
href: "/dashboard/ideas",
},
{
id: "top-innovations",
label: "نوآور برتر",
icon: Star,
href: "/dashboard/top-innovations",
},
];
const bottomMenuItems: MenuItem[] = [
{
id: "settings",
label: "تنظیمات",
icon: Settings,
href: "/dashboard/settings",
},
{
id: "logout",
label: "خروج",
icon: LogOut,
href: "#",
},
];
export function Sidebar({
isCollapsed = false,
onToggleCollapse,
className,
}: SidebarProps) {
const location = useLocation();
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const { logout } = useAuth();
const toggleExpanded = (itemId: string) => {
setExpandedItems((prev) =>
prev.includes(itemId)
? prev.filter((id) => id !== itemId)
: [...prev, itemId],
);
};
const isActiveRoute = (href?: string, children?: MenuItem[]) => {
if (href && location.pathname === href) return true;
if (children) {
return children.some(
(child) => child.href && location.pathname === child.href,
);
}
return false;
};
const renderMenuItem = (item: MenuItem, level = 0) => {
const isActive = isActiveRoute(item.href, item.children);
const isExpanded = expandedItems.includes(item.id);
const hasChildren = item.children && item.children.length > 0;
const ItemIcon = item.icon;
const handleClick = () => {
if (item.id === "logout") {
logout();
} else if (hasChildren) {
toggleExpanded(item.id);
}
};
const menuItemContent = (
<div
className={cn(
"flex items-center justify-between w-full py-2 px-3 rounded-lg transition-all duration-200 group",
level === 0 ? "mb-1" : "mb-0.5 mr-4",
isActive
? " text-emerald-400 border-r-2 border-emerald-400"
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400",
)}
onClick={handleClick}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<ItemIcon
className={cn(
"w-5 h-5 flex-shrink-0",
isActive ? "text-emerald-400" : "text-current",
)}
/>
{!isCollapsed && (
<span className="font-persian text-sm font-medium truncate">
{item.label}
</span>
)}
</div>
{!isCollapsed && (
<div className="flex items-center gap-2 flex-shrink-0">
{item.badge && (
<span className="bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 text-xs font-medium px-1.5 py-0.5 rounded-full min-w-[20px] text-center font-persian">
{item.badge}
</span>
)}
{hasChildren && (
<ChevronDown
className={cn(
"w-4 h-4 transition-transform duration-200",
isExpanded ? "rotate-180" : "rotate-0",
)}
/>
)}
</div>
)}
</div>
);
return (
<div key={item.id} className="relative">
{item.href && item.id !== "logout" ? (
<Link to={item.href} className="block">
{menuItemContent}
</Link>
) : (
<button className="w-full text-right">{menuItemContent}</button>
)}
{/* Submenu */}
{hasChildren && isExpanded && !isCollapsed && (
<div className="mt-1 space-y-0.5">
{item.children?.map((child) => renderMenuItem(child, level + 1))}
</div>
)}
{/* Tooltip for collapsed state */}
{isCollapsed && level === 0 && (
<div className="absolute right-full top-1/2 transform -translate-y-1/2 mr-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50">
<div className="bg-gray-800 border border-emerald-500/30 text-white text-sm px-2 py-1 rounded whitespace-nowrap font-persian">
{item.label}
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 translate-x-full">
<div className="w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-800"></div>
</div>
</div>
</div>
)}
</div>
);
};
return (
<div
className={cn(
"bg-gray-800/95 backdrop-blur-sm h-full flex flex-col transition-all duration-300",
isCollapsed ? "w-16" : "w-64",
className,
)}
>
{/* Header */}
<div className={cn("p-4", isCollapsed && "px-2")}>
<div className="flex items-center justify-start">
{!isCollapsed ? (
<div className="flex items-center gap-3">
<GalleryVerticalEnd
enableBackground="green"
size={32}
strokeWidth={1}
/>
<div className="font-persian">
<div className="text-sm font-semibold text-white">
سیستم اینوژن
</div>
<div className="text-xs text-gray-400">نسخه ۰.۱</div>
</div>
</div>
) : (
<div className="flex justify-center w-full">
<InogenLogo size="sm" />
</div>
)}
</div>
</div>
{/* Main Menu */}
<div className="flex-1 overflow-y-auto overflow-x-hidden p-3">
<nav className="space-y-1">
{!isCollapsed && (
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-3 px-3 font-persian"></div>
)}
{menuItems.map((item) => renderMenuItem(item))}
</nav>
</div>
{/* Bottom Menu */}
<div className="p-3">
<nav className="space-y-1">
{!isCollapsed && (
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-3 px-3 font-persian"></div>
)}
{bottomMenuItems.map((item) => renderMenuItem(item))}
</nav>
</div>
{/* Collapse Toggle */}
{onToggleCollapse && (
<div className="p-3 border-t border-emerald-500/30">
<button
onClick={onToggleCollapse}
className="w-full p-2 rounded-md hover:bg-emerald-500/20 transition-colors flex justify-center items-center gap-2"
>
<ChevronRight
className={cn(
"w-4 h-4 text-gray-400 transition-transform duration-200",
isCollapsed ? "rotate-180" : "rotate-0",
)}
/>
{!isCollapsed && (
<span className="text-sm text-gray-400 font-persian"></span>
)}
</button>
</div>
)}
</div>
);
}
export default Sidebar;