import { useState, useEffect } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { toast } from '@/hooks/use-toast'; import { Plus, Pencil, Trash2, ChevronUp, ChevronDown, GripVertical } from 'lucide-react'; import { cn } from '@/lib/utils'; interface Module { id: string; title: string; position: number; } interface Lesson { id: string; module_id: string; title: string; content: string | null; video_url: string | null; position: number; release_at: string | null; } interface CurriculumEditorProps { productId: string; } export function CurriculumEditor({ productId }: CurriculumEditorProps) { const [modules, setModules] = useState([]); const [lessons, setLessons] = useState([]); const [loading, setLoading] = useState(true); const [moduleDialogOpen, setModuleDialogOpen] = useState(false); const [editingModule, setEditingModule] = useState(null); const [moduleTitle, setModuleTitle] = useState(''); const [lessonDialogOpen, setLessonDialogOpen] = useState(false); const [editingLesson, setEditingLesson] = useState(null); const [lessonForm, setLessonForm] = useState({ module_id: '', title: '', content: '', video_url: '', release_at: '', }); const [expandedModules, setExpandedModules] = useState>(new Set()); useEffect(() => { fetchData(); }, [productId]); const fetchData = async () => { const [modulesRes, lessonsRes] = await Promise.all([ supabase .from('bootcamp_modules') .select('*') .eq('product_id', productId) .order('position'), supabase .from('bootcamp_lessons') .select('*') .order('position'), ]); if (modulesRes.data) { setModules(modulesRes.data); // Expand all modules by default setExpandedModules(new Set(modulesRes.data.map(m => m.id))); } if (lessonsRes.data) { setLessons(lessonsRes.data); } setLoading(false); }; const getLessonsForModule = (moduleId: string) => { return lessons.filter(l => l.module_id === moduleId).sort((a, b) => a.position - b.position); }; // Module CRUD const handleNewModule = () => { setEditingModule(null); setModuleTitle(''); setModuleDialogOpen(true); }; const handleEditModule = (module: Module) => { setEditingModule(module); setModuleTitle(module.title); setModuleDialogOpen(true); }; const handleSaveModule = async () => { if (!moduleTitle.trim()) { toast({ title: 'Error', description: 'Module title is required', variant: 'destructive' }); return; } if (editingModule) { const { error } = await supabase .from('bootcamp_modules') .update({ title: moduleTitle }) .eq('id', editingModule.id); if (error) { toast({ title: 'Error', description: 'Failed to update module', variant: 'destructive' }); } else { toast({ title: 'Success', description: 'Module updated' }); setModuleDialogOpen(false); fetchData(); } } else { const maxPosition = modules.length > 0 ? Math.max(...modules.map(m => m.position)) : 0; const { error } = await supabase .from('bootcamp_modules') .insert({ product_id: productId, title: moduleTitle, position: maxPosition + 1 }); if (error) { toast({ title: 'Error', description: 'Failed to create module', variant: 'destructive' }); } else { toast({ title: 'Success', description: 'Module created' }); setModuleDialogOpen(false); fetchData(); } } }; const handleDeleteModule = async (moduleId: string) => { if (!confirm('Delete this module and all its lessons?')) return; const { error } = await supabase.from('bootcamp_modules').delete().eq('id', moduleId); if (error) { toast({ title: 'Error', description: 'Failed to delete module', variant: 'destructive' }); } else { toast({ title: 'Success', description: 'Module deleted' }); fetchData(); } }; const moveModule = async (moduleId: string, direction: 'up' | 'down') => { const index = modules.findIndex(m => m.id === moduleId); if ((direction === 'up' && index === 0) || (direction === 'down' && index === modules.length - 1)) return; const swapIndex = direction === 'up' ? index - 1 : index + 1; const currentModule = modules[index]; const swapModule = modules[swapIndex]; await Promise.all([ supabase.from('bootcamp_modules').update({ position: swapModule.position }).eq('id', currentModule.id), supabase.from('bootcamp_modules').update({ position: currentModule.position }).eq('id', swapModule.id), ]); fetchData(); }; // Lesson CRUD const handleNewLesson = (moduleId: string) => { setEditingLesson(null); setLessonForm({ module_id: moduleId, title: '', content: '', video_url: '', release_at: '', }); setLessonDialogOpen(true); }; const handleEditLesson = (lesson: Lesson) => { setEditingLesson(lesson); setLessonForm({ module_id: lesson.module_id, title: lesson.title, content: lesson.content || '', video_url: lesson.video_url || '', release_at: lesson.release_at ? lesson.release_at.split('T')[0] : '', }); setLessonDialogOpen(true); }; const handleSaveLesson = async () => { if (!lessonForm.title.trim()) { toast({ title: 'Error', description: 'Lesson title is required', variant: 'destructive' }); return; } const lessonData = { module_id: lessonForm.module_id, title: lessonForm.title, content: lessonForm.content || null, video_url: lessonForm.video_url || null, release_at: lessonForm.release_at ? new Date(lessonForm.release_at).toISOString() : null, }; if (editingLesson) { const { error } = await supabase .from('bootcamp_lessons') .update(lessonData) .eq('id', editingLesson.id); if (error) { toast({ title: 'Error', description: 'Failed to update lesson', variant: 'destructive' }); } else { toast({ title: 'Success', description: 'Lesson updated' }); setLessonDialogOpen(false); fetchData(); } } else { const moduleLessons = getLessonsForModule(lessonForm.module_id); const maxPosition = moduleLessons.length > 0 ? Math.max(...moduleLessons.map(l => l.position)) : 0; const { error } = await supabase .from('bootcamp_lessons') .insert({ ...lessonData, position: maxPosition + 1 }); if (error) { toast({ title: 'Error', description: 'Failed to create lesson', variant: 'destructive' }); } else { toast({ title: 'Success', description: 'Lesson created' }); setLessonDialogOpen(false); fetchData(); } } }; const handleDeleteLesson = async (lessonId: string) => { if (!confirm('Delete this lesson?')) return; const { error } = await supabase.from('bootcamp_lessons').delete().eq('id', lessonId); if (error) { toast({ title: 'Error', description: 'Failed to delete lesson', variant: 'destructive' }); } else { toast({ title: 'Success', description: 'Lesson deleted' }); fetchData(); } }; const moveLesson = async (lessonId: string, direction: 'up' | 'down') => { const lesson = lessons.find(l => l.id === lessonId); if (!lesson) return; const moduleLessons = getLessonsForModule(lesson.module_id); const index = moduleLessons.findIndex(l => l.id === lessonId); if ((direction === 'up' && index === 0) || (direction === 'down' && index === moduleLessons.length - 1)) return; const swapIndex = direction === 'up' ? index - 1 : index + 1; const swapLesson = moduleLessons[swapIndex]; await Promise.all([ supabase.from('bootcamp_lessons').update({ position: swapLesson.position }).eq('id', lesson.id), supabase.from('bootcamp_lessons').update({ position: lesson.position }).eq('id', swapLesson.id), ]); fetchData(); }; const toggleModule = (moduleId: string) => { const newExpanded = new Set(expandedModules); if (newExpanded.has(moduleId)) { newExpanded.delete(moduleId); } else { newExpanded.add(moduleId); } setExpandedModules(newExpanded); }; if (loading) { return
Loading curriculum...
; } return (

Curriculum

{modules.length === 0 ? (

No modules yet. Create your first module to start building the curriculum.

) : (
{modules.map((module, moduleIndex) => (
{expandedModules.has(module.id) && (
{getLessonsForModule(module.id).map((lesson, lessonIndex) => (
{lesson.title} {lesson.video_url && ( Video )}
))}
)}
))}
)} {/* Module Dialog */} {editingModule ? 'Edit Module' : 'New Module'}
setModuleTitle(e.target.value)} placeholder="Module title" className="border-2" />
{/* Lesson Dialog */} {editingLesson ? 'Edit Lesson' : 'New Lesson'}
setLessonForm({ ...lessonForm, title: e.target.value })} placeholder="Lesson title" className="border-2" />
setLessonForm({ ...lessonForm, video_url: e.target.value })} placeholder="https://youtube.com/... or https://vimeo.com/..." className="border-2" />