From 49e024343ec66caf46c687087c004c8a2d1893af Mon Sep 17 00:00:00 2001 From: Justin Deal Date: Sun, 4 May 2025 10:09:05 -0700 Subject: [PATCH] remove duplicated files --- public/scripts/search/baseSearch.js | 257 ------------------------ public/scripts/search/contentSearch.js | 38 ---- public/scripts/search/index.js | 24 --- public/scripts/search/servicesSearch.js | 243 ---------------------- src/scripts/search.js | 176 ---------------- src/scripts/search/baseSearch.js | 257 ------------------------ src/scripts/search/contentSearch.js | 38 ---- src/scripts/search/index.js | 24 --- src/scripts/search/servicesSearch.js | 243 ---------------------- 9 files changed, 1300 deletions(-) delete mode 100644 public/scripts/search/baseSearch.js delete mode 100644 public/scripts/search/contentSearch.js delete mode 100644 public/scripts/search/index.js delete mode 100644 public/scripts/search/servicesSearch.js delete mode 100644 src/scripts/search.js delete mode 100644 src/scripts/search/baseSearch.js delete mode 100644 src/scripts/search/contentSearch.js delete mode 100644 src/scripts/search/index.js delete mode 100644 src/scripts/search/servicesSearch.js diff --git a/public/scripts/search/baseSearch.js b/public/scripts/search/baseSearch.js deleted file mode 100644 index f6e3f41..0000000 --- a/public/scripts/search/baseSearch.js +++ /dev/null @@ -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; - } - } - } - }; -} diff --git a/public/scripts/search/contentSearch.js b/public/scripts/search/contentSearch.js deleted file mode 100644 index af60bc7..0000000 --- a/public/scripts/search/contentSearch.js +++ /dev/null @@ -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' - }); -} diff --git a/public/scripts/search/index.js b/public/scripts/search/index.js deleted file mode 100644 index fa6b487..0000000 --- a/public/scripts/search/index.js +++ /dev/null @@ -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); diff --git a/public/scripts/search/servicesSearch.js b/public/scripts/search/servicesSearch.js deleted file mode 100644 index 50f89f1..0000000 --- a/public/scripts/search/servicesSearch.js +++ /dev/null @@ -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'); - } - } - }; -} diff --git a/src/scripts/search.js b/src/scripts/search.js deleted file mode 100644 index 5cc47a5..0000000 --- a/src/scripts/search.js +++ /dev/null @@ -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' - }); - }); -}); diff --git a/src/scripts/search/baseSearch.js b/src/scripts/search/baseSearch.js deleted file mode 100644 index f6e3f41..0000000 --- a/src/scripts/search/baseSearch.js +++ /dev/null @@ -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; - } - } - } - }; -} diff --git a/src/scripts/search/contentSearch.js b/src/scripts/search/contentSearch.js deleted file mode 100644 index af60bc7..0000000 --- a/src/scripts/search/contentSearch.js +++ /dev/null @@ -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' - }); -} diff --git a/src/scripts/search/index.js b/src/scripts/search/index.js deleted file mode 100644 index fa6b487..0000000 --- a/src/scripts/search/index.js +++ /dev/null @@ -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); diff --git a/src/scripts/search/servicesSearch.js b/src/scripts/search/servicesSearch.js deleted file mode 100644 index 50f89f1..0000000 --- a/src/scripts/search/servicesSearch.js +++ /dev/null @@ -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'); - } - } - }; -}