/** * URL router for the SipRouter dashboard. * Maps grouped URL paths to views in dees-simple-appdash. */ const SUBVIEW_MAP = { overview: ['stats'], telephony: ['calls', 'phone', 'routes', 'voicemail', 'ivr'], configuration: ['contacts', 'providers'], system: ['log'], } as const; const DEFAULT_SUBVIEW = { overview: 'stats', telephony: 'calls', configuration: 'contacts', system: 'log', } as const satisfies { [key in TViewSlug]: TSubviewSlug }; const DEFAULT_ROUTE: IRouteState = { view: 'overview', subview: 'stats', }; type TViewSlug = keyof typeof SUBVIEW_MAP; type TSubviewSlug = (typeof SUBVIEW_MAP)[TViewSlug][number]; interface IRouteState { view: TViewSlug; subview: TSubviewSlug; } function isValidView(view: string): view is TViewSlug { return Object.prototype.hasOwnProperty.call(SUBVIEW_MAP, view); } function isValidSubview(view: TViewSlug, subview: string): subview is TSubviewSlug { return (SUBVIEW_MAP[view] as readonly string[]).includes(subview); } class AppRouter { private currentRoute: IRouteState = DEFAULT_ROUTE; private onNavigate: ((view: TViewSlug, subview: TSubviewSlug) => void) | null = null; private initialized = false; init(): void { if (this.initialized) return; this.applyRoute(this.parsePath(location.pathname) ?? DEFAULT_ROUTE, false, true); // Handle browser back/forward. window.addEventListener('popstate', () => { this.applyRoute(this.parsePath(location.pathname) ?? DEFAULT_ROUTE, false, true); }); this.initialized = true; } setNavigateHandler(handler: (view: TViewSlug, subview: TSubviewSlug) => void): void { this.onNavigate = handler; } navigateToView(view: TViewSlug, subview?: TSubviewSlug, skipCallback = false): void { const targetSubview = subview && isValidSubview(view, subview) ? subview : DEFAULT_SUBVIEW[view]; this.applyRoute({ view, subview: targetSubview }, skipCallback, false); } getCurrentRoute(): IRouteState { return this.currentRoute; } isCurrentRoute(view: TViewSlug, subview: TSubviewSlug): boolean { return this.currentRoute.view === view && this.currentRoute.subview === subview; } private parsePath(pathname: string): IRouteState | null { const segments = pathname.split('/').filter(Boolean); if (segments.length === 0) return DEFAULT_ROUTE; if (segments.length > 2) return null; const [viewSegment, subviewSegment] = segments; if (!isValidView(viewSegment)) return null; const targetSubview = subviewSegment ?? DEFAULT_SUBVIEW[viewSegment]; if (!isValidSubview(viewSegment, targetSubview)) return null; return { view: viewSegment, subview: targetSubview, }; } private applyRoute(route: IRouteState, skipCallback: boolean, replace: boolean): void { this.currentRoute = route; const url = `/${route.view}/${route.subview}`; if (location.pathname !== url) { if (replace) { history.replaceState(null, '', url); } else { history.pushState(null, '', url); } } if (!skipCallback) { this.onNavigate?.(route.view, route.subview); } } } export const appRouter = new AppRouter(); export type { IRouteState, TSubviewSlug, TViewSlug };