export type RouteSearchValue = string | number | boolean | null | undefined; export type RouteSearchRecord = Record; export type RouteSearchInput = string | URLSearchParams | RouteSearchRecord | null | undefined; export const ROUTE_PATHS = { home: '/', catalogue: '/', catalog: '/', catalogueAlt: '/catalogue', catalogAlt: '/catalog', machinesIndex: '/machines', machine: '/machines/:machineId', machineViewer: '/machines/:machineId', legacyMachine: '/machine/:machineId', legacyViewer: '/viewer/:machineId', legacySystems: '/systems/:machineId', notFound: '*', } as const; export const ROUTES = ROUTE_PATHS; export const routePaths = ROUTE_PATHS; export const HOME_ROUTE = ROUTE_PATHS.home; export const CATALOGUE_ROUTE = ROUTE_PATHS.catalogue; export const CATALOG_ROUTE = ROUTE_PATHS.catalog; export const MACHINES_ROUTE = ROUTE_PATHS.machinesIndex; export const MACHINE_ROUTE = ROUTE_PATHS.machine; export const MACHINE_VIEWER_ROUTE = ROUTE_PATHS.machineViewer; const MACHINE_ROUTE_PREFIXES = ['/machines/', '/machine/', '/viewer/', '/systems/'] as const; function appendSearchValue(params: URLSearchParams, key: string, value: RouteSearchValue): void { if (value === null || value === undefined) { return; } params.append(key, String(value)); } export function serialiseRouteSearch(search: RouteSearchInput): string { if (!search) { return ''; } if (typeof search === 'string') { const trimmed = search.trim(); if (!trimmed) { return ''; } return trimmed.startsWith('?') ? trimmed : `?${trimmed}`; } if (search instanceof URLSearchParams) { const text = search.toString(); return text ? `?${text}` : ''; } const params = new URLSearchParams(); Object.entries(search).forEach(([key, value]) => { if (Array.isArray(value)) { value.forEach((entry) => appendSearchValue(params, key, entry)); return; } appendSearchValue(params, key, value); }); const text = params.toString(); return text ? `?${text}` : ''; } export function appendRouteSearch(href: string, search: RouteSearchInput): string { const searchText = serialiseRouteSearch(search); if (!searchText) { return href; } const hashIndex = href.indexOf('#'); const hrefWithoutHash = hashIndex >= 0 ? href.slice(0, hashIndex) : href; const hash = hashIndex >= 0 ? href.slice(hashIndex) : ''; const separator = hrefWithoutHash.includes('?') ? '&' : '?'; return `${hrefWithoutHash}${separator}${searchText.slice(1)}${hash}`; } export function encodeMachineId(machineId: string): string { return encodeURIComponent(machineId.trim()); } export function safeDecodeMachineId(machineId: string): string { try { return decodeURIComponent(machineId); } catch { return machineId; } } export const decodeMachineId = safeDecodeMachineId; export function cataloguePath(search?: RouteSearchInput): string { return `${ROUTE_PATHS.catalogue}${serialiseRouteSearch(search)}`; } export const catalogPath = cataloguePath; export const homePath = cataloguePath; export const getCataloguePath = cataloguePath; export const getCatalogPath = cataloguePath; export function machinesPath(search?: RouteSearchInput): string { return `${ROUTE_PATHS.machinesIndex}${serialiseRouteSearch(search)}`; } export const getMachinesPath = machinesPath; export function machinePath(machineId: string, search?: RouteSearchInput): string { return `/machines/${encodeMachineId(machineId)}${serialiseRouteSearch(search)}`; } export const machineRoute = machinePath; export const machineViewerPath = machinePath; export const viewerPath = machinePath; export const systemPath = machinePath; export const machineSharePath = machinePath; export const createMachineHref = machinePath; export const buildMachinePath = machinePath; export const getMachinePath = machinePath; export const getMachineRoute = machinePath; export const getMachineViewerPath = machinePath; export const getViewerPath = machinePath; export const toMachinePath = machinePath; export function legacyMachinePath(machineId: string, search?: RouteSearchInput): string { return `/machine/${encodeMachineId(machineId)}${serialiseRouteSearch(search)}`; } export function legacyViewerPath(machineId: string, search?: RouteSearchInput): string { return `/viewer/${encodeMachineId(machineId)}${serialiseRouteSearch(search)}`; } export function legacySystemPath(machineId: string, search?: RouteSearchInput): string { return `/systems/${encodeMachineId(machineId)}${serialiseRouteSearch(search)}`; } export function extractMachineIdFromPathname(pathname: string): string | null { for (const prefix of MACHINE_ROUTE_PREFIXES) { if (pathname.startsWith(prefix)) { const withoutPrefix = pathname.slice(prefix.length); const firstSegment = withoutPrefix.split(/[/?#]/)[0]; return firstSegment ? safeDecodeMachineId(firstSegment) : null; } } return null; } export function isMachineRoutePathname(pathname: string): boolean { return extractMachineIdFromPathname(pathname) !== null; } export function normaliseMachineHref(machineIdOrHref: string, search?: RouteSearchInput): string { const trimmed = machineIdOrHref.trim(); if (!trimmed) { return cataloguePath(search); } const extractedMachineId = extractMachineIdFromPathname(trimmed); if (extractedMachineId) { return machinePath(extractedMachineId, search); } return machinePath(trimmed, search); } export default ROUTE_PATHS;