's'
This commit is contained in:
28
app/components/NavigationLoader.tsx
Normal file
28
app/components/NavigationLoader.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
export default function NavigationLoader() {
|
||||
return (
|
||||
<div
|
||||
className="nav-loader-overlay fixed inset-0 z-[9999] flex items-center justify-center"
|
||||
role="status"
|
||||
aria-busy="true"
|
||||
aria-label="Loading"
|
||||
>
|
||||
<div className="nav-loader-backdrop absolute inset-0 bg-white/55 dark:bg-gray-950/65 backdrop-blur-md" />
|
||||
|
||||
<div className="nav-loader-progress absolute inset-x-0 top-0 h-1 overflow-hidden">
|
||||
<div className="nav-loader-progress-bar h-full w-full" />
|
||||
</div>
|
||||
|
||||
<div className="nav-loader-orbit relative flex h-28 w-28 items-center justify-center">
|
||||
<span className="nav-loader-ring nav-loader-ring-1 absolute inset-0 rounded-full border-2 border-transparent" />
|
||||
<span className="nav-loader-ring nav-loader-ring-2 absolute inset-2 rounded-full border-2 border-transparent" />
|
||||
<span className="nav-loader-ring nav-loader-ring-3 absolute inset-4 rounded-full border-2 border-transparent" />
|
||||
<span className="nav-loader-dot nav-loader-dot-1 absolute left-1/2 top-0 h-2.5 w-2.5 -translate-x-1/2 rounded-full bg-emerald-400 shadow-[0_0_12px_rgba(52,211,153,0.9)]" />
|
||||
<span className="nav-loader-dot nav-loader-dot-2 absolute bottom-1 right-2 h-2 w-2 rounded-full bg-cyan-400 shadow-[0_0_10px_rgba(34,211,238,0.85)]" />
|
||||
<span className="nav-loader-dot nav-loader-dot-3 absolute bottom-3 left-2 h-1.5 w-1.5 rounded-full bg-[#ff7a45] shadow-[0_0_8px_rgba(255,122,69,0.85)]" />
|
||||
<span className="nav-loader-core absolute h-10 w-10 rounded-full bg-gradient-to-br from-emerald-400 via-teal-500 to-cyan-500 shadow-[0_0_40px_rgba(20,184,166,0.45)]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
app/components/NavigationLoadingProvider.tsx
Normal file
95
app/components/NavigationLoadingProvider.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
"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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user