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
This commit is contained in:
parent
53a72bef2d
commit
e477595578
|
|
@ -47,6 +47,7 @@ import type {
|
||||||
GetSubCoursePrerequisitesResponse,
|
GetSubCoursePrerequisitesResponse,
|
||||||
AddSubCoursePrerequisiteRequest,
|
AddSubCoursePrerequisiteRequest,
|
||||||
GetLearningPathResponse,
|
GetLearningPathResponse,
|
||||||
|
GetHumanLanguageLessonsResponse,
|
||||||
GetSubCourseEntryAssessmentResponse,
|
GetSubCourseEntryAssessmentResponse,
|
||||||
ReorderItem,
|
ReorderItem,
|
||||||
GetRatingsResponse,
|
GetRatingsResponse,
|
||||||
|
|
@ -286,6 +287,11 @@ export const removeSubCoursePrerequisite = (subCourseId: number, prerequisiteId:
|
||||||
export const getLearningPath = (courseId: number) =>
|
export const getLearningPath = (courseId: number) =>
|
||||||
http.get<GetLearningPathResponse>(`/course-management/courses/${courseId}/learning-path`)
|
http.get<GetLearningPathResponse>(`/course-management/courses/${courseId}/learning-path`)
|
||||||
|
|
||||||
|
export const getHumanLanguageLessonsByCourse = (courseId: number, cefr_level: string) =>
|
||||||
|
http.get<GetHumanLanguageLessonsResponse>(`/course-management/human-language/courses/${courseId}/lessons`, {
|
||||||
|
params: { cefr_level },
|
||||||
|
})
|
||||||
|
|
||||||
export const getSubCourseEntryAssessment = (subCourseId: number) =>
|
export const getSubCourseEntryAssessment = (subCourseId: number) =>
|
||||||
http.get<GetSubCourseEntryAssessmentResponse>(
|
http.get<GetSubCourseEntryAssessmentResponse>(
|
||||||
`/question-sets/sub-courses/${subCourseId}/entry-assessment`,
|
`/question-sets/sub-courses/${subCourseId}/entry-assessment`,
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { BookOpen, ChevronDown, ChevronRight, Languages } from "lucide-react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
||||||
import { Button } from "../../components/ui/button"
|
import { Button } from "../../components/ui/button"
|
||||||
import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
||||||
import { getCourseCategories, getCoursesByCategory, getLearningPath } from "../../api/courses.api"
|
import { getCourseCategories, getCoursesByCategory, getHumanLanguageLessonsByCourse } from "../../api/courses.api"
|
||||||
import type { Course, CourseCategory, LearningPathSubCourse } from "../../types/course.types"
|
import type { Course, CourseCategory, HumanLanguageLesson } from "../../types/course.types"
|
||||||
|
|
||||||
const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const
|
const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const
|
||||||
type CefrLevel = (typeof CEFR_LEVELS)[number]
|
type CefrLevel = (typeof CEFR_LEVELS)[number]
|
||||||
|
|
@ -18,7 +18,9 @@ export function HumanLanguagePage() {
|
||||||
const [selectedCourseId, setSelectedCourseId] = useState<number | null>(null)
|
const [selectedCourseId, setSelectedCourseId] = useState<number | null>(null)
|
||||||
const [selectedLevel, setSelectedLevel] = useState<CefrLevel | "ALL">("ALL")
|
const [selectedLevel, setSelectedLevel] = useState<CefrLevel | "ALL">("ALL")
|
||||||
const [collapsedLevels, setCollapsedLevels] = useState<CefrLevel[]>([])
|
const [collapsedLevels, setCollapsedLevels] = useState<CefrLevel[]>([])
|
||||||
const [subCourses, setSubCourses] = useState<LearningPathSubCourse[]>([])
|
const [lessonsByLevel, setLessonsByLevel] = useState<Record<CefrLevel, HumanLanguageLesson[]>>(
|
||||||
|
Object.fromEntries(CEFR_LEVELS.map((level) => [level, []])) as Record<CefrLevel, HumanLanguageLesson[]>,
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCategories = async () => {
|
const loadCategories = async () => {
|
||||||
|
|
@ -57,35 +59,31 @@ export function HumanLanguagePage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedCourseId) return
|
if (!selectedCourseId) return
|
||||||
const loadPath = async () => {
|
const loadByLevels = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await getLearningPath(selectedCourseId)
|
if (selectedLevel === "ALL") {
|
||||||
setSubCourses(res.data?.data?.sub_courses ?? [])
|
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<CefrLevel, HumanLanguageLesson[]>)
|
||||||
|
} else {
|
||||||
|
const res = await getHumanLanguageLessonsByCourse(selectedCourseId, selectedLevel)
|
||||||
|
setLessonsByLevel((prev) => ({ ...prev, [selectedLevel]: res.data?.data?.lessons ?? [] }))
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadPath().catch(() => undefined)
|
loadByLevels().catch(() => undefined)
|
||||||
}, [selectedCourseId])
|
}, [selectedCourseId, selectedLevel])
|
||||||
|
|
||||||
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])
|
|
||||||
|
|
||||||
const levelRows = useMemo(
|
const levelRows = useMemo(
|
||||||
() => (selectedLevel === "ALL" ? CEFR_LEVELS : [selectedLevel]).map((level) => ({ level, rows: grouped[level] })),
|
() => (selectedLevel === "ALL" ? CEFR_LEVELS : [selectedLevel]).map((level) => ({ level, rows: lessonsByLevel[level] })),
|
||||||
[grouped, selectedLevel],
|
[lessonsByLevel, selectedLevel],
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleLevel = (level: CefrLevel) => {
|
const toggleLevel = (level: CefrLevel) => {
|
||||||
|
|
@ -194,14 +192,14 @@ export function HumanLanguagePage() {
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<p className="text-sm text-grayScale-500">No lessons found for this level.</p>
|
<p className="text-sm text-grayScale-500">No lessons found for this level.</p>
|
||||||
) : (
|
) : (
|
||||||
rows.map((subCourse) => (
|
rows.map((lesson) => (
|
||||||
<div key={subCourse.id} className="rounded-xl border border-grayScale-200 bg-white p-3">
|
<div key={lesson.id} className="rounded-xl border border-grayScale-200 bg-white p-3">
|
||||||
<p className="text-sm font-semibold text-grayScale-900">{subCourse.title}</p>
|
<p className="text-sm font-semibold text-grayScale-900">{lesson.title}</p>
|
||||||
<p className="mt-1 text-xs text-grayScale-500">
|
<p className="mt-1 text-xs text-grayScale-500">
|
||||||
{subCourse.videos.length} lesson video(s) • {subCourse.practices.length} practice(s)
|
{lesson.video_count} lesson video(s) • {lesson.practice_count} practice(s)
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{subCourse.videos.map((video) => (
|
{lesson.videos.map((video) => (
|
||||||
<div key={video.id} className="inline-flex items-center gap-2 rounded-md bg-grayScale-50 px-2 py-1 text-xs text-grayScale-700">
|
<div key={video.id} className="inline-flex items-center gap-2 rounded-md bg-grayScale-50 px-2 py-1 text-xs text-grayScale-700">
|
||||||
<BookOpen className="h-3.5 w-3.5" />
|
<BookOpen className="h-3.5 w-3.5" />
|
||||||
{video.title}
|
{video.title}
|
||||||
|
|
|
||||||
|
|
@ -673,6 +673,33 @@ export interface GetLearningPathResponse {
|
||||||
metadata: unknown
|
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 {
|
export interface GetSubCourseEntryAssessmentResponse {
|
||||||
message: string
|
message: string
|
||||||
data: QuestionSet | null
|
data: QuestionSet | null
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user