React frontend with: - Authentication UI - Trading dashboard - ML signals display - Portfolio management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
613 lines
17 KiB
TypeScript
613 lines
17 KiB
TypeScript
/**
|
|
* Education Service
|
|
* API client for courses, lessons, quizzes, and gamification
|
|
*/
|
|
|
|
import axios from 'axios';
|
|
import type {
|
|
CourseListItem,
|
|
CourseDetail,
|
|
CourseCategory,
|
|
CourseModule,
|
|
LessonDetail,
|
|
UserEnrollment,
|
|
EnrollmentWithCourse,
|
|
CourseFilters,
|
|
PaginatedResponse,
|
|
ApiResponse,
|
|
Quiz,
|
|
QuizAttempt,
|
|
QuizResult,
|
|
GamificationProfile,
|
|
LevelProgress,
|
|
StreakStats,
|
|
Achievement,
|
|
LeaderboardEntry,
|
|
UserLeaderboardPosition,
|
|
GamificationSummary,
|
|
} from '../types/education.types';
|
|
|
|
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
|
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Add auth token to requests
|
|
api.interceptors.request.use((config) => {
|
|
const token = localStorage.getItem('auth_token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// ============================================================================
|
|
// Categories
|
|
// ============================================================================
|
|
|
|
export async function getCategories(): Promise<CourseCategory[]> {
|
|
const response = await api.get<ApiResponse<CourseCategory[]>>('/education/categories');
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function createCategory(data: {
|
|
name: string;
|
|
slug: string;
|
|
description?: string;
|
|
icon?: string;
|
|
}): Promise<CourseCategory> {
|
|
const response = await api.post<ApiResponse<CourseCategory>>('/education/categories', data);
|
|
return response.data.data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Courses
|
|
// ============================================================================
|
|
|
|
export async function getCourses(
|
|
filters?: CourseFilters
|
|
): Promise<PaginatedResponse<CourseListItem>> {
|
|
const response = await api.get<ApiResponse<PaginatedResponse<CourseListItem>>>(
|
|
'/education/courses',
|
|
{ params: filters }
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getPopularCourses(limit = 6): Promise<CourseListItem[]> {
|
|
const response = await api.get<ApiResponse<CourseListItem[]>>('/education/courses/popular', {
|
|
params: { limit },
|
|
});
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getNewCourses(limit = 6): Promise<CourseListItem[]> {
|
|
const response = await api.get<ApiResponse<CourseListItem[]>>('/education/courses/new', {
|
|
params: { limit },
|
|
});
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getCourseById(courseId: string): Promise<CourseDetail> {
|
|
const response = await api.get<ApiResponse<CourseDetail>>(`/education/courses/${courseId}`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getCourseBySlug(slug: string): Promise<CourseDetail> {
|
|
const response = await api.get<ApiResponse<CourseDetail>>(`/education/courses/slug/${slug}`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getCourseModules(courseId: string): Promise<CourseModule[]> {
|
|
const response = await api.get<ApiResponse<CourseModule[]>>(
|
|
`/education/courses/${courseId}/modules`
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getCourseStats(courseId: string): Promise<{
|
|
totalEnrollments: number;
|
|
completionRate: number;
|
|
avgRating: number;
|
|
}> {
|
|
const response = await api.get<
|
|
ApiResponse<{ totalEnrollments: number; completionRate: number; avgRating: number }>
|
|
>(`/education/courses/${courseId}/stats`);
|
|
return response.data.data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Lessons
|
|
// ============================================================================
|
|
|
|
export async function getModuleLessons(
|
|
moduleId: string
|
|
): Promise<{ id: string; title: string; contentType: string; durationMinutes: number }[]> {
|
|
const response = await api.get<
|
|
ApiResponse<{ id: string; title: string; contentType: string; durationMinutes: number }[]>
|
|
>(`/education/modules/${moduleId}/lessons`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getLessonById(lessonId: string): Promise<LessonDetail> {
|
|
const response = await api.get<ApiResponse<LessonDetail>>(`/education/lessons/${lessonId}`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function updateLessonProgress(
|
|
lessonId: string,
|
|
data: {
|
|
videoWatchedSeconds?: number;
|
|
videoCompleted?: boolean;
|
|
userNotes?: string;
|
|
}
|
|
): Promise<{ progressId: string; isCompleted: boolean }> {
|
|
const response = await api.post<ApiResponse<{ progressId: string; isCompleted: boolean }>>(
|
|
`/education/lessons/${lessonId}/progress`,
|
|
data
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function markLessonComplete(
|
|
lessonId: string
|
|
): Promise<{ progressId: string; xpAwarded: number }> {
|
|
const response = await api.post<ApiResponse<{ progressId: string; xpAwarded: number }>>(
|
|
`/education/lessons/${lessonId}/complete`
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Enrollments
|
|
// ============================================================================
|
|
|
|
export async function getMyEnrollments(): Promise<EnrollmentWithCourse[]> {
|
|
const response = await api.get<ApiResponse<EnrollmentWithCourse[]>>('/education/my/enrollments');
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getMyLearningStats(): Promise<{
|
|
totalEnrollments: number;
|
|
completedCourses: number;
|
|
inProgressCourses: number;
|
|
totalLessonsCompleted: number;
|
|
totalTimeSpent: number;
|
|
}> {
|
|
const response = await api.get<
|
|
ApiResponse<{
|
|
totalEnrollments: number;
|
|
completedCourses: number;
|
|
inProgressCourses: number;
|
|
totalLessonsCompleted: number;
|
|
totalTimeSpent: number;
|
|
}>
|
|
>('/education/my/stats');
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function enrollInCourse(courseId: string): Promise<UserEnrollment> {
|
|
const response = await api.post<ApiResponse<UserEnrollment>>(
|
|
`/education/courses/${courseId}/enroll`
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getEnrollmentStatus(courseId: string): Promise<UserEnrollment | null> {
|
|
try {
|
|
const response = await api.get<ApiResponse<UserEnrollment>>(
|
|
`/education/courses/${courseId}/enrollment`
|
|
);
|
|
return response.data.data;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Quizzes
|
|
// ============================================================================
|
|
|
|
export async function getQuizByLessonId(lessonId: string): Promise<Quiz | null> {
|
|
try {
|
|
const response = await api.get<ApiResponse<Quiz>>(`/education/lessons/${lessonId}/quiz`);
|
|
return response.data.data;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getQuizById(quizId: string): Promise<Quiz> {
|
|
const response = await api.get<ApiResponse<Quiz>>(`/education/quizzes/${quizId}`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getCourseQuizzes(courseId: string): Promise<Quiz[]> {
|
|
const response = await api.get<ApiResponse<Quiz[]>>(`/education/courses/${courseId}/quizzes`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function startQuizAttempt(
|
|
quizId: string,
|
|
enrollmentId?: string
|
|
): Promise<{ attemptId: string; quiz: Quiz; startedAt: string; expiresAt?: string }> {
|
|
const response = await api.post<
|
|
ApiResponse<{ attemptId: string; quiz: Quiz; startedAt: string; expiresAt?: string }>
|
|
>(`/education/quizzes/${quizId}/start`, { enrollmentId });
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function submitQuizAttempt(
|
|
attemptId: string,
|
|
answers: { questionId: string; answer: string | string[] }[]
|
|
): Promise<QuizResult> {
|
|
const response = await api.post<ApiResponse<QuizResult>>(
|
|
`/education/quizzes/attempts/${attemptId}/submit`,
|
|
{ answers }
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getQuizResults(attemptId: string): Promise<QuizResult> {
|
|
const response = await api.get<ApiResponse<QuizResult>>(
|
|
`/education/quizzes/attempts/${attemptId}/results`
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getUserQuizAttempts(quizId: string): Promise<QuizAttempt[]> {
|
|
const response = await api.get<ApiResponse<QuizAttempt[]>>(
|
|
`/education/quizzes/${quizId}/my-attempts`
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getQuizStatistics(quizId: string): Promise<{
|
|
totalAttempts: number;
|
|
passRate: number;
|
|
averageScore: number;
|
|
}> {
|
|
const response = await api.get<
|
|
ApiResponse<{ totalAttempts: number; passRate: number; averageScore: number }>
|
|
>(`/education/quizzes/${quizId}/stats`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getUserQuizStats(): Promise<{
|
|
totalAttempts: number;
|
|
quizzesPassed: number;
|
|
averageScore: number;
|
|
bestScore: number;
|
|
}> {
|
|
const response = await api.get<
|
|
ApiResponse<{
|
|
totalAttempts: number;
|
|
quizzesPassed: number;
|
|
averageScore: number;
|
|
bestScore: number;
|
|
}>
|
|
>('/education/my/quiz-stats');
|
|
return response.data.data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Gamification
|
|
// ============================================================================
|
|
|
|
export async function getGamificationProfile(): Promise<GamificationProfile & { levelProgress: LevelProgress }> {
|
|
const response = await api.get<ApiResponse<GamificationProfile & { levelProgress: LevelProgress }>>(
|
|
'/education/gamification/profile'
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getPublicProfile(userId: string): Promise<{
|
|
userId: string;
|
|
totalXp: number;
|
|
currentLevel: number;
|
|
currentStreakDays: number;
|
|
totalCoursesCompleted: number;
|
|
levelProgress: { currentLevel: number; progressPercentage: number };
|
|
}> {
|
|
const response = await api.get<
|
|
ApiResponse<{
|
|
userId: string;
|
|
totalXp: number;
|
|
currentLevel: number;
|
|
currentStreakDays: number;
|
|
totalCoursesCompleted: number;
|
|
levelProgress: { currentLevel: number; progressPercentage: number };
|
|
}>
|
|
>(`/education/gamification/profile/${userId}`);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getLevelProgress(): Promise<LevelProgress> {
|
|
const response = await api.get<ApiResponse<LevelProgress>>(
|
|
'/education/gamification/level-progress'
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getStreakStats(): Promise<StreakStats> {
|
|
const response = await api.get<ApiResponse<StreakStats>>('/education/gamification/streak');
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function recordDailyActivity(): Promise<{
|
|
currentStreak: number;
|
|
longestStreak: number;
|
|
streakMaintained: boolean;
|
|
xpBonusApplied: boolean;
|
|
}> {
|
|
const response = await api.post<
|
|
ApiResponse<{
|
|
currentStreak: number;
|
|
longestStreak: number;
|
|
streakMaintained: boolean;
|
|
xpBonusApplied: boolean;
|
|
}>
|
|
>('/education/gamification/daily-activity');
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getMyAchievements(): Promise<Achievement[]> {
|
|
const response = await api.get<ApiResponse<Achievement[]>>(
|
|
'/education/gamification/achievements'
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getUserAchievements(userId: string): Promise<Achievement[]> {
|
|
const response = await api.get<ApiResponse<Achievement[]>>(
|
|
`/education/gamification/achievements/${userId}`
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getLeaderboard(
|
|
period: 'all_time' | 'month' | 'week' = 'all_time',
|
|
limit = 100
|
|
): Promise<LeaderboardEntry[]> {
|
|
const response = await api.get<ApiResponse<LeaderboardEntry[]>>(
|
|
'/education/gamification/leaderboard',
|
|
{ params: { period, limit } }
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getMyLeaderboardPosition(
|
|
period: 'all_time' | 'month' | 'week' = 'all_time'
|
|
): Promise<UserLeaderboardPosition> {
|
|
const response = await api.get<ApiResponse<UserLeaderboardPosition>>(
|
|
'/education/gamification/leaderboard/my-position',
|
|
{ params: { period } }
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getNearbyUsers(
|
|
period: 'all_time' | 'month' | 'week' = 'all_time',
|
|
radius = 5
|
|
): Promise<LeaderboardEntry[]> {
|
|
const response = await api.get<ApiResponse<LeaderboardEntry[]>>(
|
|
'/education/gamification/leaderboard/nearby',
|
|
{ params: { period, radius } }
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function getGamificationSummary(): Promise<GamificationSummary> {
|
|
const response = await api.get<ApiResponse<GamificationSummary>>(
|
|
'/education/gamification/summary'
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Admin/Instructor Operations
|
|
// ============================================================================
|
|
|
|
export async function createCourse(data: {
|
|
title: string;
|
|
slug: string;
|
|
description?: string;
|
|
shortDescription?: string;
|
|
categoryId?: string;
|
|
difficultyLevel: 'beginner' | 'intermediate' | 'advanced';
|
|
isFree: boolean;
|
|
priceUsd?: number;
|
|
thumbnailUrl?: string;
|
|
learningObjectives?: string[];
|
|
requirements?: string[];
|
|
tags?: string[];
|
|
}): Promise<CourseDetail> {
|
|
const response = await api.post<ApiResponse<CourseDetail>>('/education/courses', data);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function updateCourse(
|
|
courseId: string,
|
|
data: Partial<{
|
|
title: string;
|
|
description: string;
|
|
shortDescription: string;
|
|
categoryId: string;
|
|
difficultyLevel: 'beginner' | 'intermediate' | 'advanced';
|
|
isFree: boolean;
|
|
priceUsd: number;
|
|
thumbnailUrl: string;
|
|
learningObjectives: string[];
|
|
requirements: string[];
|
|
tags: string[];
|
|
}>
|
|
): Promise<CourseDetail> {
|
|
const response = await api.patch<ApiResponse<CourseDetail>>(
|
|
`/education/courses/${courseId}`,
|
|
data
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function deleteCourse(courseId: string): Promise<void> {
|
|
await api.delete(`/education/courses/${courseId}`);
|
|
}
|
|
|
|
export async function publishCourse(courseId: string): Promise<CourseDetail> {
|
|
const response = await api.post<ApiResponse<CourseDetail>>(
|
|
`/education/courses/${courseId}/publish`
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function createModule(
|
|
courseId: string,
|
|
data: {
|
|
title: string;
|
|
description?: string;
|
|
sortOrder?: number;
|
|
}
|
|
): Promise<CourseModule> {
|
|
const response = await api.post<ApiResponse<CourseModule>>(
|
|
`/education/courses/${courseId}/modules`,
|
|
data
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function deleteModule(moduleId: string): Promise<void> {
|
|
await api.delete(`/education/modules/${moduleId}`);
|
|
}
|
|
|
|
export async function createLesson(
|
|
moduleId: string,
|
|
data: {
|
|
title: string;
|
|
description?: string;
|
|
contentType: 'video' | 'text' | 'quiz' | 'exercise';
|
|
durationMinutes?: number;
|
|
sortOrder?: number;
|
|
isFree?: boolean;
|
|
videoUrl?: string;
|
|
contentHtml?: string;
|
|
contentMarkdown?: string;
|
|
}
|
|
): Promise<LessonDetail> {
|
|
const response = await api.post<ApiResponse<LessonDetail>>(
|
|
`/education/modules/${moduleId}/lessons`,
|
|
data
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function deleteLesson(lessonId: string): Promise<void> {
|
|
await api.delete(`/education/lessons/${lessonId}`);
|
|
}
|
|
|
|
export async function createQuiz(data: {
|
|
courseId: string;
|
|
lessonId?: string;
|
|
title: string;
|
|
description?: string;
|
|
passingScore?: number;
|
|
maxAttempts?: number;
|
|
timeLimitMinutes?: number;
|
|
shuffleQuestions?: boolean;
|
|
showCorrectAnswers?: boolean;
|
|
}): Promise<Quiz> {
|
|
const response = await api.post<ApiResponse<Quiz>>('/education/quizzes', data);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function deleteQuiz(quizId: string): Promise<void> {
|
|
await api.delete(`/education/quizzes/${quizId}`);
|
|
}
|
|
|
|
export async function createQuizQuestion(
|
|
quizId: string,
|
|
data: {
|
|
questionType: 'multiple_choice' | 'multiple_answer' | 'true_false' | 'short_answer';
|
|
questionText: string;
|
|
explanation?: string;
|
|
options?: { text: string; isCorrect: boolean }[];
|
|
points?: number;
|
|
sortOrder?: number;
|
|
}
|
|
): Promise<{ id: string }> {
|
|
const response = await api.post<ApiResponse<{ id: string }>>(
|
|
`/education/quizzes/${quizId}/questions`,
|
|
data
|
|
);
|
|
return response.data.data;
|
|
}
|
|
|
|
export async function deleteQuizQuestion(questionId: string): Promise<void> {
|
|
await api.delete(`/education/questions/${questionId}`);
|
|
}
|
|
|
|
// Export service object for convenience
|
|
export const educationService = {
|
|
// Categories
|
|
getCategories,
|
|
createCategory,
|
|
// Courses
|
|
getCourses,
|
|
getPopularCourses,
|
|
getNewCourses,
|
|
getCourseById,
|
|
getCourseBySlug,
|
|
getCourseModules,
|
|
getCourseStats,
|
|
createCourse,
|
|
updateCourse,
|
|
deleteCourse,
|
|
publishCourse,
|
|
// Modules
|
|
createModule,
|
|
deleteModule,
|
|
// Lessons
|
|
getModuleLessons,
|
|
getLessonById,
|
|
updateLessonProgress,
|
|
markLessonComplete,
|
|
createLesson,
|
|
deleteLesson,
|
|
// Enrollments
|
|
getMyEnrollments,
|
|
getMyLearningStats,
|
|
enrollInCourse,
|
|
getEnrollmentStatus,
|
|
// Quizzes
|
|
getQuizByLessonId,
|
|
getQuizById,
|
|
getCourseQuizzes,
|
|
startQuizAttempt,
|
|
submitQuizAttempt,
|
|
getQuizResults,
|
|
getUserQuizAttempts,
|
|
getQuizStatistics,
|
|
getUserQuizStats,
|
|
createQuiz,
|
|
deleteQuiz,
|
|
createQuizQuestion,
|
|
deleteQuizQuestion,
|
|
// Gamification
|
|
getGamificationProfile,
|
|
getPublicProfile,
|
|
getLevelProgress,
|
|
getStreakStats,
|
|
recordDailyActivity,
|
|
getMyAchievements,
|
|
getUserAchievements,
|
|
getLeaderboard,
|
|
getMyLeaderboardPosition,
|
|
getNearbyUsers,
|
|
getGamificationSummary,
|
|
};
|
|
|
|
export default educationService;
|