refine empty-state layout and enable first path creation

Adopt a structured empty-state layout similar to sub-category management screens, auto-create the Human Language category when missing, hide level listing until a path exists, and surface first-path creation clearly.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-07 07:04:24 -07:00
parent e6adf2850e
commit 383886156c

View File

@ -1,10 +1,10 @@
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { BookOpen, ChevronDown, ChevronRight, Languages, Loader2, Plus } from "lucide-react" import { BookOpen, ChevronDown, ChevronRight, Languages, Loader2, Plus, Search } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
import { Button } from "../../components/ui/button" import { Button } from "../../components/ui/button"
import { SpinnerIcon } from "../../components/ui/spinner-icon" import { SpinnerIcon } from "../../components/ui/spinner-icon"
import { createCourse, createHumanLanguageLesson, getHumanLanguageHierarchy } from "../../api/courses.api" import { createCourse, createCourseCategory, createHumanLanguageLesson, getHumanLanguageHierarchy } from "../../api/courses.api"
import type { HumanLanguageCourseTree, HumanLanguageSubCategoryTree } from "../../types/course.types" import type { HumanLanguageCourseTree, HumanLanguageSubCategoryTree } from "../../types/course.types"
import { toast } from "sonner" import { toast } from "sonner"
@ -22,6 +22,7 @@ export function HumanLanguagePage() {
const [creatingKey, setCreatingKey] = useState<string | null>(null) const [creatingKey, setCreatingKey] = useState<string | null>(null)
const [quickSubCategoryName, setQuickSubCategoryName] = useState("") const [quickSubCategoryName, setQuickSubCategoryName] = useState("")
const [quickCourseName, setQuickCourseName] = useState("") const [quickCourseName, setQuickCourseName] = useState("")
const [quickSearch, setQuickSearch] = useState("")
const [quickCreating, setQuickCreating] = useState(false) const [quickCreating, setQuickCreating] = useState(false)
const loadHierarchy = async () => { const loadHierarchy = async () => {
@ -151,19 +152,24 @@ export function HumanLanguagePage() {
} }
const handleQuickCreatePath = async () => { const handleQuickCreatePath = async () => {
if (!categoryId) {
toast.error("Human Language category is not available")
return
}
if (!quickSubCategoryName.trim() || !quickCourseName.trim()) { if (!quickSubCategoryName.trim() || !quickCourseName.trim()) {
toast.error("Subcategory and course names are required") toast.error("Subcategory and course names are required")
return return
} }
setQuickCreating(true) setQuickCreating(true)
try { try {
let effectiveCategoryId = categoryId
if (!effectiveCategoryId) {
const createdCategory = await createCourseCategory({ name: "Human Language" })
effectiveCategoryId = createdCategory.data?.data?.id ?? null
setCategoryId(effectiveCategoryId)
}
if (!effectiveCategoryId) {
throw new Error("Missing human language category id")
}
const title = `${quickSubCategoryName.trim()} - ${quickCourseName.trim()}` const title = `${quickSubCategoryName.trim()} - ${quickCourseName.trim()}`
await createCourse({ await createCourse({
category_id: categoryId, category_id: effectiveCategoryId,
title, title,
description: `${quickSubCategoryName.trim()} / ${quickCourseName.trim()}`, description: `${quickSubCategoryName.trim()} / ${quickCourseName.trim()}`,
}) })
@ -268,12 +274,29 @@ export function HumanLanguagePage() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{availableCourses.length === 0 ? ( {availableCourses.length === 0 ? (
<Card className="border-grayScale-200/80"> <Card className="overflow-hidden border-grayScale-200/80">
<CardContent className="flex flex-col items-start gap-3 p-4"> <div className="flex items-center justify-between border-b border-grayScale-100 bg-white px-5 py-4">
<p className="text-sm text-grayScale-600"> <h3 className="text-lg font-semibold text-grayScale-800">Sub-category Management</h3>
No Human Language subcategory/course is available yet. Create the language course path first, then you can add incremental modules and sub-modules per level. <div className="relative w-full max-w-sm">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-grayScale-400" />
<input
className="h-11 w-full rounded-xl border border-grayScale-200 bg-white pl-9 pr-3 text-sm"
placeholder="Search sub-categories..."
value={quickSearch}
onChange={(e) => setQuickSearch(e.target.value)}
/>
</div>
</div>
<CardContent className="p-5">
<div className="rounded-2xl border border-dashed border-grayScale-300 bg-grayScale-50/20 px-6 py-10 text-center">
<div className="mx-auto mb-4 grid h-12 w-12 place-items-center rounded-full bg-brand-100 text-brand-700">
<Languages className="h-6 w-6" />
</div>
<h4 className="text-xl font-semibold text-grayScale-800">No sub-categories yet</h4>
<p className="mt-2 text-sm text-grayScale-500">
Create your first human-language path. Level listing will appear automatically after creation.
</p> </p>
<div className="grid w-full grid-cols-1 gap-2 md:grid-cols-3"> <div className="mx-auto mt-5 grid max-w-3xl grid-cols-1 gap-2 md:grid-cols-3">
<input <input
className="h-10 rounded-md border border-grayScale-200 bg-white px-3 text-sm" className="h-10 rounded-md border border-grayScale-200 bg-white px-3 text-sm"
placeholder="Subcategory (e.g., English)" placeholder="Subcategory (e.g., English)"
@ -286,24 +309,17 @@ export function HumanLanguagePage() {
value={quickCourseName} value={quickCourseName}
onChange={(e) => setQuickCourseName(e.target.value)} onChange={(e) => setQuickCourseName(e.target.value)}
/> />
<Button onClick={handleQuickCreatePath} disabled={quickCreating || !categoryId}> <Button onClick={handleQuickCreatePath} disabled={quickCreating}>
{quickCreating ? "Creating..." : "Quick Create Path"} {quickCreating ? "Creating..." : "Add your first sub-category"}
</Button> </Button>
</div> </div>
<div className="flex flex-wrap gap-2">
<Link to="/content/courses">
<Button variant="outline">Open Courses</Button>
</Link>
{categoryId ? (
<Link to={`/content/category/${categoryId}/courses`}>
<Button>Open Human Language Courses</Button>
</Link>
) : null}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) : null} ) : null}
{CEFR_LEVELS.filter((l) => selectedLevel === "ALL" || l === selectedLevel).map((level) => {
{availableCourses.length > 0
? CEFR_LEVELS.filter((l) => selectedLevel === "ALL" || l === selectedLevel).map((level) => {
const modulesByCourse = selectedCourses const modulesByCourse = selectedCourses
.map((course: HumanLanguageCourseTree) => { .map((course: HumanLanguageCourseTree) => {
const levelNode = course.levels.find((item) => item.level.toUpperCase() === level) const levelNode = course.levels.find((item) => item.level.toUpperCase() === level)
@ -312,6 +328,7 @@ export function HumanLanguagePage() {
modules: levelNode?.modules ?? [], modules: levelNode?.modules ?? [],
} }
}) })
.filter((entry) => entry.modules.length > 0 || selectedCourses.length > 0)
return ( return (
<Card key={level} className="overflow-hidden border-grayScale-200/80 shadow-sm"> <Card key={level} className="overflow-hidden border-grayScale-200/80 shadow-sm">
<button <button
@ -415,7 +432,8 @@ export function HumanLanguagePage() {
) : null} ) : null}
</Card> </Card>
) )
})} })
: null}
</div> </div>
)} )}
</div> </div>