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:
parent
51ac1ad81d
commit
e5d1ba9b8d
|
|
@ -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/sub-categories", { category_id: data.parent_id, name: data.name })
|
||||||
: http.post("/course-management/categories", { 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) =>
|
export const deleteCourseSubCategory = (subCategoryId: number) =>
|
||||||
http.delete(`/course-management/sub-categories/${subCategoryId}`)
|
http.delete(`/course-management/sub-categories/${subCategoryId}`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Link } from "react-router-dom"
|
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 spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg"
|
||||||
import alertSrc from "../../assets/Alert.svg"
|
import alertSrc from "../../assets/Alert.svg"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
||||||
|
|
@ -11,10 +11,11 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "../../components/ui/dialog"
|
} 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 type { CourseCategory } from "../../types/course.types"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
|
@ -29,6 +30,8 @@ export function CourseCategoryPage() {
|
||||||
const [newSubCategoryName, setNewSubCategoryName] = useState("")
|
const [newSubCategoryName, setNewSubCategoryName] = useState("")
|
||||||
const [pendingSubCategories, setPendingSubCategories] = useState<string[]>([])
|
const [pendingSubCategories, setPendingSubCategories] = useState<string[]>([])
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<CourseCategory | null>(null)
|
||||||
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -164,12 +167,26 @@ export function CourseCategoryPage() {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
View Sub-categories
|
<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-block transition-transform duration-300 group-hover:translate-x-1">
|
View Sub-categories
|
||||||
→
|
<span className="inline-block transition-transform duration-300 group-hover:translate-x-1">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -335,7 +352,7 @@ export function CourseCategoryPage() {
|
||||||
if (createdCategoryId && pendingSubCategories.length > 0) {
|
if (createdCategoryId && pendingSubCategories.length > 0) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pendingSubCategories.map((subName) =>
|
pendingSubCategories.map((subName) =>
|
||||||
createCourseCategory({ name: subName }),
|
createCourseCategory({ name: subName, parent_id: createdCategoryId }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +388,46 @@ export function CourseCategoryPage() {
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -380,8 +380,8 @@ export function HumanLanguagePage() {
|
||||||
label?: string,
|
label?: string,
|
||||||
) => <MediaPreviewCard urlRaw={urlRaw} hint={hint} className={className} label={label} />
|
) => <MediaPreviewCard urlRaw={urlRaw} hint={hint} className={className} label={label} />
|
||||||
|
|
||||||
const loadHierarchy = async () => {
|
const loadHierarchy = async (showLoading = true) => {
|
||||||
setLoading(true)
|
if (showLoading) setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await getHumanLanguageHierarchy()
|
const res = await getHumanLanguageHierarchy()
|
||||||
const data = res.data?.data
|
const data = res.data?.data
|
||||||
|
|
@ -404,7 +404,7 @@ export function HumanLanguagePage() {
|
||||||
setCollapsedModuleIds(moduleIds)
|
setCollapsedModuleIds(moduleIds)
|
||||||
setCollapsedSubModuleIds(subModuleIds)
|
setCollapsedSubModuleIds(subModuleIds)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (showLoading) setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -525,7 +525,7 @@ export function HumanLanguagePage() {
|
||||||
const next = nextMissingPositive(usedNumbers)
|
const next = nextMissingPositive(usedNumbers)
|
||||||
const title = `Module-${next}`
|
const title = `Module-${next}`
|
||||||
await createModuleInLevel(levelNode.level_id, title, `${level} ${title}`, next)
|
await createModuleInLevel(levelNode.level_id, title, `${level} ${title}`, next)
|
||||||
await loadHierarchy()
|
await loadHierarchy(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create module:", error)
|
console.error("Failed to create module:", error)
|
||||||
toast.error("Failed to create module")
|
toast.error("Failed to create module")
|
||||||
|
|
@ -556,7 +556,7 @@ export function HumanLanguagePage() {
|
||||||
const next = nextMissingPositive(usedNumbers)
|
const next = nextMissingPositive(usedNumbers)
|
||||||
const title = `Module-${moduleNo}.${next}`
|
const title = `Module-${moduleNo}.${next}`
|
||||||
await createSubModuleInModule(moduleId, title, `${level} ${title}`, next)
|
await createSubModuleInModule(moduleId, title, `${level} ${title}`, next)
|
||||||
await loadHierarchy()
|
await loadHierarchy(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create sub-module:", error)
|
console.error("Failed to create sub-module:", error)
|
||||||
toast.error("Failed to create sub-module")
|
toast.error("Failed to create sub-module")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user