show created lessons in human language list with richer cards

Load sub-module QUIZ question sets into lesson rows so newly created lessons appear immediately, and upgrade lesson cards/detail panel styling to show status, question counts, and intro video previews.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-14 07:36:25 -07:00
parent 8c2971f217
commit 997043fac9
3 changed files with 116 additions and 22 deletions

View File

@ -768,6 +768,7 @@ export const getHumanLanguageHierarchy = () =>
id: subModuleId, id: subModuleId,
title: row.sub_module_title ?? "", title: row.sub_module_title ?? "",
videos: [], videos: [],
lessons: [],
practices: [], practices: [],
}) })
} }
@ -789,6 +790,71 @@ export const getHumanLanguageHierarchy = () =>
}), }),
})) }))
const subModuleIds = subCategories.flatMap((sub) =>
sub.courses.flatMap((course) =>
course.levels.flatMap((levelNode) => levelNode.modules.flatMap((moduleNode) => moduleNode.sub_modules.map((sm) => sm.id))),
),
)
type QuestionSetListItem = {
id: number
title?: string
set_type?: string
status?: string
intro_video_url?: string | null
question_count?: number
created_at?: string
}
const questionSetsBySubModule = new Map<number, QuestionSetListItem[]>()
await Promise.all(
subModuleIds.map(async (subModuleID) => {
try {
const questionSetRes = await http.get<GetQuestionSetsResponse>("/question-sets/by-owner", {
params: { owner_type: "SUB_MODULE", owner_id: subModuleID },
})
const payload = questionSetRes.data?.data
const sets = Array.isArray(payload)
? payload
: Array.isArray((payload as { question_sets?: QuestionSetListItem[] } | undefined)?.question_sets)
? ((payload as { question_sets: QuestionSetListItem[] }).question_sets ?? [])
: []
questionSetsBySubModule.set(subModuleID, sets as QuestionSetListItem[])
} catch {
questionSetsBySubModule.set(subModuleID, [])
}
}),
)
subCategories.forEach((sub) => {
sub.courses.forEach((course) => {
course.levels.forEach((levelNode) => {
levelNode.modules.forEach((moduleNode) => {
moduleNode.sub_modules.forEach((subModuleNode) => {
const sets = questionSetsBySubModule.get(subModuleNode.id) ?? []
const lessons = sets
.filter((set) => String(set.set_type ?? "").toUpperCase() === "QUIZ")
.sort((a, b) => {
const ad = Date.parse(String(a.created_at ?? "")) || 0
const bd = Date.parse(String(b.created_at ?? "")) || 0
return ad - bd
})
.map((set, idx) => ({
id: Number(set.id),
question_set_id: Number(set.id),
title: set.title?.trim() || `Lesson ${idx + 1}`,
status: set.status ?? "DRAFT",
question_count: Number(set.question_count ?? 0),
display_order: idx + 1,
intro_video_url: set.intro_video_url ?? null,
}))
subModuleNode.lessons = lessons
})
})
})
})
})
return { return {
...res, ...res,
data: { data: {

View File

@ -57,7 +57,6 @@ import type {
HumanLanguageCourseTree, HumanLanguageCourseTree,
HumanLanguageSubCategoryTree, HumanLanguageSubCategoryTree,
LearningPathPractice, LearningPathPractice,
LearningPathVideo,
QuestionDetail, QuestionDetail,
QuestionSetQuestion, QuestionSetQuestion,
} from "../../types/course.types" } from "../../types/course.types"
@ -1506,9 +1505,26 @@ export function HumanLanguagePage() {
const smKey = `${course.course_id}-${subModule.id}` const smKey = `${course.course_id}-${subModule.id}`
const panelTab = subModulePanelTab[smKey] ?? "lessons" const panelTab = subModulePanelTab[smKey] ?? "lessons"
const cardSel = getSubModuleSelection(smKey) const cardSel = getSubModuleSelection(smKey)
const lessonRows: LearningPathVideo[] = [...subModule.videos].sort( const lessonRows = [
(a, b) => a.display_order - b.display_order, ...(subModule.lessons ?? []).map((lesson) => ({
) id: lesson.id,
title: lesson.title,
display_order: lesson.display_order,
status: lesson.status,
question_count: lesson.question_count,
intro_video_url: lesson.intro_video_url ?? "",
})),
...((subModule.lessons?.length ?? 0) === 0
? subModule.videos.map((video) => ({
id: video.id,
title: video.title,
display_order: video.display_order,
status: "PUBLISHED",
question_count: 0,
intro_video_url: video.video_url ?? "",
}))
: []),
].sort((a, b) => a.display_order - b.display_order)
const practiceRows: LearningPathPractice[] = [...subModule.practices].sort( const practiceRows: LearningPathPractice[] = [...subModule.practices].sort(
(a, b) => (a.display_order ?? 0) - (b.display_order ?? 0), (a, b) => (a.display_order ?? 0) - (b.display_order ?? 0),
) )
@ -1643,9 +1659,8 @@ export function HumanLanguagePage() {
{panelTab === "lessons" ? ( {panelTab === "lessons" ? (
lessonRows.length === 0 ? ( lessonRows.length === 0 ? (
<div className="rounded-lg border border-dashed border-grayScale-200 bg-grayScale-50/40 px-4 py-8 text-center text-sm text-grayScale-500"> <div className="rounded-lg border border-dashed border-grayScale-200 bg-grayScale-50/40 px-4 py-8 text-center text-sm text-grayScale-500">
No lesson videos yet. Use{" "} No lessons yet. Use{" "}
<span className="font-medium text-grayScale-700">Open editor</span> to add <span className="font-medium text-grayScale-700">New lesson</span> to create one.
videos.
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
@ -1672,10 +1687,14 @@ export function HumanLanguagePage() {
<p className="line-clamp-2 text-sm font-semibold text-grayScale-900"> <p className="line-clamp-2 text-sm font-semibold text-grayScale-900">
{v.title} {v.title}
</p> </p>
<p className="mt-0.5 text-[11px] text-grayScale-500"> <div className="mt-1 flex flex-wrap items-center gap-1.5">
Lesson {idx + 1} · {formatDurationSeconds(v.duration ?? 0)} · Order{" "} <Badge className="rounded-full bg-brand-50 px-2 py-0.5 text-[10px] font-semibold text-brand-700 ring-1 ring-inset ring-brand-100">
{v.display_order} {(v.status ?? "DRAFT").replace(/_/g, " ").toLowerCase()}
</p> </Badge>
<span className="text-[11px] text-grayScale-500">
Lesson {idx + 1} · {v.question_count} Q · Order {v.display_order}
</span>
</div>
</div> </div>
</div> </div>
</button> </button>
@ -1685,7 +1704,7 @@ export function HumanLanguagePage() {
{selectedLesson ? ( {selectedLesson ? (
<div className="rounded-xl border border-grayScale-200 bg-grayScale-50/60 p-4"> <div className="rounded-xl border border-grayScale-200 bg-grayScale-50/60 p-4">
<p className="text-xs font-semibold uppercase tracking-wide text-grayScale-500"> <p className="text-xs font-semibold uppercase tracking-wide text-grayScale-500">
Lesson content Lesson detail
</p> </p>
<h4 className="mt-1 text-base font-semibold text-grayScale-900"> <h4 className="mt-1 text-base font-semibold text-grayScale-900">
{selectedLesson.title} {selectedLesson.title}
@ -1698,34 +1717,34 @@ export function HumanLanguagePage() {
</dd> </dd>
</div> </div>
<div> <div>
<dt className="text-xs text-grayScale-500">Duration</dt> <dt className="text-xs text-grayScale-500">Questions</dt>
<dd className="tabular-nums font-medium text-grayScale-800"> <dd className="tabular-nums font-medium text-grayScale-800">
{formatDurationSeconds(selectedLesson.duration ?? 0)} {selectedLesson.question_count}
</dd> </dd>
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<dt className="text-xs text-grayScale-500">Video</dt> <dt className="text-xs text-grayScale-500">Intro video</dt>
<dd className="mt-0.5 break-all"> <dd className="mt-0.5 break-all">
{selectedLesson.video_url ? ( {selectedLesson.intro_video_url ? (
<a <a
href={selectedLesson.video_url} href={selectedLesson.intro_video_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm font-medium text-brand-600 hover:underline" className="text-sm font-medium text-brand-600 hover:underline"
> >
{selectedLesson.video_url} {selectedLesson.intro_video_url}
</a> </a>
) : ( ) : (
<span className="text-sm text-grayScale-400"> <span className="text-sm text-grayScale-400">
No video URL set use Open editor to add one. No intro video URL set for this lesson.
</span> </span>
)} )}
{selectedLesson.video_url {selectedLesson.intro_video_url
? renderMediaPreview( ? renderMediaPreview(
selectedLesson.video_url, selectedLesson.intro_video_url,
"video", "video",
"mt-3", "mt-3",
"Video preview", "Intro video preview",
) )
: null} : null}
</dd> </dd>

View File

@ -719,6 +719,15 @@ export interface HumanLanguageSubModule {
id: number id: number
title: string title: string
videos: LearningPathVideo[] videos: LearningPathVideo[]
lessons?: {
id: number
question_set_id: number
title: string
status: string
question_count: number
display_order: number
intro_video_url?: string | null
}[]
practices: LearningPathPractice[] practices: LearningPathPractice[]
} }