learning flow fixes

This commit is contained in:
Yared Yemane 2026-03-07 08:15:13 -08:00
parent 6a201a0108
commit 3614244029
11 changed files with 1056 additions and 782 deletions

View File

@ -217,8 +217,41 @@ 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 reorderSubCourses = (courseId: number, items: ReorderItem[]) => const buildReorderPayload = (items: ReorderItem[]) => {
http.put(`/course-management/courses/${courseId}/reorder-sub-courses`, { items }) const normalized = items.map((item, idx) => ({
id: Number(item.id),
position: Number(item.position ?? idx),
}))
const hasInvalid = normalized.some(
(item) =>
Number.isNaN(item.id) ||
Number.isNaN(item.position) ||
!Number.isFinite(item.id) ||
!Number.isFinite(item.position),
)
if (hasInvalid) {
throw new Error("Invalid reorder payload: ids/positions must be numeric.")
}
return { items: normalized }
}
export const reorderCategories = (items: ReorderItem[]) =>
http.put("/course-management/categories/reorder", buildReorderPayload(items))
export const reorderCourses = (items: ReorderItem[]) =>
http.put("/course-management/courses/reorder", buildReorderPayload(items))
export const reorderSubCourses = (items: ReorderItem[]) =>
http.put("/course-management/sub-courses/reorder", buildReorderPayload(items))
export const reorderVideos = (items: ReorderItem[]) =>
http.put("/course-management/videos/reorder", buildReorderPayload(items))
export const reorderPractices = (items: ReorderItem[]) =>
http.put("/course-management/practices/reorder", buildReorderPayload(items))
// Ratings // Ratings
export const getRatings = (params: GetRatingsParams) => export const getRatings = (params: GetRatingsParams) =>

View File

@ -272,20 +272,34 @@ export function ProfilePage() {
const completionPct = profile.profile_completion_percentage ?? 0; const completionPct = profile.profile_completion_percentage ?? 0;
return ( return (
<div className="w-full space-y-6"> <div className="mx-auto w-full max-w-7xl space-y-6 pb-8">
{/* ─── Hero Card ─── */} {/* ─── Hero Card ─── */}
<div className="relative overflow-hidden rounded-2xl border border-grayScale-100 bg-white shadow-sm"> <div className="relative overflow-hidden rounded-3xl border border-grayScale-100 bg-white shadow-sm ring-1 ring-black/5">
{/* Tall dark gradient banner with content inside */} {/* Tall dark gradient banner with content inside */}
<div className="relative flex min-h-[200px] flex-col justify-between bg-gradient-to-br from-[#1a1f4e] via-[#2d2b6b] to-[#3b3480] px-6 py-8 sm:px-8"> <div className="relative flex min-h-[220px] flex-col justify-between bg-gradient-to-br from-[#1a1f4e] via-[#2d2b6b] to-[#3b3480] px-6 py-8 sm:px-8">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(255,255,255,0.08),transparent_60%)]" /> <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(255,255,255,0.08),transparent_60%)]" />
<div className="relative z-10 space-y-2"> <div className="relative z-10 space-y-2">
<h2 className="text-2xl font-bold tracking-tight text-white sm:text-3xl"> <h2 className="text-2xl font-bold tracking-tight text-white sm:text-3xl">
Hello {profile.first_name} Hello {profile.first_name}
</h2> </h2>
<p className="max-w-xl text-sm leading-relaxed text-white/70"> <p className="max-w-2xl text-sm leading-relaxed text-white/70">
This is your profile page. You can see the progress you've made with your work and manage your projects or assigned tasks Track your account status, keep profile details up to date, and manage your learning preferences from one place.
</p> </p>
<div className="mt-4 flex flex-wrap items-center gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-2.5 py-1 text-xs font-medium text-white/90">
<Shield className="h-3.5 w-3.5" />
{profile.role}
</span>
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-2.5 py-1 text-xs font-medium text-white/90">
<Clock className="h-3.5 w-3.5" />
Last login {formatDate(profile.last_login)}
</span>
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-2.5 py-1 text-xs font-medium text-white/90">
<Target className="h-3.5 w-3.5" />
{completionPct}% complete
</span>
</div>
</div> </div>
<div className="relative z-10 mt-6"> <div className="relative z-10 mt-6">
@ -293,7 +307,7 @@ export function ProfilePage() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="gap-1.5 border-white/30 bg-white/10 text-xs font-medium text-white shadow-sm backdrop-blur-sm hover:bg-white/20 hover:text-white" className="h-8 gap-1.5 border-white/30 bg-white/10 px-3 text-xs font-medium text-white shadow-sm backdrop-blur-sm hover:bg-white/20 hover:text-white"
onClick={startEditing} onClick={startEditing}
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
@ -304,7 +318,7 @@ export function ProfilePage() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-8 gap-1.5 border-white/30 bg-white/10 text-xs text-white backdrop-blur-sm hover:bg-white/20 hover:text-white" className="h-8 gap-1.5 border-white/30 bg-white/10 px-3 text-xs text-white backdrop-blur-sm hover:bg-white/20 hover:text-white"
onClick={cancelEditing} onClick={cancelEditing}
disabled={saving} disabled={saving}
> >
@ -330,7 +344,7 @@ export function ProfilePage() {
</div> </div>
{/* Identity info below banner */} {/* Identity info below banner */}
<div className="px-6 py-5 sm:px-8"> <div className="bg-gradient-to-b from-white to-grayScale-50/40 px-6 py-5 sm:px-8">
{editing ? ( {editing ? (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Input <Input
@ -405,7 +419,7 @@ export function ProfilePage() {
{/* ─── Detail Cards Grid ─── */} {/* ─── Detail Cards Grid ─── */}
<div className="grid gap-6 lg:grid-cols-3"> <div className="grid gap-6 lg:grid-cols-3">
{/* ── Contact & Personal ── */} {/* ── Contact & Personal ── */}
<Card className="overflow-hidden border-grayScale-100 shadow-sm lg:col-span-2"> <Card className="overflow-hidden rounded-2xl border-grayScale-100 shadow-sm transition-shadow hover:shadow-md lg:col-span-2">
<div className="h-1 bg-gradient-to-r from-brand-500 to-brand-400" /> <div className="h-1 bg-gradient-to-r from-brand-500 to-brand-400" />
<CardContent className="p-0"> <CardContent className="p-0">
<div className="grid divide-y divide-grayScale-100 sm:grid-cols-2 sm:divide-x sm:divide-y-0"> <div className="grid divide-y divide-grayScale-100 sm:grid-cols-2 sm:divide-x sm:divide-y-0">
@ -569,9 +583,9 @@ export function ProfilePage() {
</Card> </Card>
{/* ── Right Sidebar ── */} {/* ── Right Sidebar ── */}
<div className="space-y-6"> <div className="space-y-6 lg:sticky lg:top-24 lg:self-start">
{/* Profile Completion */} {/* Profile Completion */}
<Card className="overflow-hidden border-grayScale-100 shadow-sm"> <Card className="overflow-hidden rounded-2xl border-grayScale-100 shadow-sm transition-shadow hover:shadow-md">
<div className="h-1 bg-gradient-to-r from-brand-400 to-mint-400" /> <div className="h-1 bg-gradient-to-r from-brand-400 to-mint-400" />
<CardContent className="flex items-center gap-4 p-5"> <CardContent className="flex items-center gap-4 p-5">
<ProgressRing percent={completionPct} /> <ProgressRing percent={completionPct} />
@ -585,7 +599,7 @@ export function ProfilePage() {
</Card> </Card>
{/* Activity */} {/* Activity */}
<Card className="overflow-hidden border-grayScale-100 shadow-sm"> <Card className="overflow-hidden rounded-2xl border-grayScale-100 shadow-sm transition-shadow hover:shadow-md">
<div className="h-1 bg-gradient-to-r from-grayScale-300 to-grayScale-200" /> <div className="h-1 bg-gradient-to-r from-grayScale-300 to-grayScale-200" />
<CardContent className="space-y-4 p-5"> <CardContent className="space-y-4 p-5">
<p className="text-[11px] font-bold uppercase tracking-widest text-grayScale-400"> <p className="text-[11px] font-bold uppercase tracking-widest text-grayScale-400">
@ -613,7 +627,7 @@ export function ProfilePage() {
</Card> </Card>
{/* Quick Account Info */} {/* Quick Account Info */}
<Card className="overflow-hidden border-grayScale-100 shadow-sm"> <Card className="overflow-hidden rounded-2xl border-grayScale-100 shadow-sm transition-shadow hover:shadow-md">
<div className="h-1 bg-gradient-to-r from-brand-500 to-brand-600" /> <div className="h-1 bg-gradient-to-r from-brand-500 to-brand-600" />
<CardContent className="space-y-3 p-5"> <CardContent className="space-y-3 p-5">
<p className="text-[11px] font-bold uppercase tracking-widest text-grayScale-400"> <p className="text-[11px] font-bold uppercase tracking-widest text-grayScale-400">
@ -675,8 +689,13 @@ export function ProfilePage() {
</div> </div>
{/* ─── Learning & Goals Card ─── */} {/* ─── Learning & Goals Card ─── */}
<Card className="overflow-hidden border-grayScale-100 shadow-sm"> <Card className="overflow-hidden rounded-2xl border-grayScale-100 shadow-sm transition-shadow hover:shadow-md">
<div className="h-1 bg-gradient-to-r from-brand-600 via-brand-500 to-brand-400" /> <div className="h-1 bg-gradient-to-r from-brand-600 via-brand-500 to-brand-400" />
<div className="border-b border-grayScale-100 px-5 py-3">
<p className="text-[11px] font-bold uppercase tracking-widest text-grayScale-400">
Learning & Preferences
</p>
</div>
<CardContent className="p-0"> <CardContent className="p-0">
<div className="grid divide-y divide-grayScale-100 sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-4 lg:divide-x lg:divide-y-0"> <div className="grid divide-y divide-grayScale-100 sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-4 lg:divide-x lg:divide-y-0">
<div className="p-5"> <div className="p-5">

View File

@ -889,7 +889,7 @@ export function AddNewPracticePage() {
className="w-full bg-brand-500 hover:bg-brand-600" className="w-full bg-brand-500 hover:bg-brand-600"
onClick={() => navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`)} onClick={() => navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`)}
> >
Go back to Sub-course Go back to Course
</Button> </Button>
<Button <Button
variant="outline" variant="outline"

View File

@ -76,7 +76,7 @@ export function AllCoursesPage() {
setCourses(allCourses) setCourses(allCourses)
} catch (err) { } catch (err) {
console.error("Failed to load courses:", err) console.error("Failed to load courses:", err)
setError("Failed to load courses") setError("Failed to load sub-categories")
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -116,7 +116,7 @@ export function AllCoursesPage() {
description: createDescription.trim(), description: createDescription.trim(),
}) })
toast.success("Course created", { toast.success("Sub-category created", {
description: `"${createTitle.trim()}" has been created.`, description: `"${createTitle.trim()}" has been created.`,
}) })
@ -130,7 +130,7 @@ export function AllCoursesPage() {
await fetchAllCourses() await fetchAllCourses()
} catch (err: any) { } catch (err: any) {
console.error("Failed to create course:", err) console.error("Failed to create course:", err)
toast.error("Failed to create course", { toast.error("Failed to create sub-category", {
description: err?.response?.data?.message || "Please try again.", description: err?.response?.data?.message || "Please try again.",
}) })
} finally { } finally {
@ -145,7 +145,7 @@ export function AllCoursesPage() {
await fetchAllCourses() await fetchAllCourses()
} catch (err) { } catch (err) {
console.error("Failed to update course status:", err) console.error("Failed to update course status:", err)
toast.error("Failed to update course status") toast.error("Failed to update sub-category status")
} finally { } finally {
setTogglingId(null) setTogglingId(null)
} }
@ -173,13 +173,13 @@ export function AllCoursesPage() {
title: editTitle.trim(), title: editTitle.trim(),
description: editDescription.trim(), description: editDescription.trim(),
}) })
toast.success("Course updated") toast.success("Sub-category updated")
setEditOpen(false) setEditOpen(false)
setCourseToEdit(null) setCourseToEdit(null)
await fetchAllCourses() await fetchAllCourses()
} catch (err: any) { } catch (err: any) {
console.error("Failed to update course:", err) console.error("Failed to update course:", err)
toast.error("Failed to update course", { toast.error("Failed to update sub-category", {
description: err?.response?.data?.message || "Please try again.", description: err?.response?.data?.message || "Please try again.",
}) })
} finally { } finally {
@ -193,7 +193,7 @@ export function AllCoursesPage() {
<div className="rounded-2xl bg-white shadow-sm p-6"> <div className="rounded-2xl bg-white shadow-sm p-6">
<RefreshCw className="h-10 w-10 animate-spin text-brand-600" /> <RefreshCw className="h-10 w-10 animate-spin text-brand-600" />
</div> </div>
<p className="mt-4 text-sm font-medium text-grayScale-400">Loading all courses</p> <p className="mt-4 text-sm font-medium text-grayScale-400">Loading all sub-categories</p>
</div> </div>
) )
} }
@ -216,9 +216,9 @@ export function AllCoursesPage() {
{/* Header */} {/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight text-grayScale-700">All Courses</h1> <h1 className="text-2xl font-bold tracking-tight text-grayScale-700">All Sub-categories</h1>
<p className="mt-1 text-sm text-grayScale-400"> <p className="mt-1 text-sm text-grayScale-400">
View and manage courses across all categories. View and manage sub-categories across all categories.
</p> </p>
</div> </div>
<Button <Button
@ -226,14 +226,14 @@ export function AllCoursesPage() {
onClick={() => setCreateOpen(true)} onClick={() => setCreateOpen(true)}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Create Course Create Sub-category
</Button> </Button>
</div> </div>
<Card className="shadow-soft"> <Card className="shadow-soft">
<CardHeader className="border-b border-grayScale-200 pb-4"> <CardHeader className="border-b border-grayScale-200 pb-4">
<CardTitle className="text-base font-semibold text-grayScale-600"> <CardTitle className="text-base font-semibold text-grayScale-600">
Course Management Sub-category Management
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-5 pt-5"> <CardContent className="space-y-5 pt-5">
@ -375,9 +375,9 @@ export function AllCoursesPage() {
<div className="mb-4 grid h-16 w-16 place-items-center rounded-full bg-gradient-to-br from-grayScale-100 to-grayScale-200"> <div className="mb-4 grid h-16 w-16 place-items-center rounded-full bg-gradient-to-br from-grayScale-100 to-grayScale-200">
<BookOpen className="h-8 w-8 text-grayScale-400" /> <BookOpen className="h-8 w-8 text-grayScale-400" />
</div> </div>
<p className="text-base font-semibold text-grayScale-600">No courses found</p> <p className="text-base font-semibold text-grayScale-600">No sub-categories found</p>
<p className="mt-1.5 max-w-sm text-sm leading-relaxed text-grayScale-400"> <p className="mt-1.5 max-w-sm text-sm leading-relaxed text-grayScale-400">
Try adjusting your search or category filter, or create a new course. Try adjusting your search or category filter, or create a new sub-category.
</p> </p>
</div> </div>
)} )}
@ -388,7 +388,7 @@ export function AllCoursesPage() {
<Dialog open={createOpen} onOpenChange={setCreateOpen}> <Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Create course</DialogTitle> <DialogTitle>Create sub-category</DialogTitle>
<DialogDescription> <DialogDescription>
Choose a category, add basic details, and optionally attach a thumbnail and intro Choose a category, add basic details, and optionally attach a thumbnail and intro
video. video.
@ -439,7 +439,7 @@ export function AllCoursesPage() {
</div> </div>
<div className="sm:col-span-1"> <div className="sm:col-span-1">
<label className="mb-1.5 block text-sm font-medium text-grayScale-600"> <label className="mb-1.5 block text-sm font-medium text-grayScale-600">
Course title Sub-category title
</label> </label>
<Input <Input
placeholder="e.g. Beginner English A1" placeholder="e.g. Beginner English A1"
@ -455,7 +455,7 @@ export function AllCoursesPage() {
</label> </label>
<Textarea <Textarea
rows={3} rows={3}
placeholder="Short summary of what this course covers." placeholder="Short summary of what this sub-category covers."
value={createDescription} value={createDescription}
onChange={(e) => setCreateDescription(e.target.value)} onChange={(e) => setCreateDescription(e.target.value)}
/> />
@ -514,7 +514,7 @@ export function AllCoursesPage() {
disabled={creating} disabled={creating}
onClick={handleCreateCourse} onClick={handleCreateCourse}
> >
{creating ? "Creating…" : "Create course"} {creating ? "Creating…" : "Create sub-category"}
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>
@ -524,9 +524,9 @@ export function AllCoursesPage() {
<Dialog open={editOpen} onOpenChange={setEditOpen}> <Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="max-w-lg"> <DialogContent className="max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit course</DialogTitle> <DialogTitle>Edit sub-category</DialogTitle>
<DialogDescription> <DialogDescription>
Update the title and description for this course. Status can be toggled from the Update the title and description for this sub-category. Status can be toggled from the
table. table.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -534,12 +534,12 @@ export function AllCoursesPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-600"> <label className="mb-1.5 block text-sm font-medium text-grayScale-600">
Course title Sub-category title
</label> </label>
<Input <Input
value={editTitle} value={editTitle}
onChange={(e) => setEditTitle(e.target.value)} onChange={(e) => setEditTitle(e.target.value)}
placeholder="Enter course title" placeholder="Enter sub-category title"
/> />
</div> </div>
<div> <div>
@ -550,7 +550,7 @@ export function AllCoursesPage() {
rows={3} rows={3}
value={editDescription} value={editDescription}
onChange={(e) => setEditDescription(e.target.value)} onChange={(e) => setEditDescription(e.target.value)}
placeholder="Short summary of this course." placeholder="Short summary of this sub-category."
/> />
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@ const contentSections = [
pathFn: (categoryId: string | undefined) => `/content/category/${categoryId}/courses`, pathFn: (categoryId: string | undefined) => `/content/category/${categoryId}/courses`,
icon: BookOpen, icon: BookOpen,
title: "Courses", title: "Courses",
description: "Manage course videos and educational content", description: "Manage sub-categories, course videos and educational content",
action: "Manage Courses", action: "Manage Courses",
count: 12, count: 12,
countLabel: "courses", countLabel: "courses",

View File

@ -139,7 +139,7 @@ export function CourseCategoryPage() {
<CardContent> <CardContent>
<span className="inline-flex items-center gap-1.5 text-sm font-medium text-brand-500 transition-colors group-hover:text-brand-600"> <span className="inline-flex items-center gap-1.5 text-sm font-medium text-brand-500 transition-colors group-hover:text-brand-600">
View Courses View Sub-categories
<span className="inline-block transition-transform duration-300 group-hover:translate-x-1"> <span className="inline-block transition-transform duration-300 group-hover:translate-x-1">
</span> </span>

File diff suppressed because it is too large Load Diff

View File

@ -77,7 +77,7 @@ export function CoursesPage() {
setCategory(foundCategory ?? null) setCategory(foundCategory ?? null)
} catch (err) { } catch (err) {
console.error("Failed to fetch courses:", err) console.error("Failed to fetch courses:", err)
setError("Failed to load courses") setError("Failed to load sub-categories")
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -123,7 +123,7 @@ export function CoursesPage() {
await fetchCourses() await fetchCourses()
} catch (err: any) { } catch (err: any) {
console.error("Failed to create course:", err) console.error("Failed to create course:", err)
setSaveError(err.response?.data?.message || "Failed to create course") setSaveError(err.response?.data?.message || "Failed to create sub-category")
} finally { } finally {
setSaving(false) setSaving(false)
} }
@ -206,7 +206,7 @@ export function CoursesPage() {
await fetchCourses() await fetchCourses()
} catch (err: any) { } catch (err: any) {
console.error("Failed to update course:", err) console.error("Failed to update course:", err)
setUpdateError(err.response?.data?.message || "Failed to update course") setUpdateError(err.response?.data?.message || "Failed to update sub-category")
} finally { } finally {
setUpdating(false) setUpdating(false)
} }
@ -267,16 +267,16 @@ export function CoursesPage() {
</Link> </Link>
<div> <div>
<h1 className="text-xl font-bold text-grayScale-700 sm:text-2xl"> <h1 className="text-xl font-bold text-grayScale-700 sm:text-2xl">
{category?.name} Courses {category?.name} Sub-categories
</h1> </h1>
<p className="mt-0.5 text-sm text-grayScale-400"> <p className="mt-0.5 text-sm text-grayScale-400">
<span className="font-medium text-grayScale-500">{courses.length}</span> courses available <span className="font-medium text-grayScale-500">{courses.length}</span> sub-categories available
</p> </p>
</div> </div>
</div> </div>
<Button className="w-full bg-brand-500 shadow-sm transition-all hover:bg-brand-600 hover:shadow-md sm:w-auto" onClick={handleOpenModal}> <Button className="w-full bg-brand-500 shadow-sm transition-all hover:bg-brand-600 hover:shadow-md sm:w-auto" onClick={handleOpenModal}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Add New Course Add New Sub-category
</Button> </Button>
</div> </div>
</div> </div>
@ -285,16 +285,16 @@ export function CoursesPage() {
<Card className="shadow-soft"> <Card className="shadow-soft">
<CardHeader className="border-b border-grayScale-200 pb-3"> <CardHeader className="border-b border-grayScale-200 pb-3">
<CardTitle className="text-base font-semibold text-grayScale-600"> <CardTitle className="text-base font-semibold text-grayScale-600">
Course Management Sub-category Management
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-4"> <CardContent className="pt-4">
{courses.length === 0 ? ( {courses.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-grayScale-200 py-16 text-center"> <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-grayScale-200 py-16 text-center">
<img src={practiceSrc} alt="" className="h-16 w-16" /> <img src={practiceSrc} alt="" className="h-16 w-16" />
<h3 className="mt-4 text-base font-semibold text-grayScale-600">No courses yet</h3> <h3 className="mt-4 text-base font-semibold text-grayScale-600">No sub-categories yet</h3>
<p className="mt-1.5 text-sm text-grayScale-400"> <p className="mt-1.5 text-sm text-grayScale-400">
No courses found in this category. No sub-categories found in this category.
</p> </p>
<Button <Button
variant="outline" variant="outline"
@ -302,7 +302,7 @@ export function CoursesPage() {
onClick={handleOpenModal} onClick={handleOpenModal}
> >
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Add your first course Add your first sub-category
</Button> </Button>
</div> </div>
) : ( ) : (
@ -311,7 +311,7 @@ export function CoursesPage() {
<TableHeader> <TableHeader>
<TableRow className="bg-grayScale-100 hover:bg-grayScale-100"> <TableRow className="bg-grayScale-100 hover:bg-grayScale-100">
<TableHead className="py-3 text-xs font-semibold uppercase tracking-wider text-grayScale-500"> <TableHead className="py-3 text-xs font-semibold uppercase tracking-wider text-grayScale-500">
Course Sub-category
</TableHead> </TableHead>
<TableHead className="hidden py-3 text-xs font-semibold uppercase tracking-wider text-grayScale-500 md:table-cell"> <TableHead className="hidden py-3 text-xs font-semibold uppercase tracking-wider text-grayScale-500 md:table-cell">
Status Status
@ -415,7 +415,7 @@ export function CoursesPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-2xl animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-2xl animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5">
<h2 className="text-lg font-bold text-grayScale-700">Add New Course</h2> <h2 className="text-lg font-bold text-grayScale-700">Add New Sub-category</h2>
<button <button
onClick={handleCloseModal} onClick={handleCloseModal}
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600" className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
@ -441,7 +441,7 @@ export function CoursesPage() {
</label> </label>
<Input <Input
id="course-title" id="course-title"
placeholder="Enter course title" placeholder="Enter sub-category title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
/> />
@ -456,7 +456,7 @@ export function CoursesPage() {
</label> </label>
<textarea <textarea
id="course-description" id="course-description"
placeholder="Enter course description" placeholder="Enter sub-category description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
rows={4} rows={4}
@ -478,7 +478,7 @@ export function CoursesPage() {
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
> >
{saving ? "Saving..." : "Save Course"} {saving ? "Saving..." : "Save Sub-category"}
</Button> </Button>
</div> </div>
</div> </div>
@ -490,7 +490,7 @@ export function CoursesPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-md animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-md animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5">
<h2 className="text-lg font-bold text-grayScale-700">Edit Course</h2> <h2 className="text-lg font-bold text-grayScale-700">Edit Sub-category</h2>
<button <button
onClick={handleCloseEditModal} onClick={handleCloseEditModal}
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600" className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
@ -516,7 +516,7 @@ export function CoursesPage() {
</label> </label>
<Input <Input
id="edit-course-title" id="edit-course-title"
placeholder="Enter course title" placeholder="Enter sub-category title"
value={editTitle} value={editTitle}
onChange={(e) => setEditTitle(e.target.value)} onChange={(e) => setEditTitle(e.target.value)}
/> />
@ -531,7 +531,7 @@ export function CoursesPage() {
</label> </label>
<textarea <textarea
id="edit-course-description" id="edit-course-description"
placeholder="Enter course description" placeholder="Enter sub-category description"
value={editDescription} value={editDescription}
onChange={(e) => setEditDescription(e.target.value)} onChange={(e) => setEditDescription(e.target.value)}
rows={4} rows={4}
@ -564,7 +564,7 @@ export function CoursesPage() {
onClick={handleUpdate} onClick={handleUpdate}
disabled={updating} disabled={updating}
> >
{updating ? "Updating..." : "Update Course"} {updating ? "Updating..." : "Update Sub-category"}
</Button> </Button>
</div> </div>
</div> </div>
@ -576,7 +576,7 @@ export function CoursesPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-lg animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-lg animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5">
<h2 className="text-lg font-bold text-grayScale-700">Course Ratings</h2> <h2 className="text-lg font-bold text-grayScale-700">Sub-category Ratings</h2>
<button <button
onClick={() => { onClick={() => {
setShowRatingsModal(false) setShowRatingsModal(false)
@ -602,7 +602,7 @@ export function CoursesPage() {
</div> </div>
<p className="mt-4 text-sm font-semibold text-grayScale-700">No ratings yet</p> <p className="mt-4 text-sm font-semibold text-grayScale-700">No ratings yet</p>
<p className="mt-1 text-sm text-grayScale-400"> <p className="mt-1 text-sm text-grayScale-400">
Ratings will appear here once learners start reviewing this course. Ratings will appear here once learners start reviewing this sub-category.
</p> </p>
</div> </div>
) : ( ) : (
@ -690,7 +690,7 @@ export function CoursesPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-sm animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-sm animate-in fade-in zoom-in-95 rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-5">
<h2 className="text-lg font-bold text-grayScale-700">Delete Course</h2> <h2 className="text-lg font-bold text-grayScale-700">Delete Sub-category</h2>
<button <button
onClick={() => setShowDeleteModal(false)} onClick={() => setShowDeleteModal(false)}
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600" className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"

View File

@ -97,8 +97,8 @@ export function SubCourseContentPage() {
) )
setSubCourse(foundSubCourse ?? null) setSubCourse(foundSubCourse ?? null)
} catch (err) { } catch (err) {
console.error("Failed to fetch sub-course data:", err) console.error("Failed to fetch course data:", err)
setError("Failed to load sub-course") setError("Failed to load course")
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -374,7 +374,7 @@ export function SubCourseContentPage() {
return ( return (
<div className="flex flex-col items-center justify-center py-20"> <div className="flex flex-col items-center justify-center py-20">
<img src={spinnerSrc} alt="" className="h-8 w-8 animate-spin" /> <img src={spinnerSrc} alt="" className="h-8 w-8 animate-spin" />
<p className="mt-4 text-sm font-medium text-grayScale-500">Loading sub-course</p> <p className="mt-4 text-sm font-medium text-grayScale-500">Loading course</p>
</div> </div>
) )
} }
@ -396,7 +396,7 @@ export function SubCourseContentPage() {
className="group inline-flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-grayScale-500 transition-all hover:bg-grayScale-50 hover:text-grayScale-900" className="group inline-flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-grayScale-500 transition-all hover:bg-grayScale-50 hover:text-grayScale-900"
> >
<ArrowLeft className="h-4 w-4 transition-transform group-hover:-translate-x-0.5" /> <ArrowLeft className="h-4 w-4 transition-transform group-hover:-translate-x-0.5" />
Back to Sub-courses Back to Courses
</Link> </Link>
{/* SubCourse Header */} {/* SubCourse Header */}
@ -721,7 +721,7 @@ export function SubCourseContentPage() {
</div> </div>
<p className="mt-4 text-sm font-semibold text-grayScale-700">No ratings yet</p> <p className="mt-4 text-sm font-semibold text-grayScale-700">No ratings yet</p>
<p className="mt-1 text-sm text-grayScale-400"> <p className="mt-1 text-sm text-grayScale-400">
Ratings will appear here once learners start reviewing this sub-course. Ratings will appear here once learners start reviewing this course.
</p> </p>
</div> </div>
) : ( ) : (

View File

@ -1,350 +1,403 @@
import { useEffect, useState, useRef } from "react" import { useEffect, useState, useRef } from "react";
import { Link, useParams, useNavigate } from "react-router-dom" import { Link, useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, ToggleLeft, ToggleRight, MoreVertical, X, Trash2, AlertCircle, Edit, Link2, Plus, Loader2, LayoutGrid, GitBranch, ChevronDown, Lock, ArrowRight } from "lucide-react" import {
import practiceSrc from "../../assets/Practice.svg" ArrowLeft,
import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg" ToggleLeft,
import { Card, CardContent } from "../../components/ui/card" ToggleRight,
import alertSrc from "../../assets/Alert.svg" MoreVertical,
import { Badge } from "../../components/ui/badge" X,
import { Button } from "../../components/ui/button" Trash2,
import { getSubCoursesByCourse, getCoursesByCategory, getCourseCategories, createSubCourse, updateSubCourse, updateSubCourseStatus, deleteSubCourse, getSubCoursePrerequisites, addSubCoursePrerequisite, removeSubCoursePrerequisite } from "../../api/courses.api" AlertCircle,
import { Input } from "../../components/ui/input" Edit,
import type { SubCourse, Course, CourseCategory, SubCoursePrerequisite } from "../../types/course.types" Link2,
Plus,
Loader2,
LayoutGrid,
GitBranch,
ChevronDown,
Lock,
ArrowRight,
} from "lucide-react";
import practiceSrc from "../../assets/Practice.svg";
import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg";
import { Card, CardContent } from "../../components/ui/card";
import alertSrc from "../../assets/Alert.svg";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
getSubCoursesByCourse,
getCoursesByCategory,
getCourseCategories,
createSubCourse,
updateSubCourse,
updateSubCourseStatus,
deleteSubCourse,
getSubCoursePrerequisites,
addSubCoursePrerequisite,
removeSubCoursePrerequisite,
} from "../../api/courses.api";
import { Input } from "../../components/ui/input";
import type {
SubCourse,
Course,
CourseCategory,
SubCoursePrerequisite,
} from "../../types/course.types";
export function SubCoursesPage() { export function SubCoursesPage() {
const { categoryId, courseId } = useParams<{ categoryId: string; courseId: string }>() const { categoryId, courseId } = useParams<{
const navigate = useNavigate() categoryId: string;
const [subCourses, setSubCourses] = useState<SubCourse[]>([]) courseId: string;
const [course, setCourse] = useState<Course | null>(null) }>();
const [category, setCategory] = useState<CourseCategory | null>(null) const navigate = useNavigate();
const [loading, setLoading] = useState(true) const [subCourses, setSubCourses] = useState<SubCourse[]>([]);
const [error, setError] = useState<string | null>(null) const [course, setCourse] = useState<Course | null>(null);
const [category, setCategory] = useState<CourseCategory | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [openMenuId, setOpenMenuId] = useState<number | null>(null) const [openMenuId, setOpenMenuId] = useState<number | null>(null);
const [togglingId, setTogglingId] = useState<number | null>(null) const [togglingId, setTogglingId] = useState<number | null>(null);
const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false);
const [subCourseToDelete, setSubCourseToDelete] = useState<SubCourse | null>(null) const [subCourseToDelete, setSubCourseToDelete] = useState<SubCourse | null>(
const [deleting, setDeleting] = useState(false) null,
const menuRef = useRef<HTMLDivElement>(null) );
const [deleting, setDeleting] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const [showAddModal, setShowAddModal] = useState(false) const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false);
const [subCourseToEdit, setSubCourseToEdit] = useState<SubCourse | null>(null) const [subCourseToEdit, setSubCourseToEdit] = useState<SubCourse | null>(
const [title, setTitle] = useState("") null,
const [description, setDescription] = useState("") );
const [level, setLevel] = useState("") const [title, setTitle] = useState("");
const [saving, setSaving] = useState(false) const [description, setDescription] = useState("");
const [saveError, setSaveError] = useState<string | null>(null) const [level, setLevel] = useState("");
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
// View mode // View mode
const [viewMode, setViewMode] = useState<"grid" | "flow">("grid") const [viewMode, setViewMode] = useState<"grid" | "flow">("grid");
// All prerequisites map: subCourseId -> prerequisites[] // All prerequisites map: subCourseId -> prerequisites[]
const [allPrereqMap, setAllPrereqMap] = useState<Record<number, SubCoursePrerequisite[]>>({}) const [allPrereqMap, setAllPrereqMap] = useState<
const [allPrereqLoading, setAllPrereqLoading] = useState(false) Record<number, SubCoursePrerequisite[]>
>({});
const [allPrereqLoading, setAllPrereqLoading] = useState(false);
// Prerequisites state // Prerequisites state
const [showPrereqModal, setShowPrereqModal] = useState(false) const [showPrereqModal, setShowPrereqModal] = useState(false);
const [prereqSubCourse, setPrereqSubCourse] = useState<SubCourse | null>(null) const [prereqSubCourse, setPrereqSubCourse] = useState<SubCourse | null>(
const [prerequisites, setPrerequisites] = useState<SubCoursePrerequisite[]>([]) null,
const [prereqLoading, setPrereqLoading] = useState(false) );
const [prereqAdding, setPrereqAdding] = useState(false) const [prerequisites, setPrerequisites] = useState<SubCoursePrerequisite[]>(
const [prereqRemoving, setPrereqRemoving] = useState<number | null>(null) [],
const [selectedPrereqId, setSelectedPrereqId] = useState<number | 0>(0) );
const [prereqLoading, setPrereqLoading] = useState(false);
const [prereqAdding, setPrereqAdding] = useState(false);
const [prereqRemoving, setPrereqRemoving] = useState<number | null>(null);
const [selectedPrereqId, setSelectedPrereqId] = useState<number | 0>(0);
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) { if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenuId(null) setOpenMenuId(null);
} }
} };
if (openMenuId !== null) { if (openMenuId !== null) {
document.addEventListener("mousedown", handleClickOutside) document.addEventListener("mousedown", handleClickOutside);
} }
return () => document.removeEventListener("mousedown", handleClickOutside) return () => document.removeEventListener("mousedown", handleClickOutside);
}, [openMenuId]) }, [openMenuId]);
const fetchSubCourses = async () => { const fetchSubCourses = async () => {
if (!courseId) return if (!courseId) return;
try { try {
const subCoursesRes = await getSubCoursesByCourse(Number(courseId)) const subCoursesRes = await getSubCoursesByCourse(Number(courseId));
setSubCourses(subCoursesRes.data.data.sub_courses ?? []) setSubCourses(subCoursesRes.data.data.sub_courses ?? []);
} catch (err) { } catch (err) {
console.error("Failed to fetch sub-courses:", err) console.error("Failed to fetch sub-courses:", err);
} }
} };
const fetchAllPrerequisites = async (scs: SubCourse[]) => { const fetchAllPrerequisites = async (scs: SubCourse[]) => {
if (scs.length === 0) return if (scs.length === 0) return;
setAllPrereqLoading(true) setAllPrereqLoading(true);
try { try {
const results = await Promise.all( const results = await Promise.all(
scs.map((sc) => getSubCoursePrerequisites(sc.id).then((res) => ({ id: sc.id, data: res.data.data ?? [] }))) scs.map((sc) =>
) getSubCoursePrerequisites(sc.id).then((res) => ({
const map: Record<number, SubCoursePrerequisite[]> = {} id: sc.id,
data: res.data.data ?? [],
})),
),
);
const map: Record<number, SubCoursePrerequisite[]> = {};
for (const r of results) { for (const r of results) {
map[r.id] = r.data map[r.id] = r.data;
} }
setAllPrereqMap(map) setAllPrereqMap(map);
} catch (err) { } catch (err) {
console.error("Failed to fetch all prerequisites:", err) console.error("Failed to fetch all prerequisites:", err);
} finally { } finally {
setAllPrereqLoading(false) setAllPrereqLoading(false);
} }
} };
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
if (!courseId || !categoryId) return if (!courseId || !categoryId) return;
try { try {
const [subCoursesRes, coursesRes, categoriesRes] = await Promise.all([ const [subCoursesRes, coursesRes, categoriesRes] = await Promise.all([
getSubCoursesByCourse(Number(courseId)), getSubCoursesByCourse(Number(courseId)),
getCoursesByCategory(Number(categoryId)), getCoursesByCategory(Number(categoryId)),
getCourseCategories(), getCourseCategories(),
]) ]);
setSubCourses(subCoursesRes.data.data.sub_courses ?? []) setSubCourses(subCoursesRes.data.data.sub_courses ?? []);
const foundCourse = coursesRes.data.data.courses?.find( const foundCourse = coursesRes.data.data.courses?.find(
(c) => c.id === Number(courseId) (c) => c.id === Number(courseId),
) );
setCourse(foundCourse ?? null) setCourse(foundCourse ?? null);
const foundCategory = categoriesRes.data.data.categories?.find( const foundCategory = categoriesRes.data.data.categories?.find(
(c) => c.id === Number(categoryId) (c) => c.id === Number(categoryId),
) );
setCategory(foundCategory ?? null) setCategory(foundCategory ?? null);
} catch (err) { } catch (err) {
console.error("Failed to fetch sub-courses:", err) console.error("Failed to fetch sub-courses:", err);
setError("Failed to load sub-courses") setError("Failed to load courses");
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
fetchData() fetchData();
}, [courseId, categoryId]) }, [courseId, categoryId]);
useEffect(() => { useEffect(() => {
if (subCourses.length > 0) { if (subCourses.length > 0) {
fetchAllPrerequisites(subCourses) fetchAllPrerequisites(subCourses);
} }
}, [subCourses]) }, [subCourses]);
const handleToggleStatus = async (subCourse: SubCourse) => { const handleToggleStatus = async (subCourse: SubCourse) => {
setTogglingId(subCourse.id) setTogglingId(subCourse.id);
try { try {
await updateSubCourseStatus(subCourse.id, { await updateSubCourseStatus(subCourse.id, {
is_active: !subCourse.is_active, is_active: !subCourse.is_active,
level: subCourse.level, level: subCourse.level,
title: subCourse.title, title: subCourse.title,
}) });
await fetchSubCourses() await fetchSubCourses();
} catch (err) { } catch (err) {
console.error("Failed to update sub-course status:", err) console.error("Failed to update sub-course status:", err);
} finally { } finally {
setTogglingId(null) setTogglingId(null);
} }
} };
const handleDeleteClick = (subCourse: SubCourse) => { const handleDeleteClick = (subCourse: SubCourse) => {
setSubCourseToDelete(subCourse) setSubCourseToDelete(subCourse);
setShowDeleteModal(true) setShowDeleteModal(true);
} };
const handleConfirmDelete = async () => { const handleConfirmDelete = async () => {
if (!subCourseToDelete) return if (!subCourseToDelete) return;
setDeleting(true) setDeleting(true);
try { try {
await deleteSubCourse(subCourseToDelete.id) await deleteSubCourse(subCourseToDelete.id);
setShowDeleteModal(false) setShowDeleteModal(false);
setSubCourseToDelete(null) setSubCourseToDelete(null);
await fetchSubCourses() await fetchSubCourses();
} catch (err) { } catch (err) {
console.error("Failed to delete sub-course:", err) console.error("Failed to delete sub-course:", err);
} finally { } finally {
setDeleting(false) setDeleting(false);
} }
} };
const handleAddSubCourse = () => { const handleAddSubCourse = () => {
setTitle("") setTitle("");
setDescription("") setDescription("");
setLevel("") setLevel("");
setSaveError(null) setSaveError(null);
setShowAddModal(true) setShowAddModal(true);
} };
const handleSaveNewSubCourse = async () => { const handleSaveNewSubCourse = async () => {
if (!courseId) return if (!courseId) return;
setSaving(true) setSaving(true);
setSaveError(null) setSaveError(null);
try { try {
await createSubCourse({ await createSubCourse({
course_id: Number(courseId), course_id: Number(courseId),
title, title,
description, description,
level, level,
}) });
setShowAddModal(false) setShowAddModal(false);
setTitle("") setTitle("");
setDescription("") setDescription("");
setLevel("") setLevel("");
await fetchSubCourses() await fetchSubCourses();
} catch (err) { } catch (err) {
console.error("Failed to create sub-course:", err) console.error("Failed to create sub-course:", err);
setSaveError("Failed to create sub-course") setSaveError("Failed to create course");
} finally { } finally {
setSaving(false) setSaving(false);
} }
} };
const handleEditClick = (subCourse: SubCourse) => { const handleEditClick = (subCourse: SubCourse) => {
setSubCourseToEdit(subCourse) setSubCourseToEdit(subCourse);
setTitle(subCourse.title) setTitle(subCourse.title);
setDescription(subCourse.description) setDescription(subCourse.description);
setLevel(subCourse.level) setLevel(subCourse.level);
setSaveError(null) setSaveError(null);
setShowEditModal(true) setShowEditModal(true);
} };
const handleSaveEditSubCourse = async () => { const handleSaveEditSubCourse = async () => {
if (!subCourseToEdit) return if (!subCourseToEdit) return;
setSaving(true) setSaving(true);
setSaveError(null) setSaveError(null);
try { try {
await updateSubCourse(subCourseToEdit.id, { await updateSubCourse(subCourseToEdit.id, {
title, title,
description, description,
level, level,
}) });
setShowEditModal(false) setShowEditModal(false);
setSubCourseToEdit(null) setSubCourseToEdit(null);
setTitle("") setTitle("");
setDescription("") setDescription("");
setLevel("") setLevel("");
await fetchSubCourses() await fetchSubCourses();
} catch (err) { } catch (err) {
console.error("Failed to update sub-course:", err) console.error("Failed to update sub-course:", err);
setSaveError("Failed to update sub-course") setSaveError("Failed to update course");
} finally { } finally {
setSaving(false) setSaving(false);
} }
} };
const handleSubCourseClick = (subCourseId: number) => { const handleSubCourseClick = (subCourseId: number) => {
navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`) navigate(
} `/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`,
);
};
const handlePrereqClick = async (subCourse: SubCourse) => { const handlePrereqClick = async (subCourse: SubCourse) => {
setPrereqSubCourse(subCourse) setPrereqSubCourse(subCourse);
setShowPrereqModal(true) setShowPrereqModal(true);
setPrereqLoading(true) setPrereqLoading(true);
setSelectedPrereqId(0) setSelectedPrereqId(0);
try { try {
const res = await getSubCoursePrerequisites(subCourse.id) const res = await getSubCoursePrerequisites(subCourse.id);
setPrerequisites(res.data.data ?? []) setPrerequisites(res.data.data ?? []);
} catch (err) { } catch (err) {
console.error("Failed to fetch prerequisites:", err) console.error("Failed to fetch prerequisites:", err);
setPrerequisites([]) setPrerequisites([]);
} finally { } finally {
setPrereqLoading(false) setPrereqLoading(false);
} }
} };
const handleAddPrerequisite = async () => { const handleAddPrerequisite = async () => {
if (!prereqSubCourse || !selectedPrereqId) return if (!prereqSubCourse || !selectedPrereqId) return;
setPrereqAdding(true) setPrereqAdding(true);
try { try {
await addSubCoursePrerequisite(prereqSubCourse.id, { await addSubCoursePrerequisite(prereqSubCourse.id, {
prerequisite_sub_course_id: selectedPrereqId, prerequisite_sub_course_id: selectedPrereqId,
}) });
const res = await getSubCoursePrerequisites(prereqSubCourse.id) const res = await getSubCoursePrerequisites(prereqSubCourse.id);
setPrerequisites(res.data.data ?? []) setPrerequisites(res.data.data ?? []);
setSelectedPrereqId(0) setSelectedPrereqId(0);
} catch (err) { } catch (err) {
console.error("Failed to add prerequisite:", err) console.error("Failed to add prerequisite:", err);
} finally { } finally {
setPrereqAdding(false) setPrereqAdding(false);
} }
} };
const handleRemovePrerequisite = async (prereqId: number) => { const handleRemovePrerequisite = async (prereqId: number) => {
if (!prereqSubCourse) return if (!prereqSubCourse) return;
setPrereqRemoving(prereqId) setPrereqRemoving(prereqId);
try { try {
await removeSubCoursePrerequisite(prereqSubCourse.id, prereqId) await removeSubCoursePrerequisite(prereqSubCourse.id, prereqId);
const res = await getSubCoursePrerequisites(prereqSubCourse.id) const res = await getSubCoursePrerequisites(prereqSubCourse.id);
setPrerequisites(res.data.data ?? []) setPrerequisites(res.data.data ?? []);
} catch (err) { } catch (err) {
console.error("Failed to remove prerequisite:", err) console.error("Failed to remove prerequisite:", err);
} finally { } finally {
setPrereqRemoving(null) setPrereqRemoving(null);
} }
} };
// Build flow layers using topological sort // Build flow layers using topological sort
const flowLayers = (() => { const flowLayers = (() => {
if (subCourses.length === 0) return [] if (subCourses.length === 0) return [];
// Find sub-courses with no prerequisites (roots) // Find sub-courses with no prerequisites (roots)
const hasPrereqs = new Set<number>() const hasPrereqs = new Set<number>();
const isPrereqOf = new Map<number, number[]>() // prereqId -> [subCourseIds that depend on it] const isPrereqOf = new Map<number, number[]>(); // prereqId -> [subCourseIds that depend on it]
for (const sc of subCourses) { for (const sc of subCourses) {
const prereqs = allPrereqMap[sc.id] ?? [] const prereqs = allPrereqMap[sc.id] ?? [];
if (prereqs.length > 0) { if (prereqs.length > 0) {
hasPrereqs.add(sc.id) hasPrereqs.add(sc.id);
} }
for (const p of prereqs) { for (const p of prereqs) {
const dependents = isPrereqOf.get(p.prerequisite_sub_course_id) ?? [] const dependents = isPrereqOf.get(p.prerequisite_sub_course_id) ?? [];
dependents.push(sc.id) dependents.push(sc.id);
isPrereqOf.set(p.prerequisite_sub_course_id, dependents) isPrereqOf.set(p.prerequisite_sub_course_id, dependents);
} }
} }
// BFS-based layering // BFS-based layering
const layers: SubCourse[][] = [] const layers: SubCourse[][] = [];
const placed = new Set<number>() const placed = new Set<number>();
// Layer 0: no prerequisites // Layer 0: no prerequisites
const roots = subCourses.filter((sc) => !hasPrereqs.has(sc.id)) const roots = subCourses.filter((sc) => !hasPrereqs.has(sc.id));
if (roots.length > 0) { if (roots.length > 0) {
layers.push(roots) layers.push(roots);
roots.forEach((sc) => placed.add(sc.id)) roots.forEach((sc) => placed.add(sc.id));
} }
// Subsequent layers: all prereqs already placed // Subsequent layers: all prereqs already placed
let maxIterations = subCourses.length let maxIterations = subCourses.length;
while (placed.size < subCourses.length && maxIterations-- > 0) { while (placed.size < subCourses.length && maxIterations-- > 0) {
const nextLayer = subCourses.filter((sc) => { const nextLayer = subCourses.filter((sc) => {
if (placed.has(sc.id)) return false if (placed.has(sc.id)) return false;
const prereqs = allPrereqMap[sc.id] ?? [] const prereqs = allPrereqMap[sc.id] ?? [];
return prereqs.every((p) => placed.has(p.prerequisite_sub_course_id)) return prereqs.every((p) => placed.has(p.prerequisite_sub_course_id));
}) });
if (nextLayer.length === 0) { if (nextLayer.length === 0) {
// Remaining have circular deps or missing prereqs — just add them // Remaining have circular deps or missing prereqs — just add them
const remaining = subCourses.filter((sc) => !placed.has(sc.id)) const remaining = subCourses.filter((sc) => !placed.has(sc.id));
if (remaining.length > 0) layers.push(remaining) if (remaining.length > 0) layers.push(remaining);
break break;
} }
layers.push(nextLayer) layers.push(nextLayer);
nextLayer.forEach((sc) => placed.add(sc.id)) nextLayer.forEach((sc) => placed.add(sc.id));
} }
return layers return layers;
})() })();
const availablePrerequisites = subCourses.filter( const availablePrerequisites = subCourses.filter(
(sc) => (sc) =>
prereqSubCourse && prereqSubCourse &&
sc.id !== prereqSubCourse.id && sc.id !== prereqSubCourse.id &&
!prerequisites.some((p) => p.prerequisite_sub_course_id === sc.id) !prerequisites.some((p) => p.prerequisite_sub_course_id === sc.id),
) );
if (loading) { if (loading) {
return ( return (
@ -355,7 +408,7 @@ export function SubCoursesPage() {
</div> </div>
<p className="mt-4 text-sm font-medium text-grayScale-400">Loading sub-courses...</p> */} <p className="mt-4 text-sm font-medium text-grayScale-400">Loading sub-courses...</p> */}
</div> </div>
) );
} }
if (error) { if (error) {
@ -366,7 +419,7 @@ export function SubCoursesPage() {
<p className="text-sm font-medium text-red-600">{error}</p> <p className="text-sm font-medium text-red-600">{error}</p>
</div> </div>
</div> </div>
) );
} }
return ( return (
@ -382,12 +435,21 @@ export function SubCoursesPage() {
</Link> </Link>
<div className="min-w-0"> <div className="min-w-0">
<div className="mb-1 flex items-center gap-1.5 text-xs font-medium text-grayScale-400"> <div className="mb-1 flex items-center gap-1.5 text-xs font-medium text-grayScale-400">
<span className="truncate max-w-[100px] rounded bg-grayScale-50 px-1.5 py-0.5 sm:max-w-none">{category?.name}</span> <span className="truncate max-w-[100px] rounded bg-grayScale-50 px-1.5 py-0.5 sm:max-w-none">
{category?.name}
</span>
<span className="shrink-0 text-grayScale-300"></span> <span className="shrink-0 text-grayScale-300"></span>
<span className="truncate max-w-[100px] rounded bg-grayScale-50 px-1.5 py-0.5 sm:max-w-none">{course?.title}</span> <span className="truncate max-w-[100px] rounded bg-grayScale-50 px-1.5 py-0.5 sm:max-w-none">
{course?.title}
</span>
</div> </div>
<h1 className="text-2xl font-bold tracking-tight text-grayScale-700">Sub-courses</h1> <h1 className="text-2xl font-bold tracking-tight text-grayScale-700">
<p className="mt-0.5 text-sm text-grayScale-400">{subCourses.length} sub-course{subCourses.length !== 1 ? "s" : ""} available</p> Courses
</h1>
<p className="mt-0.5 text-sm text-grayScale-400">
{subCourses.length} course{subCourses.length !== 1 ? "s" : ""}{" "}
available
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -417,8 +479,11 @@ export function SubCoursesPage() {
</button> </button>
</div> </div>
)} )}
<Button className="w-full rounded-xl bg-brand-500 px-5 shadow-sm transition-all hover:bg-brand-600 hover:shadow-md sm:w-auto" onClick={handleAddSubCourse}> <Button
Add New Sub-course className="w-full rounded-xl bg-brand-500 px-5 shadow-sm transition-all hover:bg-brand-600 hover:shadow-md sm:w-auto"
onClick={handleAddSubCourse}
>
Add New Course
</Button> </Button>
</div> </div>
</div> </div>
@ -428,10 +493,17 @@ export function SubCoursesPage() {
<Card className="border border-dashed border-grayScale-200 shadow-none"> <Card className="border border-dashed border-grayScale-200 shadow-none">
<CardContent className="flex flex-col items-center justify-center py-16"> <CardContent className="flex flex-col items-center justify-center py-16">
<img src={practiceSrc} alt="" className="h-20 w-20" /> <img src={practiceSrc} alt="" className="h-20 w-20" />
<h3 className="mt-5 text-base font-semibold text-grayScale-600">No sub-courses yet</h3> <h3 className="mt-5 text-base font-semibold text-grayScale-600">
<p className="mt-1.5 max-w-xs text-center text-sm text-grayScale-400">Get started by adding your first sub-course to this course</p> No courses yet
<Button className="mt-5 rounded-xl bg-brand-500 px-5 shadow-sm hover:bg-brand-600 hover:shadow-md" onClick={handleAddSubCourse}> </h3>
Add your first sub-course <p className="mt-1.5 max-w-xs text-center text-sm text-grayScale-400">
Get started by adding your first course to this sub-category
</p>
<Button
className="mt-5 rounded-xl bg-brand-500 px-5 shadow-sm hover:bg-brand-600 hover:shadow-md"
onClick={handleAddSubCourse}
>
Add your first course
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@ -443,7 +515,7 @@ export function SubCoursesPage() {
"bg-gradient-to-br from-purple-100 to-purple-200", "bg-gradient-to-br from-purple-100 to-purple-200",
"bg-gradient-to-br from-green-100 to-green-200", "bg-gradient-to-br from-green-100 to-green-200",
"bg-gradient-to-br from-yellow-100 to-yellow-200", "bg-gradient-to-br from-yellow-100 to-yellow-200",
] ];
return ( return (
<Card <Card
key={subCourse.id} key={subCourse.id}
@ -459,7 +531,9 @@ export function SubCoursesPage() {
className="h-full w-full object-cover rounded-t-lg transition-transform duration-300 group-hover:scale-105" className="h-full w-full object-cover rounded-t-lg transition-transform duration-300 group-hover:scale-105"
/> />
) : ( ) : (
<div className={`h-full w-full rounded-t-lg transition-transform duration-300 group-hover:scale-105 ${gradients[index % gradients.length]}`} /> <div
className={`h-full w-full rounded-t-lg transition-transform duration-300 group-hover:scale-105 ${gradients[index % gradients.length]}`}
/>
)} )}
{subCourse.level && ( {subCourse.level && (
<div className="absolute bottom-2.5 right-2.5 rounded-md bg-brand-600/90 px-2.5 py-1 text-xs font-semibold tracking-wide text-white shadow-sm backdrop-blur-sm"> <div className="absolute bottom-2.5 right-2.5 rounded-md bg-brand-600/90 px-2.5 py-1 text-xs font-semibold tracking-wide text-white shadow-sm backdrop-blur-sm">
@ -479,7 +553,9 @@ export function SubCoursesPage() {
: "border border-grayScale-200 bg-grayScale-50 text-grayScale-500" : "border border-grayScale-200 bg-grayScale-50 text-grayScale-500"
}`} }`}
> >
<span className={`mr-1.5 inline-block h-1.5 w-1.5 rounded-full ${subCourse.is_active ? "bg-green-500" : "bg-grayScale-400"}`} /> <span
className={`mr-1.5 inline-block h-1.5 w-1.5 rounded-full ${subCourse.is_active ? "bg-green-500" : "bg-grayScale-400"}`}
/>
{subCourse.is_active ? "Active" : "Inactive"} {subCourse.is_active ? "Active" : "Inactive"}
</Badge> </Badge>
<div <div
@ -488,7 +564,11 @@ export function SubCoursesPage() {
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<button <button
onClick={() => setOpenMenuId(openMenuId === subCourse.id ? null : subCourse.id)} onClick={() =>
setOpenMenuId(
openMenuId === subCourse.id ? null : subCourse.id,
)
}
className="grid h-7 w-7 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600" className="grid h-7 w-7 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
> >
<MoreVertical className="h-4 w-4" /> <MoreVertical className="h-4 w-4" />
@ -497,8 +577,8 @@ export function SubCoursesPage() {
<div className="absolute right-0 top-full z-10 mt-1.5 w-44 overflow-hidden rounded-xl border border-grayScale-100 bg-white py-1 shadow-lg"> <div className="absolute right-0 top-full z-10 mt-1.5 w-44 overflow-hidden rounded-xl border border-grayScale-100 bg-white py-1 shadow-lg">
<button <button
onClick={() => { onClick={() => {
handlePrereqClick(subCourse) handlePrereqClick(subCourse);
setOpenMenuId(null) setOpenMenuId(null);
}} }}
className="flex w-full items-center gap-2.5 px-4 py-2.5 text-sm text-grayScale-600 transition-colors hover:bg-grayScale-50" className="flex w-full items-center gap-2.5 px-4 py-2.5 text-sm text-grayScale-600 transition-colors hover:bg-grayScale-50"
> >
@ -508,8 +588,8 @@ export function SubCoursesPage() {
<div className="mx-3 border-t border-grayScale-100" /> <div className="mx-3 border-t border-grayScale-100" />
<button <button
onClick={() => { onClick={() => {
handleToggleStatus(subCourse) handleToggleStatus(subCourse);
setOpenMenuId(null) setOpenMenuId(null);
}} }}
disabled={togglingId === subCourse.id} disabled={togglingId === subCourse.id}
className="flex w-full items-center gap-2.5 px-4 py-2.5 text-sm text-grayScale-600 transition-colors hover:bg-grayScale-50 disabled:opacity-50" className="flex w-full items-center gap-2.5 px-4 py-2.5 text-sm text-grayScale-600 transition-colors hover:bg-grayScale-50 disabled:opacity-50"
@ -529,8 +609,8 @@ export function SubCoursesPage() {
<div className="mx-3 border-t border-grayScale-100" /> <div className="mx-3 border-t border-grayScale-100" />
<button <button
onClick={() => { onClick={() => {
handleDeleteClick(subCourse) handleDeleteClick(subCourse);
setOpenMenuId(null) setOpenMenuId(null);
}} }}
className="flex w-full items-center gap-2.5 px-4 py-2.5 text-sm text-red-500 transition-colors hover:bg-red-50" className="flex w-full items-center gap-2.5 px-4 py-2.5 text-sm text-red-500 transition-colors hover:bg-red-50"
> >
@ -544,7 +624,9 @@ export function SubCoursesPage() {
{/* Title */} {/* Title */}
<div> <div>
<h3 className="font-semibold text-grayScale-700 group-hover:text-brand-600 transition-colors">{subCourse.title}</h3> <h3 className="font-semibold text-grayScale-700 group-hover:text-brand-600 transition-colors">
{subCourse.title}
</h3>
<p className="mt-1 text-sm leading-relaxed text-grayScale-400 line-clamp-2"> <p className="mt-1 text-sm leading-relaxed text-grayScale-400 line-clamp-2">
{subCourse.description || "No description available"} {subCourse.description || "No description available"}
</p> </p>
@ -555,8 +637,8 @@ export function SubCoursesPage() {
variant="outline" variant="outline"
className="mt-auto w-full rounded-lg border-grayScale-200 text-grayScale-500 transition-all hover:border-brand-200 hover:bg-brand-50 hover:text-brand-600" className="mt-auto w-full rounded-lg border-grayScale-200 text-grayScale-500 transition-all hover:border-brand-200 hover:bg-brand-50 hover:text-brand-600"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
handleEditClick(subCourse) handleEditClick(subCourse);
}} }}
> >
<Edit className="mr-2 h-3.5 w-3.5" /> <Edit className="mr-2 h-3.5 w-3.5" />
@ -564,7 +646,7 @@ export function SubCoursesPage() {
</Button> </Button>
</div> </div>
</Card> </Card>
) );
})} })}
</div> </div>
)} )}
@ -574,7 +656,9 @@ export function SubCoursesPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-sm rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-sm rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
<h2 className="text-lg font-semibold text-grayScale-700">Delete Sub-course</h2> <h2 className="text-lg font-semibold text-grayScale-700">
Delete Course
</h2>
<button <button
onClick={() => setShowDeleteModal(false)} onClick={() => setShowDeleteModal(false)}
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600" className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
@ -589,8 +673,10 @@ export function SubCoursesPage() {
</div> </div>
<p className="text-center text-sm leading-relaxed text-grayScale-600"> <p className="text-center text-sm leading-relaxed text-grayScale-600">
Are you sure you want to delete{" "} Are you sure you want to delete{" "}
<span className="font-semibold text-grayScale-700">{subCourseToDelete.title}</span>? This action cannot <span className="font-semibold text-grayScale-700">
be undone. {subCourseToDelete.title}
</span>
? This action cannot be undone.
</p> </p>
</div> </div>
@ -620,7 +706,9 @@ export function SubCoursesPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
<h2 className="text-lg font-semibold text-grayScale-700">Add New Sub-course</h2> <h2 className="text-lg font-semibold text-grayScale-700">
Add New Course
</h2>
<button <button
onClick={() => setShowAddModal(false)} onClick={() => setShowAddModal(false)}
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600" className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
@ -631,25 +719,31 @@ export function SubCoursesPage() {
<div className="space-y-5 px-6 py-6"> <div className="space-y-5 px-6 py-6">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-semibold text-grayScale-600">Title</label> <label className="text-sm font-semibold text-grayScale-600">
Title
</label>
<Input <Input
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Enter sub-course title" placeholder="Enter course title"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-semibold text-grayScale-600">Description</label> <label className="text-sm font-semibold text-grayScale-600">
Description
</label>
<textarea <textarea
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Enter sub-course description" placeholder="Enter course description"
className="w-full rounded-lg border border-grayScale-200 px-3 py-2.5 text-sm transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20" className="w-full rounded-lg border border-grayScale-200 px-3 py-2.5 text-sm transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
rows={3} rows={3}
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-semibold text-grayScale-600">Level</label> <label className="text-sm font-semibold text-grayScale-600">
Level
</label>
<Input <Input
value={level} value={level}
onChange={(e) => setLevel(e.target.value)} onChange={(e) => setLevel(e.target.value)}
@ -691,9 +785,14 @@ export function SubCoursesPage() {
<div className="mx-4 w-full max-w-lg rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-lg rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
<div> <div>
<h2 className="text-lg font-semibold text-grayScale-700">Prerequisites</h2> <h2 className="text-lg font-semibold text-grayScale-700">
Prerequisites
</h2>
<p className="mt-0.5 text-sm text-grayScale-400"> <p className="mt-0.5 text-sm text-grayScale-400">
Manage prerequisites for <span className="font-medium text-grayScale-600">{prereqSubCourse.title}</span> Manage prerequisites for{" "}
<span className="font-medium text-grayScale-600">
{prereqSubCourse.title}
</span>
</p> </p>
</div> </div>
<button <button
@ -708,14 +807,18 @@ export function SubCoursesPage() {
{/* Add prerequisite */} {/* Add prerequisite */}
{availablePrerequisites.length > 0 && ( {availablePrerequisites.length > 0 && (
<div className="mb-5"> <div className="mb-5">
<label className="mb-1.5 block text-sm font-semibold text-grayScale-600">Add Prerequisite</label> <label className="mb-1.5 block text-sm font-semibold text-grayScale-600">
Add Prerequisite
</label>
<div className="flex gap-2"> <div className="flex gap-2">
<select <select
value={selectedPrereqId} value={selectedPrereqId}
onChange={(e) => setSelectedPrereqId(Number(e.target.value))} onChange={(e) =>
setSelectedPrereqId(Number(e.target.value))
}
className="flex-1 rounded-lg border border-grayScale-200 bg-white px-3 py-2.5 text-sm transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20" className="flex-1 rounded-lg border border-grayScale-200 bg-white px-3 py-2.5 text-sm transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
> >
<option value={0}>Select a sub-course...</option> <option value={0}>Select a course...</option>
{availablePrerequisites.map((sc) => ( {availablePrerequisites.map((sc) => (
<option key={sc.id} value={sc.id}> <option key={sc.id} value={sc.id}>
{sc.title} {sc.level ? `(${sc.level})` : ""} {sc.title} {sc.level ? `(${sc.level})` : ""}
@ -727,7 +830,11 @@ export function SubCoursesPage() {
onClick={handleAddPrerequisite} onClick={handleAddPrerequisite}
disabled={prereqAdding || !selectedPrereqId} disabled={prereqAdding || !selectedPrereqId}
> >
{prereqAdding ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />} {prereqAdding ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Plus className="h-4 w-4" />
)}
</Button> </Button>
</div> </div>
</div> </div>
@ -736,13 +843,21 @@ export function SubCoursesPage() {
{/* Current prerequisites list */} {/* Current prerequisites list */}
{prereqLoading ? ( {prereqLoading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<img src={spinnerSrc} alt="" className="h-8 w-8 animate-spin" /> <img
src={spinnerSrc}
alt=""
className="h-8 w-8 animate-spin"
/>
</div> </div>
) : prerequisites.length === 0 ? ( ) : prerequisites.length === 0 ? (
<div className="rounded-xl border border-dashed border-grayScale-200 px-4 py-8 text-center"> <div className="rounded-xl border border-dashed border-grayScale-200 px-4 py-8 text-center">
<Link2 className="mx-auto h-8 w-8 text-grayScale-300" /> <Link2 className="mx-auto h-8 w-8 text-grayScale-300" />
<p className="mt-2 text-sm font-medium text-grayScale-500">No prerequisites</p> <p className="mt-2 text-sm font-medium text-grayScale-500">
<p className="mt-0.5 text-xs text-grayScale-400">This sub-course is accessible without completing others first</p> No prerequisites
</p>
<p className="mt-0.5 text-xs text-grayScale-400">
This course is accessible without completing others first
</p>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
@ -755,7 +870,9 @@ export function SubCoursesPage() {
className="flex items-center justify-between rounded-xl border border-grayScale-100 bg-grayScale-25 px-4 py-3 transition-colors hover:border-grayScale-200" className="flex items-center justify-between rounded-xl border border-grayScale-100 bg-grayScale-25 px-4 py-3 transition-colors hover:border-grayScale-200"
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-sm font-medium text-grayScale-700 truncate">{prereq.prerequisite_title}</p> <p className="text-sm font-medium text-grayScale-700 truncate">
{prereq.prerequisite_title}
</p>
<div className="mt-0.5 flex items-center gap-2"> <div className="mt-0.5 flex items-center gap-2">
{prereq.prerequisite_level && ( {prereq.prerequisite_level && (
<span className="rounded bg-brand-50 px-1.5 py-0.5 text-[11px] font-medium text-brand-600"> <span className="rounded bg-brand-50 px-1.5 py-0.5 text-[11px] font-medium text-brand-600">
@ -802,7 +919,9 @@ export function SubCoursesPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl"> <div className="mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4"> <div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
<h2 className="text-lg font-semibold text-grayScale-700">Edit Sub-course</h2> <h2 className="text-lg font-semibold text-grayScale-700">
Edit Course
</h2>
<button <button
onClick={() => setShowEditModal(false)} onClick={() => setShowEditModal(false)}
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600" className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
@ -813,25 +932,31 @@ export function SubCoursesPage() {
<div className="space-y-5 px-6 py-6"> <div className="space-y-5 px-6 py-6">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-semibold text-grayScale-600">Title</label> <label className="text-sm font-semibold text-grayScale-600">
Title
</label>
<Input <Input
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Enter sub-course title" placeholder="Enter course title"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-semibold text-grayScale-600">Description</label> <label className="text-sm font-semibold text-grayScale-600">
Description
</label>
<textarea <textarea
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Enter sub-course description" placeholder="Enter course description"
className="w-full rounded-lg border border-grayScale-200 px-3 py-2.5 text-sm transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20" className="w-full rounded-lg border border-grayScale-200 px-3 py-2.5 text-sm transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
rows={3} rows={3}
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-semibold text-grayScale-600">Level</label> <label className="text-sm font-semibold text-grayScale-600">
Level
</label>
<Input <Input
value={level} value={level}
onChange={(e) => setLevel(e.target.value)} onChange={(e) => setLevel(e.target.value)}
@ -867,5 +992,5 @@ export function SubCoursesPage() {
</div> </div>
)} )}
</div> </div>
) );
} }

View File

@ -498,8 +498,8 @@ export interface GetLearningPathResponse {
} }
export interface ReorderItem { export interface ReorderItem {
sub_course_id: number id: number
display_order: number position: number
} }
// Ratings // Ratings