130 lines
3.3 KiB
TypeScript
130 lines
3.3 KiB
TypeScript
|
|
export const apiBase = import.meta.env.VITE_API_URL || "";
|
||
|
|
|
||
|
|
/** 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}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function getJSON<T>(path: string): Promise<T> {
|
||
|
|
const res = await fetch(`${apiBase}${path}`);
|
||
|
|
if (!res.ok) throw new Error(await res.text());
|
||
|
|
return res.json() as Promise<T>;
|
||
|
|
}
|
||
|
|
|
||
|
|
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>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Best-effort favorite counter sync (anonymous; matches localStorage favorite). */
|
||
|
|
export function postFavoriteDelta(id: string, add: boolean) {
|
||
|
|
return postJSON(`/api/resources/${id}/favorite`, { add }).catch(() => {});
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
};
|