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>
71 lines
2.3 KiB
TypeScript
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>
|
|
);
|
|
}
|