All checks were successful
Build and Deploy LSFE Frontend / build-and-deploy (push) Successful in 4m36s
227 lines
7.9 KiB
TypeScript
227 lines
7.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { ChevronLeft, ChevronDown, ChevronRight, Menu, User, LogOut, Stethoscope } from 'lucide-react';
|
|
import * as LucideIcons from 'lucide-react';
|
|
import { NavigationItem } from '../../types/navigation';
|
|
|
|
interface SidebarProps {
|
|
isOpen: boolean;
|
|
onToggle: () => void;
|
|
activeTab: string;
|
|
onTabChange: (tab: string) => void;
|
|
user?: any;
|
|
onLogout?: () => void;
|
|
navigationItems: NavigationItem[];
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
export const Sidebar: React.FC<SidebarProps> = ({
|
|
isOpen,
|
|
onToggle,
|
|
activeTab,
|
|
onTabChange,
|
|
user,
|
|
onLogout,
|
|
navigationItems,
|
|
isLoading = false
|
|
}) => {
|
|
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
|
|
|
|
const getIconComponent = (iconName: string) => {
|
|
const IconComponent = (LucideIcons as any)[iconName];
|
|
return IconComponent || LucideIcons.Home;
|
|
};
|
|
|
|
const toggleMenu = (menuId: string, e: React.MouseEvent) => {
|
|
e.stopPropagation(); // Prevent triggering parent click
|
|
const newExpanded = new Set(expandedMenus);
|
|
if (newExpanded.has(menuId)) {
|
|
newExpanded.delete(menuId);
|
|
} else {
|
|
newExpanded.add(menuId);
|
|
}
|
|
setExpandedMenus(newExpanded);
|
|
};
|
|
|
|
const handleMenuClick = (item: NavigationItem) => {
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
|
|
// If item has a route (is clickable), navigate to it
|
|
if (item.route) {
|
|
onTabChange(item.id);
|
|
// Close sidebar on mobile only when selecting a final destination
|
|
if (window.innerWidth < 1024) {
|
|
onToggle();
|
|
}
|
|
} else if (hasChildren) {
|
|
// If no route but has children, just expand/collapse
|
|
setExpandedMenus(prev => {
|
|
const newExpanded = new Set(prev);
|
|
if (newExpanded.has(item.id)) {
|
|
newExpanded.delete(item.id);
|
|
} else {
|
|
newExpanded.add(item.id);
|
|
}
|
|
return newExpanded;
|
|
});
|
|
}
|
|
};
|
|
|
|
const renderMenuItem = (item: NavigationItem, level: number = 0) => {
|
|
const isActive = activeTab === item.id;
|
|
const isExpanded = expandedMenus.has(item.id);
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const Icon = getIconComponent(item.icon);
|
|
|
|
return (
|
|
<div key={item.id}>
|
|
{/* Parent Menu Item */}
|
|
<div className="relative group">
|
|
<div
|
|
className={`w-full flex items-center justify-between transition-all duration-200 ${
|
|
level > 0 ? 'pl-8' : 'pl-4'
|
|
} pr-4 py-3 ${
|
|
isActive
|
|
? 'bg-teal-700 border-r-4 border-teal-300'
|
|
: 'hover:bg-teal-700/50'
|
|
}`}
|
|
>
|
|
{/* Main clickable area - icon and label */}
|
|
<button
|
|
onClick={() => handleMenuClick(item)}
|
|
className={`flex items-center flex-1 text-left ${!isOpen && 'justify-center'}`}
|
|
>
|
|
<Icon className="w-5 h-5 text-white flex-shrink-0" />
|
|
{isOpen && (
|
|
<>
|
|
<span className="ml-3 font-medium">{item.label}</span>
|
|
{item.badge && !hasChildren && (
|
|
<span className="ml-auto bg-teal-600 text-xs px-2 py-1 rounded-full">
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{/* Separate expand/collapse button */}
|
|
{isOpen && hasChildren && (
|
|
<button
|
|
onClick={(e) => toggleMenu(item.id, e)}
|
|
className="ml-2 p-1 hover:bg-teal-600 rounded transition-colors"
|
|
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
>
|
|
{isExpanded ? (
|
|
<ChevronDown className="w-4 h-4" />
|
|
) : (
|
|
<ChevronRight className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tooltip for collapsed sidebar */}
|
|
{!isOpen && (
|
|
<div className="absolute left-full top-1/2 -translate-y-1/2 ml-2 px-3 py-2 bg-gray-900 text-white text-sm rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 whitespace-nowrap z-50 pointer-events-none">
|
|
{item.label}
|
|
{item.badge && (
|
|
<span className="ml-2 bg-teal-600 text-xs px-2 py-0.5 rounded-full">
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900"></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Child Menu Items */}
|
|
{hasChildren && isExpanded && isOpen && (
|
|
<div className="bg-teal-900/30">
|
|
{item.children!.map((child) => renderMenuItem(child, level + 1))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile overlay */}
|
|
{isOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
|
onClick={onToggle}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<div className={`fixed left-0 top-0 h-full bg-gradient-to-b from-teal-800 to-teal-900 text-white transition-all duration-300 z-50 ${
|
|
isOpen ? 'w-64' : 'w-16'
|
|
} lg:relative lg:translate-x-0 ${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}>
|
|
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b border-teal-700">
|
|
<div className={`flex items-center space-x-3 ${!isOpen && 'lg:justify-center'}`}>
|
|
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
|
|
<Stethoscope className="w-5 h-5 text-teal-600" />
|
|
</div>
|
|
{isOpen && (
|
|
<div>
|
|
<h1 className="text-lg font-bold">LLI-SFE</h1>
|
|
<p className="text-xs text-teal-200">{user?.company || 'Role'}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={onToggle}
|
|
className="p-2 rounded-lg hover:bg-teal-700 transition-colors"
|
|
>
|
|
{isOpen ? <ChevronLeft className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="mt-8" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
{isLoading ? (
|
|
<div className="px-4 py-3">
|
|
<div className="animate-pulse space-y-2">
|
|
<div className="h-10 bg-teal-700 rounded"></div>
|
|
<div className="h-10 bg-teal-700 rounded"></div>
|
|
<div className="h-10 bg-teal-700 rounded"></div>
|
|
</div>
|
|
</div>
|
|
) : navigationItems.length === 0 ? (
|
|
<div className="px-4 py-3 text-center text-teal-200 text-sm">
|
|
No menu items available
|
|
</div>
|
|
) : (
|
|
navigationItems.map((item) => renderMenuItem(item))
|
|
)}
|
|
</nav>
|
|
|
|
{/* User Profile */}
|
|
<div className="absolute bottom-0 w-full p-4 border-t border-teal-700">
|
|
<div className={`flex items-center ${!isOpen && 'justify-center'}`}>
|
|
<div className="w-10 h-10 bg-teal-600 rounded-full flex items-center justify-center">
|
|
<User className="w-5 h-5" />
|
|
</div>
|
|
{isOpen && (
|
|
<div className="ml-3 flex-1">
|
|
<p className="text-sm font-medium">{user?.userName || 'User'}</p>
|
|
<p className="text-xs text-teal-200">{user?.userRole || 'Role'}</p>
|
|
</div>
|
|
)}
|
|
{isOpen && onLogout && (
|
|
<button
|
|
onClick={onLogout}
|
|
className="p-2 hover:bg-teal-700 rounded-lg transition-colors"
|
|
title="Logout"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}; |