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
+106
View File
@@ -0,0 +1,106 @@
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
import { DEMO_GUIDE_STEPS } from "../data/demoGuideSteps";
const STORAGE_KEY = "mobilityops.demo-guide-progress";
interface StoredProgress {
currentIndex: number;
completed: string[];
}
function readProgress(): StoredProgress {
try {
const raw = sessionStorage.getItem(STORAGE_KEY);
if (!raw) return { currentIndex: 0, completed: [] };
const parsed = JSON.parse(raw) as StoredProgress;
return { currentIndex: parsed.currentIndex ?? 0, completed: parsed.completed ?? [] };
} catch {
return { currentIndex: 0, completed: [] };
}
}
function writeProgress(progress: StoredProgress) {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(progress));
}
interface DemoGuideState {
open: boolean;
currentIndex: number;
completed: Set<string>;
totalSteps: number;
openGuide: () => void;
closeGuide: () => void;
toggleGuide: () => void;
goToStep: (index: number) => void;
completeAndAdvance: () => void;
restart: () => void;
}
const DemoGuideContext = createContext<DemoGuideState | undefined>(undefined);
export function DemoGuideProvider({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const [completed, setCompleted] = useState<Set<string>>(new Set());
useEffect(() => {
const stored = readProgress();
setCurrentIndex(stored.currentIndex);
setCompleted(new Set(stored.completed));
}, []);
const persist = useCallback((index: number, done: Set<string>) => {
writeProgress({ currentIndex: index, completed: Array.from(done) });
}, []);
const goToStep = useCallback(
(index: number) => {
const clamped = Math.max(0, Math.min(index, DEMO_GUIDE_STEPS.length - 1));
setCurrentIndex(clamped);
persist(clamped, completed);
},
[completed, persist],
);
const completeAndAdvance = useCallback(() => {
setCompleted((prev) => {
const next = new Set(prev);
next.add(DEMO_GUIDE_STEPS[currentIndex].id);
const nextIndex = Math.min(currentIndex + 1, DEMO_GUIDE_STEPS.length - 1);
persist(nextIndex, next);
setCurrentIndex(nextIndex);
return next;
});
}, [currentIndex, persist]);
const restart = useCallback(() => {
setCurrentIndex(0);
setCompleted(new Set());
writeProgress({ currentIndex: 0, completed: [] });
}, []);
return (
<DemoGuideContext.Provider
value={{
open,
currentIndex,
completed,
totalSteps: DEMO_GUIDE_STEPS.length,
openGuide: () => setOpen(true),
closeGuide: () => setOpen(false),
toggleGuide: () => setOpen((v) => !v),
goToStep,
completeAndAdvance,
restart,
}}
>
{children}
</DemoGuideContext.Provider>
);
}
export function useDemoGuide(): DemoGuideState {
const ctx = useContext(DemoGuideContext);
if (!ctx) throw new Error("useDemoGuide must be used within DemoGuideProvider");
return ctx;
}