import { ArrowUpDown, Check, ChevronDown, Clock3, Cloud, FileClock, Folder as FolderIcon, Globe2, KeyRound, Lock, LogOut, MonitorSmartphone, Send as SendIcon, Settings as SettingsIcon, ShieldUser, SlidersHorizontal, Users } from 'lucide-preact'; import type { ComponentChildren } from 'preact'; import { useEffect, useRef, useState } from 'preact/hooks'; import { Link } from 'wouter'; import AppMainRoutes from '@/components/AppMainRoutes'; import NetworkStatusBadge from '@/components/NetworkStatusBadge'; import ThemeSwitch from '@/components/ThemeSwitch'; import type { AppMainRoutesProps } from '@/components/AppMainRoutes'; import { t } from '@/lib/i18n'; import type { Profile } from '@/lib/types'; interface AppAuthenticatedShellProps { profile: Profile | null; location: string; mobilePrimaryRoute: string; currentPageTitle: string; showSidebarToggle: boolean; sidebarToggleTitle: string; settingsAccountRoute: string; importRoute: string; isImportRoute: boolean; darkMode: boolean; themeToggleTitle: string; onLock: () => void; onLogout: () => void; onToggleTheme: () => void; onToggleMobileSidebar: () => void; mainRoutesProps: AppMainRoutesProps; } type NavLayoutMode = 'flat' | 'grouped-expanded' | 'grouped-smart'; const NAV_LAYOUT_STORAGE_KEY = 'nodewarden.navLayoutMode'; function readNavLayoutMode(): NavLayoutMode { if (typeof window === 'undefined') return 'flat'; try { const saved = window.localStorage.getItem(NAV_LAYOUT_STORAGE_KEY); if (saved === 'flat' || saved === 'grouped-expanded' || saved === 'grouped-smart') return saved; } catch { // Ignore local preference read failures. } return 'flat'; } function isAdminProfile(profile: Profile | null): boolean { return String(profile?.role || '').toLowerCase() === 'admin'; } const DEVICE_MANAGEMENT_ROUTE = '/settings/security/device-management'; const LEGACY_DEVICE_MANAGEMENT_ROUTE = '/security/devices'; export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps) { const routeAnimationKey = props.isImportRoute ? props.importRoute : props.location; const isDomainRulesRoute = props.location === '/settings/domain-rules'; const isLogRoute = props.location === '/logs'; const isAdmin = isAdminProfile(props.profile); const vaultActive = props.location === '/vault' || props.location === '/vault/totp'; const settingsActive = props.location === props.settingsAccountRoute || props.location === '/settings/domain-rules'; const dataActive = props.location === '/backup' || props.isImportRoute; const deviceManagementActive = props.location === DEVICE_MANAGEMENT_ROUTE || props.location === LEGACY_DEVICE_MANAGEMENT_ROUTE; const managementActive = props.location === '/admin' || deviceManagementActive || props.location === '/logs'; const [navLayoutMode, setNavLayoutMode] = useState(readNavLayoutMode); const [navLayoutPickerOpen, setNavLayoutPickerOpen] = useState(false); const navLayoutPickerRef = useRef(null); const [expandedGroups, setExpandedGroups] = useState({ vault: true, settings: false, data: false, management: false, }); useEffect(() => { const onPointerDown = (event: Event) => { if (!navLayoutPickerOpen) return; const target = event.target as Node | null; if (navLayoutPickerRef.current && target && !navLayoutPickerRef.current.contains(target)) { setNavLayoutPickerOpen(false); } }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setNavLayoutPickerOpen(false); }; document.addEventListener('pointerdown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('pointerdown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [navLayoutPickerOpen]); function setNavMode(mode: NavLayoutMode): void { setNavLayoutMode(mode); setNavLayoutPickerOpen(false); try { window.localStorage.setItem(NAV_LAYOUT_STORAGE_KEY, mode); } catch { // Ignore local preference write failures. } } function toggleGroup(group: keyof typeof expandedGroups): void { setExpandedGroups((current) => ({ ...current, [group]: !current[group] })); } function groupOpen(group: keyof typeof expandedGroups, active: boolean): boolean { if (navLayoutMode === 'grouped-expanded') return true; return expandedGroups[group] || active; } function renderSideLink(href: string, active: boolean, icon: ComponentChildren, label: string) { return ( {icon} {label} ); } function renderSubLink(href: string, active: boolean, label: string) { return ( {label} ); } function renderNavGroup( group: keyof typeof expandedGroups, title: string, icon: ComponentChildren, active: boolean, children: ComponentChildren ) { const open = groupOpen(group, active); return (
{children}
); } const navLayoutOptions: Array<{ mode: NavLayoutMode; label: string }> = [ { mode: 'flat', label: t('txt_nav_layout_flat'), }, { mode: 'grouped-expanded', label: t('txt_nav_layout_grouped_expanded'), }, { mode: 'grouped-smart', label: t('txt_nav_layout_grouped_smart'), }, ]; const navLayoutLabel = navLayoutOptions.find((option) => option.mode === navLayoutMode)?.label || t('txt_nav_layout_flat'); const flatNav = ( <> {renderSideLink('/vault', props.location === '/vault', , t('nav_vault_items'))} {renderSideLink('/vault/totp', props.location === '/vault/totp', , t('txt_verification_code'))} {renderSideLink('/sends', props.location === '/sends', , t('nav_sends'))} {renderSideLink(props.settingsAccountRoute, props.location === props.settingsAccountRoute, , t('nav_account_settings'))} {renderSideLink('/settings/domain-rules', props.location === '/settings/domain-rules', , t('nav_domain_rules'))} {isAdmin && renderSideLink('/backup', props.location === '/backup', , t('nav_backup_strategy'))} {renderSideLink(props.importRoute, props.isImportRoute, , t('nav_import_export'))} {isAdmin && renderSideLink('/admin', props.location === '/admin', , t('nav_admin_panel'))} {isAdmin && renderSideLink('/logs', props.location === '/logs', , t('nav_log_center'))} {renderSideLink(DEVICE_MANAGEMENT_ROUTE, deviceManagementActive, , t('nav_device_management'))} ); const groupedNav = ( <> {renderNavGroup( 'vault', t('nav_my_vault'), , vaultActive, <> {renderSubLink('/vault', props.location === '/vault', t('nav_vault_items'))} {renderSubLink('/vault/totp', props.location === '/vault/totp', t('txt_verification_code'))} )} {renderSideLink('/sends', props.location === '/sends', , t('nav_sends'))} {renderNavGroup( 'settings', t('txt_settings'), , settingsActive, <> {renderSubLink(props.settingsAccountRoute, props.location === props.settingsAccountRoute, t('nav_account_settings'))} {renderSubLink('/settings/domain-rules', props.location === '/settings/domain-rules', t('nav_domain_rules'))} )} {renderNavGroup( 'data', t('nav_group_data_backup'), , dataActive, <> {isAdmin && renderSubLink('/backup', props.location === '/backup', t('nav_backup_strategy'))} {renderSubLink(props.importRoute, props.isImportRoute, t('nav_import_export'))} )} {renderNavGroup( 'management', t('nav_group_management'), , managementActive, <> {isAdmin && renderSubLink('/admin', props.location === '/admin', t('nav_admin_panel'))} {isAdmin && renderSubLink('/logs', props.location === '/logs', t('nav_log_center'))} {renderSubLink(DEVICE_MANAGEMENT_ROUTE, deviceManagementActive, t('nav_device_management'))} )} ); return (
NodeWarden logo {props.currentPageTitle}
{props.profile?.email}
{props.showSidebarToggle && ( )}
); }