import { useState, useEffect, useCallback } from 'react' /** * Normalizes a path by removing trailing slashes (except for root path). * Also removes query parameters and hash fragments. * * @param path - The path to normalize * @returns The normalized path * * @example * ```ts * normalizePath('/billing/') // '/billing' * normalizePath('/billing/consumption/') // '/billing/consumption' * normalizePath('/') // '/' * normalizePath('/billing?param=value#section') // '/billing' * ``` */ export function normalizePath({ path }: { path?: string }): string | undefined { if (!path) return undefined // Remove query params and hash const cleanPath = path.split(/[?#]/)[0] // Remove trailing slash except for root path if (cleanPath === '/') return '/' if (cleanPath.endsWith('/')) { return cleanPath.slice(0, -1) } return cleanPath } /** * Extracts the base path from a full pathname by taking the first segment. * Strips query parameters and hash fragments. * * @param path - The full pathname (e.g., '/feeds/ethereum?param=value#section') * @returns The base path (e.g., '/feeds') * * @example * ```ts * getBasePath('/feeds/ethereum?param=value#section') // '/feeds' * getBasePath('/') // '/' * getBasePath('/feeds') // '/feeds' * getBasePath('') // '/' * getBasePath('/feeds/ethereum/bitcoin') // '/feeds' * ``` */ export function getBasePath({ path }: { path?: string }): string | undefined { if (!path) { return undefined } // strip query + hash, default to '/' const pathname = (path || '/').split(/[?#]/)[0] const cleanPath = pathname || '/' if (cleanPath === '/') return '/' const segments = cleanPath.split('/').filter(Boolean) return segments.length > 0 ? `/${segments[0]}` : '/' } /** * Finds the best matching navigation item from available links based on the current path. * Uses exact match first, then most specific (longest) prefix match. * * @param path - The current pathname * @param links - Array of available navigation links * @returns The href of the best matching item, or undefined if no match * * @example * ```ts * const links = [ * { href: '/billing', name: 'Overview' }, * { href: '/billing/consumption', name: 'Consumption' } * ] * findBestMatchingItem('/billing', links) // '/billing' * findBestMatchingItem('/billing/consumption', links) // '/billing/consumption' * findBestMatchingItem('/billing/consumption/details', links) // '/billing/consumption' * ``` */ export function findBestMatchingItem({ path, links, }: { path: string | undefined links: Array<{ href: string; name?: string }> }): string | undefined { if (!path || links.length === 0) { return undefined } // Normalize the current path const normalizedPath = normalizePath({ path }) if (!normalizedPath) return undefined // Normalize all link hrefs const normalizedLinks = links.map((link) => ({ ...link, href: normalizePath({ path: link.href }) || link.href, })) // First, try exact match const exactMatch = normalizedLinks.find( (link) => link.href === normalizedPath, ) if (exactMatch) { return exactMatch.href } // Then, try prefix matches (most specific first) const prefixMatches = normalizedLinks .filter((link) => normalizedPath.startsWith(link.href)) .sort((a, b) => b.href.length - a.href.length) // Sort by length (longest first) return prefixMatches.length > 0 ? prefixMatches[0].href : undefined } /** * Checks if a specific href is active based on the current path. * Uses exact match first, then prefix match (for non-root paths). * * @param currentPath - The current pathname * @param href - The href to check * @returns True if the href is active, false otherwise * * @example * ```ts * isActiveHref('/billing/consumption', '/billing') // true * isActiveHref('/billing/consumption', '/billing/consumption') // true * isActiveHref('/billing', '/billing/consumption') // false * ``` */ export function isActiveHref({ currentPath, href, }: { currentPath: string | null | undefined href: string | undefined }): boolean { if (!currentPath || !href) return false // Normalize both paths const normalizedPath = normalizePath({ path: currentPath }) const normalizedHref = normalizePath({ path: href }) if (!normalizedPath || !normalizedHref) return false // Exact match if (normalizedPath === normalizedHref) return true // For non-root paths, check if current path starts with the href // This means the href is a parent of the current path if ( normalizedHref !== '/' && normalizedPath.startsWith(normalizedHref + '/') ) { return true } return false } /** * Hook for managing active navigation state with state management. * This is the complete replacement for useHeaderNavTabs with enhanced functionality. * * @param path - The current pathname * @param links - Optional array of available navigation links * @returns Object with active navigation state and utilities */ export function useActiveNavigation( path?: string, links?: Array<{ href: string; name?: string }>, ) { // Use the new matching logic if links are provided, otherwise fall back to getBasePath const basePath = links && links.length > 0 ? findBestMatchingItem({ path, links }) : getBasePath({ path }) const [activeItem, setActiveItem] = useState(basePath) useEffect(() => { setActiveItem(basePath) }, [basePath]) // Helper function to check if a specific href is active const isActive = useCallback( (href: string | undefined) => isActiveHref({ currentPath: path, href }), [path], ) return { activeItem, setActiveItem, isActive, currentPath: path, } }