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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user