diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 17365da..614779d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -102,9 +102,16 @@ function App() { { - // The tour already wrote the localStorage key via - // writeComplete(); flip the App-level flag so it stays - // hidden after the next render. + // Dismiss path (X / Skip / Esc / "Got it"). The tour + // already wrote the localStorage key; flip the + // App-level flag to true so the early-return fires and + // the dialog disappears. + onboarding.markComplete() + }} + onReset={() => { + // Re-show path (?reset-tour=1). The tour already + // cleared the localStorage key; flip the App-level + // flag to false so the tour re-appears. onboarding.reset() }} /> diff --git a/frontend/src/components/OnboardingTour.tsx b/frontend/src/components/OnboardingTour.tsx index e36c341..d17aac5 100644 --- a/frontend/src/components/OnboardingTour.tsx +++ b/frontend/src/components/OnboardingTour.tsx @@ -100,9 +100,16 @@ function clearComplete(): void { } } -export function useOnboarding(): { reset: () => void; show: () => void; isComplete: boolean } { +export function useOnboarding(): { + reset: () => void + show: () => void + markComplete: () => void + isComplete: boolean +} { const [isComplete, setIsComplete] = useState(readComplete) + // Used by ?reset-tour=1 and any other "re-show" path. Clears the + // localStorage key and flips the App-level flag so the tour re-appears. const reset = useCallback(() => { clearComplete() setIsComplete(false) @@ -113,15 +120,28 @@ export function useOnboarding(): { reset: () => void; show: () => void; isComple setIsComplete(false) }, []) - return { reset, show, isComplete } + // Used by the tour's dismiss path (X / Skip / Esc / "Got it" on the + // last step). Called from App when onComplete fires. Just flips + // the App-level flag to true so the tour's early-return fires on + // the next render. The localStorage key is already written by the + // tour's finish() before it invokes onComplete. + const markComplete = useCallback(() => { + setIsComplete(true) + }, []) + + return { reset, show, markComplete, isComplete } } export function OnboardingTour({ isComplete, onComplete, + onReset, }: { isComplete: boolean + /** Called when the user dismisses the tour (X / Skip / Esc / "Got it"). */ onComplete: () => void + /** Called when the tour re-shows (e.g. ?reset-tour=1). Inverse of onComplete. */ + onReset: () => void }) { const location = useLocation() const navigate = useNavigate() @@ -135,18 +155,20 @@ export function OnboardingTour({ const isLast = step === STEPS.length - 1 // Handle ?reset-tour=1 (clears the key; tour shows on next render). + // Calls onReset() to flip the App-level flag to false (the inverse + // of onComplete, which flips it to true on dismiss). useEffect(() => { const sp = new URLSearchParams(location.search) if (sp.get(RESET_PARAM) === '1') { clearComplete() - onComplete() + onReset() // Strip the param so a refresh doesn't re-trigger the reset. sp.delete(RESET_PARAM) const next = sp.toString() navigate(`${location.pathname}${next ? `?${next}` : ''}`, { replace: true }) setStep(0) } - // We intentionally don't depend on `onComplete` (changes per render) + // We intentionally don't depend on `onReset` (changes per render) // — the only effect we want is when the URL search changes. // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.search])