126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState, useRef, Suspense } from "react";
|
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
|
|
function ProgressBarInner() {
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const [progress, setProgress] = useState(0);
|
|
const [visible, setVisible] = useState(false);
|
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const startProgress = () => {
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
setVisible(true);
|
|
setProgress(20);
|
|
|
|
timerRef.current = setInterval(() => {
|
|
setProgress((prev) => {
|
|
if (prev < 60) return prev + Math.random() * 15;
|
|
if (prev < 85) return prev + Math.random() * 5;
|
|
return prev;
|
|
});
|
|
}, 150);
|
|
};
|
|
|
|
const completeProgress = () => {
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
setProgress(100);
|
|
setTimeout(() => {
|
|
setVisible(false);
|
|
setTimeout(() => setProgress(0), 200);
|
|
}, 250);
|
|
};
|
|
|
|
// Complete progress on pathname or searchParams change
|
|
useEffect(() => {
|
|
completeProgress();
|
|
}, [pathname, searchParams]);
|
|
|
|
// Intercept click on internal links
|
|
useEffect(() => {
|
|
const handleDocumentClick = (e: MouseEvent) => {
|
|
const target = (e.target as HTMLElement)?.closest('a');
|
|
if (!target) return;
|
|
|
|
const href = target.getAttribute('href');
|
|
if (!href) return;
|
|
|
|
// Ignore external, anchor hash, tel, mailto, or new tab links
|
|
if (
|
|
href.startsWith('#') ||
|
|
href.startsWith('mailto:') ||
|
|
href.startsWith('tel:') ||
|
|
href.startsWith('javascript:') ||
|
|
target.getAttribute('target') === '_blank' ||
|
|
e.ctrlKey ||
|
|
e.metaKey ||
|
|
e.shiftKey ||
|
|
e.altKey
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Check if internal link
|
|
try {
|
|
const url = new URL(href, window.location.origin);
|
|
if (url.origin === window.location.origin) {
|
|
const currentUrl = new URL(window.location.href);
|
|
// If navigating to different path or search
|
|
if (url.pathname !== currentUrl.pathname || url.search !== currentUrl.search) {
|
|
startProgress();
|
|
}
|
|
}
|
|
} catch {
|
|
// Relative link
|
|
if (href.startsWith('/') && href !== window.location.pathname) {
|
|
startProgress();
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener('click', handleDocumentClick, { capture: true });
|
|
return () => {
|
|
document.removeEventListener('click', handleDocumentClick, { capture: true });
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
};
|
|
}, []);
|
|
|
|
if (!visible && progress === 0) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed top-0 left-0 right-0 pointer-events-none transition-opacity duration-300"
|
|
style={{
|
|
zIndex: 9999999,
|
|
opacity: visible ? 1 : 0,
|
|
}}
|
|
>
|
|
{/* Glow Progress Bar */}
|
|
<div
|
|
className="h-[3.5px] bg-gradient-to-r from-sky-400 via-canina-blue to-emerald-400 transition-all duration-200 ease-out"
|
|
style={{
|
|
width: `${progress}%`,
|
|
boxShadow: '0 0 12px rgba(2, 132, 199, 0.9), 0 0 5px rgba(56, 189, 248, 0.8)',
|
|
}}
|
|
/>
|
|
{/* Trailing Shimmer Dot */}
|
|
<div
|
|
className="absolute top-0 right-0 -mt-[1px] h-[5px] w-24 bg-white/40 blur-[2px] transition-all duration-200"
|
|
style={{
|
|
transform: `translateX(${progress - 100}%)`,
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function NavigationProgressBar() {
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<ProgressBarInner />
|
|
</Suspense>
|
|
);
|
|
}
|