import { useNavigate, useLocation } from "react-router-dom"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { api, ApiError } from "../api/client"; import { useAuth } from "../context/AuthContext"; import { useDemoGuide } from "../context/DemoGuideContext"; import { useDemoManifest } from "../context/DemoManifestContext"; import { useViewportTier } from "../hooks/useViewportTier"; import { DEMO_GUIDE_STEPS } from "../data/demoGuideSteps"; import { Icon } from "./Icons"; export function DemoGuideTrigger() { const { t } = useTranslation("demo"); const { user } = useAuth(); const { open, toggleGuide, currentIndex, completed, totalSteps } = useDemoGuide(); if (user?.role !== "operations_manager") return null; return ( ); } function highlightTarget(selector: string | undefined) { if (!selector) return; const el = document.querySelector(selector); if (!el) return; el.scrollIntoView({ behavior: "smooth", block: "center" }); const previousTabIndex = el.getAttribute("tabindex"); if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "-1"); el.focus({ preventScroll: true }); el.classList.add("demo-guide-highlight"); window.setTimeout(() => { el.classList.remove("demo-guide-highlight"); if (previousTabIndex === null) el.removeAttribute("tabindex"); }, 2200); } export function DemoGuide() { const { t } = useTranslation("demo"); const navigate = useNavigate(); const location = useLocation(); const { logout } = useAuth(); const { manifest, refresh } = useDemoManifest(); const tier = useViewportTier(); const { open, closeGuide, currentIndex, completed, totalSteps, goToStep, completeAndAdvance, restart, collapsedToChip, setCollapsedToChip, } = useDemoGuide(); const [resetting, setResetting] = useState(false); const [resetError, setResetError] = useState(null); const [mobileSheetState, setMobileSheetState] = useState<"collapsed" | "half" | "full">("half"); const pendingTarget = useRef(null); const step = DEMO_GUIDE_STEPS[currentIndex]; const isLastStep = currentIndex === totalSteps - 1; useEffect(() => { if (!open) return; function handleKeydown(event: KeyboardEvent) { if (event.key !== "Escape") return; if (tier === "standard" && !collapsedToChip) { setCollapsedToChip(true); } else if (tier === "mobile" && mobileSheetState !== "collapsed") { setMobileSheetState("collapsed"); } else { closeGuide(); } } document.addEventListener("keydown", handleKeydown); return () => document.removeEventListener("keydown", handleKeydown); }, [open, tier, collapsedToChip, mobileSheetState, closeGuide]); useEffect(() => { if (!pendingTarget.current) return; const target = pendingTarget.current; pendingTarget.current = null; const raf = requestAnimationFrame(() => highlightTarget(target)); return () => cancelAnimationFrame(raf); }, [location.pathname]); if (!open) return null; function goToStepRoute() { pendingTarget.current = step.target ?? null; navigate(step.route(manifest)); if (tier === "standard") setCollapsedToChip(true); if (tier === "mobile") setMobileSheetState("collapsed"); } async function handleRestartDemo() { setResetError(null); setResetting(true); try { await api.post("/api/v1/demo/reset"); restart(); refresh(); closeGuide(); await logout(); navigate("/login"); } catch (err) { setResetError(err instanceof ApiError ? err.message : t("guide.restartFailed")); } finally { setResetting(false); } } if (tier === "standard" && collapsedToChip) { return (
); } const panelClassName = [ "demo-guide-panel", tier === "wide" ? "is-wide" : "", tier === "mobile" ? `is-mobile sheet-${mobileSheetState}` : "", ] .filter(Boolean) .join(" "); return ( ); }