services/screensaver.jsxjavascript
/*
  Combines idle detection (useIdle) with the configured timeout/fade values
  (useConfig) into a single ScreensaverContext. Waking from idle also resets
  the kiosk session: the app re-navigates to the same route (pathname+search,
  not back to "/") while the screensaver is still fading out, so the next
  visitor starts that page fresh instead of inheriting the previous visitor's
  open menus and selections - re-navigating remounts the page component,
  which is what clears that per-page state, no explicit reset needed.
  Previously this went back to the app-selection screen ("/") instead; that
  was changed to stay on the current page/app so a visitor's search params
  and place in the exhibit survive a screensaver interruption (see
  "refactor: Refine navigation flow and screensaver exit behavior"). The
  reset deliberately happens on wake-up rather than when going idle: the
  screensaver video is defined by the current route's content file (see
  Screensaver.jsx), and navigating away at idle start would unload it
  mid-show.

  Must be mounted inside the Router (see src/HashRouter.jsx): useConfig (via
  useSearchParams) requires router context, and the session reset needs
  useNavigate/useLocation. Same reason ContentProvider now lives in
  HashRouter.jsx instead of App.jsx - TabProvider stays in App.jsx since it
  doesn't use useConfig.
*/

// allow export of contexts and hooks - shouldn't be flagged as non-component
/* eslint react-refresh/only-export-components: 0 */

import { createContext, useContext, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { useNavigate, useLocation } from "react-router";
import { useConfig } from "./config/config";
import { useIdle } from "../hooks/idle";

const ScreensaverContext = createContext();

/**
 * Tracks idle state and exposes it via ScreensaverContext.
 */
export const ScreensaverProvider = ({ children }) => {
  const { screensaverTimeout, screensaverFadeDuration } = useConfig();
  // screensaverTimeout is in seconds (see config.jsx), useIdle expects ms.
  // console.log("dont forget to return timeout to normal", screensaverTimeout);
  const { isIdle, trigger, forceIdle } = useIdle(screensaverTimeout * 1000);
  const navigate = useNavigate();
  const { pathname, search } = useLocation();

  // session reset on the idle -> active transition only (not on mount, not
  // while idle): happens behind the still-opaque fading screensaver, no view
  // transition needed; keeps the kiosk config params (config-schema.js) alive
  const wasIdleRef = useRef(false);
  useEffect(() => {
    if (wasIdleRef.current && !isIdle && pathname !== "/") navigate(`${pathname}${search}`);
    wasIdleRef.current = isIdle;
  }, [isIdle, pathname, search, navigate]);

  return (
    <ScreensaverContext.Provider
      value={{ isIdle, trigger, screensaverFadeDuration, showNow: forceIdle }}>
      {children}
    </ScreensaverContext.Provider>
  );
};

ScreensaverProvider.propTypes = {
  children: PropTypes.node.isRequired,
};

/**
 * React hook to access { isIdle, trigger, screensaverFadeDuration, showNow }.
 * Must be used within a `<ScreensaverProvider>`.
 */
export const useScreensaver = () => {
  return useContext(ScreensaverContext);
};