F(frontend): add route loading feedback
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-20 11:47:27 +03:30
parent f2d5b92b22
commit 5711961b9b
12 changed files with 312 additions and 6 deletions

View 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");
}

View File

@@ -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;