151 lines
4.2 KiB
TypeScript
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}`;
|
|
}
|