diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 32dfa07..4675a66 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -48,6 +48,7 @@ import type { AddSubCoursePrerequisiteRequest, GetLearningPathResponse, GetHumanLanguageLessonsResponse, + GetHumanLanguageHierarchyResponse, GetSubCourseEntryAssessmentResponse, ReorderItem, GetRatingsResponse, @@ -292,6 +293,9 @@ export const getHumanLanguageLessonsByCourse = (courseId: number, cefr_level: st params: { cefr_level }, }) +export const getHumanLanguageHierarchy = () => + http.get("/course-management/human-language/hierarchy") + 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 03a4b98..23732c3 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -4,99 +4,55 @@ import { BookOpen, ChevronDown, ChevronRight, Languages } 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 { getCourseCategories, getCoursesByCategory, getHumanLanguageLessonsByCourse } from "../../api/courses.api" -import type { Course, HumanLanguageLesson } from "../../types/course.types" +import { getHumanLanguageHierarchy } from "../../api/courses.api" +import type { HumanLanguageCourseTree, HumanLanguageSubCategoryTree } from "../../types/course.types" const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const type CefrLevel = (typeof CEFR_LEVELS)[number] export function HumanLanguagePage() { const [loading, setLoading] = useState(false) - const [courses, setCourses] = useState([]) - const [selectedCategoryId, setSelectedCategoryId] = useState(null) - const [selectedCourseId, setSelectedCourseId] = useState(null) - const [selectedLessonId, setSelectedLessonId] = useState("ALL") + const [categoryId, setCategoryId] = useState(null) + const [subCategories, setSubCategories] = useState([]) + const [selectedSubCategoryId, setSelectedSubCategoryId] = useState("ALL") + const [selectedCourseId, setSelectedCourseId] = useState("ALL") const [selectedLevel, setSelectedLevel] = useState("ALL") const [collapsedLevels, setCollapsedLevels] = useState([]) - const [lessonsByLevel, setLessonsByLevel] = useState>( - Object.fromEntries(CEFR_LEVELS.map((level) => [level, []])) as Record, - ) useEffect(() => { - const loadCategories = async () => { + const loadHierarchy = async () => { setLoading(true) try { - const res = await getCourseCategories() - const items = res.data?.data?.categories ?? [] - const humanLanguageCategory = - items.find((c) => c.name.toLowerCase().includes("human language")) ?? - items.find((c) => c.name.toLowerCase().includes("language")) ?? - null - setSelectedCategoryId(humanLanguageCategory?.id ?? (items[0]?.id ?? null)) + const res = await getHumanLanguageHierarchy() + const data = res.data?.data + setCategoryId(data?.category_id ?? null) + setSubCategories(data?.sub_categories ?? []) } finally { setLoading(false) } } - loadCategories().catch(() => undefined) + loadHierarchy().catch(() => undefined) }, []) - useEffect(() => { - if (!selectedCategoryId) return - const loadCourses = async () => { - setLoading(true) - try { - const res = await getCoursesByCategory(selectedCategoryId) - const items = res.data?.data?.courses ?? [] - setCourses(items) - setSelectedCourseId(items[0]?.id ?? null) - setSelectedLessonId("ALL") - } finally { - setLoading(false) - } - } - loadCourses().catch(() => undefined) - }, [selectedCategoryId]) - - useEffect(() => { - if (!selectedCourseId) return - const loadByLevels = async () => { - setLoading(true) - try { - if (selectedLevel === "ALL") { - const entries = await Promise.all( - CEFR_LEVELS.map(async (level) => { - const res = await getHumanLanguageLessonsByCourse(selectedCourseId, level) - return [level, res.data?.data?.lessons ?? []] as const - }), - ) - setLessonsByLevel(Object.fromEntries(entries) as Record) - } else { - const res = await getHumanLanguageLessonsByCourse(selectedCourseId, selectedLevel) - setLessonsByLevel((prev) => ({ ...prev, [selectedLevel]: res.data?.data?.lessons ?? [] })) - } - } finally { - setLoading(false) - } - } - loadByLevels().catch(() => undefined) - }, [selectedCourseId, selectedLevel]) - - const levelRows = useMemo( - () => (selectedLevel === "ALL" ? CEFR_LEVELS : [selectedLevel]).map((level) => ({ level, rows: lessonsByLevel[level] })), - [lessonsByLevel, selectedLevel], + const filteredSubCategories = useMemo( + () => + selectedSubCategoryId === "ALL" + ? subCategories + : subCategories.filter((s) => s.sub_category_id === selectedSubCategoryId), + [subCategories, selectedSubCategoryId], + ) + + const availableCourses = useMemo(() => { + return filteredSubCategories.flatMap((s) => s.courses) + }, [filteredSubCategories]) + + const selectedCourses = useMemo( + () => + selectedCourseId === "ALL" + ? availableCourses + : availableCourses.filter((c) => c.course_id === selectedCourseId), + [availableCourses, selectedCourseId], ) - const lessonOptions = useMemo(() => { - const seen = new Set() - const options: { id: number; title: string }[] = [] - for (const rows of Object.values(lessonsByLevel)) { - for (const lesson of rows) { - if (seen.has(lesson.id)) continue - seen.add(lesson.id) - options.push({ id: lesson.id, title: lesson.title }) - } - } - return options.sort((a, b) => a.title.localeCompare(b.title)) - }, [lessonsByLevel]) const toggleLevel = (level: CefrLevel) => { setCollapsedLevels((prev) => (prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level])) @@ -127,12 +83,15 @@ export function HumanLanguagePage() { @@ -141,15 +100,15 @@ export function HumanLanguagePage() { @@ -172,9 +131,9 @@ export function HumanLanguagePage() { - {selectedCategoryId && selectedCourseId ? ( + {categoryId && selectedCourseId !== "ALL" ? (
- +
@@ -187,9 +146,16 @@ export function HumanLanguagePage() { ) : (
- {levelRows.map(({ level, rows }) => { - const filteredRows = - selectedLessonId === "ALL" ? rows : rows.filter((lesson) => lesson.id === selectedLessonId) + {CEFR_LEVELS.filter((l) => selectedLevel === "ALL" || l === selectedLevel).map((level) => { + const modulesByCourse = selectedCourses + .map((course: HumanLanguageCourseTree) => { + const levelNode = course.levels.find((item) => item.level.toUpperCase() === level) + return { + course, + modules: levelNode?.modules ?? [], + } + }) + .filter((entry) => entry.modules.length > 0) return (
{!collapsedLevels.includes(level) ? ( - {filteredRows.length === 0 ? ( + {modulesByCourse.length === 0 ? (

No lessons found for this level.

) : ( - filteredRows.map((lesson) => ( -
-

{lesson.title}

-

- {lesson.video_count} lesson video(s) • {lesson.practice_count} practice(s) -

-
- {lesson.videos.map((video) => ( -
- - {video.title} + modulesByCourse.map((entry) => ( +
+

{entry.course.course_name}

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

Module: {module.title}

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

Sub-module: {subModule.title}

+
+ {subModule.videos.map((video) => ( +
+ + {video.title} +
+ ))} + {subModule.practices.map((practice) => ( +
+ Practice: {practice.title} ({practice.question_count} audio question(s)) +
+ ))} +
+
+ ))}
))}
diff --git a/src/types/course.types.ts b/src/types/course.types.ts index 9f2f715..24c4684 100644 --- a/src/types/course.types.ts +++ b/src/types/course.types.ts @@ -700,6 +700,48 @@ export interface GetHumanLanguageLessonsResponse { metadata: unknown } +export interface HumanLanguageSubModule { + id: number + title: string + videos: LearningPathVideo[] + practices: LearningPathPractice[] +} + +export interface HumanLanguageModule { + id: number + title: string + sub_modules: HumanLanguageSubModule[] +} + +export interface HumanLanguageLevelTree { + level: string + modules: HumanLanguageModule[] +} + +export interface HumanLanguageCourseTree { + course_id: number + course_name: string + levels: HumanLanguageLevelTree[] +} + +export interface HumanLanguageSubCategoryTree { + sub_category_id: number + sub_category_name: string + courses: HumanLanguageCourseTree[] +} + +export interface GetHumanLanguageHierarchyResponse { + message: string + data: { + category_id: number + category_name: string + sub_categories: HumanLanguageSubCategoryTree[] + } + success: boolean + status_code: number + metadata: unknown +} + export interface GetSubCourseEntryAssessmentResponse { message: string data: QuestionSet | null