add category delete UI and streamline module creation loading

Add deletion controls for course categories and avoid full-page loading overlay during module/sub-module creation so button-level spinners remain the only loading indicator.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-14 06:22:36 -07:00
parent 51ac1ad81d
commit e5d1ba9b8d
3 changed files with 73 additions and 13 deletions

View File

@ -153,6 +153,9 @@ export const createCourseCategory = (data: CreateCourseCategoryRequest) =>
? http.post("/course-management/sub-categories", { category_id: data.parent_id, name: data.name })
: http.post("/course-management/categories", { name: data.name })
export const deleteCourseCategory = (categoryId: number) =>
http.delete(`/course-management/categories/${categoryId}`)
export const deleteCourseSubCategory = (subCategoryId: number) =>
http.delete(`/course-management/sub-categories/${subCategoryId}`)

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react"
import { Link } from "react-router-dom"
import { FolderOpen, RefreshCw, BookOpen, Plus } from "lucide-react"
import { FolderOpen, RefreshCw, BookOpen, Plus, Trash2 } from "lucide-react"
import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg"
import alertSrc from "../../assets/Alert.svg"
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
@ -11,10 +11,11 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../../components/ui/dialog"
import { getCourseCategories, createCourseCategory } from "../../api/courses.api"
import { getCourseCategories, createCourseCategory, deleteCourseCategory } from "../../api/courses.api"
import type { CourseCategory } from "../../types/course.types"
import { toast } from "sonner"
@ -29,6 +30,8 @@ export function CourseCategoryPage() {
const [newSubCategoryName, setNewSubCategoryName] = useState("")
const [pendingSubCategories, setPendingSubCategories] = useState<string[]>([])
const [searchQuery, setSearchQuery] = useState("")
const [deleteTarget, setDeleteTarget] = useState<CourseCategory | null>(null)
const [deleting, setDeleting] = useState(false)
const fetchCategories = async () => {
setLoading(true)
@ -164,12 +167,26 @@ export function CourseCategoryPage() {
</CardHeader>
<CardContent>
<span className="inline-flex items-center gap-1.5 text-sm font-medium text-brand-500 transition-colors group-hover:text-brand-600">
View Sub-categories
<span className="inline-block transition-transform duration-300 group-hover:translate-x-1">
<div className="flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1.5 text-sm font-medium text-brand-500 transition-colors group-hover:text-brand-600">
View Sub-categories
<span className="inline-block transition-transform duration-300 group-hover:translate-x-1">
</span>
</span>
</span>
<button
type="button"
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-red-200 bg-white text-red-500 hover:bg-red-50"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setDeleteTarget(category)
}}
aria-label={`Delete category ${category.name}`}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</CardContent>
</Card>
</Link>
@ -335,7 +352,7 @@ export function CourseCategoryPage() {
if (createdCategoryId && pendingSubCategories.length > 0) {
await Promise.all(
pendingSubCategories.map((subName) =>
createCourseCategory({ name: subName }),
createCourseCategory({ name: subName, parent_id: createdCategoryId }),
),
)
}
@ -371,6 +388,46 @@ export function CourseCategoryPage() {
</div>
</DialogContent>
</Dialog>
<Dialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Delete category?</DialogTitle>
<DialogDescription>
{deleteTarget
? `This will permanently delete "${deleteTarget.name}" and all linked sub-categories/courses.`
: ""}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDeleteTarget(null)} disabled={deleting}>
Cancel
</Button>
<Button
type="button"
className="bg-red-600 text-white hover:bg-red-700"
disabled={deleting}
onClick={async () => {
if (!deleteTarget) return
setDeleting(true)
try {
await deleteCourseCategory(deleteTarget.id)
toast.success("Category deleted")
setDeleteTarget(null)
await fetchCategories()
} catch (err: any) {
const message = err?.response?.data?.message || "Failed to delete category."
toast.error("Could not delete category", { description: message })
} finally {
setDeleting(false)
}
}}
>
{deleting ? "Deleting..." : "Delete"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@ -380,8 +380,8 @@ export function HumanLanguagePage() {
label?: string,
) => <MediaPreviewCard urlRaw={urlRaw} hint={hint} className={className} label={label} />
const loadHierarchy = async () => {
setLoading(true)
const loadHierarchy = async (showLoading = true) => {
if (showLoading) setLoading(true)
try {
const res = await getHumanLanguageHierarchy()
const data = res.data?.data
@ -404,7 +404,7 @@ export function HumanLanguagePage() {
setCollapsedModuleIds(moduleIds)
setCollapsedSubModuleIds(subModuleIds)
} finally {
setLoading(false)
if (showLoading) setLoading(false)
}
}
@ -525,7 +525,7 @@ export function HumanLanguagePage() {
const next = nextMissingPositive(usedNumbers)
const title = `Module-${next}`
await createModuleInLevel(levelNode.level_id, title, `${level} ${title}`, next)
await loadHierarchy()
await loadHierarchy(false)
} catch (error) {
console.error("Failed to create module:", error)
toast.error("Failed to create module")
@ -556,7 +556,7 @@ export function HumanLanguagePage() {
const next = nextMissingPositive(usedNumbers)
const title = `Module-${moduleNo}.${next}`
await createSubModuleInModule(moduleId, title, `${level} ${title}`, next)
await loadHierarchy()
await loadHierarchy(false)
} catch (error) {
console.error("Failed to create sub-module:", error)
toast.error("Failed to create sub-module")