class SearchInterface { constructor() { this.searchIndex = []; this.fuse = null; this.filters = { tags: new Set(), narrators: new Set(), facilitators: new Set(), dateFrom: null, dateTo: null, useOrLogic: false }; this.init(); } async init() { try { const response = await fetch('/search.json'); this.searchIndex = await response.json(); this.fuse = new Fuse(this.searchIndex, { keys: ['title', 'content', 'summary', 'tags', 'narrator', 'facilitator'], threshold: 0.3, ignoreLocation: true, useExtendedSearch: true }); this.setupEventListeners(); this.populateFilterOptions(); this.initializeFromURL(); } catch (error) { this.handleError(error, 'Failed to initialize search'); } } handleError(error, message = 'Search error') { console.error(message, error); const resultsDiv = document.querySelector('[data-results]'); if (resultsDiv) { resultsDiv.innerHTML = `
${message}. Please try again later.
`; } } debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } setupEventListeners() { const searchInput = document.querySelector('[data-search-input]'); const filterToggle = document.querySelector('[data-filter-toggle]'); const filterPanel = document.querySelector('[data-filter-panel]'); const logicMode = document.querySelector('[data-logic-mode]'); const dateInputs = document.querySelectorAll('[data-date-from], [data-date-to]'); if (searchInput) { searchInput.addEventListener('input', this.debounce(() => this.performSearch(), 300) ); } if (filterToggle && filterPanel) { filterToggle.addEventListener('click', () => { filterPanel.classList.toggle('active'); }); } if (logicMode) { logicMode.addEventListener('change', () => { this.filters.useOrLogic = logicMode.checked; this.performSearch(); }); } dateInputs.forEach(input => { input.addEventListener('change', () => this.performSearch()); }); } performSearch() { const searchInput = document.querySelector('[data-search-input]'); const dateFrom = document.querySelector('[data-date-from]'); const dateTo = document.querySelector('[data-date-to]'); const resultsDiv = document.querySelector('[data-results]'); const countDiv = document.querySelector('[data-results-count]'); // Get current search state const query = searchInput?.value || ''; this.filters.dateFrom = this.parseDate(dateFrom?.value); this.filters.dateTo = this.parseDate(dateTo?.value); // Perform search let results = query ? this.fuse.search(query) : this.searchIndex; results = results.map(r => r.item || r); // Handle Fuse.js results format // Apply filters results = results.filter(item => { const matchesTags = this.filters.tags.size === 0 || (item.tags && (this.filters.useOrLogic ? item.tags.some(tag => this.filters.tags.has(tag)) : item.tags.every(tag => this.filters.tags.has(tag)))); const matchesNarrator = this.filters.narrators.size === 0 || this.filters.narrators.has(item.narrator); const matchesFacilitator = this.filters.facilitators.size === 0 || this.filters.facilitators.has(item.facilitator); const itemDate = new Date(item.date); const afterStartDate = !this.filters.dateFrom || itemDate >= new Date(this.filters.dateFrom); const beforeEndDate = !this.filters.dateTo || itemDate <= new Date(this.filters.dateTo); return matchesTags && matchesNarrator && matchesFacilitator && afterStartDate && beforeEndDate; }); // Update URL state const params = new URLSearchParams(); if (query) params.set('q', query); if (this.filters.dateFrom) params.set('from', this.filters.dateFrom); if (this.filters.dateTo) params.set('to', this.filters.dateTo); if (this.filters.useOrLogic) params.set('logic', 'or'); if (this.filters.tags.size) params.set('tags', Array.from(this.filters.tags).join(',')); if (this.filters.narrators.size) params.set('narrators', Array.from(this.filters.narrators).join(',')); if (this.filters.facilitators.size) params.set('facilitators', Array.from(this.filters.facilitators).join(',')); const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`; window.history.replaceState({}, '', newUrl); // Render results if (resultsDiv) { if (results.length === 0) { resultsDiv.innerHTML = `
No results found. Try adjusting your search or filters.
`; } else { resultsDiv.innerHTML = results.map(item => `

${item.title}

${item.narrator} ${new Date(item.date).toLocaleDateString()}
${item.tags ? `
${item.tags.map(tag => ` ${tag} `).join('')}
` : ''}

${item.summary}

`).join(''); } } // Update count if (countDiv) { countDiv.textContent = `${results.length} result${results.length === 1 ? '' : 's'} found`; } } populateFilterOptions() { const tagsContainer = document.querySelector('[data-filter-tags]'); const narratorsContainer = document.querySelector('[data-filter-narrators]'); const facilitatorsContainer = document.querySelector('[data-filter-facilitators]'); // Get unique values const tags = new Set(this.searchIndex.flatMap(item => item.tags || [])); const narrators = new Set(this.searchIndex.map(item => item.narrator).filter(Boolean)); const facilitators = new Set(this.searchIndex.map(item => item.facilitator).filter(Boolean)); // Helper to create filter options const createFilterOption = (value, container, filterSet) => { const label = document.createElement('label'); label.className = 'flex items-center gap-2 text-sm'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = value; checkbox.checked = filterSet.has(value); checkbox.addEventListener('change', () => { if (checkbox.checked) { filterSet.add(value); } else { filterSet.delete(value); } this.performSearch(); }); label.appendChild(checkbox); label.appendChild(document.createTextNode(value)); container?.appendChild(label); }; // Populate filters if (tagsContainer) { tagsContainer.innerHTML = ''; Array.from(tags).sort().forEach(tag => createFilterOption(tag, tagsContainer, this.filters.tags) ); } if (narratorsContainer) { narratorsContainer.innerHTML = ''; Array.from(narrators).sort().forEach(narrator => createFilterOption(narrator, narratorsContainer, this.filters.narrators) ); } if (facilitatorsContainer) { facilitatorsContainer.innerHTML = ''; Array.from(facilitators).sort().forEach(facilitator => createFilterOption(facilitator, facilitatorsContainer, this.filters.facilitators) ); } } initializeFromURL() { const params = new URLSearchParams(window.location.search); const searchInput = document.querySelector('[data-search-input]'); const dateFrom = document.querySelector('[data-date-from]'); const dateTo = document.querySelector('[data-date-to]'); const logicMode = document.querySelector('[data-logic-mode]'); // Restore search query if (searchInput && params.has('q')) { searchInput.value = params.get('q'); } // Restore date filters if (dateFrom && params.has('from')) { dateFrom.value = params.get('from'); this.filters.dateFrom = params.get('from'); } if (dateTo && params.has('to')) { dateTo.value = params.get('to'); this.filters.dateTo = params.get('to'); } // Restore logic mode if (logicMode) { this.filters.useOrLogic = params.get('logic') === 'or'; logicMode.checked = this.filters.useOrLogic; } // Restore filter selections if (params.has('tags')) { params.get('tags').split(',').forEach(tag => this.filters.tags.add(tag)); } if (params.has('narrators')) { params.get('narrators').split(',').forEach(narrator => this.filters.narrators.add(narrator)); } if (params.has('facilitators')) { params.get('facilitators').split(',').forEach(facilitator => this.filters.facilitators.add(facilitator)); } // Perform initial search if we have any parameters if ([...params.keys()].length > 0) { this.performSearch(); } } parseDate(dateString) { try { return dateString ? new Date(dateString) : null; } catch (error) { this.handleError(error, 'Invalid date format'); return null; } } } // Initialize search when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new SearchInterface()); } else { new SearchInterface(); }