Skip to content

Timeout modal

The timeout modal allows activations to reset to the attract screen if they are left idle for a certain amount of time. The modal will display a countdown timer and a button to restart the activation or continue.

Install packages

bash
npm i @radix-ui/react-dialog

Add tailwind styles, to the extend key

javascript
keyframes: {
  overlayShow: {
    from: { opacity: "0" },
    to: { opacity: "1" },
  },
  contentShow: {
    from: { opacity: "0", transform: "translate(0, -20px) scale(0.96)" },
    to: { opacity: "1", transform: "translate(0, 0) scale(1)" },
  },
},
animation: {
  overlayShow: "overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)",
  contentShow: "contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)",
},

Add your component

typescript
"use client";
import { usePathname, useRouter } from "next/navigation";
import * as Dialog from "@radix-ui/react-dialog";
import { useEffect, useState } from "react";
import Link from "next/link";
interface ITimeoutModalProps {}

const useIdleBodyInteraction = (
  onIdle: () => void,
  timeout: number = process.env.NODE_ENV !== "production" ? 60000 : 60000
): void => {
  useEffect(() => {
    let timer: ReturnType<typeof setTimeout>;

    const resetTimer = (): void => {
      clearTimeout(timer);
      timer = setTimeout(onIdle, timeout);
    };

    const events: string[] = ["click", "mousemove", "scroll", "touchstart"];
    events.forEach((event) => {
      window.addEventListener(event, resetTimer);
    });

    // Set the initial timer
    timer = setTimeout(onIdle, timeout);

    return () => {
      clearTimeout(timer);
      events.forEach((event) => {
        window.removeEventListener(event, resetTimer);
      });
    };
  }, [onIdle, timeout]);
};

const COUNTDOWN_TIME = Number(process?.env?.NEXT_PUBLIC_REDIRECT_TIMEOUT) || 10;
const useCountDown = (isOpen: boolean, onTimeout?: () => void) => {
  const [number, setNumber] = useState(COUNTDOWN_TIME);

  useEffect(() => {
    let interval: NodeJS.Timeout;
    if (isOpen) {
      interval = setInterval(() => {
        setNumber(number - 1);
        if (number - 1 === 0) {
          if (onTimeout) {
            onTimeout();
          }
        }
      }, 1000);
    }
    return () => {
      clearInterval(interval);
    };
  }, [number, isOpen, onTimeout]);

  useEffect(() => {
    if (isOpen === false) {
      setNumber(COUNTDOWN_TIME);
    }
  }, [isOpen]);

  return number;
};

const TimeoutModal = ({}: ITimeoutModalProps) => {
  const route = useRouter();
  const [open, setOpen] = useState(false);
  const pathname = usePathname();
  const handleIdle = (): void => {
    if (pathname === "/admin") return;
    if (pathname === "/") return;
    setOpen(true);
  };
  useIdleBodyInteraction(handleIdle);
  const number = useCountDown(open, () => {
    setOpen(false);
    route.push("/");
  });
  return (
    <Dialog.Root open={open}>
      <Dialog.Portal>
        <Dialog.Overlay className="data-[state=open]:animate-overlayShow fixed inset-0" />
        <Dialog.Content className="z-20 text-black data-[state=open]:animate-contentShow fixed top-0 left-0 w-screen h-screen flex flex-col items-center justify-center">
          <Dialog.Description className="text-center">
            {number}
          </Dialog.Description>
          <div className="mt-100 flex justify-end space-x-50">
            <Link href="/" onClick={() => setOpen(false)}>
              Restart
            </Link>
            <button onClick={() => setOpen(false)}>Continue</button>
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
};

export default TimeoutModal;