F(frontend): add route loading feedback
This commit is contained in:
35
src/lib/navigation-progress.ts
Normal file
35
src/lib/navigation-progress.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
type ProgressEvent = "start" | "done";
|
||||
type Listener = (event: ProgressEvent) => void;
|
||||
|
||||
const listeners = new Set<Listener>();
|
||||
let active = false;
|
||||
|
||||
function emit(event: ProgressEvent) {
|
||||
listeners.forEach((listener) => listener(event));
|
||||
}
|
||||
|
||||
export function subscribeNavigationProgress(listener: Listener) {
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
export function startNavigationProgress() {
|
||||
if (active) {
|
||||
return;
|
||||
}
|
||||
|
||||
active = true;
|
||||
emit("start");
|
||||
}
|
||||
|
||||
export function completeNavigationProgress() {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
active = false;
|
||||
emit("done");
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
usePathname,
|
||||
useRouter,
|
||||
} from "next/navigation";
|
||||
import {
|
||||
completeNavigationProgress,
|
||||
startNavigationProgress,
|
||||
} from "@/lib/navigation-progress";
|
||||
|
||||
type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
to: string;
|
||||
@@ -26,9 +30,53 @@ type NavLinkProps = Omit<LinkProps, "className"> & {
|
||||
className?: string | ((state: { isActive: boolean }) => string);
|
||||
};
|
||||
|
||||
export function Link({ to, replace, prefetch, children, ...props }: LinkProps) {
|
||||
function isPlainLeftClick(event: React.MouseEvent<HTMLAnchorElement>) {
|
||||
return (
|
||||
<NextLink href={to} replace={replace} prefetch={prefetch} {...props}>
|
||||
event.button === 0 &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
);
|
||||
}
|
||||
|
||||
function shouldTrackNavigation(to: string) {
|
||||
if (typeof window === "undefined") {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const current = new URL(window.location.href);
|
||||
const target = new URL(to, window.location.href);
|
||||
return current.origin === target.origin && current.pathname !== target.pathname;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function Link({ to, replace, prefetch, children, onClick, target, ...props }: LinkProps) {
|
||||
return (
|
||||
<NextLink
|
||||
href={to}
|
||||
replace={replace}
|
||||
prefetch={prefetch}
|
||||
target={target}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
target === "_blank" ||
|
||||
!isPlainLeftClick(event) ||
|
||||
!shouldTrackNavigation(to)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
startNavigationProgress();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NextLink>
|
||||
);
|
||||
@@ -69,6 +117,7 @@ export function useNavigate(): NavigateFunction {
|
||||
return React.useCallback(
|
||||
(to: string | number, options?: { replace?: boolean }) => {
|
||||
if (typeof to === "number") {
|
||||
startNavigationProgress();
|
||||
if (to === -1) {
|
||||
router.back();
|
||||
return;
|
||||
@@ -79,6 +128,12 @@ export function useNavigate(): NavigateFunction {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldTrackNavigation(to)) {
|
||||
startNavigationProgress();
|
||||
} else {
|
||||
completeNavigationProgress();
|
||||
}
|
||||
|
||||
if (options?.replace) {
|
||||
router.replace(to);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user