feat(demo): add Demo Guide (8-step guided tour) and scenario overview

Adds a compact "Probeer een demonstratiescenario" page listing the 5 named
scenarios with live readiness from the manifest, plus a Demo Guide side
panel (bottom sheet on mobile) that walks an Operations Manager through
all 8 steps with per-step context, live-resolved routes, sessionStorage
progress, and a "Demo opnieuw voorbereiden" restart. Login's guided-demo
CTA now actually opens the guide. Fixes a real mobile topbar overflow the
new guide trigger introduced.
This commit is contained in:
NuklearRabbit
2026-08-03 14:15:15 +02:00
parent c63903cc94
commit 9fff84dc68
11 changed files with 647 additions and 29 deletions
+134
View File
@@ -0,0 +1,134 @@
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { api, ApiError } from "../api/client";
import { useAuth } from "../context/AuthContext";
import { useDemoGuide } from "../context/DemoGuideContext";
import { useDemoManifest } from "../context/DemoManifestContext";
import { DEMO_GUIDE_STEPS } from "../data/demoGuideSteps";
import { Icon } from "./Icons";
export function DemoGuideTrigger() {
const { user } = useAuth();
const { open, toggleGuide, currentIndex, completed, totalSteps } = useDemoGuide();
if (user?.role !== "operations_manager") return null;
return (
<button
type="button"
className="demo-guide-trigger"
aria-expanded={open}
aria-haspopup="dialog"
onClick={toggleGuide}
>
<Icon name="spark" />
<span>Demo-gids</span>
<span className="demo-guide-progress-pill">{completed.size}/{totalSteps}</span>
<span className="visually-hidden">, huidige stap {currentIndex + 1}</span>
</button>
);
}
export function DemoGuide() {
const navigate = useNavigate();
const { logout } = useAuth();
const { manifest, refresh } = useDemoManifest();
const {
open,
closeGuide,
currentIndex,
completed,
totalSteps,
goToStep,
completeAndAdvance,
restart,
} = useDemoGuide();
const [resetting, setResetting] = useState(false);
const [resetError, setResetError] = useState<string | null>(null);
if (!open) return null;
const step = DEMO_GUIDE_STEPS[currentIndex];
const isLastStep = currentIndex === totalSteps - 1;
function goToStepRoute() {
navigate(step.route(manifest));
}
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 : "De demo kon niet hersteld worden.");
} finally {
setResetting(false);
}
}
return (
<aside className="demo-guide-panel" role="dialog" aria-label="Gegidste demo">
<header className="demo-guide-header">
<div>
<p className="demo-guide-kicker">Gegidste demo · stap {currentIndex + 1} van {totalSteps}</p>
<h2>{step.title}</h2>
</div>
<button type="button" className="icon-button" onClick={closeGuide} aria-label="Sluiten">
<Icon name="x" />
</button>
</header>
<div className="demo-guide-progress-bar" aria-hidden="true">
{DEMO_GUIDE_STEPS.map((s, index) => (
<span
key={s.id}
className={
index === currentIndex ? "is-current" : completed.has(s.id) ? "is-done" : ""
}
/>
))}
</div>
<div className="demo-guide-body">
<p><strong>Wat je zal zien</strong><br />{step.whatYouWillSee}</p>
<p><strong>Waarom dit relevant is</strong><br />{step.whyItMatters}</p>
<p><strong>Aan de slag</strong><br />{step.startAction}</p>
<p><strong>Verwacht resultaat</strong><br />{step.expectedOutcome}</p>
</div>
<nav className="demo-guide-steps" aria-label="Alle stappen">
{DEMO_GUIDE_STEPS.map((s, index) => (
<button
key={s.id}
type="button"
className={index === currentIndex ? "is-current" : ""}
onClick={() => goToStep(index)}
>
{completed.has(s.id) && <Icon name="check" />}
{s.title}
</button>
))}
</nav>
{resetError && <p className="error" role="alert">{resetError}</p>}
<footer className="demo-guide-footer">
<button type="button" className="button button-secondary" onClick={goToStepRoute}>
Ga naar deze stap
</button>
<button type="button" className="button button-primary" onClick={completeAndAdvance} disabled={isLastStep}>
Volgende
</button>
<button type="button" className="demo-guide-restart" onClick={handleRestartDemo} disabled={resetting}>
{resetting ? "Bezig met herstellen…" : "Demo opnieuw voorbereiden"}
</button>
</footer>
</aside>
);
}