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

@ -7,6 +7,7 @@ import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
import mermaid from 'mermaid';
import { useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
const emptyDoc = { type: 'doc', content: [] };
@ -27,22 +28,21 @@ export function LessonContentViewer({ content, className }: LessonContentViewerP
HTMLAttributes: (node: { attrs: { language?: string } }) =>
node.attrs.language === 'mermaid'
? { class: 'mermaid rounded-lg p-4 bg-muted min-h-[80px]', 'data-language': 'mermaid' }
: { class: 'rounded-lg bg-muted p-4 font-mono text-sm', 'data-language': node.attrs.language || '' },
: { class: 'rounded-xl bg-muted p-5 font-mono text-sm border', 'data-language': node.attrs.language || '' },
},
}),
Underline,
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'text-primary underline underline-offset-2' },
HTMLAttributes: { class: 'text-primary underline underline-offset-2 hover:text-primary/80 transition-colors' },
}),
Image.configure({ HTMLAttributes: { class: 'rounded-lg max-w-full h-auto' } }),
Image.configure({ HTMLAttributes: { class: 'rounded-xl max-w-full h-auto shadow-sm my-6' } }),
],
content: content ?? emptyDoc,
editable: false,
editorProps: {
attributes: {
class:
'prose prose-lg dark:prose-invert max-w-none focus:outline-none leading-relaxed [&_.ProseMirror]:outline-none [&_.ProseMirror]:text-foreground [&_.ProseMirror_p]:leading-7 [&_.ProseMirror_h1]:text-3xl [&_.ProseMirror_h2]:text-2xl [&_.ProseMirror_h3]:text-xl [&_.ProseMirror_pre]:rounded-lg [&_.ProseMirror_pre]:bg-muted [&_.ProseMirror_pre]:p-4 [&_.ProseMirror_pre]:font-mono [&_.ProseMirror_pre]:text-sm [&_.ProseMirror_pre]:border [&_.ProseMirror_blockquote]:border-primary [&_.ProseMirror_blockquote]:bg-muted/30',
class: 'outline-none text-foreground',
},
},
});
@ -63,7 +63,7 @@ export function LessonContentViewer({ content, className }: LessonContentViewerP
if (!editor) return null;
return (
<div ref={containerRef} className={className}>
<div ref={containerRef} className={cn('prose-course', className)}>
<EditorContent editor={editor} />
</div>
);