diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ff3dfcf..719da76 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 }); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 260134b..95a8980 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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.") 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")} )} + {!editing && ( + + )} {editing && (