626 lines
25 KiB
TypeScript
626 lines
25 KiB
TypeScript
/**
|
|
* Navigation Component
|
|
* Styled following Netwerk Digitaal Erfgoed (NDE) house style
|
|
* With bilingual support (NL/EN)
|
|
*/
|
|
|
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import { useLanguage, translations } from '../../contexts/LanguageContext';
|
|
import { useUIState } from '../../contexts/UIStateContext';
|
|
import { isTargetInsideAny } from '../../utils/dom';
|
|
import './Navigation.css';
|
|
|
|
export function Navigation() {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { user, logout } = useAuth();
|
|
const { language, toggleLanguage } = useLanguage();
|
|
const { state, setTheme } = useUIState();
|
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
const [transitionState, setTransitionState] = useState<'idle' | 'collapsing' | 'expanding'>('idle');
|
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
|
const userMenuRef = useRef<HTMLDivElement>(null);
|
|
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Helper to set collapsed state with transition animation
|
|
const setCollapsedWithTransition = useCallback((newCollapsed: boolean) => {
|
|
setIsCollapsed(prevCollapsed => {
|
|
if (prevCollapsed === newCollapsed) return prevCollapsed;
|
|
|
|
// Trigger transition animation
|
|
setTransitionState(newCollapsed ? 'collapsing' : 'expanding');
|
|
|
|
// Clear transition state after animation completes
|
|
setTimeout(() => {
|
|
setTransitionState('idle');
|
|
}, 350);
|
|
|
|
return newCollapsed;
|
|
});
|
|
}, []);
|
|
|
|
// Check if we're on a page where scroll-up should NOT auto-expand header
|
|
// User must click the small logo to expand on these pages
|
|
const isNoAutoExpandPage = location.pathname.startsWith('/conversation') || location.pathname.startsWith('/linkml');
|
|
|
|
// Detect scrolling from ANY scrollable element on the page
|
|
// Also detect wheel events on non-scrollable areas
|
|
useEffect(() => {
|
|
let wheelDeltaAccumulator = 0;
|
|
let lastWheelTime = 0;
|
|
|
|
// Thresholds
|
|
const COLLAPSE_WHEEL_THRESHOLD = 40; // Wheel delta to collapse (easy)
|
|
const SCROLL_COLLAPSE_THRESHOLD = 20; // Scroll position to collapse
|
|
const WHEEL_RESET_DELAY = 300; // Reset wheel accumulator after this many ms of no wheel events
|
|
|
|
// Internal scrollable containers that should NOT trigger header collapse/expand
|
|
const IGNORED_SCROLL_CONTAINERS = [
|
|
'.navigation',
|
|
'.nav-mobile-menu',
|
|
'.conversation-chat__messages',
|
|
'.conversation-chat__history-list',
|
|
'.conversation-viz__content',
|
|
'.conversation-viz__table-container'
|
|
];
|
|
|
|
const handleScroll = (e: Event) => {
|
|
// Completely ignore scroll events from internal scrollable containers
|
|
if (isTargetInsideAny(e.target, IGNORED_SCROLL_CONTAINERS)) {
|
|
return;
|
|
}
|
|
|
|
// Get scroll position from the target element
|
|
const target = e.target as Element;
|
|
const scrollTop = target instanceof HTMLElement ? target.scrollTop : 0;
|
|
|
|
// Collapse header if scrolled past threshold (works everywhere)
|
|
if (scrollTop > SCROLL_COLLAPSE_THRESHOLD) {
|
|
setCollapsedWithTransition(true);
|
|
} else if (scrollTop <= 5) {
|
|
// On no-auto-expand pages: NEVER expand via scroll (user must click small logo)
|
|
// On other pages: expand when at the very top
|
|
if (!isNoAutoExpandPage) {
|
|
setCollapsedWithTransition(false);
|
|
wheelDeltaAccumulator = 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Wheel event handler for non-scrollable areas
|
|
const handleWheel = (e: WheelEvent) => {
|
|
// Ignore wheel events from internal scrollable containers
|
|
if (isTargetInsideAny(e.target, IGNORED_SCROLL_CONTAINERS)) {
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
|
|
// Reset accumulator if enough time has passed (prevents stale accumulation)
|
|
if (now - lastWheelTime > WHEEL_RESET_DELAY) {
|
|
wheelDeltaAccumulator = 0;
|
|
}
|
|
lastWheelTime = now;
|
|
|
|
// Accumulate wheel delta
|
|
wheelDeltaAccumulator += e.deltaY;
|
|
|
|
// Collapse on scroll down intent (easy threshold)
|
|
if (wheelDeltaAccumulator > COLLAPSE_WHEEL_THRESHOLD) {
|
|
setCollapsedWithTransition(true);
|
|
}
|
|
// Note: We never expand on wheel scroll-up. Use small logo click instead.
|
|
};
|
|
|
|
// Use capture phase to catch scroll events from any element
|
|
document.addEventListener('scroll', handleScroll, { capture: true, passive: true });
|
|
// Wheel events for non-scrollable areas
|
|
document.addEventListener('wheel', handleWheel, { passive: true });
|
|
|
|
return () => {
|
|
document.removeEventListener('scroll', handleScroll, { capture: true });
|
|
document.removeEventListener('wheel', handleWheel);
|
|
};
|
|
}, [setCollapsedWithTransition, isNoAutoExpandPage]);
|
|
|
|
// Determine effective theme (for icon display)
|
|
const getEffectiveTheme = () => {
|
|
if (state.theme === 'system') {
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
return state.theme;
|
|
};
|
|
const [effectiveTheme, setEffectiveTheme] = useState(getEffectiveTheme());
|
|
|
|
// Update effective theme when state.theme changes or system preference changes
|
|
useEffect(() => {
|
|
setEffectiveTheme(getEffectiveTheme());
|
|
|
|
if (state.theme === 'system') {
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
const handleChange = () => setEffectiveTheme(getEffectiveTheme());
|
|
mediaQuery.addEventListener('change', handleChange);
|
|
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
}
|
|
}, [state.theme]);
|
|
|
|
// Cycle through themes: light -> dark -> system -> light
|
|
const cycleTheme = () => {
|
|
const themeOrder: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system'];
|
|
const currentIndex = themeOrder.indexOf(state.theme);
|
|
const nextIndex = (currentIndex + 1) % themeOrder.length;
|
|
setTheme(themeOrder[nextIndex]);
|
|
};
|
|
|
|
// Get theme icon and tooltip
|
|
const getThemeIcon = () => {
|
|
if (state.theme === 'system') {
|
|
return effectiveTheme === 'dark' ? '🌙' : '☀️';
|
|
}
|
|
return state.theme === 'dark' ? '🌙' : '☀️';
|
|
};
|
|
|
|
const getThemeTooltip = () => {
|
|
const labels = {
|
|
light: language === 'nl' ? 'Licht thema (klik voor donker)' : 'Light theme (click for dark)',
|
|
dark: language === 'nl' ? 'Donker thema (klik voor systeem)' : 'Dark theme (click for system)',
|
|
system: language === 'nl' ? 'Systeemthema (klik voor licht)' : 'System theme (click for light)',
|
|
};
|
|
return labels[state.theme];
|
|
};
|
|
|
|
// Get data backend icon and tooltip
|
|
const getBackendIcon = () => {
|
|
const icons: Record<string, string> = {
|
|
'progressive': '🚀',
|
|
'geoapi': '🗄️',
|
|
'geoapi-lite': '⚡',
|
|
'ducklake': '🦆',
|
|
'auto': '🔄',
|
|
};
|
|
return icons[state.dataBackend] || '🔄';
|
|
};
|
|
|
|
const getBackendTooltip = () => {
|
|
const labels: Record<string, { nl: string; en: string }> = {
|
|
'progressive': {
|
|
nl: 'Progressive laden (cache-first)',
|
|
en: 'Progressive loading (cache-first)'
|
|
},
|
|
'geoapi': {
|
|
nl: 'GeoAPI (volledige data)',
|
|
en: 'GeoAPI (full data)'
|
|
},
|
|
'geoapi-lite': {
|
|
nl: 'GeoAPI Lite (snel)',
|
|
en: 'GeoAPI Lite (fast)'
|
|
},
|
|
'ducklake': {
|
|
nl: 'DuckLake (client-side)',
|
|
en: 'DuckLake (client-side)'
|
|
},
|
|
'auto': {
|
|
nl: 'Automatisch (beste beschikbaar)',
|
|
en: 'Auto (best available)'
|
|
},
|
|
};
|
|
const label = labels[state.dataBackend] || labels['auto'];
|
|
const clickHint = language === 'nl' ? 'Klik om te wijzigen' : 'Click to change';
|
|
return `${label[language]} - ${clickHint}`;
|
|
};
|
|
|
|
const getBackendLabel = () => {
|
|
const labels: Record<string, string> = {
|
|
'progressive': 'PRG',
|
|
'geoapi': 'API',
|
|
'geoapi-lite': 'LTE',
|
|
'ducklake': 'DLK',
|
|
'auto': 'AUT',
|
|
};
|
|
return labels[state.dataBackend] || 'AUT';
|
|
};
|
|
|
|
// Close menus when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
|
|
setUserMenuOpen(false);
|
|
}
|
|
if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) {
|
|
setMobileMenuOpen(false);
|
|
}
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setOpenDropdown(null);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
// Close mobile menu on route change and reset collapsed state
|
|
useEffect(() => {
|
|
setMobileMenuOpen(false);
|
|
setIsCollapsed(false);
|
|
setOpenDropdown(null);
|
|
}, [location.pathname]);
|
|
|
|
const isActive = (path: string) => {
|
|
return location.pathname === path;
|
|
};
|
|
|
|
// Check if any path in the submenu is active
|
|
const isSubmenuActive = (paths: string[]) => {
|
|
return paths.some(path => location.pathname === path);
|
|
};
|
|
|
|
// Toggle dropdown menu
|
|
const toggleDropdown = (menu: string) => {
|
|
setOpenDropdown(openDropdown === menu ? null : menu);
|
|
};
|
|
|
|
// Get translated nav text
|
|
const t = (key: keyof typeof translations.nav) => {
|
|
return language === 'en' ? translations.nav[key].en : translations.nav[key].nl;
|
|
};
|
|
|
|
return (
|
|
<nav className={`navigation ${isCollapsed ? 'navigation--collapsed' : ''} ${transitionState === 'collapsing' ? 'navigation--collapsing' : ''} ${transitionState === 'expanding' ? 'navigation--expanding' : ''}`}>
|
|
{/* Small floating logo - always visible, becomes prominent when collapsed */}
|
|
{/* Clicking expands the header (primary way to expand on conversation page) */}
|
|
<button
|
|
className="nav-logo-small"
|
|
onClick={() => setCollapsedWithTransition(false)}
|
|
aria-label="Expand navigation header"
|
|
title="Klik om header te tonen"
|
|
type="button"
|
|
>
|
|
<img
|
|
src="/nde-icon-square.png"
|
|
alt="NDE"
|
|
className="nav-logo-small-img"
|
|
/>
|
|
<span className="nav-logo-small-tooltip">Klik om header te tonen</span>
|
|
</button>
|
|
|
|
<div className="nav-container">
|
|
<Link to="/" className="nav-brand">
|
|
{/* Big NDE Logo - fades out when collapsed */}
|
|
<img
|
|
src="/nde-icon-square.png"
|
|
alt="Netwerk Digitaal Erfgoed Logo"
|
|
className="nav-logo"
|
|
/>
|
|
<span className="nav-title">Bronhouder</span>
|
|
</Link>
|
|
|
|
{/* Mobile Hamburger Button */}
|
|
<button
|
|
className="nav-mobile-toggle"
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
|
|
aria-expanded={mobileMenuOpen}
|
|
>
|
|
<span className={`hamburger ${mobileMenuOpen ? 'open' : ''}`}>
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</span>
|
|
</button>
|
|
|
|
{/* Desktop Navigation Links with Dropdowns */}
|
|
<div className="nav-links" ref={dropdownRef}>
|
|
{/* Custodian Menu */}
|
|
<div className="nav-dropdown">
|
|
<button
|
|
className={`nav-dropdown-trigger ${isSubmenuActive(['/visualize', '/map', '/browse', '/stats']) ? 'active' : ''}`}
|
|
onClick={() => toggleDropdown('custodian')}
|
|
aria-expanded={openDropdown === 'custodian'}
|
|
aria-haspopup="true"
|
|
>
|
|
{t('custodian')}
|
|
<span className="nav-dropdown-chevron">{openDropdown === 'custodian' ? '▲' : '▼'}</span>
|
|
</button>
|
|
{openDropdown === 'custodian' && (
|
|
<div className="nav-dropdown-menu">
|
|
<Link to="/visualize" className={`nav-dropdown-item ${isActive('/visualize') ? 'active' : ''}`}>
|
|
{t('visualize')}
|
|
</Link>
|
|
<Link to="/map" className={`nav-dropdown-item ${isActive('/map') ? 'active' : ''}`}>
|
|
{t('map')}
|
|
</Link>
|
|
<Link to="/browse" className={`nav-dropdown-item ${isActive('/browse') ? 'active' : ''}`}>
|
|
{t('browse')}
|
|
</Link>
|
|
<Link to="/stats" className={`nav-dropdown-item ${isActive('/stats') ? 'active' : ''}`}>
|
|
{t('stats')}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Ontology Menu */}
|
|
<div className="nav-dropdown">
|
|
<button
|
|
className={`nav-dropdown-trigger ${isSubmenuActive(['/linkml', '/datamap', '/ontology']) ? 'active' : ''}`}
|
|
onClick={() => toggleDropdown('ontology')}
|
|
aria-expanded={openDropdown === 'ontology'}
|
|
aria-haspopup="true"
|
|
>
|
|
{t('ontologyMain')}
|
|
<span className="nav-dropdown-chevron">{openDropdown === 'ontology' ? '▲' : '▼'}</span>
|
|
</button>
|
|
{openDropdown === 'ontology' && (
|
|
<div className="nav-dropdown-menu">
|
|
<Link to="/linkml" className={`nav-dropdown-item ${isActive('/linkml') ? 'active' : ''}`}>
|
|
{t('linkml')}
|
|
</Link>
|
|
<Link to="/datamap" className={`nav-dropdown-item ${isActive('/datamap') ? 'active' : ''}`}>
|
|
{t('datamap')}
|
|
</Link>
|
|
<Link to="/ontology" className={`nav-dropdown-item ${isActive('/ontology') ? 'active' : ''}`}>
|
|
{t('external')}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Query Menu */}
|
|
<div className="nav-dropdown">
|
|
<button
|
|
className={`nav-dropdown-trigger ${isSubmenuActive(['/query-builder', '/conversation']) ? 'active' : ''}`}
|
|
onClick={() => toggleDropdown('query')}
|
|
aria-expanded={openDropdown === 'query'}
|
|
aria-haspopup="true"
|
|
>
|
|
{t('queryMain')}
|
|
<span className="nav-dropdown-chevron">{openDropdown === 'query' ? '▲' : '▼'}</span>
|
|
</button>
|
|
{openDropdown === 'query' && (
|
|
<div className="nav-dropdown-menu">
|
|
<Link to="/query-builder" className={`nav-dropdown-item ${isActive('/query-builder') ? 'active' : ''}`}>
|
|
{t('search')}
|
|
</Link>
|
|
<Link to="/conversation" className={`nav-dropdown-item ${isActive('/conversation') ? 'active' : ''}`}>
|
|
{t('conversation')}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Settings Menu */}
|
|
<div className="nav-dropdown">
|
|
<button
|
|
className={`nav-dropdown-trigger ${isSubmenuActive(['/settings', '/roadmap', '/review']) ? 'active' : ''}`}
|
|
onClick={() => toggleDropdown('settings')}
|
|
aria-expanded={openDropdown === 'settings'}
|
|
aria-haspopup="true"
|
|
>
|
|
{t('settings')}
|
|
<span className="nav-dropdown-chevron">{openDropdown === 'settings' ? '▲' : '▼'}</span>
|
|
</button>
|
|
{openDropdown === 'settings' && (
|
|
<div className="nav-dropdown-menu">
|
|
<Link to="/roadmap" className={`nav-dropdown-item ${isActive('/roadmap') ? 'active' : ''}`}>
|
|
{t('roadmap')}
|
|
</Link>
|
|
<Link to="/settings" className={`nav-dropdown-item ${isActive('/settings') ? 'active' : ''}`}>
|
|
{t('preferences')}
|
|
</Link>
|
|
<a
|
|
href="https://bronhouder.nl/database"
|
|
className="nav-dropdown-item"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{t('database')}
|
|
<span className="nav-external-icon" aria-hidden="true">↗</span>
|
|
</a>
|
|
<Link to="/review" className={`nav-dropdown-item ${isActive('/review') ? 'active' : ''}`}>
|
|
{language === 'nl' ? 'Entity Review' : 'Entity Review'}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* User Account Dropdown - Desktop (includes settings toggles) */}
|
|
{user && (
|
|
<div className="nav-user nav-user--desktop" ref={userMenuRef}>
|
|
<button
|
|
className="nav-user-btn"
|
|
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
|
aria-expanded={userMenuOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
<span className="nav-user-icon">👤</span>
|
|
<span className="nav-user-name">{user.username}</span>
|
|
<span className="nav-user-chevron">{userMenuOpen ? '▲' : '▼'}</span>
|
|
</button>
|
|
{userMenuOpen && (
|
|
<div className="nav-user-menu">
|
|
<div className="nav-user-info">
|
|
<span className="nav-user-role">{user.role}</span>
|
|
</div>
|
|
|
|
{/* Settings Section in User Menu */}
|
|
<div className="nav-user-settings">
|
|
<div className="nav-user-settings-header">
|
|
{language === 'nl' ? 'Snel instellen' : 'Quick Settings'}
|
|
</div>
|
|
{/* Language Toggle */}
|
|
<button
|
|
className="nav-user-setting-item"
|
|
onClick={toggleLanguage}
|
|
aria-label={language === 'nl' ? 'Switch to English' : 'Schakel naar Nederlands'}
|
|
>
|
|
<span className="nav-user-setting-label">{language === 'nl' ? 'Taal' : 'Language'}</span>
|
|
<span className="nav-user-setting-value">
|
|
<span className={language === 'nl' ? 'lang-active' : 'lang-inactive'}>NL</span>
|
|
<span className="lang-separator">|</span>
|
|
<span className={language === 'en' ? 'lang-active' : 'lang-inactive'}>EN</span>
|
|
</span>
|
|
</button>
|
|
|
|
{/* Theme Toggle */}
|
|
<button
|
|
className="nav-user-setting-item"
|
|
onClick={cycleTheme}
|
|
title={getThemeTooltip()}
|
|
>
|
|
<span className="nav-user-setting-label">{language === 'nl' ? 'Thema' : 'Theme'}</span>
|
|
<span className="nav-user-setting-value">
|
|
<span className="theme-icon">{getThemeIcon()}</span>
|
|
{state.theme === 'system' && <span className="theme-auto-badge">A</span>}
|
|
</span>
|
|
</button>
|
|
|
|
{/* Backend Indicator */}
|
|
<button
|
|
className="nav-user-setting-item"
|
|
onClick={() => { setUserMenuOpen(false); navigate('/settings'); }}
|
|
title={getBackendTooltip()}
|
|
>
|
|
<span className="nav-user-setting-label">{language === 'nl' ? 'Backend' : 'Backend'}</span>
|
|
<span className="nav-user-setting-value">
|
|
<span className="backend-icon">{getBackendIcon()}</span>
|
|
<span className="backend-label">{getBackendLabel()}</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<button onClick={logout} className="nav-user-logout">
|
|
{t('signOut')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile Menu Overlay */}
|
|
<div
|
|
className={`nav-mobile-menu ${mobileMenuOpen ? 'open' : ''}`}
|
|
ref={mobileMenuRef}
|
|
>
|
|
<div className="nav-mobile-links">
|
|
{/* Custodian Section */}
|
|
<div className="nav-mobile-section">
|
|
<div className="nav-mobile-section-title">{t('custodian')}</div>
|
|
<Link to="/visualize" className={`nav-mobile-link ${isActive('/visualize') ? 'active' : ''}`}>
|
|
{t('visualize')}
|
|
</Link>
|
|
<Link to="/map" className={`nav-mobile-link ${isActive('/map') ? 'active' : ''}`}>
|
|
{t('map')}
|
|
</Link>
|
|
<Link to="/browse" className={`nav-mobile-link ${isActive('/browse') ? 'active' : ''}`}>
|
|
{t('browse')}
|
|
</Link>
|
|
<Link to="/stats" className={`nav-mobile-link ${isActive('/stats') ? 'active' : ''}`}>
|
|
{t('stats')}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Ontology Section */}
|
|
<div className="nav-mobile-section">
|
|
<div className="nav-mobile-section-title">{t('ontologyMain')}</div>
|
|
<Link to="/linkml" className={`nav-mobile-link ${isActive('/linkml') ? 'active' : ''}`}>
|
|
{t('linkml')}
|
|
</Link>
|
|
<Link to="/datamap" className={`nav-mobile-link ${isActive('/datamap') ? 'active' : ''}`}>
|
|
{t('datamap')}
|
|
</Link>
|
|
<Link to="/ontology" className={`nav-mobile-link ${isActive('/ontology') ? 'active' : ''}`}>
|
|
{t('external')}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Query Section */}
|
|
<div className="nav-mobile-section">
|
|
<div className="nav-mobile-section-title">{t('queryMain')}</div>
|
|
<Link to="/query-builder" className={`nav-mobile-link ${isActive('/query-builder') ? 'active' : ''}`}>
|
|
{t('search')}
|
|
</Link>
|
|
<Link to="/conversation" className={`nav-mobile-link ${isActive('/conversation') ? 'active' : ''}`}>
|
|
{t('conversation')}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Settings Section */}
|
|
<div className="nav-mobile-section">
|
|
<div className="nav-mobile-section-title">{t('settings')}</div>
|
|
<Link to="/roadmap" className={`nav-mobile-link ${isActive('/roadmap') ? 'active' : ''}`}>
|
|
{t('roadmap')}
|
|
</Link>
|
|
<Link to="/settings" className={`nav-mobile-link ${isActive('/settings') ? 'active' : ''}`}>
|
|
{t('preferences')}
|
|
</Link>
|
|
<a
|
|
href="https://bronhouder.nl/database"
|
|
className="nav-mobile-link"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{t('database')}
|
|
<span className="nav-external-icon" aria-hidden="true">↗</span>
|
|
</a>
|
|
<Link to="/review" className={`nav-mobile-link ${isActive('/review') ? 'active' : ''}`}>
|
|
{language === 'nl' ? 'Entity Review' : 'Entity Review'}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Language Toggle & Theme Toggle & Backend - Mobile */}
|
|
<div className="nav-mobile-footer">
|
|
<div className="nav-mobile-toggles">
|
|
<button
|
|
className="nav-lang-toggle"
|
|
onClick={toggleLanguage}
|
|
aria-label={language === 'nl' ? 'Switch to English' : 'Schakel naar Nederlands'}
|
|
>
|
|
<span className={language === 'nl' ? 'lang-active' : 'lang-inactive'}>NL</span>
|
|
<span className="lang-separator">|</span>
|
|
<span className={language === 'en' ? 'lang-active' : 'lang-inactive'}>EN</span>
|
|
</button>
|
|
|
|
<button
|
|
className="nav-theme-toggle"
|
|
onClick={cycleTheme}
|
|
aria-label={getThemeTooltip()}
|
|
title={getThemeTooltip()}
|
|
>
|
|
<span className="theme-icon">{getThemeIcon()}</span>
|
|
{state.theme === 'system' && <span className="theme-auto-badge">A</span>}
|
|
</button>
|
|
|
|
<button
|
|
className="nav-backend-indicator"
|
|
onClick={() => { setMobileMenuOpen(false); navigate('/settings'); }}
|
|
aria-label={getBackendTooltip()}
|
|
title={getBackendTooltip()}
|
|
>
|
|
<span className="backend-icon">{getBackendIcon()}</span>
|
|
<span className="backend-label">{getBackendLabel()}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* User Account - Mobile */}
|
|
{user && (
|
|
<div className="nav-mobile-user">
|
|
<span className="nav-user-icon">👤</span>
|
|
<span className="nav-user-name">{user.username}</span>
|
|
<span className="nav-user-role">({user.role})</span>
|
|
<button onClick={logout} className="nav-mobile-logout">
|
|
{t('signOut')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|