From: Danilo M. Date: Mon, 20 Apr 2026 11:50:23 +0000 (+0200) Subject: feat: create shared search module with lazy-loading and Alpine components X-Git-Tag: release_22042026-1342~73 X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=aefc3d8c3994ba0eb1e3dfced3564ba2b0f8b73f;p=danix.xyz-2.git feat: create shared search module with lazy-loading and Alpine components - Implement loadSearchIndex() for async JSON fetching and caching - Implement filterArticles(query, articles) with case-insensitive search (max 5 results) - Register three Alpine.js components: searchOverlay, mobileSearch, notFoundPage - Support desktop modal, mobile menu, and 404 page search integration - Include Escape key handling and index lazy-loading optimizations Co-Authored-By: Claude Haiku 4.5 --- diff --git a/themes/danix-xyz-hacker/assets/js/search.js b/themes/danix-xyz-hacker/assets/js/search.js new file mode 100644 index 0000000..94c6323 --- /dev/null +++ b/themes/danix-xyz-hacker/assets/js/search.js @@ -0,0 +1,130 @@ +// Lazy-load search index from JSON file +async function loadSearchIndex() { + if (window.searchIndex) { + return window.searchIndex; + } + try { + const response = await fetch('/search-index.json'); + if (!response.ok) throw new Error('Failed to load search index'); + window.searchIndex = await response.json(); + return window.searchIndex; + } catch (error) { + console.error('Error loading search index:', error); + return []; + } +} + +// Filter articles by query (case-insensitive, max 5 results) +function filterArticles(query, articles) { + if (!query.trim()) { + return []; + } + const lowerQuery = query.toLowerCase(); + return articles + .filter(article => + article.title.toLowerCase().includes(lowerQuery) || + article.summary.toLowerCase().includes(lowerQuery) + ) + .slice(0, 5); +} + +// Register Alpine.js components +document.addEventListener('alpine:init', () => { + // Desktop search modal component + Alpine.data('searchOverlay', () => ({ + isOpen: false, + searchQuery: '', + filteredArticles: [], + allArticles: [], + indexLoaded: false, + + async open() { + this.isOpen = true; + await this.ensureIndexLoaded(); + this.$nextTick(() => { + const input = this.$el.querySelector('#search-input-desktop'); + if (input) input.focus(); + }); + }, + + close() { + this.isOpen = false; + this.searchQuery = ''; + this.filteredArticles = []; + }, + + async ensureIndexLoaded() { + if (!this.indexLoaded) { + this.allArticles = await loadSearchIndex(); + this.indexLoaded = true; + } + }, + + filterArticles(query) { + this.searchQuery = query; + this.filteredArticles = filterArticles(query, this.allArticles); + }, + + handleEscape(event) { + if (event.key === 'Escape') { + this.close(); + } + } + })); + + // Mobile search component (integrated into hamburger menu) + Alpine.data('mobileSearch', () => ({ + searchQuery: '', + filteredArticles: [], + allArticles: [], + indexLoaded: false, + + async ensureIndexLoaded() { + if (!this.indexLoaded) { + this.allArticles = await loadSearchIndex(); + this.indexLoaded = true; + } + }, + + filterArticles(query) { + this.searchQuery = query; + this.filteredArticles = filterArticles(query, this.allArticles); + } + })); + + // Refactored 404 page component + Alpine.data('notFoundPage', () => ({ + showEasterEgg: false, + searchQuery: '', + filteredArticles: [], + allArticles: [], + indexLoaded: false, + + async init() { + await this.ensureIndexLoaded(); + }, + + async ensureIndexLoaded() { + if (!this.indexLoaded) { + this.allArticles = await loadSearchIndex(); + this.indexLoaded = true; + } + }, + + filterArticles(query) { + this.searchQuery = query; + this.filteredArticles = filterArticles(query, this.allArticles); + }, + + toggleEasterEgg() { + this.showEasterEgg = !this.showEasterEgg; + }, + + goToRandomArticle() { + if (this.allArticles.length > 0) { + const randomArticle = this.allArticles[Math.floor(Math.random() * this.allArticles.length)]; + window.location.href = randomArticle.url; + } + } + })); +});