diff --git a/src/pages/content-management/AddNewPracticePage.tsx b/src/pages/content-management/AddNewPracticePage.tsx index 5dc295e..c8e6306 100644 --- a/src/pages/content-management/AddNewPracticePage.tsx +++ b/src/pages/content-management/AddNewPracticePage.tsx @@ -1,5 +1,5 @@ -import { useRef, useState, type ChangeEvent } from "react" -import { Link, useParams, useNavigate } from "react-router-dom" +import { useMemo, useRef, useState, type ChangeEvent } from "react" +import { Link, useLocation, useParams, useNavigate } from "react-router-dom" import { ArrowLeft, ArrowRight, ChevronDown, Grid3X3, Check, Plus, Trash2, GripVertical, X, Edit, Rocket, Loader2, Upload } from "lucide-react" import { toast } from "sonner" import { Card } from "../../components/ui/card" @@ -12,7 +12,7 @@ import type { QuestionOption } from "../../types/course.types" type Step = 1 | 2 | 3 | 4 | 5 type ResultStatus = "success" | "error" -type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" +type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO" type DifficultyLevel = "EASY" | "MEDIUM" | "HARD" interface Persona { @@ -37,6 +37,7 @@ interface Question { options: MCQOption[] voicePrompt: string sampleAnswerVoicePrompt: string + audioCorrectAnswerText: string shortAnswers: string[] } @@ -87,13 +88,21 @@ function createEmptyQuestion(id: string): Question { ], voicePrompt: "", sampleAnswerVoicePrompt: "", + audioCorrectAnswerText: "", shortAnswers: [], } } export function AddNewPracticePage() { const { categoryId, courseId, subCourseId } = useParams() + const location = useLocation() const navigate = useNavigate() + const searchParams = new URLSearchParams(location.search) + const source = searchParams.get("source") + const backTo = useMemo(() => { + if (source === "human-language") return "/content/human-language" + return `/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}` + }, [source, categoryId, courseId, subCourseId]) const [currentStep, setCurrentStep] = useState(1) const [saving, setSaving] = useState(false) @@ -134,7 +143,7 @@ export function AddNewPracticePage() { } const handleCancel = () => { - navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`) + navigate(backTo) } const handleIntroVideoFileChange = async (event: ChangeEvent) => { @@ -247,6 +256,7 @@ export function AddNewPracticePage() { 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, short_answers: q.shortAnswers.length > 0 ? q.shortAnswers : undefined, }) @@ -297,7 +307,7 @@ export function AddNewPracticePage() { <> {/* Back Link */} @@ -588,7 +598,7 @@ export function AddNewPracticePage() {

Step 3: Questions

- Add MCQ, True/False, or Short Answer items. Use the full width for stems and options. + Add MCQ, True/False, Short Answer, or Audio items. Use the full width for stems and options.

@@ -636,6 +646,7 @@ export function AddNewPracticePage() { + @@ -800,6 +811,19 @@ export function AddNewPracticePage() { /> + + {question.questionType === "AUDIO" && ( +
+ + updateQuestion(question.id, { audioCorrectAnswerText: e.target.value })} + placeholder="Expected correct answer text for audio response" + /> +
+ )} ))} @@ -925,7 +949,13 @@ export function AddNewPracticePage() {

{question.questionText}

- {question.questionType === "MCQ" ? "Multiple Choice" : question.questionType === "TRUE_FALSE" ? "True/False" : "Short Answer"} + {question.questionType === "MCQ" + ? "Multiple Choice" + : question.questionType === "TRUE_FALSE" + ? "True/False" + : question.questionType === "AUDIO" + ? "Audio" + : "Short Answer"} {question.difficultyLevel} @@ -1001,7 +1031,7 @@ export function AddNewPracticePage() {
diff --git a/src/pages/content-management/HumanLanguagePage.tsx b/src/pages/content-management/HumanLanguagePage.tsx index b03f000..78b03c6 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -1,10 +1,10 @@ import { useEffect, useMemo, useState } from "react" import { Link } from "react-router-dom" -import { BookOpen, ChevronDown, ChevronRight, Languages, Loader2, Plus, Search } from "lucide-react" +import { BookOpen, ChevronDown, ChevronRight, Languages, Loader2, Plus, Search, Trash2 } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card" import { Button } from "../../components/ui/button" import { SpinnerIcon } from "../../components/ui/spinner-icon" -import { createCourse, createCourseCategory, createHumanLanguageLesson, getHumanLanguageHierarchy } from "../../api/courses.api" +import { createCourse, createCourseCategory, createHumanLanguageLesson, deleteSubCourse, getHumanLanguageHierarchy } from "../../api/courses.api" import type { HumanLanguageCourseTree, HumanLanguageSubCategoryTree } from "../../types/course.types" import { toast } from "sonner" @@ -24,6 +24,7 @@ export function HumanLanguagePage() { const [quickCourseName, setQuickCourseName] = useState("") const [quickSearch, setQuickSearch] = useState("") const [quickCreating, setQuickCreating] = useState(false) + const [deletingKey, setDeletingKey] = useState(null) const loadHierarchy = async () => { setLoading(true) @@ -69,6 +70,13 @@ export function HumanLanguagePage() { [availableCourses, selectedCourseId], ) + const levelsForSelectedCourse = useMemo(() => { + if (selectedCourseId === "ALL") return [] as string[] + const course = selectedCourses.find((c) => c.course_id === selectedCourseId) + if (!course) return [] + return course.levels.filter((l) => l.modules.length > 0).map((l) => l.level.toUpperCase()) + }, [selectedCourses, selectedCourseId]) + const toggleLevel = (level: CefrLevel) => { setCollapsedLevels((prev) => (prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level])) } @@ -151,6 +159,55 @@ export function HumanLanguagePage() { } } + const handleDeleteSubModules = async (ids: number[], key: string, successMessage: string) => { + if (ids.length === 0) return + const proceed = window.confirm("This action will permanently delete selected item(s). Continue?") + if (!proceed) return + setDeletingKey(key) + try { + for (const id of ids) { + await deleteSubCourse(id) + } + toast.success(successMessage) + await loadHierarchy() + } catch (error) { + console.error("Failed to delete item(s):", error) + toast.error("Failed to delete item(s)") + } finally { + setDeletingKey(null) + } + } + + const handleCreateNextLevel = async () => { + if (selectedCourseId === "ALL") { + toast.error("Select a specific course first") + return + } + const existing = new Set(levelsForSelectedCourse) + const next = CEFR_LEVELS.find((level) => !existing.has(level)) + if (!next) { + toast.error("All CEFR levels are already created") + return + } + const key = `next-level-${selectedCourseId}-${next}` + setCreatingKey(key) + try { + await createHumanLanguageLesson({ + course_id: selectedCourseId, + cefr_level: next, + title: "Module-1", + description: `${next} Module-1`, + }) + toast.success(`${next} created with Module-1`) + await loadHierarchy() + } catch (error) { + console.error("Failed to create next level:", error) + toast.error("Failed to create next level") + } finally { + setCreatingKey(null) + } + } + const handleQuickCreatePath = async () => { if (!quickSubCategoryName.trim() || !quickCourseName.trim()) { toast.error("Subcategory and course names are required") @@ -256,6 +313,17 @@ export function HumanLanguagePage() {
+ +
+ +
+
{categoryId && selectedCourseId !== "ALL" ? ( @@ -328,7 +396,7 @@ export function HumanLanguagePage() { modules: levelNode?.modules ?? [], } }) - .filter((entry) => entry.modules.length > 0 || selectedCourses.length > 0) + .filter((entry) => entry.modules.length > 0 || (selectedCourses.length > 0 && level === "A1")) return (
{!collapsedLevels.includes(level) ? ( @@ -375,21 +459,39 @@ export function HumanLanguagePage() {

Module: {module.title}

- +
+ + +
{module.sub_modules.map((subModule) => (
@@ -400,9 +502,25 @@ export function HumanLanguagePage() { - + +
) : null}