feat: exam actions and data safety fixes

This commit is contained in:
SpecialX
2025-12-30 17:48:22 +08:00
parent e7c902e8e1
commit f7ff018490
27 changed files with 896 additions and 194 deletions

View File

@@ -32,6 +32,7 @@ import {
DialogTitle,
} from "@/shared/components/ui/dialog"
import { deleteExamAction, duplicateExamAction, updateExamAction } from "../actions"
import { Exam } from "../types"
interface ExamActionsProps {
@@ -42,31 +43,70 @@ export function ExamActions({ exam }: ExamActionsProps) {
const router = useRouter()
const [showViewDialog, setShowViewDialog] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [isWorking, setIsWorking] = useState(false)
const copyId = () => {
navigator.clipboard.writeText(exam.id)
toast.success("Exam ID copied to clipboard")
}
const publishExam = async () => {
toast.success("Exam published")
const setStatus = async (status: Exam["status"]) => {
setIsWorking(true)
try {
const formData = new FormData()
formData.set("examId", exam.id)
formData.set("status", status)
const result = await updateExamAction(null, formData)
if (result.success) {
toast.success(status === "published" ? "Exam published" : status === "archived" ? "Exam archived" : "Exam moved to draft")
router.refresh()
} else {
toast.error(result.message || "Failed to update exam")
}
} catch {
toast.error("Failed to update exam")
} finally {
setIsWorking(false)
}
}
const unpublishExam = async () => {
toast.success("Exam moved to draft")
}
const archiveExam = async () => {
toast.success("Exam archived")
const duplicateExam = async () => {
setIsWorking(true)
try {
const formData = new FormData()
formData.set("examId", exam.id)
const result = await duplicateExamAction(null, formData)
if (result.success && result.data) {
toast.success("Exam duplicated")
router.push(`/teacher/exams/${result.data}/build`)
router.refresh()
} else {
toast.error(result.message || "Failed to duplicate exam")
}
} catch {
toast.error("Failed to duplicate exam")
} finally {
setIsWorking(false)
}
}
const handleDelete = async () => {
setIsWorking(true)
try {
await new Promise((r) => setTimeout(r, 800))
toast.success("Exam deleted successfully")
setShowDeleteDialog(false)
} catch (e) {
const formData = new FormData()
formData.set("examId", exam.id)
const result = await deleteExamAction(null, formData)
if (result.success) {
toast.success("Exam deleted successfully")
setShowDeleteDialog(false)
router.refresh()
} else {
toast.error(result.message || "Failed to delete exam")
}
} catch {
toast.error("Failed to delete exam")
} finally {
setIsWorking(false)
}
}
@@ -88,25 +128,39 @@ export function ExamActions({ exam }: ExamActionsProps) {
<DropdownMenuItem onClick={() => setShowViewDialog(true)}>
<Eye className="mr-2 h-4 w-4" /> View
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push(`/teacher/exams/${exam.id}/build`)}>
<Pencil className="mr-2 h-4 w-4" /> Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push(`/teacher/exams/${exam.id}/build`)}>
<MoreHorizontal className="mr-2 h-4 w-4" /> Build
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={publishExam}>
<DropdownMenuItem onClick={duplicateExam} disabled={isWorking}>
<Copy className="mr-2 h-4 w-4" /> Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setStatus("published")}
disabled={isWorking || exam.status === "published"}
>
<UploadCloud className="mr-2 h-4 w-4" /> Publish
</DropdownMenuItem>
<DropdownMenuItem onClick={unpublishExam}>
<DropdownMenuItem
onClick={() => setStatus("draft")}
disabled={isWorking || exam.status === "draft"}
>
<Undo2 className="mr-2 h-4 w-4" /> Move to Draft
</DropdownMenuItem>
<DropdownMenuItem onClick={archiveExam}>
<DropdownMenuItem
onClick={() => setStatus("archived")}
disabled={isWorking || exam.status === "archived"}
>
<Archive className="mr-2 h-4 w-4" /> Archive
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setShowDeleteDialog(true)}
disabled={isWorking}
>
<Trash className="mr-2 h-4 w-4" /> Delete
</DropdownMenuItem>
@@ -159,6 +213,7 @@ export function ExamActions({ exam }: ExamActionsProps) {
e.preventDefault()
handleDelete()
}}
disabled={isWorking}
>
Delete
</AlertDialogAction>