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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user