'use client'; import { Suspense, useEffect, useRef, useState } from 'react'; import { usePathname, useSearchParams } from 'next/navigation'; // Minimum time the bar stays visible once shown, so very fast local // transitions don't flash imperceptibly (standard practice for top-loading // progress bars). const MIN_VISIBLE_MS = 300; function LoadingIndicatorInner() { const pathname = usePathname(); const searchParams = useSearchParams(); const [isVisible, setIsVisible] = useState(false); const shownAtRef = useRef(null); // Detects the START of a navigation: a same-origin link click gives // instant feedback on tap, before the RSC fetch even begins. Browser // back/forward (popstate) is covered the same way. useEffect(() => { const handleClick = (e: MouseEvent) => { if (e.defaultPrevented || e.button !== 0) return; if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const anchor = (e.target as HTMLElement | null)?.closest('a'); if (!anchor) return; if (anchor.target && anchor.target !== '_self') return; if (anchor.hasAttribute('download')) return; const href = anchor.getAttribute('href'); if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return; let url: URL; try { url = new URL(href, window.location.href); } catch { return; } if (url.origin !== window.location.origin) return; if (url.pathname + url.search === window.location.pathname + window.location.search) return; shownAtRef.current = Date.now(); setIsVisible(true); }; const handlePopState = () => { shownAtRef.current = Date.now(); setIsVisible(true); }; document.addEventListener('click', handleClick, true); window.addEventListener('popstate', handlePopState); return () => { document.removeEventListener('click', handleClick, true); window.removeEventListener('popstate', handlePopState); }; }, []); // Detects the END of a navigation: pathname/search settling to a new value // means the route transition has completed. useEffect(() => { if (!isVisible) return; const elapsed = shownAtRef.current ? Date.now() - shownAtRef.current : MIN_VISIBLE_MS; const remaining = Math.max(0, MIN_VISIBLE_MS - elapsed); const timer = setTimeout(() => setIsVisible(false), remaining); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname, searchParams]); if (!isVisible) return null; return (
); } export function LoadingIndicator() { return ( ); }