428 lines
13 KiB
TypeScript
428 lines
13 KiB
TypeScript
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();
|
||
|
||
// Auto-expand parent sections when their children are active
|
||
React.useEffect(() => {
|
||
const autoExpandParents = () => {
|
||
const newExpandedItems: string[] = [];
|
||
|
||
menuItems.forEach((item) => {
|
||
if (item.children) {
|
||
const hasActiveChild = item.children.some(
|
||
(child) => child.href && location.pathname === child.href
|
||
);
|
||
if (hasActiveChild) {
|
||
newExpandedItems.push(item.id);
|
||
}
|
||
}
|
||
});
|
||
|
||
setExpandedItems(newExpandedItems);
|
||
};
|
||
|
||
autoExpandParents();
|
||
}, [location.pathname]);
|
||
|
||
const toggleExpanded = (itemId: string) => {
|
||
setExpandedItems((prev) => {
|
||
// If trying to collapse, check if any child is active
|
||
if (prev.includes(itemId)) {
|
||
const item = menuItems.find(menuItem => menuItem.id === itemId);
|
||
if (item?.children) {
|
||
const hasActiveChild = item.children.some(
|
||
(child) => child.href && location.pathname === child.href
|
||
);
|
||
// Don't collapse if a child is active
|
||
if (hasActiveChild) {
|
||
return prev;
|
||
}
|
||
}
|
||
return prev.filter((id) => id !== itemId);
|
||
} else {
|
||
return [...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) ||
|
||
(item.children && item.children.some(child =>
|
||
child.href && location.pathname === child.href
|
||
));
|
||
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);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div key={item.id} className="relative">
|
||
{item.href && item.id !== "logout" ? (
|
||
<Link to={item.href} className="block">
|
||
<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",
|
||
)}
|
||
>
|
||
<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>
|
||
</Link>
|
||
) : (
|
||
<button
|
||
className={cn(
|
||
"w-full text-right",
|
||
// Disable pointer cursor when child is active (cannot collapse)
|
||
item.children && item.children.some(child =>
|
||
child.href && location.pathname === child.href
|
||
) && "cursor-not-allowed"
|
||
)}
|
||
onClick={handleClick}
|
||
>
|
||
<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",
|
||
)}
|
||
>
|
||
<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/10 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",
|
||
// Show different color when child is active (cannot collapse)
|
||
item.children && item.children.some(child =>
|
||
child.href && location.pathname === child.href
|
||
) ? "text-emerald-400" : "text-current"
|
||
)}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</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(
|
||
" 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
|
||
color="black"
|
||
size={32}
|
||
strokeWidth={1}
|
||
className="bg-green-400 p-1.5 rounded-lg"
|
||
/>
|
||
<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-gray-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;
|