From e477595578605dd1f853e91767f1a58e876320a2 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 7 Apr 2026 05:40:29 -0700 Subject: [PATCH] integrate human language page with dedicated backend API Switch the Human Language admin page to use CEFR-filtered backend endpoints and add typed API contracts for human language lessons. Made-with: Cursor --- src/api/courses.api.ts | 6 ++ .../content-management/HumanLanguagePage.tsx | 56 +++++++++---------- src/types/course.types.ts | 27 +++++++++ 3 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index a9687fe..32dfa07 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -47,6 +47,7 @@ import type { GetSubCoursePrerequisitesResponse, AddSubCoursePrerequisiteRequest, GetLearningPathResponse, + GetHumanLanguageLessonsResponse, GetSubCourseEntryAssessmentResponse, ReorderItem, GetRatingsResponse, @@ -286,6 +287,11 @@ export const removeSubCoursePrerequisite = (subCourseId: number, prerequisiteId: export const getLearningPath = (courseId: number) => http.get(`/course-management/courses/${courseId}/learning-path`) +export const getHumanLanguageLessonsByCourse = (courseId: number, cefr_level: string) => + http.get(`/course-management/human-language/courses/${courseId}/lessons`, { + params: { cefr_level }, + }) + 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 8ac9cf7..f5d911a 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -4,8 +4,8 @@ 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, getLearningPath } from "../../api/courses.api" -import type { Course, CourseCategory, LearningPathSubCourse } from "../../types/course.types" +import { getCourseCategories, getCoursesByCategory, getHumanLanguageLessonsByCourse } from "../../api/courses.api" +import type { Course, CourseCategory, HumanLanguageLesson } from "../../types/course.types" const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const type CefrLevel = (typeof CEFR_LEVELS)[number] @@ -18,7 +18,9 @@ export function HumanLanguagePage() { const [selectedCourseId, setSelectedCourseId] = useState(null) const [selectedLevel, setSelectedLevel] = useState("ALL") const [collapsedLevels, setCollapsedLevels] = useState([]) - const [subCourses, setSubCourses] = useState([]) + const [lessonsByLevel, setLessonsByLevel] = useState>( + Object.fromEntries(CEFR_LEVELS.map((level) => [level, []])) as Record, + ) useEffect(() => { const loadCategories = async () => { @@ -57,35 +59,31 @@ export function HumanLanguagePage() { useEffect(() => { if (!selectedCourseId) return - const loadPath = async () => { + const loadByLevels = async () => { setLoading(true) try { - const res = await getLearningPath(selectedCourseId) - setSubCourses(res.data?.data?.sub_courses ?? []) + 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) } } - loadPath().catch(() => undefined) - }, [selectedCourseId]) - - const grouped = useMemo(() => { - const base = Object.fromEntries(CEFR_LEVELS.map((level) => [level, [] as LearningPathSubCourse[]])) as Record< - CefrLevel, - LearningPathSubCourse[] - > - for (const subCourse of subCourses) { - const level = (subCourse.sub_level ?? "").toUpperCase() as CefrLevel - if (CEFR_LEVELS.includes(level)) { - base[level].push(subCourse) - } - } - return base - }, [subCourses]) + loadByLevels().catch(() => undefined) + }, [selectedCourseId, selectedLevel]) const levelRows = useMemo( - () => (selectedLevel === "ALL" ? CEFR_LEVELS : [selectedLevel]).map((level) => ({ level, rows: grouped[level] })), - [grouped, selectedLevel], + () => (selectedLevel === "ALL" ? CEFR_LEVELS : [selectedLevel]).map((level) => ({ level, rows: lessonsByLevel[level] })), + [lessonsByLevel, selectedLevel], ) const toggleLevel = (level: CefrLevel) => { @@ -194,14 +192,14 @@ export function HumanLanguagePage() { {rows.length === 0 ? (

No lessons found for this level.

) : ( - rows.map((subCourse) => ( -
-

{subCourse.title}

+ rows.map((lesson) => ( +
+

{lesson.title}

- {subCourse.videos.length} lesson video(s) • {subCourse.practices.length} practice(s) + {lesson.video_count} lesson video(s) • {lesson.practice_count} practice(s)

- {subCourse.videos.map((video) => ( + {lesson.videos.map((video) => (
{video.title} diff --git a/src/types/course.types.ts b/src/types/course.types.ts index 7dbc57e..9f2f715 100644 --- a/src/types/course.types.ts +++ b/src/types/course.types.ts @@ -673,6 +673,33 @@ export interface GetLearningPathResponse { metadata: unknown } +export interface HumanLanguageLesson { + id: number + course_id: number + title: string + description?: string | null + thumbnail?: string | null + display_order: number + level: string + video_count: number + practice_count: number + videos: LearningPathVideo[] + practices: LearningPathPractice[] +} + +export interface GetHumanLanguageLessonsResponse { + message: string + data: { + course_id: number + course_title: string + cefr_level: string + lessons: HumanLanguageLesson[] + } + success: boolean + status_code: number + metadata: unknown +} + export interface GetSubCourseEntryAssessmentResponse { message: string data: QuestionSet | null