refactor(filters): stop mirroring filters into the URL; localStorage is canonical
Filters/sort/search/scope were written to the address bar on every change (a leftover from sharing reproducible examples), giving two sources of truth. Make localStorage the single source: drop the automatic syncUrl from setFilters/setPage. A "Share view" link still hydrates filters on first load, after which the query is stripped from the URL (stripUrlParams) so it stays clean. syncUrl is replaced by shareUrl (builds the link on demand); the serializer now also round-trips scope.
This commit is contained in:
parent
581719401e
commit
3e90fe2e5b
2 changed files with 30 additions and 12 deletions
|
|
@ -10,7 +10,7 @@ import {
|
||||||
saveLocalTheme,
|
saveLocalTheme,
|
||||||
type ThemePrefs,
|
type ThemePrefs,
|
||||||
} from "./lib/theme";
|
} from "./lib/theme";
|
||||||
import { hasFilterParams, paramsToFilters, readPage, syncUrl, type Page } from "./lib/urlState";
|
import { hasFilterParams, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
|
||||||
import {
|
import {
|
||||||
loadLayout,
|
loadLayout,
|
||||||
normalizeLayout,
|
normalizeLayout,
|
||||||
|
|
@ -85,12 +85,10 @@ export default function App() {
|
||||||
function setFilters(next: FeedFilters) {
|
function setFilters(next: FeedFilters) {
|
||||||
setFiltersState(next);
|
setFiltersState(next);
|
||||||
localStorage.setItem(FILTERS_KEY, JSON.stringify(next));
|
localStorage.setItem(FILTERS_KEY, JSON.stringify(next));
|
||||||
syncUrl(next, page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPage(next: Page) {
|
function setPage(next: Page) {
|
||||||
setPageState(next);
|
setPageState(next);
|
||||||
syncUrl(filters, next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSidebarLayout(next: SidebarLayout) {
|
function setSidebarLayout(next: SidebarLayout) {
|
||||||
|
|
@ -101,8 +99,16 @@ export default function App() {
|
||||||
|
|
||||||
useEffect(() => applyTheme(theme), [theme]);
|
useEffect(() => applyTheme(theme), [theme]);
|
||||||
|
|
||||||
// Reflect the initial filters + page into the address bar so the URL is always shareable.
|
// Filters live in localStorage, not the address bar. If we arrived via a "Share view"
|
||||||
useEffect(() => syncUrl(filters, page), []); // eslint-disable-line react-hooks/exhaustive-deps
|
// link, its params have already hydrated the initial state — persist them and strip the
|
||||||
|
// query so the URL stays clean from here on.
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (hasFilterParams(params) || params.has("page")) {
|
||||||
|
localStorage.setItem(FILTERS_KEY, JSON.stringify(filters));
|
||||||
|
stripUrlParams();
|
||||||
|
}
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me });
|
const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
// Serialize feed filters to/from a compact, human-readable URL query string, so a
|
// Serialize feed filters to/from a compact, human-readable URL query string. Filters are
|
||||||
// pasted URL reproduces exactly what's on screen (handy for "here's the URL, I see
|
// NOT kept in the address bar during normal use (localStorage is the single source of
|
||||||
// this bug"). Only non-default values are emitted to keep URLs clean.
|
// truth); this serializer powers the explicit "Share view" link and the one-time hydration
|
||||||
|
// when someone opens such a link. Only non-default values are emitted to keep URLs clean.
|
||||||
import type { FeedFilters } from "./api";
|
import type { FeedFilters } from "./api";
|
||||||
|
|
||||||
const KEYS = [
|
const KEYS = [
|
||||||
"q",
|
"q",
|
||||||
"show",
|
"show",
|
||||||
"sort",
|
"sort",
|
||||||
|
"scope",
|
||||||
"normal",
|
"normal",
|
||||||
"shorts",
|
"shorts",
|
||||||
"live",
|
"live",
|
||||||
|
|
@ -25,6 +27,7 @@ export function filtersToParams(f: FeedFilters): URLSearchParams {
|
||||||
if (f.q) p.set("q", f.q);
|
if (f.q) p.set("q", f.q);
|
||||||
if (f.show && f.show !== "unwatched") p.set("show", f.show);
|
if (f.show && f.show !== "unwatched") p.set("show", f.show);
|
||||||
if (f.sort && f.sort !== "newest") p.set("sort", f.sort);
|
if (f.sort && f.sort !== "newest") p.set("sort", f.sort);
|
||||||
|
if (f.scope === "all") p.set("scope", "all");
|
||||||
if (!f.includeNormal) p.set("normal", "0");
|
if (!f.includeNormal) p.set("normal", "0");
|
||||||
if (f.includeShorts) p.set("shorts", "1");
|
if (f.includeShorts) p.set("shorts", "1");
|
||||||
if (f.includeLive) p.set("live", "1");
|
if (f.includeLive) p.set("live", "1");
|
||||||
|
|
@ -51,6 +54,7 @@ export function paramsToFilters(params: URLSearchParams, base: FeedFilters): Fee
|
||||||
if (params.has("q")) f.q = params.get("q") ?? "";
|
if (params.has("q")) f.q = params.get("q") ?? "";
|
||||||
if (params.has("show")) f.show = params.get("show") || "unwatched";
|
if (params.has("show")) f.show = params.get("show") || "unwatched";
|
||||||
if (params.has("sort")) f.sort = params.get("sort") || "newest";
|
if (params.has("sort")) f.sort = params.get("sort") || "newest";
|
||||||
|
if (params.has("scope")) f.scope = params.get("scope") === "all" ? "all" : "my";
|
||||||
if (params.has("normal")) f.includeNormal = params.get("normal") !== "0";
|
if (params.has("normal")) f.includeNormal = params.get("normal") !== "0";
|
||||||
if (params.has("shorts")) f.includeShorts = params.get("shorts") === "1";
|
if (params.has("shorts")) f.includeShorts = params.get("shorts") === "1";
|
||||||
if (params.has("live")) f.includeLive = params.get("live") === "1";
|
if (params.has("live")) f.includeLive = params.get("live") === "1";
|
||||||
|
|
@ -81,11 +85,19 @@ export function readPage(): Page {
|
||||||
return p === "channels" || p === "stats" ? p : "feed";
|
return p === "channels" || p === "stats" ? p : "feed";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reflect the current filters + page into the address bar without adding history entries. */
|
/** Build a shareable absolute URL that reproduces the current filters (+ page). Used by the
|
||||||
export function syncUrl(f: FeedFilters, page: Page = "feed"): void {
|
* opt-in "Share view" action — filters are not otherwise written to the address bar. */
|
||||||
|
export function shareUrl(f: FeedFilters, page: Page = "feed"): string {
|
||||||
const params = filtersToParams(f);
|
const params = filtersToParams(f);
|
||||||
if (page !== "feed") params.set("page", page);
|
if (page !== "feed") params.set("page", page);
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
|
return `${window.location.origin}${window.location.pathname}${qs ? `?${qs}` : ""}`;
|
||||||
window.history.replaceState(null, "", url);
|
}
|
||||||
|
|
||||||
|
/** Strip any filter/page query params from the address bar (after hydrating from a share
|
||||||
|
* link), leaving a clean URL without touching history. */
|
||||||
|
export function stripUrlParams(): void {
|
||||||
|
if (window.location.search) {
|
||||||
|
window.history.replaceState(null, "", window.location.pathname);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue