import type { SidebarNavItem } from './NavSidebar' /** * Normalizes a URL/path for comparison * - Removes trailing slashes * - Removes query params and hashes if present in a full URL string * - Keeps internal paths relative */ export function normalizePathForMatching(path: string | null): string { if (!path) return '' let cleanPath = path // Handle full URLs - parse to remove query/hash but keep full URL try { if (path.startsWith('http') || path.startsWith('//')) { const url = new URL(path) // Return origin + pathname (removes query/hash) cleanPath = url.origin + url.pathname } else { // Handle relative paths - manual strip const queryIndex = cleanPath.indexOf('?') if (queryIndex !== -1) cleanPath = cleanPath.substring(0, queryIndex) const hashIndex = cleanPath.indexOf('#') if (hashIndex !== -1) cleanPath = cleanPath.substring(0, hashIndex) } } catch { // Invalid URL, fallback to manual strip const queryIndex = cleanPath.indexOf('?') if (queryIndex !== -1) cleanPath = cleanPath.substring(0, queryIndex) const hashIndex = cleanPath.indexOf('#') if (hashIndex !== -1) cleanPath = cleanPath.substring(0, hashIndex) } // Remove trailing slash (except for root "/") if (cleanPath.length > 1 && cleanPath.endsWith('/')) { cleanPath = cleanPath.slice(0, -1) } return cleanPath } /** * Checks if a current path matches a target path with specificity rules * @param currentPath The current location path * @param targetPath The path defined in navigation item * @param allPaths Set of all defined paths to check for more specific matches */ export function isPathActive( currentPath: string, targetPath: string, allPaths: Set, ): boolean { const normalizedCurrent = normalizePathForMatching(currentPath) const normalizedTarget = normalizePathForMatching(targetPath) // Exact match if (normalizedCurrent === normalizedTarget) return true // Prefix match (current path starts with target path) if (normalizedCurrent.startsWith(normalizedTarget)) { // Ensure we match whole segments (e.g. "/feeds" matches "/feeds/new" but not "/feedstock") const separator = normalizedTarget === '/' ? '' : '/' const isSegmentMatch = normalizedCurrent.startsWith( `${normalizedTarget}${separator}`, ) if (!isSegmentMatch) return false // Specificity check: Is there a more specific path that also matches? // Example: if we have "/billing" and "/billing/consumption", and current is "/billing/consumption/123" // "/billing" matches prefix, but "/billing/consumption" is more specific. for (const path of allPaths) { const normalizedPath = normalizePathForMatching(path) // Skip the target itself if (normalizedPath === normalizedTarget) continue // If this path is longer/deeper than target, and also matches current prefix if ( normalizedPath.length > normalizedTarget.length && normalizedCurrent.startsWith(normalizedPath) && normalizedPath.startsWith(normalizedTarget) ) { return false // A more specific match exists } } return true } return false } /** * Collects all normalized paths from the navigation items tree */ export function collectAllPaths(navItems: SidebarNavItem[][]): Set { const paths = new Set() navItems.forEach((group) => { group.forEach((item) => { if (item.href) paths.add(normalizePathForMatching(item.href)) if (item.items) { item.items.forEach((subItem) => { if (subItem.href) paths.add(normalizePathForMatching(subItem.href)) }) } }) }) return paths } /** * Transforms a href based on baseUrl * @param href The original href * @param baseUrl The current app's base URL */ export function normalizeHref(href: string, baseUrl?: URL): string { if (!baseUrl || href.startsWith('/')) return href try { const hrefUrl = new URL(href) if (hrefUrl.hostname === baseUrl.hostname) { return hrefUrl.pathname || '/' } } catch { // Invalid URL } return href } export function normalizeNavItems( navItems: SidebarNavItem[][], baseUrl?: string, ): SidebarNavItem[][] { if (!baseUrl) return navItems try { const base = new URL(baseUrl) return navItems.map((group) => group.map((item) => { const normalizedItem = { ...item } // Normalize main href if (normalizedItem.href) { normalizedItem.href = normalizeHref(normalizedItem.href, base) } // Normalize subitem hrefs if (normalizedItem.items) { normalizedItem.items = normalizedItem.items.map((subItem) => ({ ...subItem, href: normalizeHref(subItem.href, base), })) } return normalizedItem }), ) } catch { return navItems } }