inogen/app/components/dashboard/sidebar.tsx
2025-09-14 16:07:30 +03:30

461 lines
14 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 {
Box,
Building2,
ChevronDown,
ChevronRight,
FolderKanban,
GalleryVerticalEnd,
Globe,
LayoutDashboard,
Leaf,
Lightbulb,
LogOut,
MonitorSmartphone,
Package,
Settings,
Star,
Workflow,
DiscAlbum
} from "lucide-react";
import React, { useState } from "react";
import { Link, useLocation } from "react-router";
import { useAuth } from "~/contexts/auth-context";
import { cn } from "~/lib/utils";
interface SidebarProps {
isCollapsed?: boolean;
onToggleCollapse?: () => void;
className?: string;
onStrategicAlignmentClick?: () => void;
}
interface MenuItem {
id: string;
label: string;
icon: React.ComponentType<{ className?: string }> | null;
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/manage-ideas-tech",
},
{
id: "top-innovations",
label: "نوآور برتر",
icon: Star,
href: "/dashboard/top-innovations",
},
{
id: "strategic-alignment",
label: "میزان انطباق راهبردی",
icon: null,
href: "#", // This is not a route, it opens a popup
},
];
const bottomMenuItems: MenuItem[] = [
{
id: "settings",
label: "تنظیمات",
icon: Settings,
href: "/dashboard/settings",
},
{
id: "logout",
label: "خروج",
icon: LogOut,
href: "#",
},
];
export function Sidebar({
isCollapsed = false,
onToggleCollapse,
className,
onStrategicAlignmentClick,
}: 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 === "strategic-alignment") {
onStrategicAlignmentClick?.();
} else if (item.id === "logout") {
logout();
} else if (hasChildren) {
toggleExpanded(item.id);
}
};
if (item.id === "strategic-alignment") {
return (
<button
className={cn(
"w-full text-right",
)}
onClick={handleClick}
>
<div
className={cn(
"flex items-center justify-center w-full px-2 rounded-lg mt-4 transition-all duration-200 group",
)}
>
<div className="flex justify-center rounded-xl border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-2 text-center items-center gap-3 min-w-0 flex-1">
<span className="font-persian text-sm font-medium truncate">
{item.label}
</span>
</div>
</div>
</button>
)
}
return (
<div key={item.id} className="relative">
{item.href && item.href !== "#" ? (
<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">
<GalleryVerticalEnd
color="black"
size={32}
strokeWidth={1}
className="bg-green-400 p-1.5 rounded-lg"
/>
</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;