322 lines
8.3 KiB
TypeScript
322 lines
8.3 KiB
TypeScript
import type { Leaderboard, PredictedScore } from "../types/leaderboard";
|
|
import type { Lesson, LessonsResponse } from "../types/lesson";
|
|
import type {
|
|
ClaimedRewardResponse,
|
|
QuestArc,
|
|
UserInventory,
|
|
UserTitle,
|
|
} from "../types/quest";
|
|
import type {
|
|
SessionAnswerResponse,
|
|
SessionQuestionsResponse,
|
|
SessionRequest,
|
|
SessionResponse,
|
|
SubmitAnswer,
|
|
} from "../types/session";
|
|
import type { PracticeSheet } from "../types/sheet";
|
|
import type { Topic } from "../types/topic";
|
|
|
|
const API_URL = "https://ed-dev-api.omukk.dev";
|
|
|
|
export interface LoginRequest {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
export interface RegistrationRequest {
|
|
email: string;
|
|
name: string;
|
|
avatar_url: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface User {
|
|
email: string;
|
|
name: string;
|
|
role: "STUDENT" | "TEACHER" | "ADMIN";
|
|
avatar_url: string;
|
|
id: string;
|
|
status: "ACTIVE" | "INACTIVE";
|
|
joined_at: string;
|
|
last_active: string;
|
|
total_xp: number;
|
|
current_level: number;
|
|
next_level_threshold: number;
|
|
current_level_start: number;
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
token: string;
|
|
token_type: string;
|
|
user: User;
|
|
}
|
|
|
|
export interface ApiError {
|
|
detail?: string;
|
|
message?: string;
|
|
}
|
|
|
|
class ApiClient {
|
|
private baseURL: string;
|
|
|
|
constructor(baseURL: string) {
|
|
this.baseURL = baseURL;
|
|
}
|
|
|
|
private async request<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {},
|
|
): Promise<T> {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
|
|
const config: RequestInit = {
|
|
...options,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...options.headers,
|
|
},
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(url, config);
|
|
|
|
if (!response.ok) {
|
|
const error: ApiError = await response.json().catch(() => ({
|
|
message: "An error occurred",
|
|
}));
|
|
throw new Error(error.detail || error.message || "Request failed");
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
throw error;
|
|
}
|
|
throw new Error("Network error occurred");
|
|
}
|
|
}
|
|
|
|
async fetchUser(token: string): Promise<User> {
|
|
return this.authenticatedRequest<User>("/auth/me/", token);
|
|
}
|
|
|
|
// Auth endpoints
|
|
async login(credentials: LoginRequest): Promise<LoginResponse> {
|
|
return this.request<LoginResponse>("/auth/login/", {
|
|
method: "POST",
|
|
body: JSON.stringify(credentials),
|
|
});
|
|
}
|
|
async register(
|
|
credentials: RegistrationRequest,
|
|
): Promise<{ message: string }> {
|
|
return this.request<{ message: string }>("/auth/register/", {
|
|
method: "POST",
|
|
body: JSON.stringify(credentials),
|
|
});
|
|
}
|
|
|
|
// Authenticated request helper
|
|
async authenticatedRequest<T>(
|
|
endpoint: string,
|
|
token: string,
|
|
options: RequestInit = {},
|
|
): Promise<T> {
|
|
return this.request<T>(endpoint, {
|
|
...options,
|
|
headers: {
|
|
...options.headers,
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Example: Get user profile (authenticated endpoint)
|
|
async getUserProfile(token: string): Promise<User> {
|
|
return this.authenticatedRequest<User>("/auth/me/", token);
|
|
}
|
|
|
|
async getPracticeSheets(
|
|
token: string,
|
|
page: number,
|
|
limit: number,
|
|
): Promise<any> {
|
|
const queryParams = new URLSearchParams({
|
|
page: page.toString(),
|
|
limit: limit.toString(),
|
|
}).toString();
|
|
return this.authenticatedRequest<any>(
|
|
`/practice-sheets/?${queryParams}`,
|
|
token,
|
|
);
|
|
}
|
|
|
|
async getPracticeSheetById(
|
|
token: string,
|
|
sheetId: string,
|
|
): Promise<PracticeSheet> {
|
|
return this.authenticatedRequest<PracticeSheet>(
|
|
`/practice-sheets/${sheetId}`,
|
|
token,
|
|
);
|
|
}
|
|
|
|
async startSession(
|
|
token: string,
|
|
sessionData: SessionRequest,
|
|
): Promise<SessionResponse> {
|
|
return this.authenticatedRequest<SessionResponse>(`/sessions/`, token, {
|
|
method: "POST",
|
|
body: JSON.stringify(sessionData),
|
|
});
|
|
}
|
|
|
|
async fetchSessionQuestions(
|
|
token: string,
|
|
sessionId: string,
|
|
): Promise<SessionQuestionsResponse> {
|
|
return this.authenticatedRequest<SessionQuestionsResponse>(
|
|
`/sessions/${sessionId}/questions/`,
|
|
token,
|
|
);
|
|
}
|
|
|
|
async submitAnswer(
|
|
token: string,
|
|
sessionId: string,
|
|
answerSubmissionData: SubmitAnswer,
|
|
): Promise<SessionAnswerResponse> {
|
|
return this.authenticatedRequest<SessionAnswerResponse>(
|
|
`/sessions/${sessionId}/answer/`,
|
|
token,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify(answerSubmissionData),
|
|
},
|
|
);
|
|
}
|
|
|
|
async fetchNextModule(token: string, sessionId: string): Promise<any> {
|
|
return this.authenticatedRequest<any>(
|
|
`/sessions/${sessionId}/next-module/`,
|
|
token,
|
|
{
|
|
method: "POST",
|
|
},
|
|
);
|
|
}
|
|
|
|
async fetchSessionStateById(
|
|
token: string,
|
|
sessionId: string,
|
|
): Promise<SessionResponse> {
|
|
return this.authenticatedRequest<SessionResponse>(
|
|
`/sessions/${sessionId}`,
|
|
token,
|
|
);
|
|
}
|
|
async fetchLessonVideos(token: string): Promise<LessonsResponse> {
|
|
return this.authenticatedRequest<LessonsResponse>(`/lessons/`, token);
|
|
}
|
|
|
|
async fetchLessonById(token: string, lessonId: string): Promise<Lesson> {
|
|
return this.authenticatedRequest<Lesson>(`/lessons/${lessonId}`, token);
|
|
}
|
|
async fetchAllTopics(token: string): Promise<Topic[]> {
|
|
return this.authenticatedRequest<Topic[]>(`/topics/`, token);
|
|
}
|
|
|
|
async fetchTopicById(token: string, topicId: string): Promise<Topic> {
|
|
return this.authenticatedRequest<Topic>(`/topics/${topicId}`, token);
|
|
}
|
|
|
|
async fetchLeaderboard(
|
|
token: string,
|
|
metric: string,
|
|
timeframe: string,
|
|
): Promise<Leaderboard> {
|
|
return this.authenticatedRequest<Leaderboard>(
|
|
`/leaderboard/?metric=${metric}&timeframe=${timeframe}`,
|
|
token,
|
|
);
|
|
}
|
|
|
|
async fetchPredictedScore(token: string): Promise<PredictedScore> {
|
|
return this.authenticatedRequest<PredictedScore>(`/prediction/`, token);
|
|
}
|
|
|
|
/*------------QUEST JOURNEY-------------- */
|
|
async fetchUserJourney(token: string): Promise<QuestArc[]> {
|
|
return this.authenticatedRequest<QuestArc[]>(`/journey/`, token);
|
|
}
|
|
async claimReward(
|
|
token: string,
|
|
node_id: string,
|
|
): Promise<ClaimedRewardResponse> {
|
|
return this.authenticatedRequest<ClaimedRewardResponse>(
|
|
`/journey/claim/${node_id}`,
|
|
token,
|
|
{
|
|
method: "POST",
|
|
},
|
|
);
|
|
}
|
|
|
|
/*------------INVENTORY-------------- */
|
|
async fetchUserInventory(token: string): Promise<UserInventory> {
|
|
return this.authenticatedRequest<UserInventory>(`/inventory/`, token);
|
|
}
|
|
async activateItem(token: string, itemId: string): Promise<UserInventory> {
|
|
return this.authenticatedRequest<UserInventory>(
|
|
`/inventory/use/${itemId}`,
|
|
token,
|
|
);
|
|
}
|
|
/*------------TITLES-------------- */
|
|
async fetchUserTitles(token: string): Promise<UserTitle[]> {
|
|
return this.authenticatedRequest<UserTitle[]>(`/inventory/titles/`, token);
|
|
}
|
|
async equipTitle(
|
|
token: string,
|
|
titleData: { title_id: string },
|
|
): Promise<string> {
|
|
return this.authenticatedRequest<string>(`/inventory/titles/equip`, token, {
|
|
method: "POST",
|
|
body: JSON.stringify(titleData),
|
|
});
|
|
}
|
|
|
|
/*------------UPLOADS-------------- */
|
|
// token is optional — the /uploads/ endpoint appears to be public
|
|
// (no Authorization header in the curl example). Pass a token if needed.
|
|
async uploadAvatar(file: File, token?: string): Promise<string> {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
|
|
const headers: HeadersInit = { accept: "application/json" };
|
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
// Note: Do NOT set Content-Type manually — fetch sets it automatically
|
|
// with the correct multipart/form-data boundary when the body is FormData.
|
|
|
|
const url = `${this.baseURL}/uploads/`;
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers,
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error: ApiError = await response.json().catch(() => ({
|
|
message: "Upload failed",
|
|
}));
|
|
throw new Error(error.detail || error.message || "Upload failed");
|
|
}
|
|
|
|
const data = await response.json();
|
|
// Adjust the field name below to match your API's response shape,
|
|
// e.g. data.url, data.file_url, data.avatar_url, etc.
|
|
return data.url as string;
|
|
}
|
|
}
|
|
|
|
export const api = new ApiClient(API_URL);
|