import { useState, useEffect, useRef } from 'react'; import { NavLink, Outlet } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { LayoutDashboard, Smartphone, MessageSquare, Webhook, Key, FileText, ClipboardList, LogOut, Send, Server, Puzzle, Sun, Moon, Monitor, Menu, X, ChevronLeft, ChevronRight, Languages, } from 'lucide-react'; import { useTheme } from '../hooks/useTheme'; import { type UserRole } from '../hooks/useRole'; import { languageOptions, resolveSupportedLanguage, rtlLanguages, type SupportedLanguage } from '../i18n'; import { healthApi } from '../services/api'; import './Layout.css'; interface LayoutProps { onLogout: () => void; userRole: UserRole | null; } const allNavItems = [ { to: '/', icon: LayoutDashboard, key: 'dashboard' as const, adminOnly: false }, { to: '/sessions', icon: Smartphone, key: 'sessions' as const, adminOnly: false }, { to: '/chats', icon: MessageSquare, key: 'chats' as const, adminOnly: false }, { to: '/webhooks', icon: Webhook, key: 'webhooks' as const, adminOnly: false }, { to: '/templates', icon: ClipboardList, key: 'templates' as const, adminOnly: false }, { to: '/api-keys', icon: Key, key: 'apiKeys' as const, adminOnly: true }, { to: '/message-tester', icon: Send, key: 'messageTester' as const, adminOnly: false }, // Backend /infra/* is ADMIN-only; hide the nav item from non-admins (UX + defense-in-depth). { to: '/infrastructure', icon: Server, key: 'infrastructure' as const, adminOnly: true }, { to: '/plugins', icon: Puzzle, key: 'plugins' as const, adminOnly: true }, { to: '/logs', icon: FileText, key: 'logs' as const, adminOnly: false }, ]; const themeIcons = { light: Sun, dark: Moon, system: Monitor }; export function Layout({ onLogout, userRole }: LayoutProps) { const { t, i18n } = useTranslation(); const { theme, setTheme, resolvedTheme } = useTheme(); const ThemeIcon = themeIcons[theme]; const themeLabel = t(`theme.${theme}`); const navItems = allNavItems.filter(item => !item.adminOnly || userRole === 'admin'); const [isCollapsed, setIsCollapsed] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); // Show the build-time version immediately, then replace it with the live running version from the // backend so a stale-built bundle can't display the wrong number. Falls back silently on error. const [version, setVersion] = useState(__APP_VERSION__); const [isLanguageMenuOpen, setIsLanguageMenuOpen] = useState(false); const languageMenuRef = useRef(null); useEffect(() => { const handleResize = () => { const mobile = window.innerWidth < 768; setIsMobile(mobile); if (!mobile) setIsMobileOpen(false); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); useEffect(() => { let active = true; healthApi .check() .then(info => { if (active && info?.version) setVersion(info.version); }) .catch(() => { /* keep the build-time fallback */ }); return () => { active = false; }; }, []); const handleNavClick = () => { if (isMobile) setIsMobileOpen(false); }; useEffect(() => { document.body.style.overflow = isMobileOpen ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [isMobileOpen]); useEffect(() => { if (!isLanguageMenuOpen) return; const closeOnOutsideClick = (event: MouseEvent) => { if (!languageMenuRef.current?.contains(event.target as Node)) { setIsLanguageMenuOpen(false); } }; const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setIsLanguageMenuOpen(false); }; document.addEventListener('mousedown', closeOnOutsideClick); document.addEventListener('keydown', closeOnEscape); return () => { document.removeEventListener('mousedown', closeOnOutsideClick); document.removeEventListener('keydown', closeOnEscape); }; }, [isLanguageMenuOpen]); const toggleCollapse = () => setIsCollapsed(!isCollapsed); const toggleMobile = () => setIsMobileOpen(!isMobileOpen); const currentLang = resolveSupportedLanguage(i18n.resolvedLanguage || i18n.language); const languageLabel = languageOptions.find(option => option.value === currentLang)?.compactLabel ?? 'EN'; const changeLanguage = (language: SupportedLanguage) => { setIsLanguageMenuOpen(false); void i18n.changeLanguage(language); }; const isRtl = rtlLanguages.includes(currentLang); return (
{isMobile && (
OpenWA {t('common.appName')}
)} {isMobile && isMobileOpen &&
setIsMobileOpen(false)} />}
); }