LSFE-Frontend/src/components/layout/Sidebar.tsx
rowell_m_soriano f568a8aa14
All checks were successful
Build and Deploy LSFE Frontend / build-and-deploy (push) Successful in 4m36s
for migration to new repo to be able to use the CICD
2026-07-30 13:56:40 +08:00

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>
</>
);
};