|
|
|
|
|
import { sampleResources } from '../data/resources.js'; |
|
|
import { areasData } from '../data/areas.js'; |
|
|
|
|
|
let miniSearch, miniSearchResources; |
|
|
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 |
|
|
})); |
|
|
|
|
|
|
|
|
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 |
|
|
})); |
|
|
|
|
|
|
|
|
miniSearch = createMiniSearchIndex(searchData, ['title', 'description', 'type', 'areas', 'topics', 'url', 'date']); |
|
|
|
|
|
|
|
|
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, |
|
|
resources: resourceResults |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
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).then(() => { |
|
|
console.log('Search initialized'); |
|
|
}); |
|
|
|
|
|
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, 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>`; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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('') : ''; |
|
|
|
|
|
|
|
|
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>`; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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('') : ''; |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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)'; |
|
|
} |
|
|
}; |