From 4e13aa5e9ad31d970cfafa263a28eec2cfe7227d Mon Sep 17 00:00:00 2001 From: Kevin Duong Date: Sun, 23 Aug 2026 19:32:03 -0400 Subject: [PATCH 1/3] fix(app): Clear draft when user navigates away from --- .../contribute/ContributionFlow.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/src/components/contribute/ContributionFlow.tsx b/app/src/components/contribute/ContributionFlow.tsx index df04860a..6e7ea05b 100644 --- a/app/src/components/contribute/ContributionFlow.tsx +++ b/app/src/components/contribute/ContributionFlow.tsx @@ -1,6 +1,7 @@ import { defineStepper } from "@stepperize/react"; import { ChevronRight } from "lucide-react"; import { Fragment, useEffect, useMemo, useRef, useState } from "react"; +import { useLocation } from "@tanstack/react-router"; import { toast } from "sonner"; import type { Dispatch, SetStateAction } from "react"; @@ -850,6 +851,24 @@ function Inner({ } }; + // Remove draft from localStorage if user leaves /contribute page + const location = useLocation(); + const pathname = location.pathname; + + // Handle when user leaves page + useEffect(() => { + if (pathname === "/contribute") { + sessionStorage.setItem("wasOnContributePage", "true"); + } else { + const wasOnContribute = + sessionStorage.getItem("wasOnContributePage") === "true"; + if (wasOnContribute) { + if (type) clearStoredDraft(type); + sessionStorage.removeItem("wasOnContributePage"); + } + } + }, [pathname]); + if (skipTypeStep && !type) return null; return ( From d0645efd838aec9d35c4d2b33c2e58a595c6ecd0 Mon Sep 17 00:00:00 2001 From: Kevin Duong Date: Wed, 26 Aug 2026 07:40:39 -0400 Subject: [PATCH 2/3] fix(app): Switch `localStorage` to `sessionStorage` so that data clears when browser tab is closed `sessionStorage` only lasts as long as the tab or browser is open. It can also survive page reloads and restores --- .../contribute/ContributionFlow.tsx | 2 +- app/src/lib/contributionStorage.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/components/contribute/ContributionFlow.tsx b/app/src/components/contribute/ContributionFlow.tsx index 6e7ea05b..8701e422 100644 --- a/app/src/components/contribute/ContributionFlow.tsx +++ b/app/src/components/contribute/ContributionFlow.tsx @@ -851,7 +851,7 @@ function Inner({ } }; - // Remove draft from localStorage if user leaves /contribute page + // Remove draft from sessionStorage if user leaves /contribute page const location = useLocation(); const pathname = location.pathname; diff --git a/app/src/lib/contributionStorage.ts b/app/src/lib/contributionStorage.ts index 5e23daa2..83c5739c 100644 --- a/app/src/lib/contributionStorage.ts +++ b/app/src/lib/contributionStorage.ts @@ -39,7 +39,7 @@ const DRAFT_SCHEMAS: Record = { }; /** - * Safely reads a contribution draft from localStorage. + * Safely reads a contribution draft from sessionStorage. * Handles SSR (returns null when window is not defined) and JSON parse errors. * A draft that doesn't match the expected shape is dropped and cleared, since * loading one into form state crashes the first render that touches it. @@ -51,7 +51,7 @@ export function getStoredDraft( let parsed: unknown; try { - const raw = window.localStorage.getItem(STORAGE_KEYS[type]); + const raw = window.sessionStorage.getItem(STORAGE_KEYS[type]); if (!raw) return null; parsed = JSON.parse(raw); } catch (error) { @@ -74,7 +74,7 @@ export function getStoredDraft( } /** - * Safely writes a contribution draft to localStorage. + * Safely writes a contribution draft to sessionStorage. */ export function setStoredDraft( type: ContributionType, @@ -83,20 +83,20 @@ export function setStoredDraft( if (typeof window === "undefined") return; try { - window.localStorage.setItem(STORAGE_KEYS[type], JSON.stringify(draft)); + window.sessionStorage.setItem(STORAGE_KEYS[type], JSON.stringify(draft)); } catch (error) { - console.warn(`Failed to save draft to localStorage for ${type}:`, error); + console.warn(`Failed to save draft to sessionStorage for ${type}:`, error); } } /** - * Clears the stored draft for a given contribution type from localStorage. + * Clears the stored draft for a given contribution type from sessionStorage. */ export function clearStoredDraft(type: ContributionType): void { if (typeof window === "undefined") return; try { - window.localStorage.removeItem(STORAGE_KEYS[type]); + window.sessionStorage.removeItem(STORAGE_KEYS[type]); } catch (error) { console.warn(`Failed to clear stored draft for ${type}:`, error); } @@ -109,14 +109,14 @@ export function hasStoredDraft(type: ContributionType): boolean { if (typeof window === "undefined") return false; try { - return window.localStorage.getItem(STORAGE_KEYS[type]) !== null; + return window.sessionStorage.getItem(STORAGE_KEYS[type]) !== null; } catch { return false; } } /** - * Clears all stored drafts for all contribution types from localStorage. + * Clears all stored drafts for all contribution types from sessionStorage. */ export function clearAllStoredDrafts(): void { const types = Object.keys(STORAGE_KEYS) as Array; @@ -130,7 +130,7 @@ export interface ContributionSaveControls { } /** - * Hook to automatically and debouncingly save contribution form data to localStorage. + * Hook to automatically and debouncingly save contribution form data to sessionStorage. * Passing a null type disables saving, flushing anything already queued. * Flushes pending changes immediately on component unmount. */ From 36a52dbdc0f8ffa5cff4929e15d4d20d962eacc3 Mon Sep 17 00:00:00 2001 From: Kevin Duong Date: Wed, 26 Aug 2026 18:38:05 -0400 Subject: [PATCH 3/3] fix(app): Ask for user confirmation if user navigates off `/contribute` page --- .../contribute/ContributionFlow.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/components/contribute/ContributionFlow.tsx b/app/src/components/contribute/ContributionFlow.tsx index 8701e422..4250c0f3 100644 --- a/app/src/components/contribute/ContributionFlow.tsx +++ b/app/src/components/contribute/ContributionFlow.tsx @@ -1,7 +1,7 @@ import { defineStepper } from "@stepperize/react"; import { ChevronRight } from "lucide-react"; import { Fragment, useEffect, useMemo, useRef, useState } from "react"; -import { useLocation } from "@tanstack/react-router"; +import { useBlocker, useLocation } from "@tanstack/react-router"; import { toast } from "sonner"; import type { Dispatch, SetStateAction } from "react"; @@ -851,6 +851,25 @@ function Inner({ } }; + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + + useBlocker({ + shouldBlockFn: ({ current, next }) => { + const isLeaving = + current.routeId === "/contribute" && next.routeId !== "/contribute"; + + if (isLeaving) { + const shouldLeave = window.confirm( + "Are you sure you want to leave? You have unsaved changes." + ); + return !shouldLeave; + } + + return false; + }, + enableBeforeUnload: hasUnsavedChanges, + }); + // Remove draft from sessionStorage if user leaves /contribute page const location = useLocation(); const pathname = location.pathname; @@ -863,6 +882,7 @@ function Inner({ const wasOnContribute = sessionStorage.getItem("wasOnContributePage") === "true"; if (wasOnContribute) { + setHasUnsavedChanges(true); if (type) clearStoredDraft(type); sessionStorage.removeItem("wasOnContributePage"); }