feat(web): persist list-page filters in URL search params
Resources, projects, and allocations filter state now syncs to/from URL so filters survive refresh and can be shared via link. Text inputs are debounced (300ms) to avoid URL churn. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import { useCallback, useRef, useTransition } from "react";
|
||||
|
||||
// Typed-routes are enabled in next.config.ts; bypass for dynamic URLs.
|
||||
type PlainRouter = { replace: (url: string, opts?: { scroll?: boolean }) => void };
|
||||
|
||||
/**
|
||||
* Syncs a filter object with URL search params.
|
||||
* T must be a flat record of string | undefined values.
|
||||
*
|
||||
* - Reads current filter values from URL search params, falling back to `defaults`.
|
||||
* - Returns a stable `setFilters` updater that writes to the URL via `router.replace`
|
||||
* (no back-button history entry per keystroke).
|
||||
* - Default values are NOT written to the URL, keeping URLs clean and shareable.
|
||||
*/
|
||||
export function useUrlFilters<T extends Record<string, string | undefined>>(
|
||||
defaults: T,
|
||||
): [T, (updates: Partial<T>) => void] {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter() as unknown as PlainRouter;
|
||||
const pathname = usePathname();
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
// Stable ref so setFilters doesn't need defaults in its deps array.
|
||||
const defaultsRef = useRef(defaults);
|
||||
defaultsRef.current = defaults;
|
||||
|
||||
// Read current values from URL, falling back to defaults
|
||||
const filters = Object.fromEntries(
|
||||
Object.keys(defaults).map((key) => {
|
||||
const val = searchParams.get(key);
|
||||
return [key, val ?? defaults[key]];
|
||||
}),
|
||||
) as T;
|
||||
|
||||
const setFilters = useCallback(
|
||||
(updates: Partial<T>) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
const defs = defaultsRef.current;
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value === undefined || value === defs[key as keyof T]) {
|
||||
params.delete(key); // Clean URL — don't show default values
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
},
|
||||
[searchParams, router, pathname],
|
||||
);
|
||||
|
||||
return [filters, setFilters];
|
||||
}
|
||||
Reference in New Issue
Block a user