refactor, security updates, cv extraction upgrades

This commit is contained in:
2026-04-11 01:34:32 +02:00
parent 806b200ac5
commit 27fd70a2d7
59 changed files with 6817 additions and 1561 deletions
+30 -19
View File
@@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import { api } from "../api";
import { Company } from "../types";
import { useViewResource, ViewResourceError } from "./useViewResource";
let cachedCompanies: Company[] | null = null;
let inflight: Promise<Company[]> | null = null;
@@ -10,7 +12,7 @@ async function fetchCompanies(): Promise<Company[]> {
if (inflight) return inflight;
inflight = api
.get<Company[]>("/companies")
.get<Company[]>('/companies')
.then((r) => {
cachedCompanies = r.data;
return r.data;
@@ -26,25 +28,34 @@ export function invalidateCompaniesCache() {
cachedCompanies = null;
}
export function useCompanies() {
const [companies, setCompanies] = useState<Company[]>(cachedCompanies ?? []);
const [loading, setLoading] = useState(!cachedCompanies);
export function useCompanies(): {
companies: Company[];
loading: boolean;
refreshing: boolean;
error: ViewResourceError | null;
reload: () => Promise<void>;
} {
const [cacheBust, setCacheBust] = useState(0);
const resource = useViewResource(fetchCompanies, {
initialData: cachedCompanies ?? [],
errorMessage: 'Unable to load companies right now.',
deps: [cacheBust],
});
useEffect(() => {
let mounted = true;
setLoading(!cachedCompanies);
fetchCompanies()
.then((c) => {
if (mounted) setCompanies(c);
})
.finally(() => {
if (mounted) setLoading(false);
});
return () => {
mounted = false;
};
}, []);
if (!resource.error) {
cachedCompanies = resource.data;
}
}, [resource.data, resource.error]);
return { companies, loading };
return {
companies: resource.data,
loading: resource.loading,
refreshing: resource.refreshing,
error: resource.error,
reload: async () => {
invalidateCompaniesCache();
setCacheBust((value) => value + 1);
},
};
}
@@ -0,0 +1,99 @@
import { DependencyList, Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import { getApiErrorMessage } from "../api";
export type ViewResourceErrorKind = "unauthorized" | "unavailable" | "error";
export type ViewResourceError = {
kind: ViewResourceErrorKind;
message: string;
retryable: boolean;
status?: number;
};
export type ViewResourceState<T> = {
data: T;
loading: boolean;
refreshing: boolean;
error: ViewResourceError | null;
hasLoaded: boolean;
reload: () => Promise<void>;
setData: Dispatch<SetStateAction<T>>;
};
function normalizeError(error: any, fallback: string): ViewResourceError {
const status = error?.response?.status as number | undefined;
if (status === 401 || status === 403) {
return {
kind: "unauthorized",
message: getApiErrorMessage(error, fallback),
retryable: false,
status,
};
}
if (!status || status >= 500) {
return {
kind: "unavailable",
message: getApiErrorMessage(error, fallback),
retryable: true,
status,
};
}
return {
kind: "error",
message: getApiErrorMessage(error, fallback),
retryable: true,
status,
};
}
export function useViewResource<T>(
load: () => Promise<T>,
options: {
initialData: T;
errorMessage: string;
deps?: DependencyList;
enabled?: boolean;
},
): ViewResourceState<T> {
const { initialData, errorMessage, deps = [], enabled = true } = options;
const [data, setData] = useState<T>(initialData);
const [loading, setLoading] = useState(enabled);
const [refreshing, setRefreshing] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);
const [error, setError] = useState<ViewResourceError | null>(null);
const reload = useCallback(async () => {
if (!enabled) return;
setLoading((current) => !hasLoaded && current);
setRefreshing(hasLoaded);
try {
const next = await load();
setData(next);
setError(null);
setHasLoaded(true);
} catch (err: any) {
setError(normalizeError(err, errorMessage));
setHasLoaded(true);
} finally {
setLoading(false);
setRefreshing(false);
}
}, [enabled, errorMessage, hasLoaded, load]);
useEffect(() => {
if (!enabled) {
setLoading(false);
return;
}
setLoading(!hasLoaded);
void reload();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled, reload, ...deps]);
return useMemo(() => ({ data, loading, refreshing, error, hasLoaded, reload, setData }), [data, error, hasLoaded, loading, refreshing, reload]);
}