Files
course-craft-service/apps/web/src/components/editor/lesson-content-viewer.tsx
root 2ed65f5678 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>
2026-02-06 10:44:05 +00:00

71 lines
2.3 KiB
TypeScript

'use client';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
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: [] };
interface LessonContentViewerProps {
content: Record<string, unknown> | null;
className?: string;
}
export function LessonContentViewer({ content, className }: LessonContentViewerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
codeBlock: {
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-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 hover:text-primary/80 transition-colors' },
}),
Image.configure({ HTMLAttributes: { class: 'rounded-xl max-w-full h-auto shadow-sm my-6' } }),
],
content: content ?? emptyDoc,
editable: false,
editorProps: {
attributes: {
class: 'outline-none text-foreground',
},
},
});
useEffect(() => {
if (editor && content) {
editor.commands.setContent(content);
}
}, [content, editor]);
useEffect(() => {
if (!containerRef.current || !content) return;
const mermaidNodes = containerRef.current.querySelectorAll('pre[data-language="mermaid"]');
if (mermaidNodes.length === 0) return;
mermaid.run({ nodes: Array.from(mermaidNodes) as HTMLElement[], suppressErrors: true }).catch(() => {});
}, [content]);
if (!editor) return null;
return (
<div ref={containerRef} className={cn('prose-course', className)}>
<EditorContent editor={editor} />
</div>
);
}