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:
root
2026-02-06 10:44:05 +00:00
parent dab726e8d1
commit 2ed65f5678
23 changed files with 1796 additions and 78 deletions

View File

@ -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 });