overview / js /utils /router.js
Yacine Jernite
v07
8ed13f7
raw
history blame
14.9 kB
// 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 <head>
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 = `
<!-- Background Image Main Content -->
<img src="/images/${backgroundImage}" alt="" class="w-full h-full pointer-events-none">
<div id="bg-attribution" class="absolute bottom-4 right-4 bg-black bg-opacity-75 text-white text-xs px-2 py-1 rounded opacity-0 transition-opacity duration-200 max-w-xs z-50">
<a href="${sourceUrl}" target="_blank" class="text-blue-300 hover:text-blue-100">
${attribution}
</a>
</div>
`;
// 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 = `
<div class="bg-white rounded-lg shadow-sm p-8">
<h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
<p class="text-gray-700 mb-4">Sorry, there was an error loading the home page.</p>
<pre class="text-sm bg-gray-100 p-4 rounded overflow-auto">${error.message}\n\n${error.stack}</pre>
</div>
`;
}
}
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 = `
<div class="bg-white rounded-lg shadow-sm p-8 mx-4">
<h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
<p class="text-gray-700">Sorry, there was an error loading the ${area} page.</p>
</div>
`;
}
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 = `
<div class="bg-white rounded-lg shadow-sm p-8 mx-4">
<h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
<p class="text-gray-700">Sorry, there was an error loading the resources page.</p>
</div>
`;
}
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 };