feat: add course catalog, enrollment, progress tracking, quizzes, and reviews
Backend changes: - Add Enrollment and LessonProgress models to track user progress - Add UserRole enum (USER, MODERATOR, ADMIN) - Add course verification and moderation fields - New CatalogModule: public course browsing, publishing, verification - New EnrollmentModule: enroll, progress tracking, quiz submission, reviews - Add quiz generation endpoint to LessonsController Frontend changes: - Redesign course viewer: proper course UI with lesson navigation, progress bar - Add beautiful typography styles for course content (prose-course) - Fix first-login bug with token exchange retry logic - New pages: /catalog (public courses), /catalog/[id] (course details), /learning (enrollments) - Add LessonQuiz component with scoring and results - Update sidebar navigation: add Catalog and My Learning links - Add publish/verify buttons in course editor - Integrate enrollment progress tracking with backend All courses now support: sequential progression, quiz tests, reviews, ratings, author verification badges, and full marketplace publishing workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -179,6 +179,10 @@ class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
async getLessonQuiz(courseId: string, lessonId: string) {
|
||||
return this.request<any>(`/courses/${courseId}/lessons/${lessonId}/quiz`);
|
||||
}
|
||||
|
||||
// Generation
|
||||
async startGeneration(prompt: string) {
|
||||
return this.request<{ id: string; status: string; progress: number }>('/generation/start', {
|
||||
@ -231,6 +235,65 @@ class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
// Catalog (public courses)
|
||||
async getCatalog(params?: { page?: number; limit?: number; search?: string; difficulty?: string }) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.limit) searchParams.set('limit', String(params.limit));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.difficulty) searchParams.set('difficulty', params.difficulty);
|
||||
const query = searchParams.toString();
|
||||
return this.request<{ data: any[]; meta: any }>(`/catalog${query ? `?${query}` : ''}`);
|
||||
}
|
||||
|
||||
async getPublicCourse(id: string) {
|
||||
return this.request<any>(`/catalog/${id}`);
|
||||
}
|
||||
|
||||
async publishCourse(id: string) {
|
||||
return this.request<any>(`/catalog/${id}/submit`, { method: 'POST' });
|
||||
}
|
||||
|
||||
async toggleCourseVerification(id: string) {
|
||||
return this.request<any>(`/catalog/${id}/verify`, { method: 'PATCH' });
|
||||
}
|
||||
|
||||
// Enrollment & Progress
|
||||
async enrollInCourse(courseId: string) {
|
||||
return this.request<any>(`/enrollment/${courseId}/enroll`, { method: 'POST' });
|
||||
}
|
||||
|
||||
async getMyEnrollments() {
|
||||
return this.request<any[]>('/enrollment');
|
||||
}
|
||||
|
||||
async getEnrollmentProgress(courseId: string) {
|
||||
return this.request<any>(`/enrollment/${courseId}/progress`);
|
||||
}
|
||||
|
||||
async completeLesson(courseId: string, lessonId: string) {
|
||||
return this.request<any>(`/enrollment/${courseId}/lessons/${lessonId}/complete`, { method: 'POST' });
|
||||
}
|
||||
|
||||
async submitQuizScore(courseId: string, lessonId: string, score: number) {
|
||||
return this.request<any>(`/enrollment/${courseId}/lessons/${lessonId}/quiz`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ score }),
|
||||
});
|
||||
}
|
||||
|
||||
async createReview(courseId: string, data: { rating: number; title?: string; content?: string }) {
|
||||
return this.request<any>(`/enrollment/${courseId}/review`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async getCourseReviews(courseId: string, page?: number) {
|
||||
const params = page ? `?page=${page}` : '';
|
||||
return this.request<{ data: any[]; meta: any }>(`/enrollment/${courseId}/reviews${params}`);
|
||||
}
|
||||
|
||||
// Search
|
||||
async searchCourses(query: string, filters?: { category?: string; difficulty?: string }) {
|
||||
const searchParams = new URLSearchParams({ q: query });
|
||||
|
||||
Reference in New Issue
Block a user