yjernite HF Staff commited on
Commit
98f8ae8
·
verified ·
1 Parent(s): ce2e0a2

Upload 4 files

Browse files
Files changed (4) hide show
  1. js/utils/dom.js +13 -0
  2. js/utils/router.js +409 -0
  3. js/utils/search.js +310 -0
  4. js/utils/sidebar.js +57 -0
js/utils/dom.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function scrollToSection(sectionId) {
2
+ const element = document.getElementById(sectionId);
3
+ if (element) {
4
+ const elementRect = element.getBoundingClientRect();
5
+ const absoluteElementTop = elementRect.top + window.pageYOffset;
6
+ const offset = 120; // Account for fixed header + some padding
7
+
8
+ window.scrollTo({
9
+ top: absoluteElementTop - offset,
10
+ behavior: 'smooth'
11
+ });
12
+ }
13
+ }
js/utils/router.js ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { scrollToSection } from './dom.js';
6
+
7
+ class Router {
8
+ constructor() {
9
+ this.routes = {
10
+ '/': 'home',
11
+ '/home': 'home',
12
+ '/efficiency': 'efficiency',
13
+ '/personal': 'personal',
14
+ '/rights': 'rights',
15
+ '/ecosystems': 'ecosystems',
16
+ '/about': 'resources'
17
+ };
18
+
19
+ this.currentPage = null;
20
+ this.init();
21
+ }
22
+
23
+ init() {
24
+ // Handle initial page load
25
+ console.log('Initializing router');
26
+ this.loadPage(window.location.pathname);
27
+
28
+ // Handle browser back/forward
29
+ window.addEventListener('popstate', (e) => {
30
+ const path = window.location.pathname;
31
+ const hash = window.location.hash;
32
+ const fullUrl = path + hash;
33
+
34
+ // Use navigateToUrl to ensure proper scroll behavior
35
+ this.navigateToUrl(fullUrl);
36
+ });
37
+
38
+ // Handle hash changes for navigation
39
+ window.addEventListener('hashchange', (e) => {
40
+ const hash = window.location.hash.substring(1);
41
+ if (hash) {
42
+ // Scroll to the section after a short delay to ensure content is loaded
43
+ setTimeout(() => scrollToSection(hash), 100);
44
+ }
45
+ });
46
+
47
+ // Handle initial hash if present
48
+ const initialHash = window.location.hash.substring(1);
49
+ if (initialHash) {
50
+ setTimeout(() => scrollToSection(initialHash), 200);
51
+ }
52
+
53
+ // Initialize unified navigation handling
54
+ this.initializeNavigation();
55
+ }
56
+
57
+ async loadPage(path) {
58
+ const route = this.routes[path] || 'home';
59
+
60
+ if (this.currentPage === route) {
61
+ // Same page, no need to reload
62
+ return;
63
+ }
64
+
65
+ this.currentPage = route;
66
+
67
+ console.log('Loading page:', route);
68
+ // Handle background for all pages
69
+ await this.setPageBackground(route);
70
+
71
+ console.log('Background set for:', route);
72
+
73
+ try {
74
+ switch (route) {
75
+ case 'home':
76
+ await this.loadHomePage();
77
+ break;
78
+ case 'efficiency':
79
+ case 'personal':
80
+ case 'rights':
81
+ case 'ecosystems':
82
+ await this.loadAreaPage(route);
83
+ break;
84
+ case 'resources':
85
+ await this.loadResourcesPage();
86
+ break;
87
+ default:
88
+ await this.loadHomePage();
89
+ }
90
+
91
+ } catch (error) {
92
+ console.error('Error loading page:', error);
93
+ // Fallback to home page
94
+ await this.loadHomePage();
95
+ }
96
+
97
+ console.log('Page loaded:', route);
98
+
99
+ return Promise.resolve();
100
+ }
101
+
102
+ async setPageBackground(route) {
103
+ console.log('Setting background for route:', route);
104
+ // Clear any existing background
105
+ this.clearBackground();
106
+
107
+ let backgroundImage, attribution, sourceUrl;
108
+
109
+ if (route === 'home' || route === 'resources') {
110
+ const { homeBackgroundImage } = await import('../data/areas.js');
111
+ backgroundImage = homeBackgroundImage.image;
112
+ attribution = homeBackgroundImage.attribution;
113
+ sourceUrl = homeBackgroundImage.sourceUrl;
114
+ } else {
115
+ // Area pages
116
+ const { areasData } = await import('../data/areas.js');
117
+ const area = areasData[route];
118
+ backgroundImage = area.image;
119
+ attribution = area.imageAttribution;
120
+ sourceUrl = area.imageSourceUrl;
121
+ }
122
+
123
+ // Create background element at document level
124
+ const backgroundDiv = document.createElement('div');
125
+ backgroundDiv.id = 'page-background';
126
+ backgroundDiv.className = 'fixed opacity-40 z-0';
127
+
128
+ // The positioning logic is now in updatePageBackgroundPosition, call it after adding to DOM
129
+ document.body.insertAdjacentElement('afterbegin', backgroundDiv);
130
+ updatePageBackgroundPosition(); // Set initial position
131
+
132
+ backgroundDiv.innerHTML = `
133
+ <!-- Background Image Main Content -->
134
+ <img src="images/${backgroundImage}" alt="" class="w-full h-full object-cover pointer-events-none">
135
+ <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">
136
+ <a href="${sourceUrl}" target="_blank" class="text-blue-300 hover:text-blue-100">
137
+ ${attribution}
138
+ </a>
139
+ </div>
140
+ `;
141
+
142
+ // Add hover functionality for attribution
143
+ this.initializeBackgroundAttribution();
144
+ }
145
+
146
+ clearBackground() {
147
+ const existingBg = document.getElementById('page-background');
148
+ if (existingBg) {
149
+ existingBg.remove();
150
+ }
151
+ }
152
+
153
+ initializeBackgroundAttribution() {
154
+ const backgroundContainer = document.getElementById('page-background');
155
+ const attribution = document.getElementById('bg-attribution');
156
+
157
+ if (!backgroundContainer || !attribution) return;
158
+
159
+ backgroundContainer.addEventListener('mouseenter', () => {
160
+ attribution.style.opacity = '1';
161
+ });
162
+
163
+ backgroundContainer.addEventListener('mouseleave', () => {
164
+ attribution.style.opacity = '0';
165
+ });
166
+ }
167
+
168
+ async loadHomePage() {
169
+ const mainContent = document.getElementById('main-content');
170
+ const leftSidebar = document.getElementById('left-sidebar');
171
+
172
+ if (!mainContent) {
173
+ return;
174
+ }
175
+
176
+ try {
177
+ // Get page content and sidebar
178
+ const homePage = renderHomePage();
179
+
180
+ // Update content
181
+ const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
182
+ contentContainer.innerHTML = homePage.content;
183
+
184
+ // Update sidebar if it exists
185
+ if (leftSidebar) {
186
+ leftSidebar.innerHTML = getHomePageSidebar();
187
+ }
188
+
189
+ // Initialize page
190
+ if (homePage.init) {
191
+ homePage.init();
192
+ }
193
+
194
+ // Initialize left sidebar attribution after content is loaded
195
+ initializeLeftSidebarAttribution();
196
+
197
+ // Update navigation state
198
+ this.updateNavigation('home');
199
+ } catch (error) {
200
+ // Fallback to error content
201
+ const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
202
+ contentContainer.innerHTML = `
203
+ <div class="bg-white rounded-lg shadow-sm p-8">
204
+ <h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
205
+ <p class="text-gray-700">Sorry, there was an error loading the home page.</p>
206
+ </div>
207
+ `;
208
+ }
209
+ }
210
+
211
+ async loadAreaPage(area) {
212
+ const mainContent = document.getElementById('main-content');
213
+ const leftSidebar = document.getElementById('left-sidebar');
214
+
215
+ if (!mainContent) {
216
+ return;
217
+ }
218
+
219
+ try {
220
+ // Get area page content and sidebar
221
+ const areaPage = renderAreaPage(area);
222
+
223
+ // Update content
224
+ const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
225
+ contentContainer.innerHTML = areaPage.content;
226
+
227
+ // Update sidebar
228
+ if (leftSidebar) {
229
+ leftSidebar.innerHTML = getAreaPageSidebar(area);
230
+ }
231
+
232
+ // Initialize page
233
+ if (areaPage.init) {
234
+ areaPage.init();
235
+ }
236
+
237
+ // Initialize left sidebar attribution after content is loaded
238
+ initializeLeftSidebarAttribution();
239
+
240
+ } catch (error) {
241
+ console.error(`Error loading ${area} page:`, error);
242
+
243
+ // Fallback content
244
+ const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
245
+ contentContainer.innerHTML = `
246
+ <div class="bg-white rounded-lg shadow-sm p-8">
247
+ <h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
248
+ <p class="text-gray-700">Sorry, there was an error loading the ${area} page.</p>
249
+ </div>
250
+ `;
251
+ }
252
+
253
+ this.updateNavigation(area);
254
+ }
255
+
256
+ async loadResourcesPage() {
257
+ const mainContent = document.getElementById('main-content');
258
+ const leftSidebar = document.getElementById('left-sidebar');
259
+
260
+ if (!mainContent) return;
261
+
262
+ try {
263
+ // Get resources page content and sidebar
264
+ const resourcesPage = renderResourcesPage();
265
+
266
+ // Update content
267
+ const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
268
+ contentContainer.innerHTML = resourcesPage.content;
269
+
270
+ // Update sidebar
271
+ if (leftSidebar) {
272
+ leftSidebar.innerHTML = getResourcesPageSidebar();
273
+ }
274
+
275
+ // Initialize page
276
+ if (resourcesPage.init) {
277
+ resourcesPage.init();
278
+ }
279
+
280
+ // Initialize left sidebar attribution after content is loaded
281
+ initializeLeftSidebarAttribution();
282
+
283
+ } catch (error) {
284
+ // Fallback content
285
+ const contentContainer = mainContent.querySelector('.max-w-4xl') || mainContent;
286
+ contentContainer.innerHTML = `
287
+ <div class="bg-white rounded-lg shadow-sm p-8">
288
+ <h1 class="text-3xl font-bold text-red-600 mb-6">Error Loading Page</h1>
289
+ <p class="text-gray-700">Sorry, there was an error loading the resources page.</p>
290
+ </div>
291
+ `;
292
+ }
293
+
294
+ this.updateNavigation('resources');
295
+ }
296
+
297
+ updateNavigation(currentPage) {
298
+ // Update header navigation active states
299
+ const navLinks = document.querySelectorAll('header nav a');
300
+ navLinks.forEach(link => {
301
+ link.classList.remove('text-blue-600', 'bg-blue-50');
302
+ link.classList.add('text-gray-700');
303
+
304
+ const href = link.getAttribute('href');
305
+ if ((currentPage === 'home' && (href === '/' || href === '/home')) ||
306
+ (currentPage === 'resources' && href === '/about') ||
307
+ (currentPage !== 'home' && currentPage !== 'resources' && href === `/${currentPage}`)) {
308
+ link.classList.remove('text-gray-700');
309
+ link.classList.add('text-blue-600', 'bg-blue-50');
310
+ }
311
+ });
312
+ }
313
+
314
+
315
+ initializeNavigation() {
316
+ // Use event delegation to handle all navigation links
317
+ document.addEventListener('click', (e) => {
318
+ const link = e.target.closest('a');
319
+ if (!link) return;
320
+
321
+ const href = link.getAttribute('href');
322
+ if (!href) return;
323
+
324
+ // Handle different types of links
325
+ if (href.startsWith('/') && !href.startsWith('/js/') && !href.startsWith('/css/') && !href.startsWith('/images/')) {
326
+ // Check if this is a known SPA route
327
+ const path = href.split('#')[0];
328
+ if (this.routes[path]) {
329
+ e.preventDefault();
330
+ this.navigateToUrl(href);
331
+ }
332
+ } else if (href.startsWith('#')) {
333
+ // Same-page hash navigation
334
+ e.preventDefault();
335
+ scrollToSection(href.substring(1));
336
+ }
337
+ // External links are handled normally by browser
338
+ });
339
+ }
340
+
341
+ navigateToUrl(fullUrl) {
342
+ // Parse URL into path and hash
343
+ const [path, hash] = fullUrl.split('#');
344
+
345
+ // Update browser URL
346
+ window.history.pushState({}, '', fullUrl);
347
+
348
+ // Load page and then handle hash
349
+ this.loadPage(path).then(() => {
350
+ if (hash) {
351
+ setTimeout(() => scrollToSection(hash), 100);
352
+ } else {
353
+ // No hash - scroll to top of page with a delay to ensure content is rendered
354
+ setTimeout(() => {
355
+ window.scrollTo({ top: 0, behavior: 'smooth' });
356
+ }, 100);
357
+ }
358
+ });
359
+ }
360
+
361
+ }
362
+
363
+ // Export router instance
364
+ export const router = new Router();
365
+
366
+ // New function to update the position and size of the page background
367
+ function updatePageBackgroundPosition() {
368
+ const backgroundDiv = document.getElementById('page-background');
369
+ if (!backgroundDiv) return;
370
+
371
+ const headerHeight = getComputedStyle(document.documentElement).getPropertyValue('--header-height');
372
+ const leftSidebar = document.getElementById('left-sidebar');
373
+ const searchSidebar = document.getElementById('search-sidebar');
374
+
375
+ const leftSidebarWidth = leftSidebar.classList.contains('-translate-x-full') ? 0 : leftSidebar.offsetWidth;
376
+ const rightSidebarWidth = searchSidebar.classList.contains('translate-x-full') ? 0 : searchSidebar.offsetWidth;
377
+
378
+ backgroundDiv.style.cssText = `
379
+ top: ${headerHeight};
380
+ left: ${leftSidebarWidth}px;
381
+ right: ${rightSidebarWidth}px;
382
+ bottom: 0;
383
+ width: calc(100% - ${leftSidebarWidth + rightSidebarWidth}px);
384
+ height: calc(100vh - ${headerHeight});
385
+ transition: left 0.3s ease-in-out, width 0.3s ease-in-out, right 0.3s ease-in-out; /* Add transition for smooth movement */
386
+ `;
387
+ }
388
+
389
+ // Export the new function so main.js can call it
390
+ export { updatePageBackgroundPosition };
391
+
392
+ // Function to initialize the attribution hover for the left sidebar
393
+ function initializeLeftSidebarAttribution() {
394
+ const leftSidebarContainer = document.getElementById('left-sidebar');
395
+ const attribution = document.getElementById('left-sidebar-attribution');
396
+
397
+ if (!leftSidebarContainer || !attribution) return;
398
+
399
+ leftSidebarContainer.addEventListener('mouseenter', () => {
400
+ attribution.style.opacity = '1';
401
+ });
402
+
403
+ leftSidebarContainer.addEventListener('mouseleave', () => {
404
+ attribution.style.opacity = '0';
405
+ });
406
+ }
407
+
408
+ // Export the new function as well
409
+ export { initializeLeftSidebarAttribution };
js/utils/search.js ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // search.js - Complete MiniSearch implementation with resources and scores
2
+ import { sampleResources } from '../data/resources.js';
3
+ import { areasData } from '../data/areas.js';
4
+
5
+ let miniSearch, miniSearchResources;
6
+ let allArtifacts; // no longer needs to be explicitly defined
7
+
8
+ // Helper to create and populate a MiniSearch instance
9
+ function createMiniSearchIndex(data, storeFields) {
10
+ const search = new MiniSearch({
11
+ fields: ['title', 'description', 'areas', 'topics'],
12
+ storeFields: storeFields
13
+ });
14
+ search.addAll(data);
15
+ return search;
16
+ }
17
+
18
+ // Initialize search
19
+ export async function initializeSearch(artifactsData) {
20
+ // Assign the passed artifactsData to allArtifacts
21
+ allArtifacts = artifactsData;
22
+
23
+ // The fetch for artifacts.json is removed from here since it's now loaded in main.js
24
+
25
+ // Prepare data for MiniSearch
26
+ const searchData = allArtifacts.map((artifact, index) => ({
27
+ id: index,
28
+ title: artifact.title,
29
+ description: artifact.description,
30
+ type: artifact.type,
31
+ areas: (artifact.areas || []).join(' '),
32
+ topics: (artifact.topics || []).join(' '),
33
+ url: artifact.url,
34
+ date: artifact.date
35
+ }));
36
+
37
+ // Prepare resources data
38
+ const resourcesData = sampleResources.map((resource, index) => ({
39
+ id: index,
40
+ title: resource.title,
41
+ description: resource.description,
42
+ type: resource.type,
43
+ areas: (resource.areaTags || []).join(' '),
44
+ topics: (resource.subAreaTags || []).join(' '),
45
+ url: resource.url,
46
+ date: resource.date
47
+ }));
48
+
49
+ // Initialize MiniSearch for artifacts
50
+ miniSearch = createMiniSearchIndex(searchData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']);
51
+
52
+ // Initialize MiniSearch for resources
53
+ miniSearchResources = createMiniSearchIndex(resourcesData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']);
54
+ }
55
+
56
+ export function searchContent(query) {
57
+ if (!query || query.trim().length < 2) {
58
+ return { artifacts: [], resources: [] };
59
+ }
60
+
61
+ const artifactResults = miniSearch.search(query, {
62
+ prefix: true,
63
+ fuzzy: 0.2,
64
+ boost: { title: 2, description: 1 }
65
+ });
66
+
67
+ const resourceResults = miniSearchResources.search(query, {
68
+ prefix: true,
69
+ fuzzy: 0.2,
70
+ boost: { title: 2, description: 1 }
71
+ });
72
+
73
+ return {
74
+ artifacts: artifactResults, // Show all results, not limited to 5
75
+ resources: resourceResults
76
+ };
77
+ }
78
+
79
+ // Helper functions
80
+ function getAreaDisplayName(area) {
81
+ return areasData[area]?.title || area;
82
+ }
83
+
84
+ function getSubAreaDisplayName(areaId, subArea) {
85
+ const area = areasData[areaId];
86
+ if (!area || !area.subAreas) return subArea;
87
+ const subAreaData = area.subAreas[subArea];
88
+ return typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || subArea;
89
+ }
90
+
91
+ // Search UI
92
+ export function initializeSearchUI(artifactsData) {
93
+ initializeSearch(artifactsData).then(() => {
94
+ console.log('Search initialized');
95
+ });
96
+
97
+ const searchInput = document.getElementById('search-input');
98
+ const searchResults = document.getElementById('search-results');
99
+
100
+ if (!searchInput || !searchResults) return;
101
+
102
+ let searchTimeout;
103
+
104
+ // Function to perform search
105
+ function performSearch() {
106
+ const query = searchInput.value.trim();
107
+
108
+ if (query.length < 2) {
109
+ searchResults.innerHTML = `<div class="text-gray-500 text-center py-8"><p>Enter a search term...</p></div>`;
110
+ return;
111
+ }
112
+
113
+ const results = searchContent(query);
114
+ displaySearchResults(results, query);
115
+ }
116
+
117
+ // Handle input events (typing)
118
+ searchInput.addEventListener('input', (e) => {
119
+ clearTimeout(searchTimeout);
120
+ const query = e.target.value.trim();
121
+
122
+ if (query.length < 2) {
123
+ searchResults.innerHTML = `<div class="text-gray-500 text-center py-8"><p>Enter a search term...</p></div>`;
124
+ return;
125
+ }
126
+
127
+ // Debounce search
128
+ searchTimeout = setTimeout(() => {
129
+ performSearch();
130
+ }, 300);
131
+ });
132
+
133
+ // Handle Enter key (immediate search)
134
+ searchInput.addEventListener('keydown', (e) => {
135
+ if (e.key === 'Enter') {
136
+ e.preventDefault();
137
+ clearTimeout(searchTimeout);
138
+ performSearch();
139
+ }
140
+ });
141
+ }
142
+
143
+ // Update the displaySearchResults function in search.js
144
+ export function displaySearchResults(results, query) {
145
+ const { artifacts, resources } = results;
146
+ const totalResults = artifacts.length + resources.length;
147
+
148
+ if (totalResults === 0) {
149
+ document.getElementById('search-results').innerHTML = `
150
+ <div class="text-gray-500 text-center py-8"><p>No results found for "${query}"</p></div>
151
+ `;
152
+ return;
153
+ }
154
+
155
+ let html = `<div class="text-sm text-gray-600 mb-4">
156
+ Found ${totalResults} results (${artifacts.length} writings, ${resources.length} resources)
157
+ </div>`;
158
+
159
+ // Display artifacts with collapsible header
160
+ if (artifacts.length > 0) {
161
+ html += `
162
+ <div class="mb-4">
163
+ <button onclick="toggleCategory('artifacts')" class="flex items-center justify-between w-full p-2 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors">
164
+ <h4 class="font-semibold text-gray-900">Writings (${artifacts.length})</h4>
165
+ <svg id="artifacts-arrow" class="w-4 h-4 text-gray-600 transform transition-transform" style="transform: rotate(0deg);">
166
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
167
+ </svg>
168
+ </button>
169
+ <div id="artifacts-content" class="space-y-2 mt-2">
170
+ `;
171
+
172
+ artifacts.forEach(result => {
173
+ const score = Math.round(result.score * 100);
174
+
175
+ // Create area links
176
+ const areaLinks = result.areas ? result.areas.split(' ').map(area => {
177
+ const areaData = areasData[area];
178
+ if (!areaData) return '';
179
+ const colorClass = areaData?.colors?.bg || 'bg-blue-100';
180
+ const textColorClass = areaData?.colors?.text || 'text-blue-800';
181
+ return `<a href="/${area}#overview" class="text-xs px-2 py-1 ${colorClass} ${textColorClass} rounded hover:opacity-80 transition-opacity">${areaData?.title || area}</a>`;
182
+ }).filter(link => link).join('') : '';
183
+
184
+ // Create sub-area links
185
+ const subAreaLinks = result.topics ? result.topics.split(' ').map(topic => {
186
+ const primaryArea = result.areas?.split(' ')[0] || 'efficiency';
187
+ const areaData = areasData[primaryArea];
188
+ if (!areaData || !areaData.subAreas) return '';
189
+
190
+ const subAreaData = areaData.subAreas[topic];
191
+ if (!subAreaData) return '';
192
+
193
+ const subAreaName = typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || topic;
194
+ const colorClass = subAreaData?.color || 'bg-gray-100 text-gray-800';
195
+
196
+ return `<a href="/${primaryArea}#${topic}" class="text-xs px-2 py-1 ${colorClass} rounded hover:opacity-80 transition-opacity">${subAreaName}</a>`;
197
+ }).filter(link => link).join('') : '';
198
+
199
+ html += `
200
+ <div class="p-3 bg-gray-50 rounded-lg border">
201
+ <div class="flex items-center justify-between mb-2">
202
+ <h5 class="font-medium text-sm text-gray-900">${result.title}</h5>
203
+ <div class="flex items-center gap-2">
204
+ <span class="text-xs text-gray-500">${score}%</span>
205
+ <a href="${result.url}" target="_blank" class="text-gray-400 hover:text-gray-600 transition-colors">
206
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
207
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
208
+ </svg>
209
+ </a>
210
+ </div>
211
+ </div>
212
+ <div class="flex items-center gap-2 mb-2 text-xs text-gray-600">
213
+ <span class="px-2 py-1 bg-gray-200 text-gray-700 rounded">${result.type}</span>
214
+ <span>${result.date}</span>
215
+ </div>
216
+ <div class="flex flex-wrap gap-1">
217
+ ${areaLinks}
218
+ ${subAreaLinks}
219
+ </div>
220
+ </div>
221
+ `;
222
+ });
223
+
224
+ html += `</div></div>`;
225
+ }
226
+
227
+ // Display resources with collapsible header
228
+ if (resources.length > 0) {
229
+ html += `
230
+ <div class="mb-4">
231
+ <button onclick="toggleCategory('resources')" class="flex items-center justify-between w-full p-2 bg-green-50 hover:bg-green-100 rounded-lg transition-colors">
232
+ <h4 class="font-semibold text-gray-900">Resources (${resources.length})</h4>
233
+ <svg id="resources-arrow" class="w-4 h-4 text-gray-600 transform transition-transform" style="transform: rotate(0deg);">
234
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
235
+ </svg>
236
+ </button>
237
+ <div id="resources-content" class="space-y-2 mt-2">
238
+ `;
239
+
240
+ resources.forEach(result => {
241
+ const score = Math.round(result.score * 100);
242
+
243
+ // Create area links for resources
244
+ const areaLinks = result.areas ? result.areas.split(' ').map(area => {
245
+ const areaData = areasData[area];
246
+ if (!areaData) return '';
247
+ const colorClass = areaData?.colors?.bg || 'bg-green-100';
248
+ const textColorClass = areaData?.colors?.text || 'text-green-800';
249
+ return `<a href="/${area}#overview" class="text-xs px-2 py-1 ${colorClass} ${textColorClass} rounded hover:opacity-80 transition-opacity">${areaData?.title || area}</a>`;
250
+ }).filter(link => link).join('') : '';
251
+
252
+ // Create sub-area links for resources
253
+ const subAreaLinks = result.topics ? result.topics.split(' ').map(topic => {
254
+ const primaryArea = result.areas?.split(' ')[0] || 'efficiency';
255
+ const areaData = areasData[primaryArea];
256
+ if (!areaData || !areaData.subAreas) return '';
257
+
258
+ const subAreaData = areaData.subAreas[topic];
259
+ if (!subAreaData) return '';
260
+
261
+ const subAreaName = typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || topic;
262
+ const colorClass = subAreaData?.color || 'bg-gray-100 text-gray-800';
263
+
264
+ return `<a href="/${primaryArea}#${topic}" class="text-xs px-2 py-1 ${colorClass} rounded hover:opacity-80 transition-opacity">${subAreaName}</a>`;
265
+ }).filter(link => link).join('') : '';
266
+
267
+ html += `
268
+ <div class="p-3 bg-gray-50 rounded-lg border">
269
+ <div class="flex items-center justify-between mb-2">
270
+ <h5 class="font-medium text-sm text-gray-900">${result.title}</h5>
271
+ <div class="flex items-center gap-2">
272
+ <span class="text-xs text-gray-500">${score}%</span>
273
+ <a href="${result.url}" target="_blank" class="text-gray-400 hover:text-gray-600 transition-colors">
274
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
275
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
276
+ </svg>
277
+ </a>
278
+ </div>
279
+ </div>
280
+ <div class="flex items-center gap-2 mb-2 text-xs text-gray-600">
281
+ <span class="px-2 py-1 bg-gray-200 text-gray-700 rounded">${result.type}</span>
282
+ <span>${result.date}</span>
283
+ </div>
284
+ <div class="flex flex-wrap gap-1">
285
+ ${areaLinks}
286
+ ${subAreaLinks}
287
+ </div>
288
+ </div>
289
+ `;
290
+ });
291
+
292
+ html += `</div></div>`;
293
+ }
294
+
295
+ document.getElementById('search-results').innerHTML = html;
296
+ }
297
+
298
+ // Add this function to handle category toggling
299
+ window.toggleCategory = function(category) {
300
+ const content = document.getElementById(`${category}-content`);
301
+ const arrow = document.getElementById(`${category}-arrow`);
302
+
303
+ if (content.style.display === 'none') {
304
+ content.style.display = 'block';
305
+ arrow.style.transform = 'rotate(0deg)';
306
+ } else {
307
+ content.style.display = 'none';
308
+ arrow.style.transform = 'rotate(-90deg)';
309
+ }
310
+ };
js/utils/sidebar.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { overallBackgroundImage } from '../data/areas.js';
2
+
3
+ export function renderSidebar(title, items) {
4
+ return `
5
+ <div class="relative h-full">
6
+ <nav class="p-4 overflow-y-auto h-full pb-16">
7
+ <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">${title}</h3>
8
+ <ul class="space-y-1">
9
+ ${items.map(item => {
10
+ const activeClass = item.isActive ? 'text-blue-600 bg-blue-50' : 'text-gray-700 hover:text-gray-900 hover:bg-gray-50';
11
+ const indentClass = item.isNested ? 'ml-4' : '';
12
+
13
+ if (item.isHeader) {
14
+ return `
15
+ <li class="${indentClass}">
16
+ <span class="block px-3 py-2 text-lg font-semibold ${activeClass} rounded-md">${item.label}</span>
17
+ ${item.subItems && item.subItems.length > 0 ? `
18
+ <ul class="space-y-1 mt-1">
19
+ ${item.subItems.map(subItem => {
20
+ const subActiveClass = subItem.isActive ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50';
21
+ return `
22
+ <li class="ml-4"><a href="${subItem.href}" class="page-nav-link block px-3 py-2 text-md ${subActiveClass} rounded-md transition-colors">${subItem.label}</a></li>
23
+ `;
24
+ }).join('')}
25
+ </ul>
26
+ ` : ''}
27
+ </li>
28
+ `;
29
+ } else {
30
+ return `
31
+ <li class="${indentClass}">
32
+ <a href="${item.href}" class="page-nav-link block px-3 py-2 text-lg ${activeClass} rounded-md transition-colors">${item.label}</a>
33
+ ${item.subItems && item.subItems.length > 0 ? `
34
+ <ul class="space-y-1 mt-1">
35
+ ${item.subItems.map(subItem => {
36
+ const subActiveClass = subItem.isActive ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50';
37
+ return `
38
+ <li class="ml-4"><a href="${subItem.href}" class="page-nav-link block px-3 py-2 text-md ${subActiveClass} rounded-md transition-colors">${subItem.label}</a></li>
39
+ `;
40
+ }).join('')}
41
+ </ul>
42
+ ` : ''}
43
+ </li>
44
+ `;
45
+ }
46
+ }).join('')}
47
+ </ul>
48
+ </nav>
49
+ <div id="left-sidebar-attribution" class="absolute bottom-16 left-4 bg-black bg-opacity-75 text-white text-xs px-2 py-1 rounded transition-opacity duration-200 max-w-xs z-50">
50
+ <a href="${overallBackgroundImage.sourceUrl}" target="_blank" class="text-blue-300 hover:text-blue-100">
51
+ ${overallBackgroundImage.attribution}
52
+ </a>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ `;
57
+ }