- Replace hand-coded skeleton CSS/JSX with boneyard-js auto-generated bones - Remove skeleton.css and @keyframes shimmer from base.css - Add <Skeleton> wrappers with fixtures to all 25+ page components - Generate 20 bone captures via boneyard CLI (CDP auth-gated capture) - Refactor data fetching from useEffect+useState to TanStack Query - Extract query hooks into src/admin/lib/queries/ and apiAdapter - Add usePaginatedQuery hook replacing useApiCall/useListData - Fix parseFloat || 0 anti-pattern in OfferDetail and OffersTemplates inputs - Fix customer_id mandatory validation on offer creation - Fix leave-requests comma-separated status filter (Prisma enum in: []) - Add cross-entity cache invalidation for orders/offers/invoices/projects - Make rate limits configurable via env vars (RATE_LIMIT_MAX, RATE_LIMIT_REFRESH, etc.) - Add boneyard.config.json with routes and breakpoints Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
84 lines
2.0 KiB
TypeScript
84 lines
2.0 KiB
TypeScript
import apiFetch from "../utils/api";
|
|
|
|
/**
|
|
* Thin adapter that converts apiFetch responses into the shape TanStack Query expects.
|
|
* - Checks response.ok and result.success
|
|
* - Throws on errors so TanStack Query can handle retry/error states
|
|
* - Returns result.data directly (unwrapped from the API envelope)
|
|
*/
|
|
export async function jsonQuery<T>(
|
|
url: string,
|
|
options?: RequestInit,
|
|
): Promise<T> {
|
|
const response = await apiFetch(url, options);
|
|
|
|
if (response.status === 401) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
let result: { success: boolean; data?: unknown; error?: string };
|
|
try {
|
|
result = (await response.json()) as typeof result;
|
|
} catch {
|
|
throw new Error("Invalid JSON response");
|
|
}
|
|
|
|
if (!response.ok || !result.success) {
|
|
throw new Error(result.error || `Request failed (${response.status})`);
|
|
}
|
|
|
|
return result.data as T;
|
|
}
|
|
|
|
export interface PaginationMeta {
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
total_pages: number;
|
|
}
|
|
|
|
export interface PaginatedResult<T> {
|
|
data: T[];
|
|
pagination: PaginationMeta;
|
|
}
|
|
|
|
export async function paginatedJsonQuery<T>(
|
|
url: string,
|
|
options?: RequestInit,
|
|
): Promise<PaginatedResult<T>> {
|
|
const response = await apiFetch(url, options);
|
|
|
|
if (response.status === 401) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
let result: {
|
|
success: boolean;
|
|
data?: unknown;
|
|
error?: string;
|
|
pagination?: PaginationMeta;
|
|
};
|
|
try {
|
|
result = (await response.json()) as typeof result;
|
|
} catch {
|
|
throw new Error("Invalid JSON response");
|
|
}
|
|
|
|
if (!response.ok || !result.success) {
|
|
throw new Error(result.error || `Request failed (${response.status})`);
|
|
}
|
|
|
|
const items = Array.isArray(result.data)
|
|
? result.data
|
|
: ((result.data as { items?: T[] })?.items ?? []);
|
|
const pagination = result.pagination ??
|
|
(result.data as { pagination?: PaginationMeta })?.pagination ?? {
|
|
total: items.length,
|
|
page: 1,
|
|
per_page: items.length,
|
|
total_pages: 1,
|
|
};
|
|
|
|
return { data: items as T[], pagination };
|
|
}
|