180 lines
4.6 KiB
TypeScript
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;
|
|
};
|