]> danix's work - danix.xyz-2.git/commitdiff
feat: create shared search module with lazy-loading and Alpine components
authorDanilo M. <redacted>
Mon, 20 Apr 2026 11:50:23 +0000 (13:50 +0200)
committerDanilo M. <redacted>
Mon, 20 Apr 2026 11:50:23 +0000 (13:50 +0200)
- 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 <redacted>
themes/danix-xyz-hacker/assets/js/search.js [new file with mode: 0644]

diff --git a/themes/danix-xyz-hacker/assets/js/search.js b/themes/danix-xyz-hacker/assets/js/search.js
new file mode 100644 (file)
index 0000000..94c6323
--- /dev/null
@@ -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;
+      }
+    }
+  }));
+});