diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 4675a66..4319d54 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -49,6 +49,7 @@ import type { GetLearningPathResponse, GetHumanLanguageLessonsResponse, GetHumanLanguageHierarchyResponse, + CreateHumanLanguageLessonRequest, GetSubCourseEntryAssessmentResponse, ReorderItem, GetRatingsResponse, @@ -296,6 +297,9 @@ export const getHumanLanguageLessonsByCourse = (courseId: number, cefr_level: st export const getHumanLanguageHierarchy = () => http.get("/course-management/human-language/hierarchy") +export const createHumanLanguageLesson = (data: CreateHumanLanguageLessonRequest) => + http.post("/course-management/human-language/lessons", data) + export const getSubCourseEntryAssessment = (subCourseId: number) => http.get( `/question-sets/sub-courses/${subCourseId}/entry-assessment`, diff --git a/src/pages/content-management/HumanLanguagePage.tsx b/src/pages/content-management/HumanLanguagePage.tsx index 23732c3..60d4dff 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -1,11 +1,12 @@ import { useEffect, useMemo, useState } from "react" import { Link } from "react-router-dom" -import { BookOpen, ChevronDown, ChevronRight, Languages } from "lucide-react" +import { BookOpen, ChevronDown, ChevronRight, Languages, Loader2, Plus } 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 { getHumanLanguageHierarchy } from "../../api/courses.api" +import { createHumanLanguageLesson, getHumanLanguageHierarchy } from "../../api/courses.api" import type { HumanLanguageCourseTree, HumanLanguageSubCategoryTree } from "../../types/course.types" +import { toast } from "sonner" const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const type CefrLevel = (typeof CEFR_LEVELS)[number] @@ -18,20 +19,30 @@ export function HumanLanguagePage() { const [selectedCourseId, setSelectedCourseId] = useState("ALL") const [selectedLevel, setSelectedLevel] = useState("ALL") const [collapsedLevels, setCollapsedLevels] = useState([]) + const [creatingKey, setCreatingKey] = useState(null) + + const loadHierarchy = async () => { + setLoading(true) + try { + const res = await getHumanLanguageHierarchy() + const data = res.data?.data + setCategoryId(data?.category_id ?? null) + setSubCategories(data?.sub_categories ?? []) + } finally { + setLoading(false) + } + } useEffect(() => { - const loadHierarchy = async () => { + const run = async () => { setLoading(true) try { - const res = await getHumanLanguageHierarchy() - const data = res.data?.data - setCategoryId(data?.category_id ?? null) - setSubCategories(data?.sub_categories ?? []) + await loadHierarchy() } finally { setLoading(false) } } - loadHierarchy().catch(() => undefined) + run().catch(() => undefined) }, []) const filteredSubCategories = useMemo( @@ -58,6 +69,84 @@ export function HumanLanguagePage() { setCollapsedLevels((prev) => (prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level])) } + const parseModuleNumber = (title: string): number | null => { + const match = title.match(/module-(\d+)/i) + if (!match) return null + const value = Number(match[1]) + return Number.isFinite(value) ? value : null + } + + const parseSubModuleNumber = (title: string): { module: number; sub: number } | null => { + const match = title.match(/(?:sub-)?module-(\d+)\.(\d+)/i) + if (!match) return null + const module = Number(match[1]) + const sub = Number(match[2]) + if (!Number.isFinite(module) || !Number.isFinite(sub)) return null + return { module, sub } + } + + const handleCreateModule = async (courseId: number, level: string, modules: { title: string }[]) => { + const key = `module-${courseId}-${level}` + setCreatingKey(key) + try { + const maxExisting = modules + .map((m) => parseModuleNumber(m.title)) + .filter((v): v is number => v !== null) + .reduce((acc, n) => Math.max(acc, n), 0) + const next = maxExisting + 1 + const title = `Module-${next}` + await createHumanLanguageLesson({ + course_id: courseId, + cefr_level: level, + title, + description: `${level} ${title}`, + }) + toast.success(`${title} created`) + await loadHierarchy() + } catch (error) { + console.error("Failed to create module:", error) + toast.error("Failed to create module") + } finally { + setCreatingKey(null) + } + } + + const handleCreateSubModule = async ( + courseId: number, + level: string, + moduleTitle: string, + existingSubModules: { title: string }[], + ) => { + const moduleNo = parseModuleNumber(moduleTitle) + if (!moduleNo) { + toast.error("Cannot derive module number from title") + return + } + const key = `submodule-${courseId}-${level}-${moduleNo}` + setCreatingKey(key) + try { + const maxExisting = existingSubModules + .map((s) => parseSubModuleNumber(s.title)) + .filter((v): v is { module: number; sub: number } => v !== null && v.module === moduleNo) + .reduce((acc, item) => Math.max(acc, item.sub), 0) + const next = maxExisting + 1 + const title = `Module-${moduleNo}.${next}` + await createHumanLanguageLesson({ + course_id: courseId, + cefr_level: level, + title, + description: `${level} ${title}`, + }) + toast.success(`Sub-module ${moduleNo}.${next} created`) + await loadHierarchy() + } catch (error) { + console.error("Failed to create sub-module:", error) + toast.error("Failed to create sub-module") + } finally { + setCreatingKey(null) + } + } + return (
@@ -178,14 +267,58 @@ export function HumanLanguagePage() { ) : ( modulesByCourse.map((entry) => (
-

{entry.course.course_name}

+
+

{entry.course.course_name}

+ +
{entry.modules.map((module) => (
-

Module: {module.title}

+
+

Module: {module.title}

+ +
{module.sub_modules.map((subModule) => (
-

Sub-module: {subModule.title}

+
+

Sub-module: {subModule.title}

+ {categoryId ? ( +
+ + + + + + +
+ ) : null} +
{subModule.videos.map((video) => (
diff --git a/src/types/course.types.ts b/src/types/course.types.ts index 24c4684..79cf4c3 100644 --- a/src/types/course.types.ts +++ b/src/types/course.types.ts @@ -742,6 +742,15 @@ export interface GetHumanLanguageHierarchyResponse { metadata: unknown } +export interface CreateHumanLanguageLessonRequest { + course_id: number + title: string + description?: string + thumbnail?: string + display_order?: number + cefr_level: string +} + export interface GetSubCourseEntryAssessmentResponse { message: string data: QuestionSet | null