import { useState, useRef } from 'react'; import { NavLink, useNavigate } from 'react-router-dom'; import { useTranslation } from '../contexts/LanguageContext'; import { useSidebar } from '../contexts/SidebarContext'; import { useViewMode } from '../contexts/ViewModeContext'; import { useAuth } from '../contexts/AuthContext'; import { useModules } from '../contexts/ModulesContext'; import { useSiteConfig } from '../contexts/SiteConfigContext'; import { useTheme } from '../contexts/ThemeContext'; import { useNotifications } from '../contexts/NotificationsContext'; import { appModules } from '../modules'; import UserMenu from './UserMenu'; import '../styles/Sidebar.css'; export default function Sidebar() { const { t, language, setLanguage } = useTranslation(); const { config } = useSiteConfig(); const { sidebarStyle, theme, toggleTheme, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle, hasInitializedSettings: themeInitialized } = useTheme(); const { isCollapsed, isMobileOpen, sidebarMode, toggleCollapse, closeMobileMenu, isHovered, setIsHovered, showLogo: showLogoContext } = useSidebar(); const { viewMode, toggleViewMode, isUserModeEnabled } = useViewMode(); const { user } = useAuth(); const { isModuleEnabled, isModuleEnabledForUser, moduleOrder, moduleStates, hasInitialized: modulesInitialized } = useModules(); const { unreadCount } = useNotifications(); // When admin is in "user mode", show only user-permitted modules // Otherwise, show all globally enabled modules (admin view) const shouldUseUserPermissions = viewMode === 'user' || !user?.is_superuser; // Don't show modules until initialization is complete to prevent flash const mainModulesFiltered = !modulesInitialized ? [] : (appModules .find((cat) => cat.id === 'main') ?.modules.filter((m) => { if (!m.enabled) return false; if (shouldUseUserPermissions) { return isModuleEnabledForUser(m.id, user?.permissions, user?.is_superuser || false); } return isModuleEnabled(m.id); }) || []); // Sort modules based on moduleOrder const sortedModules = [...mainModulesFiltered].sort((a, b) => { const aIndex = moduleOrder.indexOf(a.id); const bIndex = moduleOrder.indexOf(b.id); // If both are in the order array, sort by their position if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; // If only one is in the order array, it comes first if (aIndex !== -1) return -1; if (bIndex !== -1) return 1; // If neither is in the order array, maintain original order return 0; }); // Split modules by position (top = main nav, bottom = above footer) const topModules = sortedModules.filter(m => { const state = moduleStates[m.id as keyof typeof moduleStates]; return !state || state.position === 'top'; }); const bottomModules = sortedModules.filter(m => { const state = moduleStates[m.id as keyof typeof moduleStates]; return state && state.position === 'bottom'; }); const handleCollapseClick = () => { if (isMobileOpen) { closeMobileMenu(); } else { toggleCollapse(); } }; const navigate = useNavigate(); const handleNavClick = (e: React.MouseEvent, path: string) => { // Close mobile menu when clicking navigation items if (isMobileOpen) { e.preventDefault(); e.stopPropagation(); closeMobileMenu(); setTimeout(() => { navigate(path); }, 400); } }; const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const hoverTimeoutRef = useRef | null>(null); const handleMouseEnter = () => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = null; } if (sidebarMode === 'dynamic' && isCollapsed && !isMobileOpen) { setIsHovered(true); } }; const handleMouseLeave = () => { hoverTimeoutRef.current = setTimeout(() => { setIsHovered(false); // In toggle mode, collapse sidebar when mouse leaves (if expanded) if (sidebarMode === 'toggle' && !isCollapsed && !isMobileOpen) { toggleCollapse(); } }, 100); }; // derived state for expansion in dynamic mode const isDynamicExpanded = sidebarMode === 'dynamic' && isCollapsed && (isHovered || isUserMenuOpen); const handleSidebarClick = (e: React.MouseEvent) => { // Only toggle if in toggle mode and not on mobile if (sidebarMode === 'toggle' && !isMobileOpen) { // Check if the click target is an interactive element or inside one const target = e.target as HTMLElement; const isInteractive = target.closest('a, button, [role="button"], input, select, textarea'); if (!isInteractive) { toggleCollapse(); } } }; // Logo logic - use white logo on dark backgrounds, black logo on light backgrounds // sidebarStyle 'default' and 'dark' both use dark sidebar background // Only 'light' sidebarStyle uses a light background const isLightSidebar = sidebarStyle === 'light'; const logoSrc = isLightSidebar ? '/logo_black.svg' : '/logo_white.svg'; // Show toggle button ONLY on mobile const showToggle = isMobileOpen; // Show logo only if enabled in config AND toggle button is not present const showLogo = showLogoContext && !showToggle; const [tooltip, setTooltip] = useState<{ text: string; top: number; visible: boolean }>({ text: '', top: 0, visible: false, }); const handleItemMouseEnter = (text: string, e: React.MouseEvent) => { if (isCollapsed && !isMobileOpen && sidebarMode !== 'dynamic') { const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); setTooltip({ text, top: rect.top + rect.height / 2, visible: true, }); } }; const handleItemMouseLeave = () => { setTooltip((prev) => ({ ...prev, visible: false })); }; const updateTooltipText = (text: string) => { setTooltip((prev) => prev.visible ? { ...prev, text } : prev); }; return ( <> {/* Mobile overlay */}
{/* Sidebar Tooltip */} {tooltip.visible && (
{tooltip.text}
)} ); }