121 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|