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

@ -4,6 +4,9 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"dependencies": {
"fuse.js": "^7.1.0"
},
"devDependencies": { "devDependencies": {
"esbuild": "^0.27.2", "esbuild": "^0.27.2",
"purgecss": "^6.0.0" "purgecss": "^6.0.0"
@ -654,6 +657,15 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/fuse.js": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "10.5.0", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",

View file

@ -6,5 +6,8 @@
}, },
"scripts": { "scripts": {
"analyze-css": "node analyze-css.js" "analyze-css": "node analyze-css.js"
},
"dependencies": {
"fuse.js": "^7.1.0"
} }
} }

View file

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

View file

@ -1,15 +1,133 @@
/** /**
* Services Controller * 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 { export class ServicesController {
private fuse: Fuse<ServiceItem> | null = null;
private services: ServiceItem[] = [];
constructor() { constructor() {
this.init(); this.init();
} }
private init(): void { private init(): void {
this.setupCategoryToggle(); 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 { private setupCategoryToggle(): void {
@ -18,7 +136,6 @@ export class ServicesController {
if (!categoryRow) return; if (!categoryRow) return;
const isExpanded = categoryRow.getAttribute('data-expanded') !== 'false'; const isExpanded = categoryRow.getAttribute('data-expanded') !== 'false';
const categoryId = categoryRow.getAttribute('data-category');
// Find all service rows belonging to this category // Find all service rows belonging to this category
const serviceRows = this.getServiceRowsForCategory(categoryRow); const serviceRows = this.getServiceRowsForCategory(categoryRow);
@ -77,7 +194,7 @@ export class ServicesController {
} }
private expandRows(rows: HTMLElement[]): void { private expandRows(rows: HTMLElement[]): void {
rows.forEach((row, index) => { rows.forEach((row) => {
// First make visible but with 0 height // First make visible but with 0 height
row.style.display = 'grid'; row.style.display = 'grid';
row.style.height = '0'; row.style.height = '0';