All files / apps/web/src/utils navigation.ts

88% Statements 22/25
66.66% Branches 2/3
87.5% Functions 14/16
91.3% Lines 21/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102                                                    332x   1727x 1727x 14x       58x 58x           1218x 1424x   4803x 4803x     4741x 4741x   4741x 26x 26x                               332x         3652x 3652x 6308x   6308x                     3652x                        
import React from "react";
 
import type { SearchMiddleware } from "@tanstack/react-router";
import {
	stripSearchParams,
	useParams,
	useNavigate as useRawNavigate,
	useRouter,
	useRouterState,
} from "@tanstack/react-router";
import type { ValidateNavigateOptions } from "@tanstack/router-core";
import { mapValues, omitBy } from "remeda";
import { z } from "zod";
 
import type { NavigationContextType } from "~app/contexts/navigation-context";
import type { OutputRouteSearchParams, RouteId } from "~app/utils/navigation";
import { searchParamsMapping } from "~app/utils/navigation";
import { updateSetStateAction } from "~utils/react";
 
declare module "@react-types/shared" {
	// oxlint-disable-next-line typescript/consistent-type-definitions
	interface RouterConfig {
		routerOptions: ValidateNavigateOptions;
	}
}
 
export const navigationContext: NavigationContextType = {
	useNavigate: () => {
		const navigate = useRawNavigate();
		return (options) => {
			void navigate(options);
		};
	},
	usePush: () => {
		const router = useRouter();
		return (url) => router.history.push(url);
	},
	useBack: () => {
		const router = useRouter();
		return () => router.history.back();
	},
	useParams: (key) => useParams({ from: key }),
	usePathname: () => useRouterState().location.pathname,
	useSearchParams: <K extends RouteId>(key: K) => {
		const router = useRouter();
		return router.routesById[key].useSearch();
	},
	useUpdateSearchParam: <K extends RouteId>() => {
		const navigate = useRawNavigate();
		return React.useCallback(
			(param) =>
				(setStateAction, options = {}) => {
					void navigate({
						search: (prev) => ({
							...prev,
							[param]: updateSetStateAction(
								setStateAction,
								(prev as OutputRouteSearchParams<K>)[param],
							),
						}),
						to: ".",
						...options,
					});
				},
			[navigate],
		);
	},
};
 
export const searchParamsWithDefaults = <
	K extends keyof typeof searchParamsMapping,
>(
	key: K,
) => {
	const schema = searchParamsMapping[key];
	const defaultValues = mapValues(
		omitBy(schema.shape, (value) => !(value instanceof z.ZodCatch)),
		(value) =>
			value instanceof z.ZodCatch
				? value.def.catchValue({
						/* oxlint-disable typescript/no-deprecated */
						error: { issues: [] },
						input: undefined,
						/* oxlint-enable typescript/no-deprecated */
						value: undefined,
						issues: [],
					})
				: undefined,
	);
	return {
		validateSearch: schema,
		search: {
			middlewares: [
				// Apparently, the type inference is different in linter and typescript compiler
				// Any is needed because we don't use search middleware functions
				// oxlint-disable-next-line typescript/no-unnecessary-type-assertion typescript/no-explicit-any
				stripSearchParams(defaultValues) as SearchMiddleware<any>,
			],
		},
	};
};