import { Outlet, useLocation } from 'react-router-dom'; import { useState, useEffect, useRef } from 'react'; import Navbar from './Navbar'; import Sidebar from './Sidebar'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { AlertTriangle, RefreshCw } from 'lucide-react'; import api from '../services/api'; import toast from 'react-hot-toast'; export default function Layout() { const [sidebarOpen, setSidebarOpen] = useState(false); const { user } = useAuth(); const { t } = useLanguage(); const [resendCooldown, setResendCooldown] = useState(0); const [resending, setResending] = useState(false); const location = useLocation(); const isFirstRoute = useRef(true); // On SPA route changes, move focus to the main region so keyboard and // screen-reader users land on the new page content instead of staying // on the link they clicked in the sidebar. useEffect(() => { if (isFirstRoute.current) { isFirstRoute.current = false; return; } document.getElementById('main-content')?.focus(); }, [location.pathname]); // Countdown timer for resend cooldown useEffect(() => { if (resendCooldown <= 0) return; const timer = setTimeout(() => setResendCooldown(c => c - 1), 1000); return () => clearTimeout(timer); }, [resendCooldown]); // Close the mobile sidebar drawer with Escape useEffect(() => { if (!sidebarOpen) return; const handleKey = (e) => { if (e.key === 'Escape') setSidebarOpen(false); }; document.addEventListener('keydown', handleKey); return () => document.removeEventListener('keydown', handleKey); }, [sidebarOpen]); const handleResendVerification = async () => { if (resendCooldown > 0 || resending) return; setResending(true); try { await api.post('/auth/resend-verification', { email: user.email }); toast.success(t('auth.emailVerificationResendSuccess')); setResendCooldown(60); } catch (err) { const wait = err.response?.data?.waitSeconds; if (wait) { setResendCooldown(wait); } toast.error(err.response?.data?.error || t('auth.emailVerificationResendFailed')); } finally { setResending(false); } }; return (
{/* Skip link — first tab stop, jumps past sidebar and navbar */} {t('common.skipToContent')} {/* Sidebar */} setSidebarOpen(false)} /> {/* Main content */}
setSidebarOpen(true)} sidebarOpen={sidebarOpen} /> {/* Email verification banner */} {user && user.email_verified === 0 && (
{t('auth.emailVerificationBanner')}
)}
{/* Mobile overlay */} {sidebarOpen && (
setSidebarOpen(false)} aria-hidden="true" /> )}
); }