Files
guilan-ace-frontend/src/components/RouteProgress.tsx
Amirhossein Khalili 5711961b9b
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
F(frontend): add route loading feedback
2026-05-20 11:47:27 +03:30

121 lines
3.1 KiB
TypeScript

"use client";
import * as React from "react";
import { usePathname, useSearchParams } from "next/navigation";
import {
completeNavigationProgress,
subscribeNavigationProgress,
} from "@/lib/navigation-progress";
const START_VALUE = 18;
const MAX_ACTIVE_VALUE = 90;
export default function RouteProgress() {
const pathname = usePathname();
const searchParams = useSearchParams();
const [visible, setVisible] = React.useState(false);
const [progress, setProgress] = React.useState(0);
const intervalRef = React.useRef<number | null>(null);
const finishTimeoutRef = React.useRef<number | null>(null);
const safetyTimeoutRef = React.useRef<number | null>(null);
const activeRef = React.useRef(false);
const routeKey = `${pathname ?? ""}?${searchParams?.toString() ?? ""}`;
const clearTimers = React.useCallback(() => {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (finishTimeoutRef.current !== null) {
window.clearTimeout(finishTimeoutRef.current);
finishTimeoutRef.current = null;
}
if (safetyTimeoutRef.current !== null) {
window.clearTimeout(safetyTimeoutRef.current);
safetyTimeoutRef.current = null;
}
}, []);
const finish = React.useCallback(() => {
if (!activeRef.current) {
return;
}
activeRef.current = false;
clearTimers();
setProgress(100);
finishTimeoutRef.current = window.setTimeout(() => {
setVisible(false);
setProgress(0);
}, 220);
}, [clearTimers]);
const start = React.useCallback(() => {
clearTimers();
activeRef.current = true;
setVisible(true);
setProgress(START_VALUE);
window.requestAnimationFrame(() => {
setProgress(28);
});
intervalRef.current = window.setInterval(() => {
setProgress((current) => {
if (current >= MAX_ACTIVE_VALUE) {
return current;
}
const delta = Math.max((MAX_ACTIVE_VALUE - current) * 0.14, 1.5);
return Math.min(MAX_ACTIVE_VALUE, current + delta);
});
}, 180);
safetyTimeoutRef.current = window.setTimeout(() => {
finish();
}, 12000);
}, [clearTimers, finish]);
React.useEffect(() => {
return subscribeNavigationProgress((event) => {
if (event === "start") {
start();
return;
}
finish();
});
}, [finish, start]);
React.useEffect(() => {
finish();
}, [finish, routeKey]);
React.useEffect(() => {
return () => {
clearTimers();
};
}, [clearTimers]);
return (
<div
aria-hidden="true"
className="pointer-events-none fixed inset-x-0 top-0 z-[100] h-1 overflow-hidden"
>
<div
className={`absolute inset-0 transition-opacity duration-200 ${
visible ? "opacity-100" : "opacity-0"
}`}
>
<div
className="h-full origin-right bg-[linear-gradient(90deg,hsl(var(--primary))_0%,hsl(var(--route-progress))_100%)] shadow-[0_0_16px_hsl(var(--route-progress)/0.55)] transition-[transform] duration-200 ease-out"
style={{ transform: `scaleX(${progress / 100})` }}
/>
</div>
</div>
);
}