File size: 13,548 Bytes
98f8ae8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
// search.js - Complete MiniSearch implementation with resources and scores
import { sampleResources } from '../data/resources.js';
import { areasData } from '../data/areas.js';
let miniSearch, miniSearchResources;
let allArtifacts; // no longer needs to be explicitly defined
// Helper to create and populate a MiniSearch instance
function createMiniSearchIndex(data, storeFields) {
const search = new MiniSearch({
fields: ['title', 'description', 'areas', 'topics'],
storeFields: storeFields
});
search.addAll(data);
return search;
}
// Initialize search
export async function initializeSearch(artifactsData) {
// Assign the passed artifactsData to allArtifacts
allArtifacts = artifactsData;
// The fetch for artifacts.json is removed from here since it's now loaded in main.js
// Prepare data for MiniSearch
const searchData = allArtifacts.map((artifact, index) => ({
id: index,
title: artifact.title,
description: artifact.description,
type: artifact.type,
areas: (artifact.areas || []).join(' '),
topics: (artifact.topics || []).join(' '),
url: artifact.url,
date: artifact.date
}));
// Prepare resources data
const resourcesData = sampleResources.map((resource, index) => ({
id: index,
title: resource.title,
description: resource.description,
type: resource.type,
areas: (resource.areaTags || []).join(' '),
topics: (resource.subAreaTags || []).join(' '),
url: resource.url,
date: resource.date
}));
// Initialize MiniSearch for artifacts
miniSearch = createMiniSearchIndex(searchData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']);
// Initialize MiniSearch for resources
miniSearchResources = createMiniSearchIndex(resourcesData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']);
}
export function searchContent(query) {
if (!query || query.trim().length < 2) {
return { artifacts: [], resources: [] };
}
const artifactResults = miniSearch.search(query, {
prefix: true,
fuzzy: 0.2,
boost: { title: 2, description: 1 }
});
const resourceResults = miniSearchResources.search(query, {
prefix: true,
fuzzy: 0.2,
boost: { title: 2, description: 1 }
});
return {
artifacts: artifactResults, // Show all results, not limited to 5
resources: resourceResults
};
}
// Helper functions
function getAreaDisplayName(area) {
return areasData[area]?.title || area;
}
function getSubAreaDisplayName(areaId, subArea) {
const area = areasData[areaId];
if (!area || !area.subAreas) return subArea;
const subAreaData = area.subAreas[subArea];
return typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || subArea;
}
// Search UI
export function initializeSearchUI(artifactsData) {
initializeSearch(artifactsData).then(() => {
console.log('Search initialized');
});
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
if (!searchInput || !searchResults) return;
let searchTimeout;
// Function to perform search
function performSearch() {
const query = searchInput.value.trim();
if (query.length < 2) {
searchResults.innerHTML = `<div class="text-gray-500 text-center py-8"><p>Enter a search term...</p></div>`;
return;
}
const results = searchContent(query);
displaySearchResults(results, query);
}
// Handle input events (typing)
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
searchResults.innerHTML = `<div class="text-gray-500 text-center py-8"><p>Enter a search term...</p></div>`;
return;
}
// Debounce search
searchTimeout = setTimeout(() => {
performSearch();
}, 300);
});
// Handle Enter key (immediate search)
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
clearTimeout(searchTimeout);
performSearch();
}
});
}
// Update the displaySearchResults function in search.js
export function displaySearchResults(results, query) {
const { artifacts, resources } = results;
const totalResults = artifacts.length + resources.length;
if (totalResults === 0) {
document.getElementById('search-results').innerHTML = `
<div class="text-gray-500 text-center py-8"><p>No results found for "${query}"</p></div>
`;
return;
}
let html = `<div class="text-sm text-gray-600 mb-4">
Found ${totalResults} results (${artifacts.length} writings, ${resources.length} resources)
</div>`;
// Display artifacts with collapsible header
if (artifacts.length > 0) {
html += `
<div class="mb-4">
<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">
<h4 class="font-semibold text-gray-900">Writings (${artifacts.length})</h4>
<svg id="artifacts-arrow" class="w-4 h-4 text-gray-600 transform transition-transform" style="transform: rotate(0deg);">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="artifacts-content" class="space-y-2 mt-2">
`;
artifacts.forEach(result => {
const score = Math.round(result.score * 100);
// Create area links
const areaLinks = result.areas ? result.areas.split(' ').map(area => {
const areaData = areasData[area];
if (!areaData) return '';
const colorClass = areaData?.colors?.bg || 'bg-blue-100';
const textColorClass = areaData?.colors?.text || 'text-blue-800';
return `<a href="/${area}#overview" class="text-xs px-2 py-1 ${colorClass} ${textColorClass} rounded hover:opacity-80 transition-opacity">${areaData?.title || area}</a>`;
}).filter(link => link).join('') : '';
// Create sub-area links
const subAreaLinks = result.topics ? result.topics.split(' ').map(topic => {
const primaryArea = result.areas?.split(' ')[0] || 'efficiency';
const areaData = areasData[primaryArea];
if (!areaData || !areaData.subAreas) return '';
const subAreaData = areaData.subAreas[topic];
if (!subAreaData) return '';
const subAreaName = typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || topic;
const colorClass = subAreaData?.color || 'bg-gray-100 text-gray-800';
return `<a href="/${primaryArea}#${topic}" class="text-xs px-2 py-1 ${colorClass} rounded hover:opacity-80 transition-opacity">${subAreaName}</a>`;
}).filter(link => link).join('') : '';
html += `
<div class="p-3 bg-gray-50 rounded-lg border">
<div class="flex items-center justify-between mb-2">
<h5 class="font-medium text-sm text-gray-900">${result.title}</h5>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">${score}%</span>
<a href="${result.url}" target="_blank" class="text-gray-400 hover:text-gray-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
</svg>
</a>
</div>
</div>
<div class="flex items-center gap-2 mb-2 text-xs text-gray-600">
<span class="px-2 py-1 bg-gray-200 text-gray-700 rounded">${result.type}</span>
<span>${result.date}</span>
</div>
<div class="flex flex-wrap gap-1">
${areaLinks}
${subAreaLinks}
</div>
</div>
`;
});
html += `</div></div>`;
}
// Display resources with collapsible header
if (resources.length > 0) {
html += `
<div class="mb-4">
<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">
<h4 class="font-semibold text-gray-900">Resources (${resources.length})</h4>
<svg id="resources-arrow" class="w-4 h-4 text-gray-600 transform transition-transform" style="transform: rotate(0deg);">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="resources-content" class="space-y-2 mt-2">
`;
resources.forEach(result => {
const score = Math.round(result.score * 100);
// Create area links for resources
const areaLinks = result.areas ? result.areas.split(' ').map(area => {
const areaData = areasData[area];
if (!areaData) return '';
const colorClass = areaData?.colors?.bg || 'bg-green-100';
const textColorClass = areaData?.colors?.text || 'text-green-800';
return `<a href="/${area}#overview" class="text-xs px-2 py-1 ${colorClass} ${textColorClass} rounded hover:opacity-80 transition-opacity">${areaData?.title || area}</a>`;
}).filter(link => link).join('') : '';
// Create sub-area links for resources
const subAreaLinks = result.topics ? result.topics.split(' ').map(topic => {
const primaryArea = result.areas?.split(' ')[0] || 'efficiency';
const areaData = areasData[primaryArea];
if (!areaData || !areaData.subAreas) return '';
const subAreaData = areaData.subAreas[topic];
if (!subAreaData) return '';
const subAreaName = typeof subAreaData === 'string' ? subAreaData : subAreaData?.name || topic;
const colorClass = subAreaData?.color || 'bg-gray-100 text-gray-800';
return `<a href="/${primaryArea}#${topic}" class="text-xs px-2 py-1 ${colorClass} rounded hover:opacity-80 transition-opacity">${subAreaName}</a>`;
}).filter(link => link).join('') : '';
html += `
<div class="p-3 bg-gray-50 rounded-lg border">
<div class="flex items-center justify-between mb-2">
<h5 class="font-medium text-sm text-gray-900">${result.title}</h5>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">${score}%</span>
<a href="${result.url}" target="_blank" class="text-gray-400 hover:text-gray-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
</svg>
</a>
</div>
</div>
<div class="flex items-center gap-2 mb-2 text-xs text-gray-600">
<span class="px-2 py-1 bg-gray-200 text-gray-700 rounded">${result.type}</span>
<span>${result.date}</span>
</div>
<div class="flex flex-wrap gap-1">
${areaLinks}
${subAreaLinks}
</div>
</div>
`;
});
html += `</div></div>`;
}
document.getElementById('search-results').innerHTML = html;
}
// Add this function to handle category toggling
window.toggleCategory = function(category) {
const content = document.getElementById(`${category}-content`);
const arrow = document.getElementById(`${category}-arrow`);
if (content.style.display === 'none') {
content.style.display = 'block';
arrow.style.transform = 'rotate(0deg)';
} else {
content.style.display = 'none';
arrow.style.transform = 'rotate(-90deg)';
}
}; |