完整性更新
Some checks failed
CI / build-and-test (push) Failing after 3m50s
CI / deploy (push) Has been skipped

现在已经实现了大部分基础功能
This commit is contained in:
SpecialX
2026-01-08 11:14:03 +08:00
parent 0da2eac0b4
commit 57807def37
155 changed files with 26421 additions and 1036 deletions

View File

@@ -0,0 +1,158 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { MoreHorizontal, UserCheck, UserX } from "lucide-react"
import { toast } from "sonner"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { cn } from "@/shared/lib/utils"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/shared/components/ui/alert-dialog"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/components/ui/table"
import type { ClassStudent } from "../types"
import { setStudentEnrollmentStatusAction } from "../actions"
export function StudentsTable({ students }: { students: ClassStudent[] }) {
const router = useRouter()
const [workingKey, setWorkingKey] = useState<string | null>(null)
const [removeTarget, setRemoveTarget] = useState<ClassStudent | null>(null)
const setStatus = async (student: ClassStudent, status: "active" | "inactive") => {
const key = `${student.classId}:${student.id}:${status}`
setWorkingKey(key)
try {
const res = await setStudentEnrollmentStatusAction(student.classId, student.id, status)
if (res.success) {
toast.success(res.message)
router.refresh()
} else {
toast.error(res.message || "Failed to update student")
}
} catch {
toast.error("Failed to update student")
} finally {
setWorkingKey(null)
}
}
return (
<>
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="text-xs font-medium uppercase text-muted-foreground">Student</TableHead>
<TableHead className="text-xs font-medium uppercase text-muted-foreground">Email</TableHead>
<TableHead className="text-xs font-medium uppercase text-muted-foreground">Class</TableHead>
<TableHead className="text-xs font-medium uppercase text-muted-foreground">Status</TableHead>
<TableHead className="text-xs font-medium uppercase text-muted-foreground text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{students.map((s) => (
<TableRow key={`${s.classId}:${s.id}`} className={cn("h-12", s.status !== "active" && "opacity-70")}>
<TableCell className="font-medium">{s.name}</TableCell>
<TableCell className="text-muted-foreground">{s.email}</TableCell>
<TableCell>{s.className}</TableCell>
<TableCell>
<Badge variant={s.status === "active" ? "secondary" : "outline"}>
{s.status === "active" ? "Active" : "Inactive"}
</Badge>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" disabled={workingKey !== null}>
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{s.status !== "active" ? (
<DropdownMenuItem onClick={() => setStatus(s, "active")} disabled={workingKey !== null}>
<UserCheck className="mr-2 size-4" />
Set active
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={() => setStatus(s, "inactive")} disabled={workingKey !== null}>
<UserX className="mr-2 size-4" />
Set inactive
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setRemoveTarget(s)}
className="text-destructive focus:text-destructive"
disabled={s.status === "inactive" || workingKey !== null}
>
<UserX className="mr-2 size-4" />
Remove from class
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<AlertDialog
open={Boolean(removeTarget)}
onOpenChange={(open) => {
if (workingKey !== null) return
if (!open) setRemoveTarget(null)
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Remove student from class?</AlertDialogTitle>
<AlertDialogDescription>
{removeTarget ? (
<>
This will set <span className="font-medium text-foreground">{removeTarget.name}</span> to inactive in{" "}
<span className="font-medium text-foreground">{removeTarget.className}</span>.
</>
) : null}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={workingKey !== null}>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={workingKey !== null}
onClick={() => {
if (!removeTarget) return
setRemoveTarget(null)
setStatus(removeTarget, "inactive")
}}
>
Remove
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}