Enhance bootcamp with rich text editor, curriculum management, and video toggle

Phase 1: Rich Text Editor with Code Syntax Highlighting
- Add TipTap CodeBlock extension with lowlight for syntax highlighting
- Support multiple languages (JavaScript, TypeScript, Python, Java, C++, HTML, CSS, JSON)
- Add copy-to-clipboard button on code blocks
- Add line numbers display with CSS
- Replace textarea with RichTextEditor in CurriculumEditor
- Add DOMPurify sanitization in Bootcamp display
- Add dark theme syntax highlighting styles

Phase 2: Admin Curriculum Management Page
- Create dedicated ProductCurriculum page at /admin/products/:id/curriculum
- Three-column layout: Modules (3) | Lessons (5) | Editor (4)
- Full-page UX with drag-and-drop reordering
- Add "Manage Curriculum" button for bootcamp products in AdminProducts
- Breadcrumb navigation back to products

Phase 3: Product-Level Video Source Toggle
- Add youtube_url and embed_code columns to bootcamp_lessons table
- Add video_source and video_source_config columns to products table
- Update ProductCurriculum with separate YouTube URL and Embed Code fields
- Create smart VideoPlayer component in Bootcamp.tsx
- Support YouTube ↔ Embed switching with smart fallback
- Show "Konten tidak tersedia" warning when no video configured
- Maintain backward compatibility with existing video_url field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dwindown
2025-12-30 17:07:31 +07:00
parent 52ec0b9b86
commit da71acb431
10 changed files with 1114 additions and 34 deletions

View File

@@ -4,12 +4,13 @@ import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder';
import TextAlign from '@tiptap/extension-text-align';
import CodeBlock from '@tiptap/extension-code-block';
import { Node } from '@tiptap/core';
import { Button } from '@/components/ui/button';
import {
Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon,
Image as ImageIcon, Heading1, Heading2, Undo, Redo,
Maximize2, Minimize2, MousePointer, Square, AlignLeft, AlignCenter, AlignRight, AlignJustify, MoreVertical, Minus
Maximize2, Minimize2, MousePointer, Square, AlignLeft, AlignCenter, AlignRight, AlignJustify, MoreVertical, Minus, Code, Copy, Check
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useCallback, useEffect, useState } from 'react';
@@ -18,6 +19,38 @@ import { toast } from '@/hooks/use-toast';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { common, createLowlight } from 'lowlight';
// Register common languages for syntax highlighting
const lowlight = createLowlight(common);
// Code Block Component with Copy Button
const CodeBlockWithCopy = ({ node }: { node: any }) => {
const [copied, setCopied] = useState(false);
const code = node.textContent;
const handleCopy = async () => {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="relative group">
<Button
size="sm"
variant="ghost"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 h-7 px-2"
onClick={handleCopy}
>
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
</Button>
<pre className="line-numbers">
<code>{code}</code>
</pre>
</div>
);
};
interface RichTextEditorProps {
content: string;
@@ -249,6 +282,20 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
levels: [1, 2, 3],
},
horizontalRule: true,
codeBlock: false, // Disable default code block to use custom one
}),
CodeBlock.configure({
lowlight,
defaultLanguage: 'text',
HTMLAttributes: {
class: 'code-block-wrapper',
},
}).extend({
addKeyboardShortcuts() {
return {
'Mod-Shift-c': () => this.editor.commands.toggleCodeBlock(),
};
},
}),
TextAlign.configure({
types: ['heading', 'paragraph'],
@@ -516,6 +563,16 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
>
<Quote className="w-4 h-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'bg-primary text-primary-foreground' : ''}
title="Code Block (Ctrl+Shift+C)"
>
<Code className="w-4 h-4" />
</Button>
<Button
type="button"
variant="ghost"

View File

@@ -9,6 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { toast } from '@/hooks/use-toast';
import { Plus, Pencil, Trash2, ChevronUp, ChevronDown, GripVertical } from 'lucide-react';
import { cn } from '@/lib/utils';
import { RichTextEditor } from '@/components/RichTextEditor';
interface Module {
id: string;
@@ -442,14 +443,16 @@ export function CurriculumEditor({ productId }: CurriculumEditorProps) {
/>
</div>
<div className="space-y-2">
<Label>Content (HTML)</Label>
<Textarea
value={lessonForm.content}
onChange={(e) => setLessonForm({ ...lessonForm, content: e.target.value })}
placeholder="Lesson content..."
rows={6}
className="border-2 font-mono text-sm"
<Label>Content</Label>
<RichTextEditor
content={lessonForm.content}
onChange={(html) => setLessonForm({ ...lessonForm, content: html })}
placeholder="Write your lesson content here... Use code blocks for syntax highlighting."
className="min-h-[400px]"
/>
<p className="text-sm text-muted-foreground">
Supports rich text formatting, code blocks with syntax highlighting, images, and more.
</p>
</div>
<div className="space-y-2">
<Label>Release Date (optional)</Label>