- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
/** Base path or absolute URL for Odoo JSON API (dev: `/api` + Vite proxy). */
|
|
export const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL?.trim() || "/api").replace(/\/$/, "");
|
|
const BASE_URL = API_BASE_URL;
|
|
|
|
export function extractApiErrorMessage(data: unknown, status: number, statusText: string): string {
|
|
if (data && typeof data === "object" && data !== null && "error" in data) {
|
|
const msg = (data as { error: unknown }).error;
|
|
if (msg != null && String(msg).trim()) return String(msg);
|
|
}
|
|
return `${status} ${statusText}`;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public statusText: string,
|
|
public data: unknown,
|
|
) {
|
|
super(extractApiErrorMessage(data, status, statusText));
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
function getToken(): string | null {
|
|
return localStorage.getItem("encoach_token");
|
|
}
|
|
|
|
export function setToken(token: string): void {
|
|
localStorage.setItem("encoach_token", token);
|
|
}
|
|
|
|
export function clearToken(): void {
|
|
localStorage.removeItem("encoach_token");
|
|
}
|
|
|
|
async function handleResponse<T>(response: Response): Promise<T> {
|
|
const data = await response.json().catch(() => null);
|
|
|
|
if (response.status === 401) {
|
|
const hadToken = !!getToken();
|
|
clearToken();
|
|
// Login failure is also 401 — do not hard-redirect when no session existed.
|
|
if (hadToken) {
|
|
window.location.href = "/login";
|
|
}
|
|
throw new ApiError(401, response.statusText, data);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new ApiError(response.status, response.statusText, data);
|
|
}
|
|
|
|
return data as T;
|
|
}
|
|
|
|
function buildHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
...extra,
|
|
};
|
|
|
|
const token = getToken();
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
function buildUrl(path: string, params?: Record<string, string | number | boolean | undefined>): string {
|
|
const url = new URL(`${BASE_URL}${path}`, window.location.origin);
|
|
if (params) {
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
url.searchParams.set(key, String(value));
|
|
}
|
|
});
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
export const api = {
|
|
async get<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T> {
|
|
const res = await fetch(buildUrl(path, params), {
|
|
method: "GET",
|
|
headers: buildHeaders(),
|
|
});
|
|
return handleResponse<T>(res);
|
|
},
|
|
|
|
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(buildUrl(path), {
|
|
method: "POST",
|
|
headers: buildHeaders(),
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
return handleResponse<T>(res);
|
|
},
|
|
|
|
async patch<T>(path: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(buildUrl(path), {
|
|
method: "PATCH",
|
|
headers: buildHeaders(),
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
return handleResponse<T>(res);
|
|
},
|
|
|
|
async put<T>(path: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(buildUrl(path), {
|
|
method: "PUT",
|
|
headers: buildHeaders(),
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
return handleResponse<T>(res);
|
|
},
|
|
|
|
async delete<T>(path: string): Promise<T> {
|
|
const res = await fetch(buildUrl(path), {
|
|
method: "DELETE",
|
|
headers: buildHeaders(),
|
|
});
|
|
return handleResponse<T>(res);
|
|
},
|
|
|
|
async upload<T>(path: string, formData: FormData): Promise<T> {
|
|
const token = getToken();
|
|
const headers: Record<string, string> = {};
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(buildUrl(path), {
|
|
method: "POST",
|
|
headers,
|
|
body: formData,
|
|
});
|
|
return handleResponse<T>(res);
|
|
},
|
|
};
|