ai_api / src /shared /components /NavigationProgress.tsx
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
3.85 kB
"use client";
import { useEffect, useRef, useState } from "react";
import { usePathname, useSearchParams } from "next/navigation";
const TICK_MS = 150;
const STEP_MAX = 12;
const CEILING = 88;
const FINISH_DELAY_MS = 220;
const RESET_DELAY_MS = 320;
export default function NavigationProgress() {
const pathname = usePathname();
const searchParams = useSearchParams();
const [active, setActive] = useState(false);
const [progress, setProgress] = useState(0);
const lastKeyRef = useRef<string>(`${pathname}?${searchParams?.toString() ?? ""}`);
const tickRef = useRef<ReturnType<typeof setInterval> | null>(null);
const finishTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const resetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const onClick = (e: MouseEvent) => {
if (e.defaultPrevented) return;
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
if (e.button !== 0) return;
const anchor = (e.target as HTMLElement | null)?.closest?.("a") as HTMLAnchorElement | null;
if (!anchor) return;
if (anchor.target && anchor.target !== "" && anchor.target !== "_self") return;
if (anchor.hasAttribute("download")) return;
const href = anchor.getAttribute("href");
if (!href) return;
if (href.startsWith("#")) return;
if (href.startsWith("mailto:") || href.startsWith("tel:")) return;
try {
const url = new URL(anchor.href, window.location.href);
if (url.origin !== window.location.origin) return;
const currentKey = `${window.location.pathname}?${window.location.search}`;
const nextKey = `${url.pathname}?${url.search}`;
if (currentKey === nextKey) return;
} catch {
return;
}
// Defer to next microtask to avoid setState-in-handler-during-event issues.
queueMicrotask(() => {
setActive(true);
setProgress(8);
});
};
document.addEventListener("click", onClick, true);
return () => document.removeEventListener("click", onClick, true);
}, []);
useEffect(() => {
if (!active) return;
tickRef.current = setInterval(() => {
setProgress((prev) => {
if (prev >= CEILING) return prev;
const remaining = CEILING - prev;
const next = prev + Math.min(STEP_MAX, Math.max(1, remaining * 0.18));
return Math.min(CEILING, next);
});
}, TICK_MS);
return () => {
if (tickRef.current) {
clearInterval(tickRef.current);
tickRef.current = null;
}
};
}, [active]);
useEffect(() => {
const key = `${pathname}?${searchParams?.toString() ?? ""}`;
if (key === lastKeyRef.current) return;
lastKeyRef.current = key;
if (finishTimerRef.current) clearTimeout(finishTimerRef.current);
if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
finishTimerRef.current = setTimeout(() => {
setProgress(100);
resetTimerRef.current = setTimeout(() => {
setActive(false);
setProgress(0);
}, RESET_DELAY_MS);
}, FINISH_DELAY_MS);
return () => {
if (finishTimerRef.current) clearTimeout(finishTimerRef.current);
if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
};
}, [pathname, searchParams]);
if (!active && progress === 0) return null;
return (
<div className="pointer-events-none fixed inset-x-0 top-0 z-[9999] h-0.5" aria-hidden="true">
<div
className="h-full origin-left bg-gradient-to-r from-primary via-accent to-primary transition-[width,opacity] duration-200 ease-out"
style={{
width: `${progress}%`,
opacity: progress >= 100 ? 0 : 1,
boxShadow: "0 0 10px rgba(99, 102, 241, 0.6), 0 0 4px rgba(229, 77, 94, 0.4)",
}}
/>
</div>
);
}