import { useState, useCallback, useEffect, useRef } from "react"; import { Outlet, Navigate, useLocation } from "react-router-dom"; import { motion } from "framer-motion"; import Box from "@mui/material/Box"; import Drawer from "@mui/material/Drawer"; import IconButton from "@mui/material/IconButton"; import ScopedCssBaseline from "@mui/material/ScopedCssBaseline"; import { useAuth } from "../context/AuthContext"; import ThemeToggle from "./ThemeToggle"; import { setLogoutAlert } from "../utils/api"; import SidebarNav from "./SidebarNav"; import LoadingState from "./LoadingState"; import ShortcutsHelp from "../components/ShortcutsHelp"; const DRAWER_WIDTH = 248; export default function AppShell() { const { isAuthenticated, loading, user, logout } = useAuth(); const [mobileOpen, setMobileOpen] = useState(false); const [loggingOut, setLoggingOut] = useState(false); const location = useLocation(); const logoutTimer = useRef | undefined>( undefined, ); // Chat-style pages own the full mobile viewport: under md the shell header // disappears and main loses its padding — the page brings its own back // button. Desktop keeps the normal shell. const immersiveOnMobile = location.pathname === "/odin"; const handleLogout = useCallback(() => { setLoggingOut(true); setMobileOpen(false); setLogoutAlert(); // Delay so the blur/scale exit animation can play; store the id so an // unmount within the 400ms window cancels it (avoids logout() firing on // an unmounted tree). logoutTimer.current = setTimeout(() => logout(), 400); }, [logout]); useEffect(() => { return () => { if (logoutTimer.current) clearTimeout(logoutTimer.current); }; }, []); if (loading) { return ( ); } if (!isAuthenticated) return ; if (user?.require2FA && !user?.totpEnabled && location.pathname !== "/") { return ; } return ( setMobileOpen(false)} ModalProps={{ keepMounted: true }} sx={{ display: { xs: "block", md: "none" }, "& .MuiDrawer-paper": { width: DRAWER_WIDTH, boxSizing: "border-box", }, }} > setMobileOpen(false)} onLogout={handleLogout} /> setMobileOpen(true)} aria-label="Otevřít menu" sx={{ display: { md: "none" } }} > ); }