v3: consistent tabs url across app, maxy small fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s

This commit is contained in:
khannurien
2026-06-22 17:25:40 +00:00
parent dbd2e15158
commit a1b71ad0c8
22 changed files with 295 additions and 236 deletions

47
src/hooks/useTabParam.ts Normal file
View File

@@ -0,0 +1,47 @@
import { useCallback } from "react";
import { useLocation, useNavigate } from "react-router";
const SEP = "/~/";
/**
* Reflects the active tab in the URL path using a `/~/` delimiter, e.g.
* `/users/vincent/~/playlists` or `/~/new`. The segment before `/~/` is the
* base path and the segment after is the tab. When the base is non-empty the
* default tab collapses to the clean base path (`/users/vincent`); at the root
* the base is empty, so the default stays explicit (`/~/hot`). Generic and
* path-agnostic: the base is derived from the current location, so the same
* hook works for any route.
*
* Returns the validated active tab (falling back to `defaultTab`) and a setter
* that navigates to the matching path (replacing history).
*/
export function useTabParam<T extends string>(
validTabs: Iterable<T>,
defaultTab: T,
): [T, (tab: T) => void] {
const location = useLocation();
const navigate = useNavigate();
const valid = new Set<string>(validTabs as Iterable<string>);
const path = location.pathname;
const sepAt = path.indexOf(SEP);
let base = sepAt === -1 ? path : path.slice(0, sepAt);
if (base.endsWith("/")) base = base.slice(0, -1); // root "/" → ""
const raw = sepAt === -1
? null
: path.slice(sepAt + SEP.length).replace(/\/$/, "");
const tab = (raw && valid.has(raw) ? raw : defaultTab) as T;
const setTab = useCallback(
(t: T) => {
const target = t === defaultTab && base ? base : `${base}${SEP}${t}`;
// Preserve any unrelated query string (e.g. the search page's `?q=`).
navigate(`${target}${location.search}`, { replace: true });
},
[navigate, base, defaultTab, location.search],
);
return [tab, setTab];
}