diff --git a/apps/web/src/components/command-palette.tsx b/apps/web/src/components/command-palette.tsx index bd2c15f..15e2e64 100644 --- a/apps/web/src/components/command-palette.tsx +++ b/apps/web/src/components/command-palette.tsx @@ -14,7 +14,6 @@ import { useNavigate } from "@tanstack/react-router"; import { useAtom } from "jotai"; import { useCallback, useEffect, useRef, useState } from "react"; -import { useProgress } from "@/components/navigation-progress"; import { Command, CommandEmpty, @@ -82,7 +81,6 @@ interface SearchResult { export function CommandPalette() { const { t } = useLingui(); const navigate = useNavigate(); - const progress = useProgress(); const [commandPaletteOpen, setCommandPaletteOpen] = useAtom(commandPaletteOpenAtom); const [helpOpen, setHelpOpen] = useAtom(helpOpenAtom); const [recentSearches, setRecentSearches] = useAtom(recentSearchesAtom); @@ -103,7 +101,6 @@ export function CommandPalette() { useHotkeySequence( ["G", "H"], () => { - progress.start(); void navigate({ to: "/dashboard" }); }, { enabled, timeout: 500 }, @@ -111,7 +108,6 @@ export function CommandPalette() { useHotkeySequence( ["G", "E"], () => { - progress.start(); void navigate({ to: "/explore" }); }, { enabled, timeout: 500 }, @@ -146,14 +142,13 @@ export function CommandPalette() { (result: SearchResult) => { if (!result.id) return; setCommandPaletteOpen(false); - progress.start(); if (result.type === "person") { void navigate({ to: "/people/$id", params: { id: result.id } }); } else { void navigate({ to: "/titles/$id", params: { id: result.id } }); } }, - [setCommandPaletteOpen, progress, navigate], + [setCommandPaletteOpen, navigate], ); const handleRecentSearch = useCallback((q: string) => { @@ -337,7 +332,6 @@ export function CommandPalette() { { setCommandPaletteOpen(false); - progress.start(); void navigate({ to: "/dashboard" }); }} > @@ -348,7 +342,6 @@ export function CommandPalette() { { setCommandPaletteOpen(false); - progress.start(); void navigate({ to: "/explore" }); }} > diff --git a/apps/web/src/components/navigation-progress.tsx b/apps/web/src/components/navigation-progress.tsx index 0a9ff3c..db5b6a4 100644 --- a/apps/web/src/components/navigation-progress.tsx +++ b/apps/web/src/components/navigation-progress.tsx @@ -1,29 +1,12 @@ -import { useLocation } from "@tanstack/react-router"; -import { - createContext, - type ReactNode, - useContext, - useEffect, - useEffectEvent, - useMemo, - useRef, - useState, -} from "react"; - -type ProgressApi = { - start: () => void; - done: () => void; - set: (pct: number) => void; -}; - -const ProgressContext = createContext(null); +import { useRouter } from "@tanstack/react-router"; +import { useEffect, useEffectEvent, useRef, useState } from "react"; function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); } -export function ProgressProvider({ children }: { children: ReactNode }) { - const { pathname, searchStr } = useLocation(); +export function NavigationProgress() { + const router = useRouter(); const [visible, setVisible] = useState(false); const [progress, setProgress] = useState(0); @@ -41,7 +24,7 @@ export function ProgressProvider({ children }: { children: ReactNode }) { clearTimeout(safetyTimerRef.current); } - function start() { + const start = useEffectEvent(() => { if (inFlightRef.current) return; inFlightRef.current = true; clearTimers(); @@ -65,10 +48,10 @@ export function ProgressProvider({ children }: { children: ReactNode }) { clearTimers(); setVisible(false); setProgress(0); - }, 12000); - } + }, 12_000); + }); - function done() { + const done = useEffectEvent(() => { if (!inFlightRef.current) return; inFlightRef.current = false; clearTimers(); @@ -80,67 +63,19 @@ export function ProgressProvider({ children }: { children: ReactNode }) { setVisible(false); setTimeout(() => setProgress(0), 200); }, 200); - } - - function set(pct: number) { - const next = clamp(pct, 0, 100); - if (next >= 100) { - done(); - return; - } - if (!inFlightRef.current) start(); - setProgress(next); - } - - // Effect events — always see latest closures, don't appear in deps - const onLinkClick = useEffectEvent((e: MouseEvent) => { - if (e.defaultPrevented || e.button !== 0) return; - if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return; - - const anchor = (e.target as Element)?.closest?.("a[href]"); - if (!(anchor instanceof HTMLAnchorElement)) return; - if (anchor.target && anchor.target !== "_self") return; - if (anchor.hasAttribute("download")) return; - - const href = anchor.getAttribute("href"); - if (!href || href.startsWith("#")) return; - - try { - const url = new URL(href, window.location.href); - if (url.origin !== window.location.origin) return; - } catch { - return; - } - - start(); }); - const onPopState = useEffectEvent(() => start()); - - const onRouteChange = useEffectEvent(() => { - if (inFlightRef.current) done(); - }); - - // Set up listeners once + // Subscribe to TanStack Router navigation lifecycle events useEffect(() => { - document.addEventListener("click", onLinkClick, true); - window.addEventListener("popstate", onPopState); + const unsubBeforeLoad = router.subscribe("onBeforeLoad", (event) => { + if (event.pathChanged) start(); + }); + const unsubResolved = router.subscribe("onResolved", () => done()); return () => { - document.removeEventListener("click", onLinkClick, true); - window.removeEventListener("popstate", onPopState); + unsubBeforeLoad(); + unsubResolved(); }; - }, []); - - // Finish on route change — routeKey is intentionally a dep to trigger on navigation - const routeKey = pathname + (searchStr ?? ""); - const firstRenderRef = useRef(true); - useEffect(() => { - if (firstRenderRef.current) { - firstRenderRef.current = false; - return; - } - onRouteChange(); - }, [routeKey]); + }, [router]); // Cleanup timers on unmount useEffect(() => { @@ -152,40 +87,16 @@ export function ProgressProvider({ children }: { children: ReactNode }) { }; }, []); - // Stable context API via ref indirection - const apiRef = useRef({ start, done, set }); - apiRef.current = { start, done, set }; - - const api = useMemo( - () => ({ - start: () => apiRef.current.start(), - done: () => apiRef.current.done(), - set: (pct) => apiRef.current.set(pct), - }), - [], - ); - return ( - +