remove duplicated files
All checks were successful
Build and Deploy / build (push) Successful in 38s

This commit is contained in:
Justin Deal 2025-05-04 10:09:05 -07:00
parent 356ba16c88
commit 49e024343e
9 changed files with 0 additions and 1300 deletions

View File

@ -1,257 +0,0 @@
/**
* Base search module that provides core search functionality
* This module can be extended for specific search implementations
*/
/**
* Initialize search functionality for any content type
* @param {string} contentSelector - CSS selector for searchable items
* @param {Object} options - Configuration options
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeBaseSearch(contentSelector = '.searchable-item', options = {}) {
const defaults = {
nameAttribute: 'data-name',
tagsAttribute: 'data-tags',
categoryAttribute: 'data-category',
additionalAttributes: [],
noResultsMessage: 'No results found',
allItemsMessage: 'Showing all items',
resultCountMessage: (count) => `Found ${count} items`,
itemLabel: 'items',
debounceTime: 150 // ms to debounce search input
};
const config = { ...defaults, ...options };
return {
searchQuery: '',
hasResults: true,
visibleCount: 0,
loading: false,
focusedItemIndex: -1,
debounceTimeout: null,
init() {
// Initialize the visible count
this.visibleCount = document.querySelectorAll(contentSelector).length;
this.setupWatchers();
this.setupKeyboardShortcuts();
// Handle theme changes
window.addEventListener('theme-changed', () => {
this.filterContent(this.searchQuery);
});
},
setupWatchers() {
this.$watch('searchQuery', (query) => {
// Debounce search for better performance
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout);
}
this.debounceTimeout = setTimeout(() => {
this.filterContent(query);
}, config.debounceTime);
});
},
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();
this.focusedItemIndex = -1;
this.clearItemFocus();
}
// Arrow key navigation through results
if (this.searchQuery && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
e.preventDefault();
this.handleArrowNavigation(e.key);
}
// Enter key selects the focused item
if (e.key === 'Enter' && this.focusedItemIndex >= 0) {
this.handleEnterSelection();
}
});
},
handleArrowNavigation(key) {
const visibleItems = this.getVisibleItems();
if (visibleItems.length === 0) return;
// Update focused item index
if (key === 'ArrowDown') {
this.focusedItemIndex = Math.min(this.focusedItemIndex + 1, visibleItems.length - 1);
} else {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, -1);
}
// Clear previous focus
this.clearItemFocus();
// If we're back at -1, focus the search input
if (this.focusedItemIndex === -1) {
document.getElementById('app-search').focus();
return;
}
// Focus the new item
const itemToFocus = visibleItems[this.focusedItemIndex];
this.focusItem(itemToFocus);
},
handleEnterSelection() {
const visibleItems = this.getVisibleItems();
if (visibleItems.length === 0) return;
const selectedItem = visibleItems[this.focusedItemIndex];
const link = selectedItem.querySelector('a');
if (link) {
link.click();
}
},
getVisibleItems() {
return Array.from(document.querySelectorAll(contentSelector))
.filter(item => item.style.display !== 'none');
},
clearItemFocus() {
// Remove focus styling from all items
document.querySelectorAll(`${contentSelector}.keyboard-focus`).forEach(item => {
item.classList.remove('keyboard-focus');
});
},
focusItem(item) {
// Add focus styling
item.classList.add('keyboard-focus');
// Scroll into view with options for smooth scrolling
const scrollBehavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
item.scrollIntoView({ behavior: scrollBehavior, block: 'nearest' });
},
filterContent(query) {
query = query.toLowerCase().trim();
let anyResults = false;
let visibleCount = 0;
// Process all content items
document.querySelectorAll(contentSelector).forEach((item) => {
const isMatch = this.isItemMatch(item, query);
if (isMatch) {
item.style.display = '';
anyResults = true;
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Update category visibility if applicable
this.updateCategoryVisibility(query);
// Update parent containers if needed
this.updateContainerVisibility(query);
this.updateResultsStatus(query, anyResults, visibleCount);
},
isItemMatch(item, query) {
// If query is empty, show all items
if (query === '') {
return true;
}
// Get searchable attributes
const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
// Check additional attributes if specified
const additionalMatches = config.additionalAttributes.some(attr => {
const value = (item.getAttribute(attr) || '').toLowerCase();
return value.includes(query);
});
// Check if any attribute matches the query
return name.includes(query) ||
tags.includes(query) ||
category.includes(query) ||
additionalMatches;
},
updateCategoryVisibility(query) {
// Only proceed if we have category sections
const categorySections = document.querySelectorAll('.category-section');
if (categorySections.length === 0) return;
// For each category section, check if it has any visible items
categorySections.forEach((categorySection) => {
const categoryId = categorySection.getAttribute('data-category');
const items = categorySection.querySelectorAll(contentSelector);
// Count visible items in this category
const visibleItems = Array.from(items).filter(item =>
item.style.display !== 'none'
).length;
// If no visible items and we're searching, hide the category
if (query !== '' && visibleItems === 0) {
categorySection.style.display = 'none';
} else {
categorySection.style.display = '';
}
});
},
updateContainerVisibility(query) {
// If there are container elements that should be hidden when empty
const containers = document.querySelectorAll('.content-container');
if (containers.length > 0) {
containers.forEach((container) => {
const hasVisibleItems = Array.from(
container.querySelectorAll(contentSelector)
).some((item) => item.style.display !== 'none');
if (query === '' || hasVisibleItems) {
container.style.display = '';
} else {
container.style.display = 'none';
}
});
}
},
updateResultsStatus(query, anyResults, count) {
// Update results status
this.hasResults = query === '' || anyResults;
this.visibleCount = count;
// Update screen reader status
const statusEl = document.getElementById('search-status');
if (statusEl) {
if (query === '') {
statusEl.textContent = config.allItemsMessage;
this.visibleCount = document.querySelectorAll(contentSelector).length;
} else if (this.hasResults) {
statusEl.textContent = config.resultCountMessage(count);
} else {
statusEl.textContent = config.noResultsMessage;
}
}
}
};
}

View File

@ -1,38 +0,0 @@
/**
* Content search module for articles and projects
* Provides specialized search functionality for content items
*/
import { initializeBaseSearch } from './baseSearch.js';
/**
* Initialize search functionality for articles
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeArticlesSearch() {
return initializeBaseSearch('.article-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description'],
noResultsMessage: 'No articles found',
allItemsMessage: 'Showing all articles',
resultCountMessage: (count) => `Found ${count} articles`,
itemLabel: 'articles'
});
}
/**
* Initialize search functionality for projects
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeProjectsSearch() {
return initializeBaseSearch('.project-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description', 'data-github', 'data-live'],
noResultsMessage: 'No projects found',
allItemsMessage: 'Showing all projects',
resultCountMessage: (count) => `Found ${count} projects`,
itemLabel: 'projects'
});
}

View File

@ -1,24 +0,0 @@
/**
* Main search module that registers all search components with Alpine.js
*/
import { initializeServicesSearch } from './servicesSearch.js';
import { initializeArticlesSearch, initializeProjectsSearch } from './contentSearch.js';
/**
* Register all search components with Alpine.js
* This function is called when Alpine.js is initialized
*/
export function registerSearchComponents() {
// Register services search
window.Alpine.data('searchServices', initializeServicesSearch);
// Register articles search
window.Alpine.data('searchArticles', initializeArticlesSearch);
// Register projects search
window.Alpine.data('searchProjects', initializeProjectsSearch);
}
// Register components when Alpine.js is initialized
document.addEventListener('alpine:init', registerSearchComponents);

View File

@ -1,243 +0,0 @@
/**
* Services search module for homelab services
* Extends the base search functionality with service-specific features
*/
import { initializeBaseSearch } from './baseSearch.js';
/**
* Initialize search functionality for homelab services
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeServicesSearch() {
// Create base search with service-specific configuration
const baseSearch = initializeBaseSearch('.app-card', {
nameAttribute: 'data-app-name',
tagsAttribute: 'data-app-tags',
categoryAttribute: 'data-app-category',
noResultsMessage: 'No services found',
allItemsMessage: 'Showing all services',
resultCountMessage: (count) => `Found ${count} services`,
itemLabel: 'services'
});
// Extend with service-specific functionality
return {
...baseSearch,
// View mode properties
iconSizeValue: 2, // Slider value: 1=small, 2=medium, 3=large
iconSize: 'medium', // small, medium, large
viewMode: 'grid', // grid or list
displayMode: 'both', // both, image, or name
debounceTimeout: null, // For debouncing slider changes
init() {
// Call the base init method
baseSearch.init.call(this);
// Apply initial icon size, view mode, and display mode
this.applyIconSize();
this.applyViewMode();
this.applyDisplayMode();
// Save preferences to localStorage if available
this.loadPreferences();
// Listen for window resize events to optimize layout
this.setupResizeListener();
},
// Load user preferences from localStorage
loadPreferences() {
if (typeof localStorage !== 'undefined') {
try {
// Load icon size
const savedIconSize = localStorage.getItem('services-icon-size');
if (savedIconSize) {
this.setIconSize(parseFloat(savedIconSize));
}
// Load view mode
const savedViewMode = localStorage.getItem('services-view-mode');
if (savedViewMode) {
this.setViewMode(savedViewMode);
}
// Load display mode
const savedDisplayMode = localStorage.getItem('services-display-mode');
if (savedDisplayMode) {
this.setDisplayMode(savedDisplayMode);
}
} catch (e) {
console.error('Error loading preferences:', e);
}
}
},
// Save user preferences to localStorage
savePreferences() {
if (typeof localStorage !== 'undefined') {
try {
localStorage.setItem('services-icon-size', this.iconSizeValue.toString());
localStorage.setItem('services-view-mode', this.viewMode);
localStorage.setItem('services-display-mode', this.displayMode);
} catch (e) {
console.error('Error saving preferences:', e);
}
}
},
// Setup listener for window resize events
setupResizeListener() {
const handleResize = () => {
// Switch to list view on small screens if not explicitly set by user
const userHasSetViewMode = localStorage.getItem('services-view-mode') !== null;
if (!userHasSetViewMode) {
const smallScreen = window.innerWidth < 640; // sm breakpoint
if (smallScreen && this.viewMode !== 'list') {
this.setViewMode('list', false); // Don't save to preferences
} else if (!smallScreen && this.viewMode !== 'grid') {
this.setViewMode('grid', false); // Don't save to preferences
}
}
};
// Initial check
handleResize();
// Add resize listener with debounce
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(handleResize, 250);
});
},
// Icon size methods
setIconSize(size, savePreference = true) {
if (typeof size === 'string') {
// Handle legacy string values (small, medium, large)
this.iconSize = size;
this.iconSizeValue = size === 'small' ? 1 : size === 'medium' ? 2 : 3;
} else {
// Handle slider numeric values
this.iconSizeValue = parseFloat(size);
// Map slider value to size name
if (this.iconSizeValue <= 1.33) {
this.iconSize = 'small';
} else if (this.iconSizeValue <= 2.33) {
this.iconSize = 'medium';
} else {
this.iconSize = 'large';
}
}
this.applyIconSize();
// Save preference if requested
if (savePreference) {
this.savePreferences();
}
},
// Handle slider input with debounce
handleSliderChange(event) {
const value = event.target.value;
// Clear any existing timeout
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout);
}
// Set a new timeout
this.debounceTimeout = setTimeout(() => {
this.setIconSize(value);
}, 50); // 50ms debounce
},
applyIconSize() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing size classes
appList.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large');
// Add the new size class
appList.classList.add(`icon-size-${this.iconSize}`);
// Apply custom CSS variable for fine-grained control
appList.style.setProperty('--icon-scale', this.iconSizeValue);
},
// View mode methods
toggleViewMode() {
this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid';
this.applyViewMode();
this.savePreferences();
},
setViewMode(mode, savePreference = true) {
this.viewMode = mode;
this.applyViewMode();
// Save preference if requested
if (savePreference) {
this.savePreferences();
}
},
applyViewMode() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing view mode classes
appList.classList.remove('view-mode-grid', 'view-mode-list');
// Add the new view mode class
appList.classList.add(`view-mode-${this.viewMode}`);
// Update all category sections
document.querySelectorAll('.category-section').forEach(section => {
const gridContainer = section.querySelector('.grid');
if (gridContainer) {
// Update grid classes based on view mode
if (this.viewMode === 'grid') {
gridContainer.classList.remove('grid-cols-1');
gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
} else {
gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
gridContainer.classList.add('grid-cols-1');
}
}
});
},
// Display mode methods
setDisplayMode(mode) {
this.displayMode = mode;
this.applyDisplayMode();
this.savePreferences();
},
applyDisplayMode() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing display mode classes
appList.classList.remove('display-both', 'display-image-only', 'display-name-only');
// Add the new display mode class
if (this.displayMode === 'image') {
appList.classList.add('display-image-only');
} else if (this.displayMode === 'name') {
appList.classList.add('display-name-only');
} else {
appList.classList.add('display-both');
}
}
};
}

View File

@ -1,176 +0,0 @@
/**
* Client-side search functionality for filtering content across the site
*/
/**
* Initialize search functionality for any content type
* @param {string} contentSelector - CSS selector for the items to filter
* @param {Object} options - Configuration options
* @returns {Object} Alpine.js data object with search functionality
*/
function initializeSearch(contentSelector = '.searchable-item', options = {}) {
const defaults = {
nameAttribute: 'data-name',
tagsAttribute: 'data-tags',
categoryAttribute: 'data-category',
additionalAttributes: [],
noResultsMessage: 'No results found',
allItemsMessage: 'Showing all items',
resultCountMessage: (count) => `Found ${count} items`,
itemLabel: 'items'
};
const config = { ...defaults, ...options };
return {
searchQuery: '',
hasResults: true,
visibleCount: 0,
init() {
// Initialize the visible count
this.visibleCount = document.querySelectorAll(contentSelector).length;
this.setupWatchers();
this.setupKeyboardShortcuts();
},
setupWatchers() {
this.$watch('searchQuery', (query) => {
this.filterContent(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();
}
});
},
filterContent(query) {
query = query.toLowerCase();
let anyResults = false;
let visibleCount = 0;
// Process all content items
document.querySelectorAll(contentSelector).forEach((item) => {
// Get searchable attributes
const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
// Check additional attributes if specified
const additionalMatches = config.additionalAttributes.some(attr => {
const value = (item.getAttribute(attr) || '').toLowerCase();
return value.includes(query);
});
const isMatch = query === '' ||
name.includes(query) ||
tags.includes(query) ||
category.includes(query) ||
additionalMatches;
if (isMatch) {
item.style.display = '';
anyResults = true;
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Update parent containers if needed
this.updateContainerVisibility(query);
this.updateResultsStatus(query, anyResults, visibleCount);
},
updateContainerVisibility(query) {
// If there are container elements that should be hidden when empty
const containers = document.querySelectorAll('.content-container');
if (containers.length > 0) {
containers.forEach((container) => {
const hasVisibleItems = Array.from(
container.querySelectorAll(contentSelector)
).some((item) => item.style.display !== 'none');
if (query === '' || hasVisibleItems) {
container.style.display = '';
} else {
container.style.display = 'none';
}
});
}
},
updateResultsStatus(query, anyResults, count) {
// Update results status
this.hasResults = query === '' || anyResults;
this.visibleCount = count;
// Update screen reader status
const statusEl = document.getElementById('search-status');
if (statusEl) {
if (query === '') {
statusEl.textContent = config.allItemsMessage;
this.visibleCount = document.querySelectorAll(contentSelector).length;
} else if (this.hasResults) {
statusEl.textContent = config.resultCountMessage(count);
} else {
statusEl.textContent = config.noResultsMessage;
}
}
}
};
}
// Register Alpine.js data components when Alpine is loaded
document.addEventListener('alpine:init', () => {
// Homelab search
window.Alpine.data('searchServices', () => {
return initializeSearch('.app-card', {
nameAttribute: 'data-app-name',
tagsAttribute: 'data-app-tags',
categoryAttribute: 'data-app-category',
noResultsMessage: 'No services found',
allItemsMessage: 'Showing all services',
resultCountMessage: (count) => `Found ${count} services`,
itemLabel: 'services'
});
});
// Blog search
window.Alpine.data('searchArticles', () => {
return initializeSearch('.article-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description'],
noResultsMessage: 'No articles found',
allItemsMessage: 'Showing all articles',
resultCountMessage: (count) => `Found ${count} articles`,
itemLabel: 'articles'
});
});
// Projects search
window.Alpine.data('searchProjects', () => {
return initializeSearch('.project-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description', 'data-github', 'data-live'],
noResultsMessage: 'No projects found',
allItemsMessage: 'Showing all projects',
resultCountMessage: (count) => `Found ${count} projects`,
itemLabel: 'projects'
});
});
});

View File

@ -1,257 +0,0 @@
/**
* Base search module that provides core search functionality
* This module can be extended for specific search implementations
*/
/**
* Initialize search functionality for any content type
* @param {string} contentSelector - CSS selector for searchable items
* @param {Object} options - Configuration options
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeBaseSearch(contentSelector = '.searchable-item', options = {}) {
const defaults = {
nameAttribute: 'data-name',
tagsAttribute: 'data-tags',
categoryAttribute: 'data-category',
additionalAttributes: [],
noResultsMessage: 'No results found',
allItemsMessage: 'Showing all items',
resultCountMessage: (count) => `Found ${count} items`,
itemLabel: 'items',
debounceTime: 150 // ms to debounce search input
};
const config = { ...defaults, ...options };
return {
searchQuery: '',
hasResults: true,
visibleCount: 0,
loading: false,
focusedItemIndex: -1,
debounceTimeout: null,
init() {
// Initialize the visible count
this.visibleCount = document.querySelectorAll(contentSelector).length;
this.setupWatchers();
this.setupKeyboardShortcuts();
// Handle theme changes
window.addEventListener('theme-changed', () => {
this.filterContent(this.searchQuery);
});
},
setupWatchers() {
this.$watch('searchQuery', (query) => {
// Debounce search for better performance
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout);
}
this.debounceTimeout = setTimeout(() => {
this.filterContent(query);
}, config.debounceTime);
});
},
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();
this.focusedItemIndex = -1;
this.clearItemFocus();
}
// Arrow key navigation through results
if (this.searchQuery && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
e.preventDefault();
this.handleArrowNavigation(e.key);
}
// Enter key selects the focused item
if (e.key === 'Enter' && this.focusedItemIndex >= 0) {
this.handleEnterSelection();
}
});
},
handleArrowNavigation(key) {
const visibleItems = this.getVisibleItems();
if (visibleItems.length === 0) return;
// Update focused item index
if (key === 'ArrowDown') {
this.focusedItemIndex = Math.min(this.focusedItemIndex + 1, visibleItems.length - 1);
} else {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, -1);
}
// Clear previous focus
this.clearItemFocus();
// If we're back at -1, focus the search input
if (this.focusedItemIndex === -1) {
document.getElementById('app-search').focus();
return;
}
// Focus the new item
const itemToFocus = visibleItems[this.focusedItemIndex];
this.focusItem(itemToFocus);
},
handleEnterSelection() {
const visibleItems = this.getVisibleItems();
if (visibleItems.length === 0) return;
const selectedItem = visibleItems[this.focusedItemIndex];
const link = selectedItem.querySelector('a');
if (link) {
link.click();
}
},
getVisibleItems() {
return Array.from(document.querySelectorAll(contentSelector))
.filter(item => item.style.display !== 'none');
},
clearItemFocus() {
// Remove focus styling from all items
document.querySelectorAll(`${contentSelector}.keyboard-focus`).forEach(item => {
item.classList.remove('keyboard-focus');
});
},
focusItem(item) {
// Add focus styling
item.classList.add('keyboard-focus');
// Scroll into view with options for smooth scrolling
const scrollBehavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
item.scrollIntoView({ behavior: scrollBehavior, block: 'nearest' });
},
filterContent(query) {
query = query.toLowerCase().trim();
let anyResults = false;
let visibleCount = 0;
// Process all content items
document.querySelectorAll(contentSelector).forEach((item) => {
const isMatch = this.isItemMatch(item, query);
if (isMatch) {
item.style.display = '';
anyResults = true;
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Update category visibility if applicable
this.updateCategoryVisibility(query);
// Update parent containers if needed
this.updateContainerVisibility(query);
this.updateResultsStatus(query, anyResults, visibleCount);
},
isItemMatch(item, query) {
// If query is empty, show all items
if (query === '') {
return true;
}
// Get searchable attributes
const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
// Check additional attributes if specified
const additionalMatches = config.additionalAttributes.some(attr => {
const value = (item.getAttribute(attr) || '').toLowerCase();
return value.includes(query);
});
// Check if any attribute matches the query
return name.includes(query) ||
tags.includes(query) ||
category.includes(query) ||
additionalMatches;
},
updateCategoryVisibility(query) {
// Only proceed if we have category sections
const categorySections = document.querySelectorAll('.category-section');
if (categorySections.length === 0) return;
// For each category section, check if it has any visible items
categorySections.forEach((categorySection) => {
const categoryId = categorySection.getAttribute('data-category');
const items = categorySection.querySelectorAll(contentSelector);
// Count visible items in this category
const visibleItems = Array.from(items).filter(item =>
item.style.display !== 'none'
).length;
// If no visible items and we're searching, hide the category
if (query !== '' && visibleItems === 0) {
categorySection.style.display = 'none';
} else {
categorySection.style.display = '';
}
});
},
updateContainerVisibility(query) {
// If there are container elements that should be hidden when empty
const containers = document.querySelectorAll('.content-container');
if (containers.length > 0) {
containers.forEach((container) => {
const hasVisibleItems = Array.from(
container.querySelectorAll(contentSelector)
).some((item) => item.style.display !== 'none');
if (query === '' || hasVisibleItems) {
container.style.display = '';
} else {
container.style.display = 'none';
}
});
}
},
updateResultsStatus(query, anyResults, count) {
// Update results status
this.hasResults = query === '' || anyResults;
this.visibleCount = count;
// Update screen reader status
const statusEl = document.getElementById('search-status');
if (statusEl) {
if (query === '') {
statusEl.textContent = config.allItemsMessage;
this.visibleCount = document.querySelectorAll(contentSelector).length;
} else if (this.hasResults) {
statusEl.textContent = config.resultCountMessage(count);
} else {
statusEl.textContent = config.noResultsMessage;
}
}
}
};
}

View File

@ -1,38 +0,0 @@
/**
* Content search module for articles and projects
* Provides specialized search functionality for content items
*/
import { initializeBaseSearch } from './baseSearch.js';
/**
* Initialize search functionality for articles
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeArticlesSearch() {
return initializeBaseSearch('.article-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description'],
noResultsMessage: 'No articles found',
allItemsMessage: 'Showing all articles',
resultCountMessage: (count) => `Found ${count} articles`,
itemLabel: 'articles'
});
}
/**
* Initialize search functionality for projects
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeProjectsSearch() {
return initializeBaseSearch('.project-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description', 'data-github', 'data-live'],
noResultsMessage: 'No projects found',
allItemsMessage: 'Showing all projects',
resultCountMessage: (count) => `Found ${count} projects`,
itemLabel: 'projects'
});
}

View File

@ -1,24 +0,0 @@
/**
* Main search module that registers all search components with Alpine.js
*/
import { initializeServicesSearch } from './servicesSearch.js';
import { initializeArticlesSearch, initializeProjectsSearch } from './contentSearch.js';
/**
* Register all search components with Alpine.js
* This function is called when Alpine.js is initialized
*/
export function registerSearchComponents() {
// Register services search
window.Alpine.data('searchServices', initializeServicesSearch);
// Register articles search
window.Alpine.data('searchArticles', initializeArticlesSearch);
// Register projects search
window.Alpine.data('searchProjects', initializeProjectsSearch);
}
// Register components when Alpine.js is initialized
document.addEventListener('alpine:init', registerSearchComponents);

View File

@ -1,243 +0,0 @@
/**
* Services search module for homelab services
* Extends the base search functionality with service-specific features
*/
import { initializeBaseSearch } from './baseSearch.js';
/**
* Initialize search functionality for homelab services
* @returns {Object} Alpine.js data object with search functionality
*/
export function initializeServicesSearch() {
// Create base search with service-specific configuration
const baseSearch = initializeBaseSearch('.app-card', {
nameAttribute: 'data-app-name',
tagsAttribute: 'data-app-tags',
categoryAttribute: 'data-app-category',
noResultsMessage: 'No services found',
allItemsMessage: 'Showing all services',
resultCountMessage: (count) => `Found ${count} services`,
itemLabel: 'services'
});
// Extend with service-specific functionality
return {
...baseSearch,
// View mode properties
iconSizeValue: 2, // Slider value: 1=small, 2=medium, 3=large
iconSize: 'medium', // small, medium, large
viewMode: 'grid', // grid or list
displayMode: 'both', // both, image, or name
debounceTimeout: null, // For debouncing slider changes
init() {
// Call the base init method
baseSearch.init.call(this);
// Apply initial icon size, view mode, and display mode
this.applyIconSize();
this.applyViewMode();
this.applyDisplayMode();
// Save preferences to localStorage if available
this.loadPreferences();
// Listen for window resize events to optimize layout
this.setupResizeListener();
},
// Load user preferences from localStorage
loadPreferences() {
if (typeof localStorage !== 'undefined') {
try {
// Load icon size
const savedIconSize = localStorage.getItem('services-icon-size');
if (savedIconSize) {
this.setIconSize(parseFloat(savedIconSize));
}
// Load view mode
const savedViewMode = localStorage.getItem('services-view-mode');
if (savedViewMode) {
this.setViewMode(savedViewMode);
}
// Load display mode
const savedDisplayMode = localStorage.getItem('services-display-mode');
if (savedDisplayMode) {
this.setDisplayMode(savedDisplayMode);
}
} catch (e) {
console.error('Error loading preferences:', e);
}
}
},
// Save user preferences to localStorage
savePreferences() {
if (typeof localStorage !== 'undefined') {
try {
localStorage.setItem('services-icon-size', this.iconSizeValue.toString());
localStorage.setItem('services-view-mode', this.viewMode);
localStorage.setItem('services-display-mode', this.displayMode);
} catch (e) {
console.error('Error saving preferences:', e);
}
}
},
// Setup listener for window resize events
setupResizeListener() {
const handleResize = () => {
// Switch to list view on small screens if not explicitly set by user
const userHasSetViewMode = localStorage.getItem('services-view-mode') !== null;
if (!userHasSetViewMode) {
const smallScreen = window.innerWidth < 640; // sm breakpoint
if (smallScreen && this.viewMode !== 'list') {
this.setViewMode('list', false); // Don't save to preferences
} else if (!smallScreen && this.viewMode !== 'grid') {
this.setViewMode('grid', false); // Don't save to preferences
}
}
};
// Initial check
handleResize();
// Add resize listener with debounce
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(handleResize, 250);
});
},
// Icon size methods
setIconSize(size, savePreference = true) {
if (typeof size === 'string') {
// Handle legacy string values (small, medium, large)
this.iconSize = size;
this.iconSizeValue = size === 'small' ? 1 : size === 'medium' ? 2 : 3;
} else {
// Handle slider numeric values
this.iconSizeValue = parseFloat(size);
// Map slider value to size name
if (this.iconSizeValue <= 1.33) {
this.iconSize = 'small';
} else if (this.iconSizeValue <= 2.33) {
this.iconSize = 'medium';
} else {
this.iconSize = 'large';
}
}
this.applyIconSize();
// Save preference if requested
if (savePreference) {
this.savePreferences();
}
},
// Handle slider input with debounce
handleSliderChange(event) {
const value = event.target.value;
// Clear any existing timeout
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout);
}
// Set a new timeout
this.debounceTimeout = setTimeout(() => {
this.setIconSize(value);
}, 50); // 50ms debounce
},
applyIconSize() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing size classes
appList.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large');
// Add the new size class
appList.classList.add(`icon-size-${this.iconSize}`);
// Apply custom CSS variable for fine-grained control
appList.style.setProperty('--icon-scale', this.iconSizeValue);
},
// View mode methods
toggleViewMode() {
this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid';
this.applyViewMode();
this.savePreferences();
},
setViewMode(mode, savePreference = true) {
this.viewMode = mode;
this.applyViewMode();
// Save preference if requested
if (savePreference) {
this.savePreferences();
}
},
applyViewMode() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing view mode classes
appList.classList.remove('view-mode-grid', 'view-mode-list');
// Add the new view mode class
appList.classList.add(`view-mode-${this.viewMode}`);
// Update all category sections
document.querySelectorAll('.category-section').forEach(section => {
const gridContainer = section.querySelector('.grid');
if (gridContainer) {
// Update grid classes based on view mode
if (this.viewMode === 'grid') {
gridContainer.classList.remove('grid-cols-1');
gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
} else {
gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
gridContainer.classList.add('grid-cols-1');
}
}
});
},
// Display mode methods
setDisplayMode(mode) {
this.displayMode = mode;
this.applyDisplayMode();
this.savePreferences();
},
applyDisplayMode() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing display mode classes
appList.classList.remove('display-both', 'display-image-only', 'display-name-only');
// Add the new display mode class
if (this.displayMode === 'image') {
appList.classList.add('display-image-only');
} else if (this.displayMode === 'name') {
appList.classList.add('display-name-only');
} else {
appList.classList.add('display-both');
}
}
};
}