Files
anatomia/frontend/src/lib/api.ts
T

151 lines
4.2 KiB
TypeScript

export interface AnatomicalSystem {
id: string;
slug: string;
namePt: string;
nameEn: string;
colorHex: string;
order: number;
_count?: { structures: number };
}
export interface Structure {
id: string;
fmaId: string | null;
slug: string;
namePt: string;
nameEn: string;
synonyms: string[];
description: string | null;
regionPath: string[];
modelFile: string;
meshName: string;
systemId: string;
system: AnatomicalSystem;
}
export interface QuizCard {
id: string;
structureId: string;
easeFactor: number;
intervalDays: number;
repetitions: number;
dueAt: string;
}
export interface PageVisit {
id: string;
path: string;
system: string | null;
ip: string;
port: number | null;
country: string | null;
city: string | null;
userAgent: string | null;
createdAt: string;
}
export interface FeedbackEntry {
id: string;
message: string;
email: string | null;
rating: number | null;
page: string | null;
ip: string | null;
createdAt: string;
}
export interface AdminStats {
onlineNow: number;
totalVisits: number;
visitsLast24h: number;
visitsLast7d: number;
visitsBySystem: { system: string | null; count: number }[];
visitsByCountry: { country: string; count: number }[];
recentVisits: PageVisit[];
feedback: FeedbackEntry[];
}
const BASE = "/api";
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`GET ${url} failed: ${res.status}`);
return res.json() as Promise<T>;
}
async function postJson<T>(url: string, body: unknown, token?: string): Promise<T> {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(body),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
throw new Error(data?.error ?? `POST ${url} failed: ${res.status}`);
}
return res.json() as Promise<T>;
}
export const api = {
systems: () => getJson<AnatomicalSystem[]>(`${BASE}/systems`),
structures: (params: { system?: string; q?: string } = {}) => {
const usp = new URLSearchParams();
if (params.system) usp.set("system", params.system);
if (params.q) usp.set("q", params.q);
const qs = usp.toString();
return getJson<Structure[]>(`${BASE}/structures${qs ? `?${qs}` : ""}`);
},
structure: (id: string) => getJson<Structure>(`${BASE}/structures/${id}`),
quizNext: (params: { system?: string; limit?: number } = {}) => {
const usp = new URLSearchParams();
if (params.system) usp.set("system", params.system);
if (params.limit) usp.set("limit", String(params.limit));
const qs = usp.toString();
return getJson<(Structure & { quizCard: QuizCard | null })[]>(
`${BASE}/quiz/next${qs ? `?${qs}` : ""}`,
);
},
quizReview: async (structureId: string, quality: number): Promise<QuizCard> => {
const res = await fetch(`${BASE}/quiz/${structureId}/review`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quality }),
});
if (!res.ok) throw new Error(`quizReview failed: ${res.status}`);
return res.json();
},
onlineCount: () => getJson<{ count: number }>(`${BASE}/analytics/online-count`),
trackVisit: (path: string, system?: string) =>
postJson<{ ok: true }>(`${BASE}/analytics/visit`, { path, system }).catch(() => {}),
heartbeat: (sessionId: string) =>
postJson<{ ok: true }>(`${BASE}/analytics/heartbeat`, { sessionId }).catch(() => {}),
sendFeedback: (data: { message: string; email?: string; rating?: number; page?: string }) =>
postJson<{ ok: true }>(`${BASE}/feedback`, data),
adminLogin: (email: string, password: string) =>
postJson<{ token: string }>(`${BASE}/admin/login`, { email, password }),
adminStats: async (token: string): Promise<AdminStats> => {
const res = await fetch(`${BASE}/admin/stats`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`GET admin/stats failed: ${res.status}`);
return res.json();
},
};
export function modelUrl(modelFile: string): string {
return `/models/${modelFile}`;
}