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:
parent
8c2971f217
commit
997043fac9
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user