feat: add shared UI nav + fetch hook + table/select/label/textarea primitives
Basic infra for a functional (no-polish) UI pass over existing entity APIs.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
// Small shared data-fetching hook used by every entity page — both for its
|
||||
// own list table (GET /api/<entity>) and for populating FK <Select>
|
||||
// dropdowns from other entities' GET endpoints (e.g. an AssetItem page
|
||||
// fetching /api/employees to build an "assigned to" picker). Deliberately
|
||||
// minimal: no caching/dedupe/revalidation library, just fetch + state,
|
||||
// matching the "no polish, just clickable manual testing" scope of this
|
||||
// task (see CLAUDE.md's "basic functional UI" batch note).
|
||||
//
|
||||
// Every API route in this app returns a JSON array on success (200) or
|
||||
// `{error: string}` on failure (see e.g. src/app/api/employees/route.ts's
|
||||
// GET/POST) — this hook follows that same shape convention.
|
||||
type UseApiListResult<T> = {
|
||||
data: T[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
const GENERIC_ERROR_MESSAGE = "Request failed";
|
||||
|
||||
export function useApiList<T>(endpoint: string): UseApiListResult<T> {
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refetchCount, setRefetchCount] = useState(0);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setRefetchCount((count) => count + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function load(): Promise<void> {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint);
|
||||
let body: unknown;
|
||||
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
body !== null &&
|
||||
typeof body === "object" &&
|
||||
"error" in body &&
|
||||
typeof (body as { error: unknown }).error === "string"
|
||||
? (body as { error: string }).error
|
||||
: GENERIC_ERROR_MESSAGE;
|
||||
|
||||
if (!cancelled) {
|
||||
setData([]);
|
||||
setError(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setData(Array.isArray(body) ? (body as T[]) : []);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setData([]);
|
||||
setError(GENERIC_ERROR_MESSAGE);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [endpoint, refetchCount]);
|
||||
|
||||
return { data, loading, error, refetch };
|
||||
}
|
||||
Reference in New Issue
Block a user