Adds fuzzy search and enhances services UI interactions

Implements advanced service search using Fuse.js
Improves category expand/collapse animations
Adds interactive search functionality for service list

Enhances user experience by enabling quick service filtering and smooth UI interactions
This commit is contained in:
Janus C. H. Knudsen 2026-01-16 01:05:11 +01:00
parent 4cf30e1f27
commit fad5e46dfb
4 changed files with 141 additions and 3 deletions

View file

@ -37,10 +37,16 @@ swp-search-input {
border: none;
background: transparent;
outline: none;
box-shadow: none;
font-size: var(--font-size-base);
color: var(--color-text);
width: 100%;
&:focus {
outline: none;
box-shadow: none;
}
&::placeholder {
color: var(--color-text-tertiary);
}

View file

@ -1,15 +1,133 @@
/**
* Services Controller
* Handles category collapse/expand animations
* Handles category collapse/expand animations and fuzzy search
*/
import Fuse from 'fuse.js';
interface ServiceItem {
id: string;
name: string;
element: HTMLElement;
categoryRow: HTMLElement;
}
export class ServicesController {
private fuse: Fuse<ServiceItem> | null = null;
private services: ServiceItem[] = [];
constructor() {
this.init();
}
private init(): void {
this.setupCategoryToggle();
this.setupSearch();
}
private setupSearch(): void {
const searchInput = document.querySelector('swp-search-input input') as HTMLInputElement;
if (!searchInput) return;
// Build service index from DOM
this.buildServiceIndex();
// Initialize Fuse.js
this.fuse = new Fuse(this.services, {
keys: ['name'],
threshold: 0.3,
minMatchCharLength: 2
});
// Listen for input
searchInput.addEventListener('input', (e) => {
const query = (e.target as HTMLInputElement).value.trim();
this.filterServices(query);
});
}
private buildServiceIndex(): void {
const serviceRows = document.querySelectorAll('swp-card.services-list swp-data-table-row');
serviceRows.forEach((row) => {
const element = row as HTMLElement;
const id = element.getAttribute('data-service-detail') || '';
const nameCell = element.querySelector('swp-data-table-cell:first-child');
const name = nameCell?.textContent?.trim() || '';
// Find the category row for this service
let categoryRow: HTMLElement | null = null;
let sibling = element.previousElementSibling;
while (sibling) {
if (sibling.tagName.toLowerCase() === 'swp-category-row') {
categoryRow = sibling as HTMLElement;
break;
}
sibling = sibling.previousElementSibling;
}
if (categoryRow) {
this.services.push({
id,
name,
element,
categoryRow
});
}
});
}
private filterServices(query: string): void {
if (!query || query.length < 2) {
// Show all services and categories
this.showAll();
return;
}
if (!this.fuse) return;
// Get matching services
const results = this.fuse.search(query);
const matchingIds = new Set(results.map(r => r.item.id));
// Track which categories have visible services
const visibleCategories = new Set<HTMLElement>();
// Show/hide services
this.services.forEach(service => {
if (matchingIds.has(service.id)) {
service.element.style.display = 'grid';
visibleCategories.add(service.categoryRow);
} else {
service.element.style.display = 'none';
}
});
// Show/hide categories based on whether they have visible services
const allCategoryRows = document.querySelectorAll('swp-card.services-list swp-category-row');
allCategoryRows.forEach((row) => {
const categoryRow = row as HTMLElement;
if (visibleCategories.has(categoryRow)) {
categoryRow.style.display = 'grid';
// Ensure category is expanded when filtering
categoryRow.setAttribute('data-expanded', 'true');
} else {
categoryRow.style.display = 'none';
}
});
}
private showAll(): void {
// Show all services
this.services.forEach(service => {
service.element.style.display = 'grid';
});
// Show all categories
const allCategoryRows = document.querySelectorAll('swp-card.services-list swp-category-row');
allCategoryRows.forEach((row) => {
(row as HTMLElement).style.display = 'grid';
});
}
private setupCategoryToggle(): void {
@ -18,7 +136,6 @@ export class ServicesController {
if (!categoryRow) return;
const isExpanded = categoryRow.getAttribute('data-expanded') !== 'false';
const categoryId = categoryRow.getAttribute('data-category');
// Find all service rows belonging to this category
const serviceRows = this.getServiceRowsForCategory(categoryRow);
@ -77,7 +194,7 @@ export class ServicesController {
}
private expandRows(rows: HTMLElement[]): void {
rows.forEach((row, index) => {
rows.forEach((row) => {
// First make visible but with 0 height
row.style.display = 'grid';
row.style.height = '0';