From 92a8f5688f370db3ce083d79552b47a89430e014 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 16 Jun 2026 01:27:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(nav):=20N2=20=E2=80=94=20browser=20Back=20?= =?UTF-8?q?navigates=20in-app,=20not=20to=20OAuth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Push an in-app history entry on each page switch (page rides in history.state, URL stays clean — filters still never go in the URL) and sync the page from history.state on popstate. The initial entry is stamped after the strip-params effect (which nulls history.state). Now the mouse/browser Back button steps through visited pages instead of jumping straight to the Google consent/redirect. Second step of Epic N. --- frontend/src/App.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8bee085..d74470c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -106,8 +106,13 @@ export default function App() { } function setPage(next: Page) { + if (next === page) return; setPageState(next); localStorage.setItem(PAGE_KEY, next); + // Push an in-app history entry so the browser/mouse Back button steps through pages + // instead of leaving the app (e.g. back to the OAuth redirect). The URL stays clean — + // the page rides in history.state, not the query string (filters never go in the URL). + window.history.pushState({ ...window.history.state, sfPage: next }, ""); } function setSidebarLayout(next: SidebarLayout) { @@ -129,6 +134,23 @@ export default function App() { } }, []); // eslint-disable-line react-hooks/exhaustive-deps + // In-app Back/Forward: stamp the initial entry with the current page, then sync `page` + // from history.state on popstate. Runs after the strip-params effect above (which resets + // history.state to null), so the initial stamp isn't clobbered. + useEffect(() => { + window.history.replaceState( + { ...window.history.state, sfPage: page }, + "" + ); + function onPop(e: PopStateEvent) { + const p = (e.state?.sfPage as Page) ?? "feed"; + setPageState(p); + localStorage.setItem(PAGE_KEY, p); + } + window.addEventListener("popstate", onPop); + return () => window.removeEventListener("popstate", onPop); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me }); // First-login onboarding: prompt the user to connect YouTube (and resume the flow after