Merge feature/s3b-deurl-filter-state: de-URL filter state + share-view link
S3b: - refactor(filters): stop mirroring filters to the URL; localStorage canonical - feat(filters): opt-in "Share view" link button (filters/sort/scope)
This commit is contained in:
commit
c69d02d01a
6 changed files with 61 additions and 12 deletions
|
|
@ -10,7 +10,7 @@ import {
|
|||
saveLocalTheme,
|
||||
type ThemePrefs,
|
||||
} from "./lib/theme";
|
||||
import { hasFilterParams, paramsToFilters, readPage, syncUrl, type Page } from "./lib/urlState";
|
||||
import { hasFilterParams, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
|
||||
import {
|
||||
loadLayout,
|
||||
normalizeLayout,
|
||||
|
|
@ -85,12 +85,10 @@ export default function App() {
|
|||
function setFilters(next: FeedFilters) {
|
||||
setFiltersState(next);
|
||||
localStorage.setItem(FILTERS_KEY, JSON.stringify(next));
|
||||
syncUrl(next, page);
|
||||
}
|
||||
|
||||
function setPage(next: Page) {
|
||||
setPageState(next);
|
||||
syncUrl(filters, next);
|
||||
}
|
||||
|
||||
function setSidebarLayout(next: SidebarLayout) {
|
||||
|
|
@ -101,8 +99,16 @@ export default function App() {
|
|||
|
||||
useEffect(() => applyTheme(theme), [theme]);
|
||||
|
||||
// Reflect the initial filters + page into the address bar so the URL is always shareable.
|
||||
useEffect(() => syncUrl(filters, page), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
// Filters live in localStorage, not the address bar. If we arrived via a "Share view"
|
||||
// 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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Pencil,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Share2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
|
|
@ -33,6 +34,8 @@ import {
|
|||
type SidebarLayout,
|
||||
type WidgetId,
|
||||
} from "../lib/sidebarLayout";
|
||||
import { shareUrl } from "../lib/urlState";
|
||||
import { notify } from "../lib/notifications";
|
||||
|
||||
// Filter ids; display labels resolved at render time via t("sidebar.sort.<id>") etc.
|
||||
const SORT_IDS = [
|
||||
|
|
@ -196,6 +199,15 @@ export default function Sidebar({
|
|||
(dateActive ? 1 : 0);
|
||||
const active = activeCount > 0;
|
||||
|
||||
async function shareView() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl(filters));
|
||||
notify({ message: t("sidebar.shareCopied") });
|
||||
} catch {
|
||||
notify({ level: "warning", message: t("sidebar.shareFailed") });
|
||||
}
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
setCustomDates(false);
|
||||
setFilters({ ...DEFAULT_SIDEBAR_FILTERS, q: filters.q, scope: filters.scope });
|
||||
|
|
@ -486,6 +498,16 @@ export default function Sidebar({
|
|||
{t("sidebar.clearAll")}
|
||||
</button>
|
||||
)}
|
||||
{!editing && (
|
||||
<button
|
||||
onClick={shareView}
|
||||
title={t("sidebar.shareView")}
|
||||
aria-label={t("sidebar.shareView")}
|
||||
className="p-1.5 rounded-lg text-muted hover:text-fg hover:bg-card transition"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{editing && (
|
||||
<button
|
||||
onClick={() => setLayout({ ...DEFAULT_LAYOUT })}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
"clearDates": "Daten löschen",
|
||||
"reshuffle": "Neu mischen",
|
||||
"noMatchingTags": "Keine passenden Tags",
|
||||
"shareView": "Link zu dieser Ansicht kopieren",
|
||||
"shareCopied": "Ansichts-Link in die Zwischenablage kopiert",
|
||||
"shareFailed": "Link konnte nicht kopiert werden",
|
||||
"widget": {
|
||||
"show": "Anzeigen",
|
||||
"sort": "Sortierung",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
"clearDates": "clear dates",
|
||||
"reshuffle": "Reshuffle",
|
||||
"noMatchingTags": "No matching tags here",
|
||||
"shareView": "Copy a link to this view",
|
||||
"shareCopied": "View link copied to clipboard",
|
||||
"shareFailed": "Couldn't copy the link",
|
||||
"widget": {
|
||||
"show": "Show",
|
||||
"sort": "Sort",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
"clearDates": "dátumok törlése",
|
||||
"reshuffle": "Újrakeverés",
|
||||
"noMatchingTags": "Nincs ide illő címke",
|
||||
"shareView": "Hivatkozás másolása erre a nézetre",
|
||||
"shareCopied": "Nézet-hivatkozás a vágólapra másolva",
|
||||
"shareFailed": "Nem sikerült a hivatkozás másolása",
|
||||
"widget": {
|
||||
"show": "Megjelenítés",
|
||||
"sort": "Rendezés",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// Serialize feed filters to/from a compact, human-readable URL query string, so a
|
||||
// pasted URL reproduces exactly what's on screen (handy for "here's the URL, I see
|
||||
// this bug"). Only non-default values are emitted to keep URLs clean.
|
||||
// Serialize feed filters to/from a compact, human-readable URL query string. Filters are
|
||||
// NOT kept in the address bar during normal use (localStorage is the single source of
|
||||
// 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";
|
||||
|
||||
const KEYS = [
|
||||
"q",
|
||||
"show",
|
||||
"sort",
|
||||
"scope",
|
||||
"normal",
|
||||
"shorts",
|
||||
"live",
|
||||
|
|
@ -25,6 +27,7 @@ export function filtersToParams(f: FeedFilters): URLSearchParams {
|
|||
if (f.q) p.set("q", f.q);
|
||||
if (f.show && f.show !== "unwatched") p.set("show", f.show);
|
||||
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.includeShorts) p.set("shorts", "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("show")) f.show = params.get("show") || "unwatched";
|
||||
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("shorts")) f.includeShorts = params.get("shorts") === "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";
|
||||
}
|
||||
|
||||
/** Reflect the current filters + page into the address bar without adding history entries. */
|
||||
export function syncUrl(f: FeedFilters, page: Page = "feed"): void {
|
||||
/** Build a shareable absolute URL that reproduces the current filters (+ page). Used by the
|
||||
* 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);
|
||||
if (page !== "feed") params.set("page", page);
|
||||
const qs = params.toString();
|
||||
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
|
||||
window.history.replaceState(null, "", url);
|
||||
return `${window.location.origin}${window.location.pathname}${qs ? `?${qs}` : ""}`;
|
||||
}
|
||||
|
||||
/** 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