PageSourceSearch

https://auroraforecast.app/navbar.js

js auroraforecast.app collected 2026-10-02 15:58:57 UTC 11,449 bytes, 334 lines download raw bytes

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.