import type { ReactIsland } from "@ethicdevs/react-monolith";
import React, { useCallback, useEffect, useRef, useState } from "react";
import styled, { css, keyframes } from "styled-components";
interface InstantRouterIndicatorProps {}
export const ClientSideRouterEventPrefix = `app_router_`;
export const ClientSideRouterEvents = {
LOADING: `${ClientSideRouterEventPrefix}loading`,
LOADED: `${ClientSideRouterEventPrefix}loaded`,
LOAD_ERROR: `${ClientSideRouterEventPrefix}load_error`,
NAVIGATING: `${ClientSideRouterEventPrefix}navigating`,
NAVIGATED: `${ClientSideRouterEventPrefix}navigated`,
NAVIGATION_ERROR: `${ClientSideRouterEventPrefix}navigation_error`,
};
const InstantRouterIndicator: ReactIsland<InstantRouterIndicatorProps> = () => {
const [isInstantRouteIndicatorVisible, setInstantRouteIndicatorVisible] =
useState<boolean>(false);
const [isInstantRouteLoading, setInstantRouteLoading] =
useState<boolean>(false);
const [instantRouteLoadPercent, setInstantRouteLoadPercent] =
useState<number>(0);
const incrementProgressIntervalIdRef = useRef<null | NodeJS.Timer>(null);
const startIncrementProgress = useCallback(() => {
setTimeout(() => {
setInstantRouteLoadPercent(0);
}, 0);
setInstantRouteIndicatorVisible(true);
setInstantRouteLoading(true);
incrementProgressIntervalIdRef.current = setInterval(() => {
if (isInstantRouteLoading) {
setInstantRouteLoadPercent((prev) => prev + 10);
}
}, 500);
}, [
isInstantRouteLoading,
setInstantRouteLoading,
setInstantRouteLoadPercent,
setInstantRouteIndicatorVisible,
]);
const stopIncrementProgress = useCallback(() => {
if (incrementProgressIntervalIdRef.current != null) {
clearInterval(incrementProgressIntervalIdRef.current);
incrementProgressIntervalIdRef.current = null;
}
setInstantRouteIndicatorVisible(false);
setInstantRouteLoading(false);
setInstantRouteLoadPercent(0);
}, [
setInstantRouteLoadPercent,
setInstantRouteLoading,
setInstantRouteIndicatorVisible,
]);
const onClientSideRouterLoadStart = useCallback(() => {
setInstantRouteLoading(true);
}, [setInstantRouteLoading]);
const onClientSideRouterLoadComplete = useCallback(() => {
setInstantRouteLoading(false);
}, [setInstantRouteLoading]);
const onClientSideRouterLoadError = useCallback(() => {
setInstantRouteLoading(false);
}, [setInstantRouteLoading]);
const onClientSideRouterNavigationError = useCallback(() => {
setInstantRouteLoading(false);
}, [setInstantRouteLoading]);
useEffect(() => {
if (typeof document !== "undefined") {
document.addEventListener(
ClientSideRouterEvents.LOADING,
onClientSideRouterLoadStart
);
document.addEventListener(
ClientSideRouterEvents.NAVIGATED,
onClientSideRouterLoadComplete
);
document.addEventListener(
ClientSideRouterEvents.LOAD_ERROR,
onClientSideRouterLoadError
);
document.addEventListener(
ClientSideRouterEvents.NAVIGATION_ERROR,
onClientSideRouterNavigationError
);
return () => {
document.removeEventListener(
ClientSideRouterEvents.LOADING,
onClientSideRouterLoadStart
);
document.removeEventListener(
ClientSideRouterEvents.NAVIGATED,
onClientSideRouterLoadComplete
);
document.removeEventListener(
ClientSideRouterEvents.LOAD_ERROR,
onClientSideRouterLoadError
);
document.removeEventListener(
ClientSideRouterEvents.NAVIGATION_ERROR,
onClientSideRouterNavigationError
);
};
}
return () => undefined;
}, []);
useEffect(() => {
if (
isInstantRouteLoading &&
incrementProgressIntervalIdRef.current == null
) {
startIncrementProgress();
} else if (
isInstantRouteLoading === false &&
incrementProgressIntervalIdRef.current != null
) {
stopIncrementProgress();
}
}, [isInstantRouteLoading, startIncrementProgress, stopIncrementProgress]);
if (isInstantRouteIndicatorVisible === false) {
return null;
}
return (
<StyledInstantRouteProgressIndicator>
<StyledInstantRouteProgressBar
progressPercent={instantRouteLoadPercent}
/>
</StyledInstantRouteProgressIndicator>
);
};
const blinkAnimation = keyframes`
0% {
opacity: .67;
}
20% {
opacity: 1;
}
100% {
opacity: .67;
}
`;
const StyledInstantRouteProgressIndicator = styled.div`
display: flex;
height: 4px;
width: 100vw;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 25000;
background-color: transparent;
`;
const StyledInstantRouteProgressBar = styled.div<{ progressPercent?: number }>`
display: flex;
height: 4px;
width: 100%;
position: absolute;
top: 0;
left: 0;
background: #29d;
z-index: 26000;
animation: ${blinkAnimation} 500ms ease-in-out infinite;
transition: all 200ms ease 0s;
${({ progressPercent = 0 }) => css`
transform: translate3d(
-${100 - Math.max(0, Math.min(progressPercent, 100))}%,
0px,
0px
);
`};
`;
InstantRouterIndicator.displayName = "InstantRouterIndicator";
export default InstantRouterIndicator;