export const apiPrefix = import.meta.env.VITE_API_PREFIX || ""; export const apiBase = (import.meta.env.VITE_API_URL || "") + apiPrefix; /** Go JSON encodes nil slices as null — normalize before .map() */ export function itemsOrEmpty(items: T[] | null | undefined): T[] { return Array.isArray(items) ? items : []; } export function assetUrl(path: string | undefined | null) { if (!path) return ""; if (path.startsWith("http")) return path; return `${apiBase}${path}`; } type JsonCacheEntry = { cachedAt: number; value: T; }; const jsonMemoryCache = new Map>(); const jsonStoragePrefix = "ark-json-cache:v1:"; function jsonCacheKey(path: string): string { return `${apiBase}${path}`; } export function readJSONCache(path: string): T | null { const key = jsonCacheKey(path); const memoryEntry = jsonMemoryCache.get(key) as JsonCacheEntry | undefined; if (memoryEntry) return memoryEntry.value; if (typeof window === "undefined") return null; try { const raw = window.localStorage.getItem(`${jsonStoragePrefix}${key}`); if (!raw) return null; const entry = JSON.parse(raw) as JsonCacheEntry; jsonMemoryCache.set(key, entry as JsonCacheEntry); return entry.value; } catch { return null; } } function writeJSONCache(path: string, value: T): void { const key = jsonCacheKey(path); const entry: JsonCacheEntry = { cachedAt: Date.now(), value }; jsonMemoryCache.set(key, entry as JsonCacheEntry); if (typeof window === "undefined") return; try { window.localStorage.setItem( `${jsonStoragePrefix}${key}`, JSON.stringify(entry), ); } catch { // Ignore storage quota / privacy-mode failures; memory cache still works. } } export async function getJSON(path: string): Promise { const res = await fetch(`${apiBase}${path}`); if (!res.ok) throw new Error(await res.text()); const data = (await res.json()) as T; writeJSONCache(path, data); return data; } export async function getJSONAuth(path: string, token: string): Promise { const res = await fetch(`${apiBase}${path}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(await res.text()); return res.json() as Promise; } export async function postJSON( path: string, body: unknown, token?: string, ): Promise { const headers: Record = { "Content-Type": "application/json", }; if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(`${apiBase}${path}`, { method: "POST", headers, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await res.text()); return res.json() as Promise; } export async function postNoBody(path: string): Promise { const res = await fetch(`${apiBase}${path}`, { method: "POST" }); if (!res.ok) throw new Error(await res.text()); } export async function putJSON( path: string, body: unknown, token: string, ): Promise { const res = await fetch(`${apiBase}${path}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await res.text()); return res.json() as Promise; } export async function del(path: string, token: string) { const res = await fetch(`${apiBase}${path}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(await res.text()); } export async function uploadFile( file: File, token: string, ): Promise<{ url: string }> { const fd = new FormData(); fd.append("file", file); const res = await fetch(`${apiBase}/api/admin/upload`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: fd, }); if (!res.ok) throw new Error(await res.text()); return res.json(); } export type Category = { id: number; slug: string; name: string; description?: string; iconKey: string; sortOrder: number; }; export type Resource = { id: string; title: string; description?: string; type: string; language: string; categoryId: number; categorySlug: string; categoryName: string; coverImage?: string; fileUrl?: string; previewUrl?: string; externalUrl?: string; bodyText?: string; badgeLabel?: string; isDownloadable: boolean; isRecommended: boolean; publishedAt?: string; updatedAt: string; tags?: string[]; }; export type AdminResource = Resource & { isPublic: boolean; sortOrder: number; status: string; publishedAt?: string; viewCount?: number; downloadCount?: number; };