56 lines
1.7 KiB
TypeScript
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;
|
|
}
|