// File Path: web/frontend/src/components/Layout/Layout.jsx // Fixed Layout Component - Mobile Menu Now Works Properly import React, { useState, useEffect, useCallback, useRef } from "react"; import TopNavbar from "./TopNavbar"; import Sidebar from "./Sidebar"; import { useInactivityTimeout } from "../../hooks/useInactivityTimeout"; import { useAuth } from "../../services/Services"; import { useUIXTheme } from "../UIX/themes/useUIXTheme"; function Layout({ children }) { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isMobile, setIsMobile] = useState(false); const [isTablet, setIsTablet] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [isIOS, setIsIOS] = useState(false); const [isAndroid, setIsAndroid] = useState(false); const [iosKeyboardHeight, setIosKeyboardHeight] = useState(0); const [androidKeyboardVisible, setAndroidKeyboardVisible] = useState(false); const { meManager } = useAuth(); const { switchTheme, getThemeClasses } = useUIXTheme(); // Initialize inactivity timeout - auto-logout after 15 minutes of inactivity // Will redirect to /logout when timeout is reached useInactivityTimeout(15 * 60 * 1000, true); // Load user's theme preference on mount (for authenticated pages) useEffect(() => { const loadThemePreference = async () => { try { const userProfile = await meManager.getCurrentUser(); if (userProfile?.themePreference) { if (import.meta.env.DEV) { console.log("Layout: Applying user's saved theme preference:", userProfile.themePreference); } switchTheme(userProfile.themePreference); } } catch (error) { if (import.meta.env.DEV) { console.error("Layout: Failed to load theme preference:", error); } // Don't block the UI if theme loading fails } }; loadThemePreference(); }, [meManager, switchTheme]); // Run when meManager is available // iOS detection function const detectIOS = () => { return ( /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) ); }; // Android detection function const detectAndroid = () => { return /Android/.test(navigator.userAgent); }; // Enhanced device detection with iOS and Android support (memoized) const updateDeviceType = useCallback(() => { const width = window.innerWidth; const mobile = width < 768; const tablet = width >= 768 && width < 1024; const ios = detectIOS(); const android = detectAndroid(); setIsMobile(mobile); setIsTablet(tablet); setIsIOS(ios); setIsAndroid(android); return { mobile, tablet, ios, android }; }, []); // Check localStorage for sidebar collapsed state useEffect(() => { const savedState = localStorage.getItem("sidebarCollapsed"); const { mobile } = updateDeviceType(); if (savedState === "true" && !mobile) { setSidebarCollapsed(true); } }, [updateDeviceType]); // iOS-specific optimizations useEffect(() => { if (!isIOS) return; // Fix iOS viewport height issues const setIOSViewportHeight = () => { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty("--vh", `${vh}px`); }; // iOS keyboard handling const handleIOSKeyboard = () => { const initialViewport = window.visualViewport?.height || window.innerHeight; const onViewportChange = () => { if (window.visualViewport) { const currentHeight = window.visualViewport.height; const keyboardHeight = initialViewport - currentHeight; setIosKeyboardHeight(keyboardHeight > 150 ? keyboardHeight : 0); } }; if (window.visualViewport) { window.visualViewport.addEventListener("resize", onViewportChange); return () => window.visualViewport.removeEventListener("resize", onViewportChange); } }; // Prevent iOS bounce/overscroll const preventBounce = (e) => { if (e.target.closest(".sidebar-content, .main-content")) return; e.preventDefault(); }; // Apply iOS fixes setIOSViewportHeight(); const keyboardCleanup = handleIOSKeyboard(); // Prevent zoom on double tap let lastTouchEnd = 0; const preventZoom = (e) => { const now = Date.now(); if (now - lastTouchEnd <= 300) { e.preventDefault(); } lastTouchEnd = now; }; // Add event listeners window.addEventListener("resize", setIOSViewportHeight); document.addEventListener("touchend", preventZoom, { passive: false }); document.addEventListener("touchmove", preventBounce, { passive: false }); return () => { window.removeEventListener("resize", setIOSViewportHeight); document.removeEventListener("touchend", preventZoom); document.removeEventListener("touchmove", preventBounce); keyboardCleanup?.(); }; }, [isIOS]); // Android-specific optimizations useEffect(() => { if (!isAndroid) return; // Android viewport height fixes for Chrome Mobile const setAndroidViewportHeight = () => { // Chrome on Android has dynamic toolbar behavior const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty("--android-vh", `${vh}px`); }; // Android keyboard detection (different approach than iOS) const handleAndroidKeyboard = () => { const initialHeight = window.innerHeight; let resizeTimer; const onResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { const currentHeight = window.innerHeight; const heightDifference = initialHeight - currentHeight; // Android keyboard typically reduces viewport by 150px+ const keyboardVisible = heightDifference > 150; setAndroidKeyboardVisible(keyboardVisible); // Update viewport height setAndroidViewportHeight(); }, 100); // Debounce for performance }; window.addEventListener("resize", onResize); return () => { window.removeEventListener("resize", onResize); clearTimeout(resizeTimer); }; }; // Android Chrome scroll behavior optimization const optimizeAndroidScroll = () => { // Prevent overscroll in Android Chrome const preventOverscroll = (e) => { const target = e.target; const scrollableParent = target.closest( ".scrollable, .main-content, .sidebar-content", ); if (!scrollableParent) { e.preventDefault(); return; } const { scrollTop, scrollHeight, clientHeight } = scrollableParent; const isAtTop = scrollTop === 0; const isAtBottom = scrollTop + clientHeight >= scrollHeight; if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) { e.preventDefault(); } }; // Android-specific touch optimizations const optimizeTouch = () => { // Improve scrolling performance on Android document.body.style.touchAction = "pan-y"; document.body.style.overscrollBehavior = "none"; }; optimizeTouch(); document.addEventListener("wheel", preventOverscroll, { passive: false }); return () => { document.removeEventListener("wheel", preventOverscroll); }; }; // Apply Android fixes setAndroidViewportHeight(); const keyboardCleanup = handleAndroidKeyboard(); const scrollCleanup = optimizeAndroidScroll(); return () => { keyboardCleanup?.(); scrollCleanup?.(); }; }, [isAndroid]); // Listen for storage changes to sync collapsed state useEffect(() => { const handleStorageChange = () => { const savedState = localStorage.getItem("sidebarCollapsed"); setSidebarCollapsed(savedState === "true"); }; window.addEventListener("storage", handleStorageChange); // Also listen for custom event for same-tab updates window.addEventListener("sidebarCollapsedChanged", handleStorageChange); return () => { window.removeEventListener("storage", handleStorageChange); window.removeEventListener( "sidebarCollapsedChanged", handleStorageChange, ); }; }, []); // FIXED: Resize handler with proper dependencies and refs to prevent infinite loops useEffect(() => { const handleResize = () => { const width = window.innerWidth; const wasMobile = isMobile; const mobile = width < 768; const tablet = width >= 768 && width < 1024; setIsMobile(mobile); setIsTablet(tablet); setIsIOS(detectIOS()); setIsAndroid(detectAndroid()); // Only close sidebar when transitioning FROM desktop TO mobile // Use callback to get current sidebar state to avoid stale closure setIsSidebarOpen(currentIsOpen => { if (!wasMobile && mobile && currentIsOpen) { return false; } return currentIsOpen; }); // Reset collapsed state when becoming mobile if (!wasMobile && mobile) { setSidebarCollapsed(false); } }; // Initial device detection handleResize(); // Only listen to actual window resize events window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [isMobile]); // Include isMobile but use functional state updates to avoid loops // Track last toggle time to prevent double-clicks const lastToggleRef = useRef(0); const handleMenuToggle = useCallback(() => { const now = Date.now(); // Prevent double-clicks only (100ms window) if (now - lastToggleRef.current < 100) { return; } lastToggleRef.current = now; setIsSidebarOpen(current => !current); }, []); const handleSidebarClose = useCallback(() => { setIsSidebarOpen(false); }, []); // Track last collapse toggle time to prevent double-clicks const lastCollapseToggleRef = useRef(0); // Handle collapse toggle for desktop view const handleCollapseToggle = useCallback(() => { const now = Date.now(); // Prevent double-clicks only (100ms window) if (now - lastCollapseToggleRef.current < 100) { return; } lastCollapseToggleRef.current = now; setSidebarCollapsed(current => { const newCollapsedState = !current; localStorage.setItem("sidebarCollapsed", newCollapsedState.toString()); window.dispatchEvent(new Event("sidebarCollapsedChanged")); return newCollapsedState; }); }, []); // Calculate sidebar margins based on device type and state const getSidebarMargin = () => { if (isMobile) return ""; if (isTablet) { // Tablet: smaller sidebar widths return sidebarCollapsed ? "ml-12" : "ml-48"; } // Desktop: full sidebar widths return sidebarCollapsed ? "ml-16" : "ml-64"; }; // Calculate responsive padding based on device const getContentPadding = () => { if (isMobile) { return "px-3 sm:px-4 py-4"; } if (isTablet) { return "px-4 md:px-6 py-5"; } // Desktop and larger screens return "px-4 sm:px-6 lg:px-8 py-6"; }; return (
{/* Top Navigation Bar */} {/* Sidebar */} {/* Main Content Area */}
0 ? `calc(var(--vh, 1vh) * 100 - 60px - ${iosKeyboardHeight}px)` : "calc(var(--vh, 1vh) * 100 - 60px)", WebkitOverflowScrolling: "touch", overflowY: "auto", }), ...(isAndroid && { minHeight: androidKeyboardVisible ? "calc(var(--android-vh, 1vh) * 50)" : "calc(var(--android-vh, 1vh) * 100 - 60px)", WebkitOverflowScrolling: "touch", overflowY: "auto", touchAction: "pan-y", overscrollBehavior: "contain", }), ...(!isIOS && !isAndroid && { minHeight: "calc(100vh - 60px)", }), }} > {/* Responsive margin based on device and sidebar state */}
{/* Content Container */}
0 ? `calc(var(--vh, 1vh) * 100 - 180px - ${iosKeyboardHeight}px)` : "calc(var(--vh, 1vh) * 100 - 120px)", touchAction: "pan-y", }), ...(isAndroid && isMobile && { minHeight: androidKeyboardVisible ? "calc(var(--android-vh, 1vh) * 40)" : "calc(var(--android-vh, 1vh) * 100 - 120px)", touchAction: "pan-y", overscrollBehavior: "contain", }), }} > {/* Responsive container for content */}
{children}
{/* Mobile overlay when sidebar is open - REMOVED the redundant one that was here */} {/* iOS and Android specific styles */} {(isIOS || isAndroid) && ( )}
); } export default Layout;