Upload 2 files
Browse files- js/main.js +63 -150
- js/router.js +292 -0
js/main.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// Team member component
|
| 2 |
-
function createTeamMember(name, role, hfUsername, tags) {
|
| 3 |
-
const tagData = {
|
| 4 |
-
'efficiency': { name: 'Efficiency, Costs & Environment', id: 'efficiency' },
|
| 5 |
-
'personal': { name: 'Consent & Personal Interactions', id: 'personal' },
|
| 6 |
-
'rights': { name: 'Rights & Regulation', id: 'rights' },
|
| 7 |
-
'ecosystems': { name: 'Socio-economic & Technical Ecosystems', id: 'ecosystems' }
|
| 8 |
-
};
|
| 9 |
|
| 10 |
const colors = ['blue', 'green', 'purple', 'orange', 'indigo', 'pink'];
|
| 11 |
const colorIndex = name.length % colors.length;
|
|
@@ -14,8 +14,8 @@ function createTeamMember(name, role, hfUsername, tags) {
|
|
| 14 |
const initials = name.split(' ').map(n => n[0]).join('');
|
| 15 |
|
| 16 |
const tagElements = tags.map(tag => {
|
| 17 |
-
const tagInfo =
|
| 18 |
-
return `<span class="inline-block px-2 py-0.5 text-xs bg-gray-100 text-gray-700 rounded-full hover:bg-blue-100 hover:text-blue-800 cursor-pointer transition-colors whitespace-nowrap" onclick="scrollToSection('${tagInfo.id}')">${tagInfo.name}</span>`;
|
| 19 |
}).join('');
|
| 20 |
|
| 21 |
return `
|
|
@@ -46,170 +46,83 @@ function createTeamMember(name, role, hfUsername, tags) {
|
|
| 46 |
`;
|
| 47 |
}
|
| 48 |
|
| 49 |
-
//
|
| 50 |
-
|
| 51 |
-
const flexDirection = imagePosition === 'left' ? 'lg:flex-row' : 'lg:flex-row-reverse';
|
| 52 |
-
|
| 53 |
-
const subAreasList = subAreas.map(area => `<li>${area}</li>`).join('');
|
| 54 |
-
|
| 55 |
-
// Use provided alt text or fallback to title
|
| 56 |
-
const imgAlt = imageAltText || title;
|
| 57 |
-
|
| 58 |
-
// Create attribution text if provided
|
| 59 |
-
const attribution = imageAttribution ?
|
| 60 |
-
`<p class="text-xs text-gray-500 mt-2 text-center">${imageAttribution}</p>` : '';
|
| 61 |
-
|
| 62 |
-
return `
|
| 63 |
-
<div id="${id}" class="bg-white rounded-lg shadow-sm overflow-hidden" style="min-height: 300px;">
|
| 64 |
-
<div class="flex flex-col ${flexDirection}">
|
| 65 |
-
<div class="lg:w-2/3 p-8">
|
| 66 |
-
<h2 class="text-2xl font-bold text-gray-900 mb-4">${title}</h2>
|
| 67 |
-
<p class="text-gray-700 mb-6">${description}</p>
|
| 68 |
-
|
| 69 |
-
${openness ? `
|
| 70 |
-
<div class="mb-6 px-4 pt-4 pb-6 bg-gradient-to-r from-orange-50 to-yellow-50 border-l-4 border-orange-300 rounded-r-lg">
|
| 71 |
-
<p class="font-bold text-orange-900 mb-3">The Role of Openness 🤗</p>
|
| 72 |
-
<p class="text-orange-800 leading-relaxed">${openness}</p>
|
| 73 |
-
</div>
|
| 74 |
-
` : ''}
|
| 75 |
-
|
| 76 |
-
<div class="mb-6">
|
| 77 |
-
<h3 class="text-lg font-semibold text-gray-900 mb-3">Sub-areas</h3>
|
| 78 |
-
<ul class="list-disc list-inside text-gray-700 space-y-1">
|
| 79 |
-
${subAreasList}
|
| 80 |
-
</ul>
|
| 81 |
-
</div>
|
| 82 |
-
</div>
|
| 83 |
-
<div class="lg:w-1/3 bg-gray-100">
|
| 84 |
-
<div class="h-full flex flex-col items-center justify-center p-8">
|
| 85 |
-
<img src="images/${id}.png" alt="${imgAlt}" class="max-w-full max-h-full object-contain">
|
| 86 |
-
<a href="${imageSourceUrl}" target="_blank" class="text-xs text-gray-500 mt-2 text-center">${attribution}</a>
|
| 87 |
-
</div>
|
| 88 |
-
</div>
|
| 89 |
-
</div>
|
| 90 |
-
</div>
|
| 91 |
-
`;
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
// Unified scroll function - used by both team tags and left navigation
|
| 95 |
-
function scrollToSection(sectionId) {
|
| 96 |
const element = document.getElementById(sectionId);
|
| 97 |
if (element) {
|
| 98 |
-
element.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
}
|
| 101 |
|
|
|
|
|
|
|
|
|
|
| 102 |
// Initialize team members
|
| 103 |
function initializeTeamMembers() {
|
| 104 |
const teamContainer = document.getElementById('team-grid');
|
| 105 |
if (!teamContainer) return;
|
| 106 |
|
| 107 |
-
const teamMembers = [
|
| 108 |
-
{ name: 'Yacine Jernite', role: 'Head of ML & Society', username: 'yjernite', tags: ['rights', 'ecosystems'] },
|
| 109 |
-
{ name: 'Sasha Luccioni', role: 'AI & Climate Lead', username: 'sasha', tags: ['efficiency'] },
|
| 110 |
-
{ name: 'Giada Pistilli', role: 'Principal Ethicist', username: 'giadap', tags: ['personal'] },
|
| 111 |
-
{ name: 'Lucie-Aimée Kaffee', role: 'Applied Policy Researcher, EU Policy', username: 'frimelle', tags: ['ecosystems', 'rights'] }
|
| 112 |
-
];
|
| 113 |
-
|
| 114 |
teamContainer.innerHTML = teamMembers.map(member =>
|
| 115 |
createTeamMember(member.name, member.role, member.username, member.tags)
|
| 116 |
).join('');
|
| 117 |
}
|
| 118 |
|
| 119 |
-
//
|
| 120 |
-
function initializeAreaCards() {
|
| 121 |
-
const areasContainer = document.getElementById('research-areas');
|
| 122 |
-
if (!areasContainer) return;
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
description: 'The question of costs is essential to understanding and managing the impact of AI technology; it determines who gets to develop it, use it, and how externalized costs are borne by people who do not choose or benefit from the technology.',
|
| 129 |
-
openness: 'Open development of AI systems greatly facilitates transparency on the training and deployment costs. Users and developers of open models typically have stronger incentives to favor and invest in efficiency.',
|
| 130 |
-
subAreas: [
|
| 131 |
-
'Environmental impact across the supply chains',
|
| 132 |
-
'Measuring energy and financial costs',
|
| 133 |
-
'Making AI less compute-intensive'
|
| 134 |
-
],
|
| 135 |
-
imagePosition: 'left',
|
| 136 |
-
imageAttribution: 'Hanna Barakat & Archival Images of AI + AIxDESIGN | BetterImagesOfAI, CC-BY-4.0',
|
| 137 |
-
imageAltText: 'The image shows a surreal landscape with vast green fields extending toward distant mountains under a cloudy sky. Embedded in the fields are digital circuit patterns, resembling an intricate network of blue lines, representing a technological infrastructure. Five large computer monitors with keyboards are placed in a row, each with a Navajo woman sitting in front, weaving the computers. In the far distance, a cluster of teepees is visible.',
|
| 138 |
-
imageSourceUrl: 'https://betterimagesofai.org/images?artist=HannaBarakat&title=WeavingWires2',
|
| 139 |
-
},
|
| 140 |
-
{
|
| 141 |
-
id: 'personal',
|
| 142 |
-
title: 'Consent and Personal Interactions in the Age of AI',
|
| 143 |
-
description: 'Individuals\' experiences of AI systems are shaped both by their personal interactions and by the ways the systems interact with their digital identities - often without our awareness or ability to meaningfully consent.',
|
| 144 |
-
openness: 'Openness at the level of a model\'s training data and of its inputs are necessary to support informed consent, and research on characterizing the different companionship and value dynamics of AI systems enables replicable research into those characteristics.',
|
| 145 |
-
subAreas: [
|
| 146 |
-
'Characterizing personal and parasocial AI interactions',
|
| 147 |
-
'Consent and privacy'
|
| 148 |
-
],
|
| 149 |
-
imagePosition: 'right',
|
| 150 |
-
imageAttribution: 'Kathryn Conrad | BetterImagesOfAI, CC-BY-4.0',
|
| 151 |
-
imageAltText: 'Students at computers with screens that include a representation of a retinal scanner with pixelation and binary data overlays and a brightly coloured datawave heatmap at the top.',
|
| 152 |
-
imageSourceUrl: 'https://betterimagesofai.org/images?artist=KathrynConrad&title=Datafication',
|
| 153 |
-
},
|
| 154 |
-
{
|
| 155 |
-
id: 'rights',
|
| 156 |
-
title: 'Rights and Regulations',
|
| 157 |
-
description: 'AI is not exempt from regulation; but understanding how new and existing rules apply to technical paradigms involving unprecedented scales of data and automation can present unique challenges.',
|
| 158 |
-
openness: 'Applications of existing regulation as well as the design of new ones to meet the challenges of AI technology require understanding how it works, the trade-offs it entails, and the space of technical interventions that are feasible. Open access to the technology supports independent research from stakeholders with different incentives from those of the largest developers.',
|
| 159 |
-
subAreas: [
|
| 160 |
-
'How does existing regulation apply to AI',
|
| 161 |
-
'Navigating new AI-specific regulation',
|
| 162 |
-
'The place of open-source in regulation'
|
| 163 |
-
],
|
| 164 |
-
imagePosition: 'left',
|
| 165 |
-
imageAttribution: 'Emily Rand & LOTI | BetterImagesOfAI, CC-BY-4.0',
|
| 166 |
-
imageAltText: 'Building blocks are overlayed with digital squares that highlight people living their day-to-day lives through windows. Some of the squares are accompanied by cursors.',
|
| 167 |
-
imageSourceUrl: 'https://betterimagesofai.org/images?artist=EmilyRand&title=AICity',
|
| 168 |
-
},
|
| 169 |
-
{
|
| 170 |
-
id: 'ecosystems',
|
| 171 |
-
title: 'Socio-economic and Technical Ecosystems',
|
| 172 |
-
description: 'While discussions of the impact of AI often focus on technical characteristics of individual systems, the trajectory and impact of the technology are often better explained by looking to broader dynamics of market power and economic incentives.',
|
| 173 |
-
openness: 'Openness is an important factor in the diffusion of the technology, and enables a greater variety of actors to reliably assess its suitability and to adapt it to their specific contexts and requirements; as well as to understand the role of different resources and the consequences of their concentration among a few actors.',
|
| 174 |
-
subAreas: [
|
| 175 |
-
'Labor impacts of AI',
|
| 176 |
-
'Power, monopolies, and sovereignty',
|
| 177 |
-
'How and where is (open) AI used'
|
| 178 |
-
],
|
| 179 |
-
imagePosition: 'right',
|
| 180 |
-
imageAttribution: 'Lone Thomasky & Bits&Bäume | BetterImagesOfAI, CC-BY-4.0',
|
| 181 |
-
imageAltText: 'A simplified illustration of urban life near the sea showing groups of people, buildings and bridges, as well as polluting power plants, opencast mining, exploitative work, data centres and wind power stations on a hill. Several small icons indicate destructive processes.',
|
| 182 |
-
imageSourceUrl: 'https://betterimagesofai.org/images?artist=LoneThomasky&title=DigitalSocietyBell',
|
| 183 |
-
}
|
| 184 |
-
];
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
).join('');
|
| 189 |
-
}
|
| 190 |
|
| 191 |
-
//
|
| 192 |
-
function
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
e.preventDefault();
|
| 198 |
-
const href = this.getAttribute('href');
|
| 199 |
-
if (href && href.startsWith('#')) {
|
| 200 |
-
const sectionId = href.substring(1);
|
| 201 |
-
scrollToSection(sectionId);
|
| 202 |
-
}
|
| 203 |
-
});
|
| 204 |
-
});
|
| 205 |
}
|
| 206 |
|
| 207 |
// Main initialization
|
| 208 |
document.addEventListener('DOMContentLoaded', function() {
|
| 209 |
-
//
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
// Sidebar toggle functionality
|
| 215 |
const searchToggle = document.getElementById('search-toggle');
|
|
|
|
| 1 |
+
// main.js
|
| 2 |
+
import { createHomeAreaCard } from './cards/HomeAreaCard.js';
|
| 3 |
+
import { createArtifactSummaryCard, createArtifactCarousel } from './cards/ArtifactSummaryCard.js';
|
| 4 |
+
import { teamMembers, teamTagData } from './data/team.js';
|
| 5 |
+
import { router } from './router.js';
|
| 6 |
+
|
| 7 |
// Team member component
|
| 8 |
+
export function createTeamMember(name, role, hfUsername, tags) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
const colors = ['blue', 'green', 'purple', 'orange', 'indigo', 'pink'];
|
| 11 |
const colorIndex = name.length % colors.length;
|
|
|
|
| 14 |
const initials = name.split(' ').map(n => n[0]).join('');
|
| 15 |
|
| 16 |
const tagElements = tags.map(tag => {
|
| 17 |
+
const tagInfo = teamTagData[tag];
|
| 18 |
+
return `<span class="inline-block px-2 py-0.5 text-xs bg-gray-100 text-gray-700 rounded-full hover:bg-blue-100 hover:text-blue-800 cursor-pointer transition-colors whitespace-nowrap" onclick="window.scrollToSection('${tagInfo.id}')">${tagInfo.name}</span>`;
|
| 19 |
}).join('');
|
| 20 |
|
| 21 |
return `
|
|
|
|
| 46 |
`;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
// Make router's scrollToSection globally available for onclick handlers
|
| 50 |
+
window.scrollToSection = (sectionId) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
const element = document.getElementById(sectionId);
|
| 52 |
if (element) {
|
| 53 |
+
const elementRect = element.getBoundingClientRect();
|
| 54 |
+
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
| 55 |
+
const offset = 120; // Account for fixed header + some padding
|
| 56 |
+
|
| 57 |
+
window.scrollTo({
|
| 58 |
+
top: absoluteElementTop - offset,
|
| 59 |
+
behavior: 'smooth'
|
| 60 |
+
});
|
| 61 |
}
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
// Scroll to top functionality
|
| 65 |
+
function scrollToTop() {
|
| 66 |
+
window.scrollTo({
|
| 67 |
+
top: 0,
|
| 68 |
+
behavior: 'smooth'
|
| 69 |
+
});
|
| 70 |
}
|
| 71 |
|
| 72 |
+
// Make scrollToTop globally available
|
| 73 |
+
window.scrollToTop = scrollToTop;
|
| 74 |
+
|
| 75 |
// Initialize team members
|
| 76 |
function initializeTeamMembers() {
|
| 77 |
const teamContainer = document.getElementById('team-grid');
|
| 78 |
if (!teamContainer) return;
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
teamContainer.innerHTML = teamMembers.map(member =>
|
| 81 |
createTeamMember(member.name, member.role, member.username, member.tags)
|
| 82 |
).join('');
|
| 83 |
}
|
| 84 |
|
| 85 |
+
// Note: Navigation handling moved to router.js for unified control
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
// Initialize scroll to top button functionality
|
| 88 |
+
function initializeScrollToTop() {
|
| 89 |
+
const scrollToTopBtn = document.getElementById('scroll-to-top');
|
| 90 |
+
if (!scrollToTopBtn) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
// Add click event listener
|
| 93 |
+
scrollToTopBtn.addEventListener('click', scrollToTop);
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
// Show/hide button based on scroll position
|
| 96 |
+
function toggleScrollToTopButton() {
|
| 97 |
+
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
| 98 |
+
const showThreshold = 300; // Show button after scrolling 300px
|
| 99 |
+
|
| 100 |
+
if (scrollPosition > showThreshold) {
|
| 101 |
+
scrollToTopBtn.classList.remove('opacity-0', 'invisible');
|
| 102 |
+
scrollToTopBtn.classList.add('opacity-100', 'visible');
|
| 103 |
+
} else {
|
| 104 |
+
scrollToTopBtn.classList.remove('opacity-100', 'visible');
|
| 105 |
+
scrollToTopBtn.classList.add('opacity-0', 'invisible');
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Listen for scroll events
|
| 110 |
+
window.addEventListener('scroll', toggleScrollToTopButton);
|
| 111 |
|
| 112 |
+
// Initial check
|
| 113 |
+
toggleScrollToTopButton();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
| 115 |
|
| 116 |
// Main initialization
|
| 117 |
document.addEventListener('DOMContentLoaded', function() {
|
| 118 |
+
// Navigation now handled by router.js
|
| 119 |
+
|
| 120 |
+
// Initialize scroll to top functionality
|
| 121 |
+
initializeScrollToTop();
|
| 122 |
+
|
| 123 |
+
// The router will handle page-specific component initialization
|
| 124 |
+
// No need to call initializeTeamMembers, initializeHomeAreaCards, etc. here
|
| 125 |
+
// as they will be called by the router when loading the home page
|
| 126 |
|
| 127 |
// Sidebar toggle functionality
|
| 128 |
const searchToggle = document.getElementById('search-toggle');
|
js/router.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// router.js - Simple SPA router
|
| 2 |
+
import { renderHomePage, getHomePageSidebar } from './pages/HomePage.js';
|
| 3 |
+
import { renderAreaPage, getAreaPageSidebar } from './pages/AreaPage.js';
|
| 4 |
+
import { renderResourcesPage, getResourcesPageSidebar } from './pages/ResourcesPage.js';
|
| 5 |
+
|
| 6 |
+
class Router {
|
| 7 |
+
constructor() {
|
| 8 |
+
this.routes = {
|
| 9 |
+
'/': 'home',
|
| 10 |
+
'/home': 'home',
|
| 11 |
+
'/efficiency': 'efficiency',
|
| 12 |
+
'/personal': 'personal',
|
| 13 |
+
'/rights': 'rights',
|
| 14 |
+
'/ecosystems': 'ecosystems',
|
| 15 |
+
'/about': 'resources'
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
this.currentPage = null;
|
| 19 |
+
this.init();
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
init() {
|
| 23 |
+
// Handle initial page load
|
| 24 |
+
this.loadPage(window.location.pathname);
|
| 25 |
+
|
| 26 |
+
// Handle browser back/forward
|
| 27 |
+
window.addEventListener('popstate', (e) => {
|
| 28 |
+
const path = window.location.pathname;
|
| 29 |
+
const hash = window.location.hash;
|
| 30 |
+
const fullUrl = path + hash;
|
| 31 |
+
|
| 32 |
+
// Use navigateToUrl to ensure proper scroll behavior
|
| 33 |
+
this.navigateToUrl(fullUrl);
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
// Handle hash changes for navigation
|
| 37 |
+
window.addEventListener('hashchange', (e) => {
|
| 38 |
+
const hash = window.location.hash.substring(1);
|
| 39 |
+
if (hash) {
|
| 40 |
+
// Scroll to the section after a short delay to ensure content is loaded
|
| 41 |
+
setTimeout(() => this.scrollToSection(hash), 100);
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Handle initial hash if present
|
| 46 |
+
const initialHash = window.location.hash.substring(1);
|
| 47 |
+
if (initialHash) {
|
| 48 |
+
setTimeout(() => this.scrollToSection(initialHash), 200);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Initialize unified navigation handling
|
| 52 |
+
this.initializeNavigation();
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
async loadPage(path) {
|
| 56 |
+
const route = this.routes[path] || 'home';
|
| 57 |
+
|
| 58 |
+
if (this.currentPage === route) {
|
| 59 |
+
// Same page, no need to reload
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
this.currentPage = route;
|
| 64 |
+
|
| 65 |
+
try {
|
| 66 |
+
switch (route) {
|
| 67 |
+
case 'home':
|
| 68 |
+
await this.loadHomePage();
|
| 69 |
+
break;
|
| 70 |
+
case 'efficiency':
|
| 71 |
+
case 'personal':
|
| 72 |
+
case 'rights':
|
| 73 |
+
case 'ecosystems':
|
| 74 |
+
await this.loadAreaPage(route);
|
| 75 |
+
break;
|
| 76 |
+
case 'resources':
|
| 77 |
+
await this.loadResourcesPage();
|
| 78 |
+
break;
|
| 79 |
+
default:
|
| 80 |
+
await this.loadHomePage();
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
} catch (error) {
|
| 84 |
+
console.error('Error loading page:', error);
|
| 85 |
+
// Fallback to home page
|
| 86 |
+
await this.loadHomePage();
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
return Promise.resolve();
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
async loadHomePage() {
|
| 93 |
+
const mainContent = document.getElementById('main-content');
|
| 94 |
+
const leftSidebar = document.getElementById('left-sidebar');
|
| 95 |
+
|
| 96 |
+
if (!mainContent) {
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
try {
|
| 101 |
+
// Get page content and sidebar
|
| 102 |
+
const homePage = renderHomePage();
|
| 103 |
+
|
| 104 |
+
// Update content
|
| 105 |
+
const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
|
| 106 |
+
contentContainer.innerHTML = homePage.content;
|
| 107 |
+
|
| 108 |
+
// Update sidebar if it exists
|
| 109 |
+
if (leftSidebar) {
|
| 110 |
+
leftSidebar.innerHTML = getHomePageSidebar();
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Initialize page
|
| 114 |
+
if (homePage.init) {
|
| 115 |
+
homePage.init();
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Update navigation state
|
| 119 |
+
this.updateNavigation('home');
|
| 120 |
+
} catch (error) {
|
| 121 |
+
// Fallback to error content
|
| 122 |
+
const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
|
| 123 |
+
contentContainer.innerHTML = `
|
| 124 |
+
<div class="bg-white rounded-lg shadow-sm p-8">
|
| 125 |
+
<h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
|
| 126 |
+
<p class="text-gray-700">Sorry, there was an error loading the home page.</p>
|
| 127 |
+
</div>
|
| 128 |
+
`;
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async loadAreaPage(area) {
|
| 133 |
+
const mainContent = document.getElementById('main-content');
|
| 134 |
+
const leftSidebar = document.getElementById('left-sidebar');
|
| 135 |
+
|
| 136 |
+
if (!mainContent) {
|
| 137 |
+
return;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
try {
|
| 141 |
+
// Get area page content and sidebar
|
| 142 |
+
const areaPage = renderAreaPage(area);
|
| 143 |
+
|
| 144 |
+
// Update content
|
| 145 |
+
const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
|
| 146 |
+
contentContainer.innerHTML = areaPage.content;
|
| 147 |
+
|
| 148 |
+
// Update sidebar
|
| 149 |
+
if (leftSidebar) {
|
| 150 |
+
leftSidebar.innerHTML = getAreaPageSidebar(area);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// Initialize page
|
| 154 |
+
if (areaPage.init) {
|
| 155 |
+
areaPage.init();
|
| 156 |
+
}
|
| 157 |
+
} catch (error) {
|
| 158 |
+
console.error(`Error loading ${area} page:`, error);
|
| 159 |
+
|
| 160 |
+
// Fallback content
|
| 161 |
+
const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
|
| 162 |
+
contentContainer.innerHTML = `
|
| 163 |
+
<div class="bg-white rounded-lg shadow-sm p-8">
|
| 164 |
+
<h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
|
| 165 |
+
<p class="text-gray-700">Sorry, there was an error loading the ${area} page.</p>
|
| 166 |
+
</div>
|
| 167 |
+
`;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
this.updateNavigation(area);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
async loadResourcesPage() {
|
| 174 |
+
const mainContent = document.getElementById('main-content');
|
| 175 |
+
const leftSidebar = document.getElementById('left-sidebar');
|
| 176 |
+
|
| 177 |
+
if (!mainContent) return;
|
| 178 |
+
|
| 179 |
+
try {
|
| 180 |
+
// Get resources page content and sidebar
|
| 181 |
+
const resourcesPage = renderResourcesPage();
|
| 182 |
+
|
| 183 |
+
// Update content
|
| 184 |
+
const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
|
| 185 |
+
contentContainer.innerHTML = resourcesPage.content;
|
| 186 |
+
|
| 187 |
+
// Update sidebar
|
| 188 |
+
if (leftSidebar) {
|
| 189 |
+
leftSidebar.innerHTML = getResourcesPageSidebar();
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Initialize page
|
| 193 |
+
if (resourcesPage.init) {
|
| 194 |
+
resourcesPage.init();
|
| 195 |
+
}
|
| 196 |
+
} catch (error) {
|
| 197 |
+
// Fallback content
|
| 198 |
+
const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
|
| 199 |
+
contentContainer.innerHTML = `
|
| 200 |
+
<div class="bg-white rounded-lg shadow-sm p-8">
|
| 201 |
+
<h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
|
| 202 |
+
<p class="text-gray-700">Sorry, there was an error loading the resources page.</p>
|
| 203 |
+
</div>
|
| 204 |
+
`;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
this.updateNavigation('resources');
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
updateNavigation(currentPage) {
|
| 211 |
+
// Update header navigation active states
|
| 212 |
+
const navLinks = document.querySelectorAll('header nav a');
|
| 213 |
+
navLinks.forEach(link => {
|
| 214 |
+
link.classList.remove('text-blue-600', 'bg-blue-50');
|
| 215 |
+
link.classList.add('text-gray-700');
|
| 216 |
+
|
| 217 |
+
const href = link.getAttribute('href');
|
| 218 |
+
if ((currentPage === 'home' && (href === '/' || href === '/home')) ||
|
| 219 |
+
(currentPage === 'resources' && href === '/about') ||
|
| 220 |
+
(currentPage !== 'home' && currentPage !== 'resources' && href === `/${currentPage}`)) {
|
| 221 |
+
link.classList.remove('text-gray-700');
|
| 222 |
+
link.classList.add('text-blue-600', 'bg-blue-50');
|
| 223 |
+
}
|
| 224 |
+
});
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
initializeNavigation() {
|
| 229 |
+
// Use event delegation to handle all navigation links
|
| 230 |
+
document.addEventListener('click', (e) => {
|
| 231 |
+
const link = e.target.closest('a');
|
| 232 |
+
if (!link) return;
|
| 233 |
+
|
| 234 |
+
const href = link.getAttribute('href');
|
| 235 |
+
if (!href) return;
|
| 236 |
+
|
| 237 |
+
// Handle different types of links
|
| 238 |
+
if (href.startsWith('/') && !href.startsWith('/js/') && !href.startsWith('/css/') && !href.startsWith('/images/')) {
|
| 239 |
+
// Check if this is a known SPA route
|
| 240 |
+
const path = href.split('#')[0];
|
| 241 |
+
if (this.routes[path]) {
|
| 242 |
+
e.preventDefault();
|
| 243 |
+
this.navigateToUrl(href);
|
| 244 |
+
}
|
| 245 |
+
} else if (href.startsWith('#')) {
|
| 246 |
+
// Same-page hash navigation
|
| 247 |
+
e.preventDefault();
|
| 248 |
+
this.scrollToSection(href.substring(1));
|
| 249 |
+
}
|
| 250 |
+
// External links are handled normally by browser
|
| 251 |
+
});
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
navigateToUrl(fullUrl) {
|
| 255 |
+
// Parse URL into path and hash
|
| 256 |
+
const [path, hash] = fullUrl.split('#');
|
| 257 |
+
|
| 258 |
+
// Update browser URL
|
| 259 |
+
window.history.pushState({}, '', fullUrl);
|
| 260 |
+
|
| 261 |
+
// Load page and then handle hash
|
| 262 |
+
this.loadPage(path).then(() => {
|
| 263 |
+
if (hash) {
|
| 264 |
+
setTimeout(() => this.scrollToSection(hash), 100);
|
| 265 |
+
} else {
|
| 266 |
+
// No hash - scroll to top of page with a delay to ensure content is rendered
|
| 267 |
+
setTimeout(() => {
|
| 268 |
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
| 269 |
+
}, 100);
|
| 270 |
+
}
|
| 271 |
+
});
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
scrollToSection(sectionId) {
|
| 275 |
+
const element = document.getElementById(sectionId);
|
| 276 |
+
if (element) {
|
| 277 |
+
// Scroll to element with offset to show title
|
| 278 |
+
const elementRect = element.getBoundingClientRect();
|
| 279 |
+
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
| 280 |
+
const offset = 120; // Account for fixed header + some padding
|
| 281 |
+
|
| 282 |
+
window.scrollTo({
|
| 283 |
+
top: absoluteElementTop - offset,
|
| 284 |
+
behavior: 'smooth'
|
| 285 |
+
});
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// Export router instance
|
| 292 |
+
export const router = new Router();
|