89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import { Fragment, useEffect, useState } from "react";
|
|
import { useNavigation } from "react-router";
|
|
import { useSpinDelay } from "spin-delay";
|
|
import { Transition } from "@headlessui/react";
|
|
import { useTranslation } from "react-i18next";
|
|
// Subframe migration (strangler shim): only the spinner glyph swaps to the Subframe
|
|
// design-system Loader. The floating panel, spin-delay gating, navigation-state word
|
|
// cycling, transitions and path display are all preserved unchanged.
|
|
import { Loader } from "~/ui/components/Loader";
|
|
|
|
let firstRender = false;
|
|
|
|
interface Props {
|
|
loading?: boolean;
|
|
}
|
|
export default function FloatingLoader({ loading }: Props) {
|
|
const { t } = useTranslation();
|
|
const navigation = useNavigation();
|
|
const [, setWords] = useState<Array<string>>([]);
|
|
const [pendingPath, setPendingPath] = useState("");
|
|
const showLoader = useSpinDelay(Boolean(navigation.state !== "idle" || loading), {
|
|
delay: 400,
|
|
minDuration: 1000,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (navigation.location?.pathname === "/") return;
|
|
if (navigation.state === "idle") return;
|
|
if (navigation.state === "loading") setWords(LOADER_WORDS);
|
|
if (navigation.state === "submitting") setWords(ACTION_WORDS);
|
|
|
|
const interval = setInterval(() => {
|
|
setWords(([first, ...rest]) => [...rest, first] as Array<string>);
|
|
}, 2000);
|
|
|
|
return () => clearInterval(interval);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [pendingPath, navigation.state]);
|
|
|
|
useEffect(() => {
|
|
if (firstRender) return;
|
|
if (navigation.state === "idle") return;
|
|
setPendingPath(navigation.location.pathname);
|
|
}, [navigation]);
|
|
|
|
useEffect(() => {
|
|
firstRender = false;
|
|
}, []);
|
|
|
|
const action = navigation.state === "loading" ? t("shared.loading") : t("shared.processing");
|
|
|
|
return (
|
|
<div aria-live="assertive" className="pointer-events-none fixed inset-0 z-50 flex items-end px-4 py-6 sm:p-6">
|
|
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
|
|
{/* Notification panel, dynamically insert this into the live region when it needs to be displayed */}
|
|
<Transition
|
|
show={showLoader}
|
|
as={Fragment}
|
|
enter="transform ease-out duration-300 transition"
|
|
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4"
|
|
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
|
|
leave="transition ease-in duration-100"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
<div className="bg-background ring-opacity-5 pointer-events-auto w-64 max-w-sm overflow-hidden rounded-lg p-2 shadow-lg ring-1 ring-black">
|
|
<div className="space-y-2 pt-2 pb-2 text-center">
|
|
<div className="flex h-auto w-full items-center space-x-4 px-3 text-center">
|
|
<Loader size="large" className="shrink-0 text-left" />
|
|
<div className="text-muted-foreground flex flex-col truncate">
|
|
<div className="text-left">{action}...</div>
|
|
{navigation.location?.pathname && (
|
|
<div className="text-muted-foreground truncate text-left text-xs lowercase">
|
|
{t("shared.path")}: {navigation.location?.pathname}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const LOADER_WORDS = ["loading", "checking cdn", "checking cache", "fetching from db", "compiling mdx", "updating cache", "transfer"];
|
|
const ACTION_WORDS = ["packaging", "zapping", "validating", "processing", "calculating", "computing", "computering"];
|