yjernite HF Staff commited on
Commit
a640871
·
verified ·
1 Parent(s): aeeb334

Upload 2 files

Browse files
Files changed (2) hide show
  1. js/main.js +63 -150
  2. 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 = tagData[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="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
- // Area card component
50
- function createAreaCard(id, title, description, openness, subAreas, imagePosition = 'left', imageAttribution = null, imageAltText = null, imageSourceUrl = null) {
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.scrollIntoView({ behavior: 'smooth', block: 'start' });
 
 
 
 
 
 
 
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
- // Initialize area cards
120
- function initializeAreaCards() {
121
- const areasContainer = document.getElementById('research-areas');
122
- if (!areasContainer) return;
123
 
124
- const areas = [
125
- {
126
- id: 'efficiency',
127
- title: 'Efficiency, Costs, and Environment',
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
- areasContainer.innerHTML = areas.map(area =>
187
- createAreaCard(area.id, area.title, area.description, area.openness, area.subAreas, area.imagePosition, area.imageAttribution, area.imageAltText, area.imageSourceUrl)
188
- ).join('');
189
- }
190
 
191
- // Initialize left navigation with click handlers
192
- function initializeNavigation() {
193
- const navLinks = document.querySelectorAll('.page-nav-link');
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
- navLinks.forEach(link => {
196
- link.addEventListener('click', function(e) {
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
- // Initialize components
210
- initializeTeamMembers();
211
- initializeAreaCards();
212
- initializeNavigation();
 
 
 
 
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();