Enhances Services module with detail view and interactions

Adds comprehensive service detail view with multiple tabs and dynamic interactions
Implements client-side navigation between service list and detail views
Introduces mock service data catalog for flexible component rendering
Extends localization support for new service detail screens

Improves user experience by adding edit capabilities and smooth view transitions
This commit is contained in:
Janus C. H. Knudsen 2026-01-16 22:03:22 +01:00
parent fad5e46dfb
commit 120367acbb
22 changed files with 1780 additions and 597 deletions

View file

@ -1,6 +1,12 @@
/**
* Services Controller
* Handles category collapse/expand animations and fuzzy search
*
* Handles:
* - Category collapse/expand animations
* - Fuzzy search
* - Content swap between list view and detail view
* - Tab switching within detail view
* - History API for browser back/forward navigation
*/
import Fuse from 'fuse.js';
@ -15,14 +21,27 @@ interface ServiceItem {
export class ServicesController {
private fuse: Fuse<ServiceItem> | null = null;
private services: ServiceItem[] = [];
private listView: HTMLElement | null = null;
private detailView: HTMLElement | null = null;
constructor() {
this.listView = document.getElementById('services-list-view');
this.detailView = document.getElementById('service-detail-view');
// Only initialize if we're on the services page
if (!this.listView) return;
this.init();
}
private init(): void {
this.setupCategoryToggle();
this.setupSearch();
this.setupDetailTabs();
this.setupChevronNavigation();
this.setupBackNavigation();
this.setupHistoryNavigation();
this.restoreStateFromUrl();
}
private setupSearch(): void {
@ -225,4 +244,180 @@ export class ServicesController {
});
}, 200);
}
/**
* Setup popstate listener for browser back/forward
*/
private setupHistoryNavigation(): void {
window.addEventListener('popstate', (e: PopStateEvent) => {
if (e.state?.serviceKey) {
this.showDetailViewInternal(e.state.serviceKey);
} else {
this.showListViewInternal();
}
});
}
/**
* Restore view state from URL on page load
*/
private restoreStateFromUrl(): void {
const hash = window.location.hash;
if (hash.startsWith('#service-')) {
const serviceKey = hash.substring(1); // Remove #
this.showDetailViewInternal(serviceKey);
}
}
/**
* Setup tab switching for the detail view
*/
private setupDetailTabs(): void {
if (!this.detailView) return;
const tabs = this.detailView.querySelectorAll<HTMLElement>('swp-tab-bar > swp-tab[data-tab]');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const targetTab = tab.dataset.tab;
if (targetTab) {
this.switchTab(this.detailView!, targetTab);
}
});
});
}
/**
* Switch to a specific tab within a container
*/
private switchTab(container: HTMLElement, targetTab: string): void {
const tabs = container.querySelectorAll<HTMLElement>('swp-tab-bar > swp-tab[data-tab]');
const contents = container.querySelectorAll<HTMLElement>('swp-tab-content[data-tab]');
tabs.forEach(t => {
t.classList.toggle('active', t.dataset.tab === targetTab);
});
contents.forEach(content => {
content.classList.toggle('active', content.dataset.tab === targetTab);
});
}
/**
* Setup row click to show detail view
* Ignores clicks on category rows
*/
private setupChevronNavigation(): void {
document.addEventListener('click', (e: Event) => {
const target = e.target as HTMLElement;
// Ignore clicks on category rows
if (target.closest('swp-category-row')) {
return;
}
const row = target.closest<HTMLElement>('swp-data-table-row[data-service-detail]');
if (row) {
const serviceKey = row.dataset.serviceDetail;
if (serviceKey) {
this.showDetailView(serviceKey);
}
}
});
}
/**
* Setup back button to return to list view
*/
private setupBackNavigation(): void {
document.addEventListener('click', (e: Event) => {
const target = e.target as HTMLElement;
const backLink = target.closest<HTMLElement>('[data-service-back]');
if (backLink) {
this.showListView();
}
});
}
/**
* Show the detail view and hide list view (with history push)
*/
private showDetailView(serviceKey: string): void {
// Push state to history
history.pushState(
{ serviceKey },
'',
`#${serviceKey}`
);
this.showDetailViewInternal(serviceKey);
}
/**
* Show detail view without modifying history (for popstate)
*/
private showDetailViewInternal(serviceKey: string): void {
if (this.listView && this.detailView) {
// Fade out list view
this.listView.classList.add('view-fade-out');
// After fade, switch views
setTimeout(() => {
this.listView!.style.display = 'none';
this.listView!.classList.remove('view-fade-out');
// Show detail view with fade in
this.detailView!.style.display = 'block';
this.detailView!.classList.add('view-fade-out');
this.detailView!.dataset.service = serviceKey;
// Reset to first tab
this.switchTab(this.detailView!, 'general');
// Trigger fade in
requestAnimationFrame(() => {
this.detailView!.classList.remove('view-fade-out');
});
}, 100);
}
}
/**
* Show the list view and hide detail view (with history push)
*/
private showListView(): void {
// Push state to history (clear hash)
history.pushState(
{},
'',
window.location.pathname
);
this.showListViewInternal();
}
/**
* Show list view without modifying history (for popstate)
*/
private showListViewInternal(): void {
if (this.listView && this.detailView) {
// Fade out detail view
this.detailView.classList.add('view-fade-out');
// After fade, switch views
setTimeout(() => {
this.detailView!.style.display = 'none';
this.detailView!.classList.remove('view-fade-out');
// Show list view with fade in
this.listView!.style.display = 'block';
this.listView!.classList.add('view-fade-out');
// Trigger fade in
requestAnimationFrame(() => {
this.listView!.classList.remove('view-fade-out');
});
}, 100);
}
}
}