import { useMemo, useState, type ChangeEvent } from "react" import { ArrowLeft, ArrowRight, Check, GripVertical, Plus, Rocket, Trash2, Upload } from "lucide-react" import { Link, useLocation, useNavigate, useParams } from "react-router-dom" import { toast } from "sonner" import { addQuestionToSet, createLesson, createQuestion } from "../../api/courses.api" import { uploadVideoFile } from "../../api/files.api" import { PracticeQuestionEditorFields } from "../../components/content-management/PracticeQuestionEditorFields" import { Button } from "../../components/ui/button" import { Card } from "../../components/ui/card" import { Input } from "../../components/ui/input" import { SpinnerIcon } from "../../components/ui/spinner-icon" import type { QuestionOption } from "../../types/course.types" type Step = 1 | 2 | 3 | 4 type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO" type DifficultyLevel = "EASY" | "MEDIUM" | "HARD" type ResultStatus = "success" | "error" interface MCQOption { text: string isCorrect: boolean } interface Question { id: string questionText: string questionType: QuestionType difficultyLevel: DifficultyLevel points: number tips: string explanation: string options: MCQOption[] voicePrompt: string sampleAnswerVoicePrompt: string audioCorrectAnswerText: string shortAnswers: string[] imageUrl: string } const STEPS = [ { number: 1, label: "Context" }, { number: 2, label: "Questions" }, { number: 3, label: "Review" }, ] function createEmptyQuestion(id: string): Question { return { id, questionText: "", questionType: "MCQ", difficultyLevel: "EASY", points: 1, tips: "", explanation: "", options: [ { text: "", isCorrect: true }, { text: "", isCorrect: false }, { text: "", isCorrect: false }, { text: "", isCorrect: false }, ], voicePrompt: "", sampleAnswerVoicePrompt: "", audioCorrectAnswerText: "", shortAnswers: [], imageUrl: "", } } function introVideoUrlFromUploadResponse(data: { url?: string; embed_url?: string } | undefined): string | null { if (!data) return null const pageUrl = data.url?.trim() const embedUrl = data.embed_url?.trim() if (embedUrl) { const hashFromUrl = pageUrl ? pageUrl.split("/").filter(Boolean).at(-1) : undefined return hashFromUrl ? `${embedUrl}?h=${hashFromUrl}` : embedUrl } return pageUrl || null } function toVimeoEmbedUrl(rawUrl: string): string | null { try { const parsed = new URL(rawUrl.trim()) const host = parsed.hostname.toLowerCase() if (!host.includes("vimeo.com")) return null if (host.includes("player.vimeo.com") && parsed.pathname.includes("/video/")) return parsed.toString() const segments = parsed.pathname.split("/").filter(Boolean) const videoId = segments.find((segment) => /^\d+$/.test(segment)) if (!videoId) return null const hash = parsed.searchParams.get("h") return hash ? `https://player.vimeo.com/video/${videoId}?h=${encodeURIComponent(hash)}` : `https://player.vimeo.com/video/${videoId}` } catch { return null } } function isDirectVideoFile(url: string): boolean { const clean = url.split("?")[0].toLowerCase() return /\.(mp4|webm|ogg|mov|m4v)$/.test(clean) } function questionTypeLabel(type: QuestionType): string { if (type === "TRUE_FALSE") return "True/False" if (type === "SHORT") return "Short Answer" if (type === "AUDIO") return "Audio" return "Multiple Choice" } export function AddNewLessonPage() { const { categoryId, courseId, subModuleId } = useParams() const navigate = useNavigate() const location = useLocation() const backTo = useMemo(() => { if (location.pathname.includes("/content/human-language/") && location.pathname.includes("/sub-module/")) { return `/content/human-language/${categoryId}/${courseId}/sub-module/${subModuleId}` } return `/content/category/${categoryId}/courses/${courseId}/sub-modules/${subModuleId}` }, [categoryId, courseId, subModuleId, location.pathname]) const [currentStep, setCurrentStep] = useState(1) const [saving, setSaving] = useState(false) const [resultStatus, setResultStatus] = useState(null) const [resultMessage, setResultMessage] = useState("") const [lastSavedStatus, setLastSavedStatus] = useState<"DRAFT" | "PUBLISHED" | null>(null) const [lessonTitle, setLessonTitle] = useState("") const [lessonDescription, setLessonDescription] = useState("") const [introVideoUrl, setIntroVideoUrl] = useState("") const [uploadingIntroVideo, setUploadingIntroVideo] = useState(false) const [questions, setQuestions] = useState([createEmptyQuestion("1")]) const handleNext = () => setCurrentStep((s) => (s < 3 ? ((s + 1) as Step) : s)) const handleBack = () => setCurrentStep((s) => (s > 1 ? ((s - 1) as Step) : s)) const handleIntroVideoFileChange = async (event: ChangeEvent) => { const file = event.target.files?.[0] event.target.value = "" if (!file) return setUploadingIntroVideo(true) try { const uploadRes = await uploadVideoFile(file, { title: lessonTitle.trim() || file.name.replace(/\.[^.]+$/, "") || "Lesson intro", description: lessonDescription.trim() || undefined, }) const finalUrl = introVideoUrlFromUploadResponse(uploadRes.data?.data) if (!finalUrl) throw new Error("Missing uploaded video url") setIntroVideoUrl(finalUrl) toast.success("Intro video uploaded") } catch (error) { console.error("Failed to upload lesson intro video:", error) toast.error("Failed to upload intro video") } finally { setUploadingIntroVideo(false) } } const handleIntroVideoUrlBlur = async () => { const source = introVideoUrl.trim() if (!source || !/^https?:\/\//i.test(source)) return const vimeoEmbed = toVimeoEmbedUrl(source) if (vimeoEmbed) { setIntroVideoUrl(vimeoEmbed) return } if (isDirectVideoFile(source)) { setIntroVideoUrl(source) return } // For non-direct URLs, automatically try server-side import via /files/upload. setUploadingIntroVideo(true) try { const uploadRes = await uploadVideoFile(source, { title: lessonTitle.trim() || "Lesson intro", description: lessonDescription.trim() || undefined, }) const finalUrl = introVideoUrlFromUploadResponse(uploadRes.data?.data) if (!finalUrl) throw new Error("Missing uploaded video url") setIntroVideoUrl(finalUrl) toast.success("Intro video URL imported") } catch (error) { console.error("Failed to import intro video URL:", error) toast.error("Failed to import intro video URL") } finally { setUploadingIntroVideo(false) } } const introVideoPreview = useMemo(() => { const raw = introVideoUrl.trim() if (!raw) return null const vimeoEmbedUrl = toVimeoEmbedUrl(raw) if (vimeoEmbedUrl) return { kind: "vimeo" as const, url: vimeoEmbedUrl } if (isDirectVideoFile(raw)) return { kind: "video" as const, url: raw } return null }, [introVideoUrl]) const reviewQuestions = useMemo(() => questions, [questions]) const addQuestion = () => setQuestions((prev) => [...prev, createEmptyQuestion(String(Date.now()))]) const removeQuestion = (id: string) => setQuestions((prev) => (prev.length > 1 ? prev.filter((q) => q.id !== id) : prev)) const updateQuestion = (id: string, updates: Partial) => setQuestions((prev) => prev.map((q) => (q.id === id ? { ...q, ...updates } : q))) const saveLesson = async (status: "DRAFT" | "PUBLISHED") => { if (!subModuleId) { toast.error("Missing sub-module id") return } setSaving(true) try { const lessonRes = await createLesson({ sub_module_id: Number(subModuleId), title: lessonTitle.trim() || "Untitled Lesson", description: lessonDescription.trim() || undefined, intro_video_url: introVideoUrl.trim() || undefined, status, }) const questionSetId = lessonRes.data?.data?.id if (questionSetId) { for (let i = 0; i < questions.length; i++) { const q = questions[i] if (!q.questionText.trim()) continue const options: QuestionOption[] = q.questionType === "MCQ" ? q.options.map((opt, idx) => ({ option_order: idx + 1, option_text: opt.text, is_correct: opt.isCorrect, })) : [] const qRes = await createQuestion({ question_text: q.questionText, question_type: q.questionType, difficulty_level: q.difficultyLevel, points: q.points, tips: q.tips || undefined, explanation: q.explanation || undefined, status: "PUBLISHED", options: options.length > 0 ? options : undefined, voice_prompt: q.voicePrompt || undefined, sample_answer_voice_prompt: q.sampleAnswerVoicePrompt || undefined, audio_correct_answer_text: q.audioCorrectAnswerText || undefined, image_url: q.imageUrl.trim() || undefined, short_answers: q.shortAnswers.length > 0 ? q.shortAnswers : undefined, }) const questionId = qRes.data?.data?.id if (questionId) { await addQuestionToSet(questionSetId, { question_id: questionId, display_order: i + 1 }) } } } setResultStatus("success") setResultMessage(status === "PUBLISHED" ? "Lesson published successfully." : "Lesson saved as draft.") setLastSavedStatus(status) setCurrentStep(4) } catch (error) { console.error("Failed to save lesson:", error) setResultStatus("error") setResultMessage(error instanceof Error ? error.message : "Failed to save lesson") setLastSavedStatus(null) setCurrentStep(4) } finally { setSaving(false) } } return (
{currentStep !== 4 ? ( <> Back to Sub-course

Add New Lesson

Create a lesson backed by `question_sets` and attach it through `sub_module_lessons`.

{STEPS.map((step, index) => (
step.number ? "bg-brand-500 text-white" : "border-2 border-grayScale-300 bg-white text-grayScale-400" }`} > {currentStep > step.number ? : step.number}
{step.label}
{index < STEPS.length - 1 ? (
step.number ? "bg-brand-500" : "bg-grayScale-200"}`} /> ) : null}
))}
) : null} {currentStep === 1 ? (

Step 1: Context

Define lesson metadata that will be stored in the linked question set.

setLessonTitle(e.target.value)} placeholder="Enter lesson title" className="h-11" />