Files
MobilityOps/frontend/src/context/DemoManifestContext.tsx
T
NuklearRabbit 81e3fd63bd
MobilityOps acceptance / backend (push) Failing after 19s
MobilityOps acceptance / frontend (push) Successful in 25s
MobilityOps acceptance / e2e (push) Skipped
M54: harden operations and demo resilience
2026-08-24 03:31:03 +02:00

56 lines
1.7 KiB
TypeScript

import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import { api } from "../api/client";
import type { DemoManifest } from "../api/types";
interface DemoManifestState {
manifest: DemoManifest | null;
loading: boolean;
error: boolean;
refresh: () => void;
}
const DemoManifestContext = createContext<DemoManifestState | undefined>(undefined);
export function DemoManifestProvider({ children }: { children: ReactNode }) {
const [manifest, setManifest] = useState<DemoManifest | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [version, setVersion] = useState(0);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(false);
// Public endpoint by design: the demo-entry screen needs this before any session
// exists, so it is never gated behind auth.
api
.get<DemoManifest>("/api/v1/demo/manifest", { signal: controller.signal })
.then((result) => {
if (!controller.signal.aborted) setManifest(result);
})
.catch(() => {
if (controller.signal.aborted) return;
setManifest(null);
setError(true);
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [version]);
return (
<DemoManifestContext.Provider
value={{ manifest, loading, error, refresh: () => setVersion((v) => v + 1) }}
>
{children}
</DemoManifestContext.Provider>
);
}
export function useDemoManifest(): DemoManifestState {
const ctx = useContext(DemoManifestContext);
if (!ctx) throw new Error("useDemoManifest must be used within DemoManifestProvider");
return ctx;
}