From 1b3788d58763bcce40817bdb8c0227bb97af00af Mon Sep 17 00:00:00 2001 From: Justin Deal Date: Sat, 3 May 2025 00:44:33 -0700 Subject: [PATCH] Update Home Lab Search Abilities and Appearence --- src/components/common/SearchBar.astro | 76 +++++++ src/components/homelab/CategorySection.astro | 73 +++++++ src/components/homelab/searchUtils.js | 100 +++++++++ src/pages/homelab/index.astro | 208 ++++++++----------- 4 files changed, 332 insertions(+), 125 deletions(-) create mode 100644 src/components/common/SearchBar.astro create mode 100644 src/components/homelab/CategorySection.astro create mode 100644 src/components/homelab/searchUtils.js diff --git a/src/components/common/SearchBar.astro b/src/components/common/SearchBar.astro new file mode 100644 index 0000000..db1ffea --- /dev/null +++ b/src/components/common/SearchBar.astro @@ -0,0 +1,76 @@ +--- +interface Props { + placeholder?: string; + ariaLabel?: string; + className?: string; +} + +const { + placeholder = "Search...", + ariaLabel = "Search", + className = "", +} = Astro.props; +--- + +
+ +
+ Showing all services +
+ +
+ + +
+ + + +
+ + +
+ + +
+ +
+ / to search, Esc to clear +
+ + +
+
+
diff --git a/src/components/homelab/CategorySection.astro b/src/components/homelab/CategorySection.astro new file mode 100644 index 0000000..d302751 --- /dev/null +++ b/src/components/homelab/CategorySection.astro @@ -0,0 +1,73 @@ +--- +interface Props { + category: string; + apps: Array<{ + name: string; + link: string; + icon: string; + alt: string; + tags?: string[]; + }>; +} + +const { category, apps } = Astro.props; +import ServiceCard from "../common/ServiceCard.astro"; + +// Pre-compute values during server-side rendering +const categoryId = `category-${category.toLowerCase().replace(/\s+/g, '-')}`; +const categoryLower = category.toLowerCase(); +--- + +
+ + +
+
+ {apps.length > 0 ? ( + apps.map(app => { + const appName = app.name.toLowerCase(); + const appTags = app.tags ? app.tags.join(' ').toLowerCase() : ''; + + return ( +
+ +
+ ); + }) + ) : ( +

Coming soon...

+ )} +
+
+
diff --git a/src/components/homelab/searchUtils.js b/src/components/homelab/searchUtils.js new file mode 100644 index 0000000..7d25e87 --- /dev/null +++ b/src/components/homelab/searchUtils.js @@ -0,0 +1,100 @@ +/** + * Initialize search functionality for the homelab page + * This function sets up the search filtering, keyboard shortcuts, + * and status updates for the search feature + */ +function initializeSearch() { + return { + searchQuery: '', + hasResults: true, + + init() { + this.setupWatchers(); + this.setupKeyboardShortcuts(); + }, + + setupWatchers() { + this.$watch('searchQuery', (query) => { + this.filterServices(query); + }); + }, + + setupKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // '/' key focuses the search input + if (e.key === '/' && document.activeElement.id !== 'app-search') { + e.preventDefault(); + document.getElementById('app-search').focus(); + } + + // Escape key clears the search + if (e.key === 'Escape' && this.searchQuery !== '') { + this.searchQuery = ''; + document.getElementById('app-search').focus(); + } + }); + }, + + filterServices(query) { + query = query.toLowerCase(); + let anyResults = false; + let visibleCount = 0; + + // Process all service cards + document.querySelectorAll('.app-card').forEach((card) => { + const serviceName = card.getAttribute('data-app-name') || ''; + const serviceTags = card.getAttribute('data-app-tags') || ''; + const serviceCategory = card.getAttribute('data-app-category') || ''; + + const isMatch = query === '' || + serviceName.includes(query) || + serviceTags.includes(query) || + serviceCategory.includes(query); + + if (isMatch) { + card.style.display = ''; + anyResults = true; + visibleCount++; + } else { + card.style.display = 'none'; + } + }); + + this.updateCategoryVisibility(query); + this.updateResultsStatus(query, anyResults, visibleCount); + }, + + updateCategoryVisibility(query) { + document.querySelectorAll('.category-section').forEach((category) => { + const hasVisibleApps = Array.from( + category.querySelectorAll('.app-card') + ).some((card) => card.style.display !== 'none'); + + if (query === '' || hasVisibleApps) { + category.style.display = ''; + } else { + category.style.display = 'none'; + } + }); + }, + + updateResultsStatus(query, anyResults, visibleCount) { + // Update results status + this.hasResults = query === '' || anyResults; + + // Update screen reader status + const statusEl = document.getElementById('search-status'); + if (statusEl) { + if (query === '') { + statusEl.textContent = 'Showing all services'; + } else if (this.hasResults) { + statusEl.textContent = 'Found ' + visibleCount + ' services matching ' + query; + } else { + statusEl.textContent = 'No services found matching ' + query; + } + } + } + }; +} + +export { initializeSearch }; diff --git a/src/pages/homelab/index.astro b/src/pages/homelab/index.astro index 8ac9aeb..b098b38 100644 --- a/src/pages/homelab/index.astro +++ b/src/pages/homelab/index.astro @@ -2,17 +2,13 @@ import { GLOBAL } from "../../lib/variables"; import Layout from "../../layouts/Layout.astro"; import Section from "../../components/common/Section.astro"; -import ServiceCard from "../../components/common/ServiceCard.astro"; +import SearchBar from "../../components/common/SearchBar.astro"; +import CategorySection from "../../components/homelab/CategorySection.astro"; import { services } from "./services.ts"; --- - {GLOBAL.username} • {GLOBAL.shortDescription} @@ -29,22 +25,57 @@ import { services } from "./services.ts";
-
{ + this.filterServices(query); + }); + }, + + setupKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // '/' key focuses the search input + if (e.key === '/' && document.activeElement.id !== 'app-search') { + e.preventDefault(); + document.getElementById('app-search').focus(); + } + + // Escape key clears the search + if (e.key === 'Escape' && this.searchQuery !== '') { + this.searchQuery = ''; + document.getElementById('app-search').focus(); + } + }); + }, + + filterServices(query) { query = query.toLowerCase(); let anyResults = false; let visibleCount = 0; - // Process all app cards - document.querySelectorAll('.app-card').forEach(card => { - const appName = card.getAttribute('data-app-name') || ''; - const appTags = card.getAttribute('data-app-tags') || ''; - const appCategory = card.getAttribute('data-app-category') || ''; + // Process all service cards + document.querySelectorAll('.app-card').forEach((card) => { + const serviceName = card.getAttribute('data-app-name') || ''; + const serviceTags = card.getAttribute('data-app-tags') || ''; + const serviceCategory = card.getAttribute('data-app-category') || ''; - if (query === '' || - appName.includes(query) || - appTags.includes(query) || - appCategory.includes(query)) { + const isMatch = query === '' || + serviceName.includes(query) || + serviceTags.includes(query) || + serviceCategory.includes(query); + + if (isMatch) { card.style.display = ''; anyResults = true; visibleCount++; @@ -53,11 +84,15 @@ import { services } from "./services.ts"; } }); - // Check each category - document.querySelectorAll('.category-section').forEach(category => { + this.updateCategoryVisibility(query); + this.updateResultsStatus(query, anyResults, visibleCount); + }, + + updateCategoryVisibility(query) { + document.querySelectorAll('.category-section').forEach((category) => { const hasVisibleApps = Array.from( category.querySelectorAll('.app-card') - ).some(card => card.style.display !== 'none'); + ).some((card) => card.style.display !== 'none'); if (query === '' || hasVisibleApps) { category.style.display = ''; @@ -65,134 +100,57 @@ import { services } from "./services.ts"; category.style.display = 'none'; } }); - + }, + + updateResultsStatus(query, anyResults, count) { // Update results status - hasResults = query === '' || anyResults; + this.hasResults = query === '' || anyResults; + this.visibleCount = count; // Update screen reader status const statusEl = document.getElementById('search-status'); if (statusEl) { if (query === '') { - statusEl.textContent = 'Showing all apps'; - } else if (hasResults) { - statusEl.textContent = 'Found ' + visibleCount + ' apps matching ' + query; + statusEl.textContent = 'Showing all services'; + this.visibleCount = document.querySelectorAll('.app-card').length; + } else if (this.hasResults) { + statusEl.textContent = 'Found ' + count + ' services matching ' + query; } else { - statusEl.textContent = 'No apps found matching ' + query; + statusEl.textContent = 'No services found matching ' + query; } } - }); - - // Add keyboard support for Escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && searchQuery !== '') { - searchQuery = ''; - document.getElementById('app-search').focus(); - } - }); - "> -
-

Homelab

+ } + }" x-init="init()"> + +
+
+ +
-
- -
- Showing all apps -
- -
- - - -
+ +
+

Homelab

-

No Results

+

No Results

+
- {Object.entries(services).map(([category, apps], index) => ( -
- - -
-
- {apps.length > 0 ? ( - apps.map(app => ( -
- -
- )) - ) : ( -

Coming soon...

- )} -
-
-
+ {Object.entries(services).map(([category, apps]) => ( + ))}
-