// router.js - Simple SPA router import { renderHomePage } from '../pages/HomePage.js'; import { renderAreaPage } from '../pages/AreaPage.js'; import { renderResourcesPage } from '../pages/ResourcesPage.js'; import { scrollToSection } from '../main.js'; class Router { constructor() { // Base routes (non-area pages) this.routes = { '/': 'home', '/home': 'home', '/about': 'resources' }; // Generate area routes dynamically from window.areasData const areasData = window.areasData || {}; Object.keys(areasData).forEach(areaId => { this.routes[`/${areaId}`] = areaId; }); // Generate areas list dynamically this.areas = Object.keys(areasData); this.currentPage = null; this.currentArea = null; this.currentTopic = null; this.init(); } init() { // Handle initial page load this.loadPage(window.location.pathname); // Handle browser back/forward window.addEventListener('popstate', (e) => { const path = window.location.pathname; const hash = window.location.hash; const fullUrl = path + hash; // Use navigateToUrl to ensure proper scroll behavior this.navigateToUrl(fullUrl, false); // false = don't push to history }); // Handle hash changes for navigation window.addEventListener('hashchange', (e) => { const hash = window.location.hash.substring(1); if (hash) { // Scroll to the section after a short delay to ensure content is loaded setTimeout(() => scrollToSection(hash), 100); } }); // Handle initial hash if present const initialHash = window.location.hash.substring(1); if (initialHash) { setTimeout(() => scrollToSection(initialHash), 200); } // Initialize unified navigation handling this.initializeNavigation(); } async loadPage(path) { // Parse path to support /area/topic format const pathParts = path.split('/').filter(p => p); let route, area, topic; if (pathParts.length === 0) { // Root path route = 'home'; } else if (pathParts.length === 1) { // Single segment: /efficiency or /home route = this.routes[path] || 'home'; if (this.areas.includes(pathParts[0])) { area = pathParts[0]; } } else if (pathParts.length === 2 && this.areas.includes(pathParts[0])) { // Two segments: /efficiency/environment area = pathParts[0]; topic = pathParts[1]; route = area; } else { // Unknown route, default to home route = 'home'; } // Update current state this.currentPage = route; this.currentArea = area || null; this.currentTopic = topic || null; // Update Alpine.js store if available if (window.Alpine && window.Alpine.store) { const navStore = window.Alpine.store('navigation'); if (navStore) { navStore.currentPage = this.currentPage; navStore.currentArea = this.currentArea; navStore.currentTopic = this.currentTopic; navStore.updateTopicNav(); } } // Dispatch event for header and other components window.dispatchEvent(new CustomEvent('navigation-changed', { detail: { currentPage: this.currentPage, currentArea: this.currentArea, currentTopic: this.currentTopic } })); // Handle background for all pages await this.setPageBackground(route); try { // Route to appropriate page if (route === 'home') { await this.loadHomePage(); } else if (route === 'resources') { await this.loadResourcesPage(); } else if (this.areas.includes(route)) { // Dynamically handle any area page await this.loadAreaPage(route, topic); } else { // Unknown route, default to home await this.loadHomePage(); } } catch (error) { console.error('Error loading page:', error); // Fallback to home page await this.loadHomePage(); } return Promise.resolve(); } async setPageBackground(route) { // Clear any existing background this.clearBackground(); let backgroundImage, attribution, sourceUrl; // Use global data loaded in index.html if (route === 'home' || route === 'resources') { const homeBackgroundImage = window.homeBackgroundImage; backgroundImage = homeBackgroundImage.image; attribution = homeBackgroundImage.attribution; sourceUrl = homeBackgroundImage.sourceUrl; } else { // Area pages const areasData = window.areasData; const area = areasData[route]; backgroundImage = area.image; attribution = area.imageAttribution; sourceUrl = area.imageSourceUrl; } // Create background element at document level const backgroundDiv = document.createElement('div'); backgroundDiv.id = 'page-background'; backgroundDiv.className = 'fixed opacity-40 z-0'; // The positioning logic is now in updatePageBackgroundPosition, call it after adding to DOM document.body.insertAdjacentElement('afterbegin', backgroundDiv); updatePageBackgroundPosition(); // Set initial position backgroundDiv.innerHTML = `
${attribution}
`; // Add hover functionality for attribution this.initializeBackgroundAttribution(); } clearBackground() { const existingBg = document.getElementById('page-background'); if (existingBg) { existingBg.remove(); } } initializeBackgroundAttribution() { const backgroundContainer = document.getElementById('page-background'); const attribution = document.getElementById('bg-attribution'); if (!backgroundContainer || !attribution) return; backgroundContainer.addEventListener('mouseenter', () => { attribution.style.opacity = '1'; }); backgroundContainer.addEventListener('mouseleave', () => { attribution.style.opacity = '0'; }); } async loadHomePage() { const mainContent = document.getElementById('main-content'); if (!mainContent) { return; } try { // Get page content const homePage = renderHomePage(); // Update content - render directly to main content mainContent.innerHTML = homePage.content; // Initialize page if (homePage.init) { homePage.init(); } // Update navigation state this.updateNavigation('home'); } catch (error) { console.error('Error loading home page:', error); console.error('Error stack:', error.stack); console.error('Error message:', error.message); // Fallback to error content mainContent.innerHTML = `

Error Loading Page

Sorry, there was an error loading the home page.

${error.message}\n\n${error.stack}
`; } } async loadAreaPage(area, topic = null) { const mainContent = document.getElementById('main-content'); if (!mainContent) { return; } try { // Get area page content (pass topic parameter) const areaPage = renderAreaPage(area, topic); // Update content - render directly to main content (no wrapper needed) mainContent.innerHTML = areaPage.content; // Initialize page if (areaPage.init) { areaPage.init(); } // Scroll to top of page after content loads window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (error) { console.error(`Error loading ${area} page:`, error); // Fallback content mainContent.innerHTML = `

Error Loading Page

Sorry, there was an error loading the ${area} page.

`; } this.updateNavigation(area); } async loadResourcesPage() { const mainContent = document.getElementById('main-content'); if (!mainContent) return; try { // Get resources page content const resourcesPage = renderResourcesPage(); // Update content - render directly to main content mainContent.innerHTML = resourcesPage.content; // Initialize page if (resourcesPage.init) { resourcesPage.init(); } } catch (error) { // Fallback content mainContent.innerHTML = `

Error Loading Page

Sorry, there was an error loading the resources page.

`; } this.updateNavigation('resources'); } updateNavigation(currentPage) { // Update header navigation active states const navLinks = document.querySelectorAll('header nav a'); navLinks.forEach(link => { // Remove active state link.classList.remove('text-blue-600', 'bg-blue-50', 'font-semibold'); const href = link.getAttribute('href'); // Add active state to current page if ((currentPage === 'home' && (href === '/' || href === '/home')) || (currentPage === 'resources' && href === '/about') || (currentPage !== 'home' && currentPage !== 'resources' && href === `/${currentPage}`)) { link.classList.add('text-blue-600', 'bg-blue-50', 'font-semibold'); } }); } initializeNavigation() { // Use event delegation to handle all navigation links document.addEventListener('click', (e) => { const link = e.target.closest('a'); if (!link) return; const href = link.getAttribute('href'); if (!href) return; // Handle different types of links if (href.startsWith('/') && !href.startsWith('/js/') && !href.startsWith('/css/') && !href.startsWith('/images/')) { // Check if this is a known SPA route or area/topic route const path = href.split('#')[0]; const pathParts = path.split('/').filter(p => p); const isKnownRoute = this.routes[path] || (pathParts.length === 2 && this.areas.includes(pathParts[0])); if (isKnownRoute) { e.preventDefault(); this.navigateToUrl(href); } } else if (href.startsWith('#')) { // Same-page hash navigation e.preventDefault(); scrollToSection(href.substring(1)); } // External links are handled normally by browser }); } navigateToUrl(fullUrl, pushState = true) { // Parse URL into path and hash const [path, hash] = fullUrl.split('#'); // Update browser URL if needed if (pushState) { window.history.pushState({}, '', fullUrl); } // Load page and then handle hash this.loadPage(path).then(() => { if (hash) { setTimeout(() => scrollToSection(hash), 100); } else { // No hash - scroll to top of page with a delay to ensure content is rendered setTimeout(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }, 100); } }); } } // Export router instance export const router = new Router(); // Make router globally accessible for Alpine.js and other scripts window.router = router; // New function to update the position and size of the page background function updatePageBackgroundPosition() { const backgroundDiv = document.getElementById('page-background'); if (!backgroundDiv) return; const headerHeight = getComputedStyle(document.documentElement).getPropertyValue('--header-height'); const searchSidebar = document.getElementById('search-sidebar'); const rightSidebarWidth = searchSidebar && !searchSidebar.classList.contains('translate-x-full') ? searchSidebar.offsetWidth : 0; backgroundDiv.style.cssText = ` top: ${headerHeight}; left: 0; right: ${rightSidebarWidth}px; bottom: 0; width: calc(100% - ${rightSidebarWidth}px); height: calc(100vh - ${headerHeight}); transition: width 0.3s ease-in-out, right 0.3s ease-in-out; `; } // Export the new function so main.js can call it export { updatePageBackgroundPosition };