import React, { useState, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import { Link, useNavigate, useLocation } from 'react-router' import { useAuthStore } from '../../store/authStore' import { useSettingsStore } from '../../store/settingsStore' import { useAddonStore } from '../../store/addonStore' import { usePluginStore } from '../../store/pluginStore' import { useTranslation } from '../../i18n' import { Plane, LogOut, Settings, ChevronDown, Shield, ArrowLeft, ChevronLeft, Users, Moon, Sun, Monitor, CalendarDays, Briefcase, Globe, Compass, BookOpen, Bookmark } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import InAppNotificationBell from './InAppNotificationBell.tsx' import HelpButton from '../Help/HelpButton' import { resolvePluginIcon } from '../shared/PluginIcon' import { visibleManagedNavItems } from '../../managed' import { useCollapseOnOverflow } from '../../hooks/useCollapseOnOverflow' import { useIsDark } from '../../hooks/useIsDark' const ADDON_ICONS: Record = { CalendarDays, Briefcase, Globe, Compass, Bookmark } interface NavbarProps { tripTitle?: string tripId?: number | string onBack?: () => void showBack?: boolean onShare?: () => void } interface Addon { id: string name: string icon: string type: string enabled: boolean } export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: NavbarProps): React.ReactElement { const { user, logout, isPrerelease, appVersion } = useAuthStore() const { settings, updateSetting } = useSettingsStore() const { addons: allAddons, loadAddons } = useAddonStore() const { t, locale } = useTranslation() const navigate = useNavigate() const location = useLocation() const [userMenuOpen, setUserMenuOpen] = useState(false) const [scrolled, setScrolled] = useState(false) // Off the .dark class: a special theme is dark whatever the stored mode says. const dark = useIsDark() useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8 || (document.body.scrollTop || 0) > 8) onScroll() window.addEventListener('scroll', onScroll, { passive: true }) document.body.addEventListener('scroll', onScroll, { passive: true }) return () => { window.removeEventListener('scroll', onScroll) document.body.removeEventListener('scroll', onScroll) } }, []) // Only show 'global' type addons in the navbar — 'integration' addons have no dedicated page const globalAddons = allAddons.filter((a: Addon) => a.type === 'global' && a.enabled) const pagePlugins = usePluginStore(s => s.plugins).filter(p => p.type === 'page') useEffect(() => { if (user) loadAddons() }, [user, location.pathname]) const handleLogout = () => { logout() navigate('/login', { state: { noRedirect: true } }) } // Keep track of the pending theme-transition cleanup so we can cancel it // on unmount. Without this the timer fires after jsdom teardown in unit // tests (document is gone) and triggers an unhandled ReferenceError that // trips vitest's exit code. const themeTransitionTimer = useRef(null) useEffect(() => () => { if (themeTransitionTimer.current !== null) { window.clearTimeout(themeTransitionTimer.current) themeTransitionTimer.current = null } }, []) const toggleDarkMode = () => { document.documentElement.classList.add('trek-theme-transitioning') updateSetting('dark_mode', dark ? 'light' : 'dark').catch(() => {}) if (themeTransitionTimer.current !== null) window.clearTimeout(themeTransitionTimer.current) themeTransitionTimer.current = window.setTimeout(() => { document.documentElement.classList.remove('trek-theme-transitioning') themeTransitionTimer.current = null }, 360) } const getAddonName = (addon: Addon): string => { const key = `admin.addons.catalog.${addon.id}.name` const translated = t(key) return translated !== key ? translated : addon.name } const tabs = [{ id: '__trips', path: '/dashboard', label: t('nav.myTrips'), Icon: Briefcase }, ...globalAddons.map(a => ({ id: a.id, path: `/${a.id}`, label: getAddonName(a), Icon: ADDON_ICONS[a.icon] || CalendarDays })), ...pagePlugins.map(p => ({ id: `plugin:${p.id}`, path: `/plugins/${p.id}`, label: p.name, Icon: resolvePluginIcon(p.icon) })), // Empty in this repository — see client/src/managed. ...visibleManagedNavItems(user?.role === 'admin').map(m => ({ id: `managed:${m.id}`, path: m.path, label: m.label, Icon: m.Icon })) ] // Labels go when they no longer fit beside the logo and the actions; the // title and aria-label on each tab keep it named. const { ref: pillRef, collapsed: pillCollapsed } = useCollapseOnOverflow(tabs.map(tab => tab.label).join('|')) return ( ) }