96 lines
2.5 KiB
TypeScript
96 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, type ReactNode } from "react";
|
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
import NavigationLoader from "./NavigationLoader";
|
|
import {
|
|
isInternalNavigationClick,
|
|
startNavigation,
|
|
stopNavigation,
|
|
subscribeNavigationLoading,
|
|
} from "@/app/lib/navigation-loading";
|
|
|
|
const LOADING_TIMEOUT_MS = 12000;
|
|
|
|
function shouldStartForHistoryUrl(nextUrl: string | URL | null | undefined) {
|
|
if (!nextUrl) return false;
|
|
try {
|
|
const url = new URL(String(nextUrl), window.location.href);
|
|
return (
|
|
url.pathname !== window.location.pathname ||
|
|
url.search !== window.location.search
|
|
);
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export default function NavigationLoadingProvider({
|
|
children,
|
|
}: {
|
|
children: ReactNode;
|
|
}) {
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => subscribeNavigationLoading(setVisible), []);
|
|
|
|
useEffect(() => {
|
|
stopNavigation();
|
|
}, [pathname, searchParams]);
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
const timeout = window.setTimeout(() => stopNavigation(), LOADING_TIMEOUT_MS);
|
|
return () => window.clearTimeout(timeout);
|
|
}, [visible]);
|
|
|
|
useEffect(() => {
|
|
const onDocumentClick = (event: MouseEvent) => {
|
|
const target = event.target;
|
|
if (!(target instanceof Element)) return;
|
|
const anchor = target.closest("a");
|
|
if (!(anchor instanceof HTMLAnchorElement)) return;
|
|
if (isInternalNavigationClick(event, anchor)) {
|
|
startNavigation();
|
|
}
|
|
};
|
|
|
|
const onPopState = () => startNavigation();
|
|
|
|
const originalPushState = history.pushState.bind(history);
|
|
const originalReplaceState = history.replaceState.bind(history);
|
|
|
|
history.pushState = (...args) => {
|
|
if (shouldStartForHistoryUrl(args[2])) {
|
|
startNavigation();
|
|
}
|
|
return originalPushState(...args);
|
|
};
|
|
history.replaceState = (...args) => {
|
|
if (shouldStartForHistoryUrl(args[2])) {
|
|
startNavigation();
|
|
}
|
|
return originalReplaceState(...args);
|
|
};
|
|
|
|
document.addEventListener("click", onDocumentClick, true);
|
|
window.addEventListener("popstate", onPopState);
|
|
|
|
return () => {
|
|
document.removeEventListener("click", onDocumentClick, true);
|
|
window.removeEventListener("popstate", onPopState);
|
|
history.pushState = originalPushState;
|
|
history.replaceState = originalReplaceState;
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
{children}
|
|
{visible ? <NavigationLoader /> : null}
|
|
</>
|
|
);
|
|
}
|