1/** 2 * Centralized Navigation Component 3 * Manages navigation bar rendering and active state across all pages 4 */ 5 6class NavigationBar { 7 constructor() { 8 this.currentPath = window.location.pathname; 9 this.navItems = [ 10 { href: '/', label: 'Home', key: 'home', translationKey: 'nav.home' }, 11 { href: '/solar-activity', label: 'Solar Activity', key: 'solar-activity', translationKey: 'nav.solarActivity' }, 12 { href: '/aurora-map', label: 'Aurora Map', key: 'aurora-map', translationKey: 'nav.auroraMap' }, 13 { href: '/forecast', label: 'Forecast', key: 'forecast', translationKey: 'nav.forecast' }, 14 { href: '/northern-lights-guide', label: 'Aurorapedia', key: 'aurorapedia', translationKey: 'nav.aurorapedia' }, 15 { href: '/app', label: 'App', key: 'app', translationKey: 'nav.app' } 16 ]; 17 // Determine logo path based on current location 18 this.logoPath = this.getLogoPath(); 19 20 // Listen for language changes 21 window.addEventListener('languagechange', () => { 22 this.updateNavLabels(); 23 }); 24 } 25 26 /** 27 * Gets the correct logo path based on current page location 28 */ 29 getLogoPath() { 30 const path = window.location.pathname; 31 // If in /app or /guide subdirectory, use ../ 32 if (path.startsWith('/app') || path.startsWith('/guide')) { 33 return '../aurora_rounded.png'; 34 } 35 return 'aurora_rounded.png'; 36 } 37 38 /** 39 * Determines if a navigation item should be active 40 */ 41 isActive(href) { 42 // Normalize paths for comparison. Works with both clean URLs (/forecast, 43 // as served by Cloudflare Pages) and legacy .html paths (e.g. when opened 44 // directly or via a local static server). 45 const normalize = (p) => { 46 if (!p || p === '') return '/'; 47 const stripped = p.replace(/\.html$/, '').replace(/\/index$/, '').replace(/\/+$/, ''); 48 return stripped === '' ? '/' : stripped; 49 }; 50 const currentPath = normalize(this.currentPath); 51 const normalizedHref = normalize(href); 52 53 if (normalizedHref === '/') return currentPath === '/'; 54 return currentPath === normalizedHref || currentPath.startsWith(normalizedHref + '/'); 55 } 56 57 /** 58 * Renders the navigation HTML 59 */ 60 render() { 61 const navHTML = ` 62 <nav class="main-nav"> 63 <div class="nav-container"> 64 <a href="/" class="nav-logo"> 65 <img src="${this.logoPath}" alt="Aurora" class="nav-logo-img" /> 66 Northern Lights Tracker 67 </a> 68 <button class="nav-toggle" aria-label="Toggle navigation" id="navToggle"> 69 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"> 70 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/> 71 </svg> 72 </button> 73 <ul class="nav-menu" id="navMenu"> 74 ${this.renderNavItems()} 75 <li class="nav-divider"></li> 76 ${this.renderLanguageSwitcher()} 77 </ul> 78 </div> 79 </nav> 80 `; 81 82 return navHTML; 83 } 84 85 /** 86 * Renders individual navigation items 87 */ 88 renderNavItems() { 89 return this.navItems.map(item => { 90 const activeClass = this.isActive(item.href) ? ' nav-link--active' : ''; 91 const label = window.i18n ? window.i18n.t(item.translationKey) : item.label; 92 return `<li><a href="${item.href}" class="nav-link${activeClass}" data-nav-key="${item.key}">${label}</a></li>`; 93 }).join(''); 94 } 95 96 /** 97 * Renders the language switcher 98 */ 99 renderLanguageSwitcher() { 100 // Get current language with fallback 101 let currentLang = 'en'; 102 if (window.i18n && !window.i18nInitializing) { 103 currentLang = window.i18n.currentLang; 104 } else { 105 // Check localStorage for initial render 106 const stored = localStorage.getItem('aurora-lang'); 107 if (stored) currentLang = stored; 108 } 109 110 const languages = [ 111 { code: 'en', name: 'English', flag: 'ðºð¸' }, 112 { code: 'de', name: 'Deutsch', flag: 'ð©ðª' }, 113 { code: 'es', name: 'Español', flag: 'ðªð¸' }, 114 { code: 'fr', name: 'Français', flag: 'ð«ð·' }, 115 { code: 'no', name: 'Norsk', flag: 'ð³ð´' }, 116 { code: 'sv', name: 'Svenska', flag: 'ð¸ðª' }, 117 { code: 'fi', name: 'Suomi', flag: 'ð«ð®' }, 118 { code: 'ja', name: 'æ¥æ¬èª', flag: 'ð¯ðµ' }, 119 { code: 'zh', name: '䏿', flag: 'ð¨ð³' }, 120 { code: 'it', name: 'Italiano', flag: 'ð®ð¹' }, 121 { code: 'nl', name: 'Nederlands', flag: 'ð³ð±' }, 122 { code: 'ru', name: 'Ð ÑÑÑкий', flag: 'ð·ðº' }, 123 { code: 'ko', name: 'íêµì´', flag: 'ð°ð·' }, 124 { code: 'pt', name: 'Português', flag: 'ðµð¹' }, 125 { code: 'pl', name: 'Polski', flag: 'ðµð±' }, 126 { code: 'da', name: 'Dansk', flag: 'ð©ð°' }, 127 { code: 'tr', name: 'Türkçe', flag: 'ð¹ð·' } 128 ]; 129 130 const currentLangData = languages.find(l => l.code === currentLang) || languages[0]; 131 132 return ` 133 <li class="lang-switcher"> 134 <button class="lang-switcher-btn" id="langSwitcherBtn" aria-label="Change language">
135 <span class="lang-flag">${currentLangData.flag}</span> 136 <span class="lang-code">${currentLang.toUpperCase()}</span> 137 <svg class="lang-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24" width="16" height="16"> 138 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/> 139 </svg> 140 </button> 141 <ul class="lang-dropdown" id="langDropdown"> 142 ${languages.map(lang => ` 143 <li> 144 <button class="lang-option ${currentLang === lang.code ? 'active' : ''}" 145 data-lang="${lang.code}" 146 aria-label="Switch to ${lang.name}"> 147 <span class="lang-flag">${lang.flag}</span> 148 <span class="lang-name">${lang.name}</span> 149 ${currentLang === lang.code ? '<span class="lang-check">â</span>' : ''} 150 </button> 151 </li> 152 `).join('')} 153 </ul> 154 </li> 155 `; 156 } 157 158 /** 159 * Updates navigation labels when language changes 160 */ 161 updateNavLabels() { 162 if (!window.i18n) return; 163 164 // Update nav item labels 165 this.navItems.forEach(item => { 166 const link = document.querySelector(`[data-nav-key="${item.key}"]`); 167 if (link) { 168 link.textContent = window.i18n.t(item.translationKey); 169 } 170 }); 171 172 // Update language switcher button 173 const langBtn = document.getElementById('langSwitcherBtn'); 174 if (langBtn) { 175 const currentLang = window.i18n.currentLang; 176 const languages = [ 177 { code: 'en', flag: 'ðºð¸' }, 178 { code: 'de', flag: 'ð©ðª' }, 179 { code: 'es', flag: 'ðªð¸' }, 180 { code: 'fr', flag: 'ð«ð·' }, 181 { code: 'no', flag: 'ð³ð´' }, 182 { code: 'sv', flag: 'ð¸ðª' }, 183 { code: 'fi', flag: 'ð«ð®' }, 184 { code: 'ja', flag: 'ð¯ðµ' }, 185 { code: 'zh', flag: 'ð¨ð³' }, 186 { code: 'it', flag: 'ð®ð¹' }, 187 { code: 'nl', flag: 'ð³ð±' }, 188 { code: 'ru', flag: 'ð·ðº' }, 189 { code: 'ko', flag: 'ð°ð·' }, 190 { code: 'pt', flag: 'ðµð¹' }, 191 { code: 'pl', flag: 'ðµð±' }, 192 { code: 'da', flag: 'ð©ð°' }, 193 { code: 'tr', flag: 'ð¹ð·' } 194 ]; 195 const flag = languages.find(l => l.code === currentLang)?.flag || 'ð'; 196 langBtn.querySelector('.lang-flag').textContent = flag; 197 langBtn.querySelector('.lang-code').textContent = currentLang.toUpperCase(); 198 } 199 200 // Update active state 201 document.querySelectorAll('.lang-option').forEach(btn => { 202 btn.classList.toggle('active', btn.dataset.lang === window.i18n.currentLang); 203 const check = btn.querySelector('.lang-check'); 204 if (btn.dataset.lang === window.i18n.currentLang) { 205 if (!check) { 206 btn.insertAdjacentHTML('beforeend', '<span class="lang-check">â</span>'); 207 } 208 } else { 209 if (check) check.remove(); 210 } 211 }); 212 } 213 214 /** 215 * Initializes navigation event listeners 216 */ 217 initEventListeners() { 218 const navToggle = document.getElementById('navToggle'); 219 const navMenu = document.getElementById('navMenu'); 220 221 if (!navToggle || !navMenu) return; 222 223 // Toggle mobile menu 224 navToggle.addEventListener('click', () => { 225 navMenu.classList.toggle('active'); 226 }); 227 228 // Close menu when clicking on a link 229 navMenu.querySelectorAll('.nav-link').forEach(link => { 230 link.addEventListener('click', () => { 231 navMenu.classList.remove('active'); 232 }); 233 }); 234 235 // Close menu when clicking outside 236 document.addEventListener('click', (e) => { 237 if (!navMenu.contains(e.target) && !navToggle.contains(e.target)) { 238 navMenu.classList.remove('active'); 239 } 240 }); 241 242 // Language switcher event listeners 243 this.initLanguageSwitcher(); 244 } 245 246 /** 247 * Initializes language switcher functionality 248 */ 249 initLanguageSwitcher() { 250 const langBtn = document.getElementById('langSwitcherBtn'); 251 const langDropdown = document.getElementById('langDropdown'); 252 253 if (!langBtn || !langDropdown) return; 254 255 // Toggle dropdown 256 langBtn.addEventListener('click', (e) => { 257 e.stopPropagation(); 258 langDropdown.classList.toggle('active'); 259 }); 260 261 // Handle language selection 262 document.querySelectorAll('.lang-option').forEach(btn => { 263 btn.addEventListener('click', async (e) => { 264 e.stopPropagation(); 265 const lang = btn.dataset.lang; 266 267 if (window.i18n && lang !== window.i18n.currentLang) { 268 // Load translations if not already loaded 269 await window.i18n.loadTranslations(lang); 270 271 // Set new language 272 window.i18n.setLanguage(lang); 273 274 // Update meta tags 275 window.i18n.updateMetaTags(); 276 277 // Translate page 278 window.i18n.translatePage(); 279 280 // Update nav labels 281 this.updateNavLabels(); 282 283 // Track language change in analytics 284 if (typeof gtag !== 'undefined') { 285 gtag('event', 'language_change', { 286 'event_category': 'engagement', 287 'event_label': lang, 288 'value': lang 289 }); 290 } 291 } 292 293 langDropdown.classList.remove('active'); 294 }); 295 }); 296 297 // Close dropdown when clicking outside 298 document.addEventListener('click', (e) => { 299 if (!langDropdown.contains(e.target) && !langBtn.contains(e.target)) { 300 langDropdown.classList.remove('active'); 301 } 302 }); 303 } 304 305 /** 306 * Initializes the navigation bar 307 */ 308 init() { 309 // Insert navigation into the page 310 const navPlaceholder = document.getElementById('navigation-placeholder'); 311 if (navPlaceholder) { 312 navPlaceholder.outerHTML = this.render(); 313 this.initEventListeners(); 314 } else { 315 console.error('Navigation placeholder not found. Add <div id="navigation-placeholder"></div> to your HTML.'); 316 } 317 318 // Re-render when i18n is ready (fixes initial load issue) 319 window.addEventListener('i18nready', () => { 320 this.updateNavLabels(); 321 }); 322 } 323} 324 325// Auto-initialize when DOM is ready 326if (document.readyState === 'loading') { 327 document.addEventListener('DOMContentLoaded', () => {
328 const navbar = new NavigationBar(); 329 navbar.init(); 330 }); 331} else { 332 const navbar = new NavigationBar(); 333 navbar.init(); 334}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.