lead-to-policy/src/api/provider.tsx
Bhanu Prakash Sai Potteri 66d4c91f32 Initial import of lead-to-policy (dev env)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:28:12 +05:30

128 lines
3.6 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { ReactNode } from 'react';
import { ZinoClient } from './client';
import type { User } from './types';
interface ZinoContextValue {
client: ZinoClient;
user: User | null;
login: (email: string, password: string, orgId?: string) => Promise<void>;
logout: () => void;
}
const ZinoContext = createContext<ZinoContextValue | null>(null);
export function ZinoProvider({ baseUrl, children }: { baseUrl: string; children: ReactNode }) {
// One client per provider instance. onAuthError clears the user so routes
// bounce back to /login.
const clientRef = useRef<ZinoClient>();
if (!clientRef.current) {
clientRef.current = new ZinoClient(baseUrl);
}
const client = clientRef.current;
// Restore the session synchronously from the persisted JWT so a hard
// navigation to a protected route doesn't flash through /login.
const [user, setUser] = useState<User | null>(() => client.currentUser());
useEffect(() => {
client.setAuthErrorHandler(() => setUser(null));
}, [client]);
const value = useMemo<ZinoContextValue>(
() => ({
client,
user,
login: async (email, password, orgId) => {
const res = await client.login(email, password, orgId);
setUser(res.user ?? client.currentUser());
},
logout: () => {
client.logout();
setUser(null);
},
}),
[client, user],
);
return <ZinoContext.Provider value={value}>{children}</ZinoContext.Provider>;
}
export function useZino(): ZinoContextValue {
const ctx = useContext(ZinoContext);
if (!ctx) throw new Error('useZino must be used within <ZinoProvider>');
return ctx;
}
// --- minimal data-fetching hook (no extra deps) ---
export interface QueryState<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
/**
* Fire `fn` whenever a dependency in `deps` changes. Tracks loading/error and
* ignores results from a stale call. `enabled=false` short-circuits.
* `refetchMs` (opt-in) polls on that interval; the interval is cleared on
* unmount, dep change, or when disabled.
*/
export function useQuery<T>(
fn: () => Promise<T>,
deps: unknown[],
enabled = true,
refetchMs?: number,
): QueryState<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(enabled);
const [error, setError] = useState<string | null>(null);
const [tick, setTick] = useState(0);
const refetch = useCallback(() => setTick((t) => t + 1), []);
useEffect(() => {
if (!enabled) {
setLoading(false);
return;
}
let live = true;
setLoading(true);
setError(null);
fn()
.then((d) => {
if (live) setData(d);
})
.catch((e: unknown) => {
if (live) setError(e instanceof Error ? e.message : ((e as { message?: string })?.message ?? 'Request failed'));
})
.finally(() => {
if (live) setLoading(false);
});
return () => {
live = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, tick, enabled]);
// Opt-in background polling. Kept separate from the fetch effect so the
// interval doesn't restart on every refetch; ticking triggers it instead.
useEffect(() => {
if (!enabled || !refetchMs || refetchMs <= 0) return;
const id = setInterval(() => setTick((t) => t + 1), refetchMs);
return () => clearInterval(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, enabled, refetchMs]);
return { data, loading, error, refetch };
}