import { useCallback, useMemo } from 'react'; import { useLocation, useSearchParams } from 'react-router-dom'; import { type CatalogueQueryState, type CatalogueQueryStatePatch, type CatalogueUrlStateOptions, type CatalogueUrlStateWarning, buildCatalogueUrl, createCatalogueSearchParams, isCatalogueQueryStateDefault, mergeCatalogueQueryState, normaliseCatalogueQueryState, parseCatalogueSearchParams, } from '../utils/catalogueUrlState'; export interface UseCatalogueUrlStateOptions extends CatalogueUrlStateOptions { /** * Keep unrelated query parameters such as campaign tags while replacing all * catalogue-owned keys and aliases with their canonical form. */ preserveUnknownParams?: boolean; /** * Default navigation behaviour when setting catalogue state. */ replace?: boolean; /** * Reset pagination to page 1 when filters, search, sort, or favourites change. */ resetPageOnFilterChange?: boolean; } export interface CatalogueUrlStateNavigationOptions { replace?: boolean; state?: unknown; preventScrollReset?: boolean; resetPage?: boolean; } export type SetCatalogueUrlState = ( patch: CatalogueQueryStatePatch, options?: CatalogueUrlStateNavigationOptions, ) => void; export type ResetCatalogueUrlState = ( options?: CatalogueUrlStateNavigationOptions, ) => void; export type CatalogueUrlStateHrefFactory = ( patch?: CatalogueQueryStatePatch, pathname?: string, ) => string; export interface CatalogueUrlStateController { state: CatalogueQueryState; warnings: CatalogueUrlStateWarning[]; canonicalSearch: string; canonicalHref: string; isCanonical: boolean; isDefault: boolean; setState: SetCatalogueUrlState; resetState: ResetCatalogueUrlState; hrefForState: CatalogueUrlStateHrefFactory; } /** * React Router adapter for the catalogue URL contract. It centralises parsing, * canonical serialisation, and pagination reset behaviour so catalogue controls * can update shareable URLs without each component re-implementing query logic. */ export function useCatalogueUrlState( options: UseCatalogueUrlStateOptions = {}, ): CatalogueUrlStateController { const [searchParams, setSearchParams] = useSearchParams(); const location = useLocation(); const parsed = useMemo( () => parseCatalogueSearchParams(searchParams, options), [options, searchParams], ); const isDefault = useMemo( () => isCatalogueQueryStateDefault(parsed.state, options), [options, parsed.state], ); const canonicalHref = useMemo( () => buildCatalogueUrl(location.pathname, parsed.state, options), [location.pathname, options, parsed.state], ); const setState = useCallback( (patch, navigationOptions = {}) => { setSearchParams( (currentParams) => { const currentState = parseCatalogueSearchParams( currentParams, options, ).state; const nextState = mergeCatalogueQueryState( currentState, patch, options, { resetPage: navigationOptions.resetPage ?? options.resetPageOnFilterChange ?? true, }, ); return createCatalogueSearchParams( nextState, options, options.preserveUnknownParams ? currentParams : undefined, ); }, createRouterNavigationOptions(options, navigationOptions), ); }, [options, setSearchParams], ); const resetState = useCallback( (navigationOptions = {}) => { setSearchParams( (currentParams) => createCatalogueSearchParams( normaliseCatalogueQueryState({}, options), options, options.preserveUnknownParams ? currentParams : undefined, ), createRouterNavigationOptions(options, navigationOptions), ); }, [options, setSearchParams], ); const hrefForState = useCallback( (patch = {}, pathname = location.pathname) => { const nextState = mergeCatalogueQueryState( parsed.state, patch, options, { resetPage: false, }, ); return buildCatalogueUrl( pathname, nextState, options, options.preserveUnknownParams ? searchParams : undefined, ); }, [location.pathname, options, parsed.state, searchParams], ); return { state: parsed.state, warnings: parsed.warnings, canonicalSearch: parsed.canonicalSearch, canonicalHref, isCanonical: parsed.wasCanonical, isDefault, setState, resetState, hrefForState, }; } function createRouterNavigationOptions( hookOptions: UseCatalogueUrlStateOptions, navigationOptions: CatalogueUrlStateNavigationOptions, ): { replace?: boolean; state?: unknown; preventScrollReset?: boolean; } { const routerOptions: { replace?: boolean; state?: unknown; preventScrollReset?: boolean; } = { replace: navigationOptions.replace ?? hookOptions.replace ?? false, }; if ('state' in navigationOptions) { routerOptions.state = navigationOptions.state; } if (navigationOptions.preventScrollReset !== undefined) { routerOptions.preventScrollReset = navigationOptions.preventScrollReset; } return routerOptions; }