// ========== SERVICE FILTER ========== (function() { const searchInput = document.getElementById('service-filter-search'); const statusSelect = document.getElementById('service-filter-status'); const categorySelect = document.getElementById('service-filter-category'); const countSpan = document.getElementById('service-filter-count'); // Build a single category list from both the API categories and any // categories present on the actual rendered cards (covers custom services // whose category isn't in TEMPLATE_CATEGORIES). function getCategoryList() { const seen = new Set(); const fromCards = new Set(); document.querySelectorAll('#cards .card[data-category]').forEach(c => { const cat = c.dataset.category.trim(); if (cat) fromCards.add(cat); }); const apiCats = (window.DC_CATEGORIES || (typeof DC !== 'undefined' && DC.CATEGORIES)) || {}; const all = Object.keys(apiCats).concat([...fromCards].filter(c => !apiCats[c])); all.forEach(c => seen.add(c)); return { list: [...seen], apiCats }; } function refreshCategoryDropdown() { if (!categorySelect) return; const { list, apiCats } = getCategoryList(); const current = categorySelect.value; categorySelect.innerHTML = ''; list.sort().forEach(name => { const info = apiCats[name]; const opt = document.createElement('option'); opt.value = name; opt.textContent = info ? `${info.icon || ''} ${name}`.trim() : name; categorySelect.appendChild(opt); }); // Restore selection if it still exists if (current && [...categorySelect.options].some(o => o.value === current)) { categorySelect.value = current; } else { categorySelect.value = 'all'; } } function updateFilter() { refreshCategoryDropdown(); const query = searchInput.value.toLowerCase().trim(); const statusFilter = statusSelect.value; // 'all', 'on', or 'off' const categoryFilter = categorySelect ? categorySelect.value : 'all'; const cards = document.querySelectorAll('#cards .card'); let visibleCount = 0; cards.forEach(card => { const name = card.querySelector('.name')?.textContent?.toLowerCase() || ''; const app = card.dataset.app?.toLowerCase() || ''; const status = card.dataset.status || 'off'; // 'on' or 'off' const category = card.dataset.category || ''; const matchesSearch = !query || name.includes(query) || app.includes(query); const matchesStatus = statusFilter === 'all' || status === statusFilter; const matchesCategory = categoryFilter === 'all' || category === categoryFilter; if (matchesSearch && matchesStatus && matchesCategory) { card.style.display = ''; visibleCount++; } else { card.style.display = 'none'; } }); if (countSpan) { const total = cards.length; countSpan.textContent = `${visibleCount} of ${total} services`; } } // Debounce helper function debounce(fn, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => fn.apply(this, args), delay); }; } searchInput?.addEventListener('input', debounce(updateFilter, 200)); statusSelect?.addEventListener('change', updateFilter); categorySelect?.addEventListener('change', updateFilter); // Initial count on page load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(updateFilter, 500)); } else { setTimeout(updateFilter, 500); } // Expose for external triggers (called after buildGrid to repopulate categories) window.refreshServiceFilter = updateFilter; window.refreshCategoryDropdown = refreshCategoryDropdown; })();