# Module: `src/components/Header.tsx` > **Source**: `src/components/Header.tsx` (133 lines) > **Topo batch**: B4 (depends on B3: `auth/AuthContext`, `components/FlightContext`, `components/HelpModal`, `types/index`) ## Purpose The persistent top navigation bar of the authenticated SPA. Combines five concerns into one component: brand mark, currently-selected-flight dropdown, top-level navigation links (permission-gated), session info (user email + Logout), language toggle (EN ↔ UA), and a `?` button that opens `HelpModal`. Also renders a duplicate bottom-nav on the mobile breakpoint. Replaces the legacy WPF top ribbon + window chrome (`_docs/legacy/wpf-era.md` §4 / §"What survived"). ## Public interface ```ts export default function Header(): JSX.Element ``` No props — Header reads everything it needs from `AuthContext`, `FlightContext`, and `react-i18next`. Mounted exactly once in `App.tsx` above the protected route tree. ## Internal logic - **Local state**: - `showDropdown: boolean` — flight selector open/closed. - `filter: string` — text filter for the flight selector list. - `showHelp: boolean` — controls the `HelpModal` `open` prop. - `dropdownRef: RefObject` — used to detect outside clicks. - **Outside-click effect**: registers a `mousedown` listener on `document` while mounted; if the event target is not inside `dropdownRef.current`, closes the dropdown. Listener is removed on unmount. (Always-on, even while the dropdown is closed — cheap, but not the most surgical pattern; flag to consider gating on `showDropdown` in Step 8.) - **Filtered flights**: `flights.filter(f => f.name.toLowerCase().includes(filter.toLowerCase()))` — case-insensitive substring match on the flight name. Empty filter shows everything. - **Logout**: `await logout(); navigate('/login')`. Always navigates, even if `logout()` throws (it never does — `AuthContext.logout` swallows network errors by design). - **Language toggle**: `i18n.changeLanguage(i18n.language === 'en' ? 'ua' : 'en')`. Same two-language assumption as `HelpModal` (treats every non-`'ua'` value as English-equivalent). The toggle button label is the *target* language ("UA" while EN is active, "EN" while UA is active). - **Permission-gated nav** (`navItems`): | `to` | `label` key | `perm` | |---|---|---| | `/flights` | `nav.flights` | `FL` | | `/annotations` | `nav.annotations` | `ANN` | | `/dataset` | `nav.dataset` | `DATASET` | | `/admin` | `nav.admin` | `ADM` | `Settings` (`/settings`) is always rendered — no permission gate. Filter applied via `navItems.filter(n => hasPermission(n.perm))`. - **Layout**: a single `
` containing brand → flight dropdown → primary nav (`hidden sm:flex`) → spacer (`flex-1`) → user email (`hidden sm:block`) → language toggle → `?` → `⚙` (settings link) → logout button. A second `