Files
Arkie-Library-Frontend/src/api.ts
2026-05-28 23:09:18 +08:00

180 lines
4.6 KiB
TypeScript

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<T>(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<T> = {
cachedAt: number;
value: T;
};
const jsonMemoryCache = new Map<string, JsonCacheEntry<unknown>>();
const jsonStoragePrefix = "ark-json-cache:v1:";
function jsonCacheKey(path: string): string {
return `${apiBase}${path}`;
}
export function readJSONCache<T>(path: string): T | null {
const key = jsonCacheKey(path);
const memoryEntry = jsonMemoryCache.get(key) as JsonCacheEntry<T> | 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<T>;
jsonMemoryCache.set(key, entry as JsonCacheEntry<unknown>);
return entry.value;
} catch {
return null;
}
}
function writeJSONCache<T>(path: string, value: T): void {
const key = jsonCacheKey(path);
const entry: JsonCacheEntry<T> = { cachedAt: Date.now(), value };
jsonMemoryCache.set(key, entry as JsonCacheEntry<unknown>);
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<T>(path: string): Promise<T> {
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<T>(path: string, token: string): Promise<T> {
const res = await fetch(`${apiBase}${path}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(await res.text());
return res.json() as Promise<T>;
}
export async function postJSON<T>(
path: string,
body: unknown,
token?: string,
): Promise<T> {
const headers: Record<string, string> = {
"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<T>;
}
export async function postNoBody(path: string): Promise<void> {
const res = await fetch(`${apiBase}${path}`, { method: "POST" });
if (!res.ok) throw new Error(await res.text());
}
export async function putJSON<T>(
path: string,
body: unknown,
token: string,
): Promise<T> {
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<T>;
}
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;
};