|
|
|
|
|
import { renderSearchTags } from './tags.js'; |
|
|
|
|
|
|
|
|
const areasData = window.areasData; |
|
|
|
|
|
let miniSearch; |
|
|
let allArtifacts; |
|
|
|
|
|
|
|
|
function createMiniSearchIndex(data, storeFields) { |
|
|
const search = new MiniSearch({ |
|
|
fields: ['title', 'description', 'areas', 'topics'], |
|
|
storeFields: storeFields |
|
|
}); |
|
|
search.addAll(data); |
|
|
return search; |
|
|
} |
|
|
|
|
|
|
|
|
export async function initializeSearch(artifactsData) { |
|
|
allArtifacts = artifactsData; |
|
|
|
|
|
|
|
|
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 |
|
|
})); |
|
|
|
|
|
|
|
|
miniSearch = createMiniSearchIndex(searchData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']); |
|
|
} |
|
|
|
|
|
export function searchContent(query) { |
|
|
if (!query || query.trim().length < 2) { |
|
|
return { artifacts: [] }; |
|
|
} |
|
|
|
|
|
const artifactResults = miniSearch.search(query, { |
|
|
prefix: true, |
|
|
fuzzy: 0.2, |
|
|
boost: { title: 2, description: 1 } |
|
|
}); |
|
|
|
|
|
return { |
|
|
artifacts: artifactResults |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
export function initializeSearchUI(artifactsData) { |
|
|
initializeSearch(artifactsData); |
|
|
|
|
|
const searchInput = document.getElementById('search-input'); |
|
|
const searchResults = document.getElementById('search-results'); |
|
|
|
|
|
if (!searchInput || !searchResults) return; |
|
|
|
|
|
let searchTimeout; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
searchTimeout = setTimeout(() => { |
|
|
performSearch(); |
|
|
}, 300); |
|
|
}); |
|
|
|
|
|
|
|
|
searchInput.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter') { |
|
|
e.preventDefault(); |
|
|
clearTimeout(searchTimeout); |
|
|
performSearch(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
export function displaySearchResults(results, query) { |
|
|
const { artifacts } = results; |
|
|
const totalResults = artifacts.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-base text-gray-600 mb-4 font-medium"> |
|
|
Found ${totalResults} result${totalResults === 1 ? '' : 's'} |
|
|
</div>`; |
|
|
|
|
|
|
|
|
if (artifacts.length > 0) { |
|
|
html += `<div class="space-y-2">`; |
|
|
|
|
|
artifacts.forEach(result => { |
|
|
const score = Math.round(result.score * 100); |
|
|
|
|
|
|
|
|
const areas = result.areas ? result.areas.split(' ') : []; |
|
|
const topics = result.topics ? result.topics.split(' ') : []; |
|
|
const { areaLinksHtml: areaLinks, topicLinksHtml: subAreaLinks } = renderSearchTags(areas, topics); |
|
|
|
|
|
html += ` |
|
|
<div class="p-4 bg-white/95 rounded-lg border border-gray-300 shadow-sm hover:shadow-md transition-shadow"> |
|
|
<div class="flex items-center justify-between mb-2"> |
|
|
<h5 class="font-medium text-base text-gray-900 leading-snug">${result.title}</h5> |
|
|
<div class="flex items-center gap-2 flex-shrink-0 ml-2"> |
|
|
<span class="text-sm text-gray-500">${score}%</span> |
|
|
<a href="${result.url}" target="_blank" class="text-gray-400 hover:text-gray-600 transition-colors" aria-label="Open ${result.title}"> |
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> |
|
|
<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-sm text-gray-600"> |
|
|
<span class="px-2.5 py-1 bg-gray-200 text-gray-700 rounded font-medium">${result.type}</span> |
|
|
<span class="text-gray-500">${result.date}</span> |
|
|
</div> |
|
|
<div class="flex flex-wrap gap-2"> |
|
|
${areaLinks} |
|
|
${subAreaLinks} |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}); |
|
|
|
|
|
html += `</div>`; |
|
|
} |
|
|
|
|
|
document.getElementById('search-results').innerHTML = html; |
|
|
} |