feat(classes): optimize teacher dashboard ui and implement grade management
This commit is contained in:
@@ -6,6 +6,7 @@ import { MoreHorizontal, Eye, Pencil, Trash, Archive, UploadCloud, Undo2, Copy }
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -27,13 +28,14 @@ import {
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
|
||||
import { deleteExamAction, duplicateExamAction, updateExamAction } from "../actions"
|
||||
import { deleteExamAction, duplicateExamAction, updateExamAction, getExamPreviewAction } from "../actions"
|
||||
import { Exam } from "../types"
|
||||
import { ExamPaperPreview } from "./assembly/exam-paper-preview"
|
||||
import type { ExamNode } from "./assembly/selected-question-list"
|
||||
import type { Question } from "@/modules/questions/types"
|
||||
|
||||
interface ExamActionsProps {
|
||||
exam: Exam
|
||||
@@ -44,6 +46,46 @@ export function ExamActions({ exam }: ExamActionsProps) {
|
||||
const [showViewDialog, setShowViewDialog] = useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [isWorking, setIsWorking] = useState(false)
|
||||
const [previewNodes, setPreviewNodes] = useState<ExamNode[] | null>(null)
|
||||
const [loadingPreview, setLoadingPreview] = useState(false)
|
||||
|
||||
const handleView = async () => {
|
||||
setLoadingPreview(true)
|
||||
setShowViewDialog(true)
|
||||
try {
|
||||
const result = await getExamPreviewAction(exam.id)
|
||||
if (result.success && result.data) {
|
||||
const { structure, questions } = result.data
|
||||
const questionById = new Map<string, Question>()
|
||||
for (const q of questions) questionById.set(q.id, q as unknown as Question)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const hydrate = (nodes: any[]): ExamNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.type === "question") {
|
||||
const q = node.questionId ? questionById.get(node.questionId) : undefined
|
||||
return { ...node, question: q }
|
||||
}
|
||||
if (node.type === "group") {
|
||||
return { ...node, children: hydrate(node.children || []) }
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
const nodes = Array.isArray(structure) ? hydrate(structure) : []
|
||||
setPreviewNodes(nodes)
|
||||
} else {
|
||||
toast.error("Failed to load exam preview")
|
||||
setShowViewDialog(false)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error("Failed to load exam preview")
|
||||
setShowViewDialog(false)
|
||||
} finally {
|
||||
setLoadingPreview(false)
|
||||
}
|
||||
}
|
||||
|
||||
const copyId = () => {
|
||||
navigator.clipboard.writeText(exam.id)
|
||||
@@ -112,25 +154,35 @@ export function ExamActions({ exam }: ExamActionsProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={copyId}>
|
||||
<Copy className="mr-2 h-4 w-4" /> Copy ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setShowViewDialog(true)}>
|
||||
<Eye className="mr-2 h-4 w-4" /> View
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => router.push(`/teacher/exams/${exam.id}/build`)}>
|
||||
<Pencil className="mr-2 h-4 w-4" /> Edit
|
||||
</DropdownMenuItem>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleView()
|
||||
}}
|
||||
title="Preview Exam"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={copyId}>
|
||||
<Copy className="mr-2 h-4 w-4" /> Copy ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<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>
|
||||
@@ -166,49 +218,21 @@ export function ExamActions({ exam }: ExamActionsProps) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Dialog open={showViewDialog} onOpenChange={setShowViewDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Exam Details</DialogTitle>
|
||||
<DialogDescription>ID: {exam.id}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<span className="font-medium">Title:</span>
|
||||
<span className="col-span-3">{exam.title}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<span className="font-medium">Subject:</span>
|
||||
<span className="col-span-3">{exam.subject}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<span className="font-medium">Grade:</span>
|
||||
<span className="col-span-3">{exam.grade}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<span className="font-medium">Total Score:</span>
|
||||
<span className="col-span-3">{exam.totalScore}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<span className="font-medium">Duration:</span>
|
||||
<span className="col-span-3">{exam.durationMin} min</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete exam?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the exam.
|
||||
This action cannot be undone. This will permanently delete the exam
|
||||
"{exam.title}" and remove all associated data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleDelete()
|
||||
@@ -220,6 +244,34 @@ export function ExamActions({ exam }: ExamActionsProps) {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<Dialog open={showViewDialog} onOpenChange={setShowViewDialog}>
|
||||
<DialogContent className="max-w-4xl h-[90vh] flex flex-col p-0 gap-0">
|
||||
<div className="p-4 border-b shrink-0 flex items-center justify-between">
|
||||
<DialogTitle className="text-lg font-semibold tracking-tight">{exam.title}</DialogTitle>
|
||||
</div>
|
||||
<ScrollArea className="flex-1">
|
||||
{loadingPreview ? (
|
||||
<div className="py-20 text-center text-muted-foreground">Loading preview...</div>
|
||||
) : previewNodes && previewNodes.length > 0 ? (
|
||||
<div className="max-w-3xl mx-auto py-8 px-6">
|
||||
<ExamPaperPreview
|
||||
title={exam.title}
|
||||
subject={exam.subject}
|
||||
grade={exam.grade}
|
||||
durationMin={exam.durationMin}
|
||||
totalScore={exam.totalScore}
|
||||
nodes={previewNodes}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-20 text-center text-muted-foreground">
|
||||
No questions in this exam.
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user