Redirect /teacher/exams/grading* to /teacher/homework/submissions; remove exam grading UI/actions/data-access; add homework student workflow and update design docs.
153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import { Suspense } from "react"
|
|
import Link from "next/link"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { EmptyState } from "@/shared/components/ui/empty-state"
|
|
import { Skeleton } from "@/shared/components/ui/skeleton"
|
|
import { ExamDataTable } from "@/modules/exams/components/exam-data-table"
|
|
import { examColumns } from "@/modules/exams/components/exam-columns"
|
|
import { ExamFilters } from "@/modules/exams/components/exam-filters"
|
|
import { getExams } from "@/modules/exams/data-access"
|
|
import { FileText, PlusCircle } from "lucide-react"
|
|
|
|
type SearchParams = { [key: string]: string | string[] | undefined }
|
|
|
|
const getParam = (params: SearchParams, key: string) => {
|
|
const v = params[key]
|
|
return Array.isArray(v) ? v[0] : v
|
|
}
|
|
|
|
async function ExamsResults({ searchParams }: { searchParams: Promise<SearchParams> }) {
|
|
const params = await searchParams
|
|
|
|
const q = getParam(params, "q")
|
|
const status = getParam(params, "status")
|
|
const difficulty = getParam(params, "difficulty")
|
|
|
|
const exams = await getExams({
|
|
q,
|
|
status,
|
|
difficulty,
|
|
})
|
|
|
|
const hasFilters = Boolean(q || (status && status !== "all") || (difficulty && difficulty !== "all"))
|
|
|
|
const counts = exams.reduce(
|
|
(acc, e) => {
|
|
acc.total += 1
|
|
if (e.status === "draft") acc.draft += 1
|
|
if (e.status === "published") acc.published += 1
|
|
if (e.status === "archived") acc.archived += 1
|
|
return acc
|
|
},
|
|
{ total: 0, draft: 0, published: 0, archived: 0 }
|
|
)
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col gap-3 rounded-md border bg-card px-4 py-3 md:flex-row md:items-center md:justify-between">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm text-muted-foreground">Showing</span>
|
|
<span className="text-sm font-medium">{counts.total}</span>
|
|
<span className="text-sm text-muted-foreground">exams</span>
|
|
<Badge variant="outline" className="ml-0 md:ml-2">
|
|
Draft {counts.draft}
|
|
</Badge>
|
|
<Badge variant="outline">Published {counts.published}</Badge>
|
|
<Badge variant="outline">Archived {counts.archived}</Badge>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button asChild size="sm">
|
|
<Link href="/teacher/exams/create" className="inline-flex items-center gap-2">
|
|
<PlusCircle className="h-4 w-4" />
|
|
Create Exam
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{exams.length === 0 ? (
|
|
<EmptyState
|
|
icon={FileText}
|
|
title={hasFilters ? "No exams match your filters" : "No exams yet"}
|
|
description={
|
|
hasFilters
|
|
? "Try clearing filters or adjusting keywords."
|
|
: "Create your first exam to start assigning and grading."
|
|
}
|
|
action={
|
|
hasFilters
|
|
? {
|
|
label: "Clear filters",
|
|
href: "/teacher/exams/all",
|
|
}
|
|
: {
|
|
label: "Create Exam",
|
|
href: "/teacher/exams/create",
|
|
}
|
|
}
|
|
className="h-[360px] bg-card"
|
|
/>
|
|
) : (
|
|
<ExamDataTable columns={examColumns} data={exams} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ExamsResultsFallback() {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col gap-3 rounded-md border bg-card px-4 py-3 md:flex-row md:items-center md:justify-between">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Skeleton className="h-4 w-[160px]" />
|
|
<Skeleton className="h-5 w-[92px]" />
|
|
<Skeleton className="h-5 w-[112px]" />
|
|
<Skeleton className="h-5 w-[106px]" />
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Skeleton className="h-9 w-[120px]" />
|
|
<Skeleton className="h-9 w-[132px]" />
|
|
</div>
|
|
</div>
|
|
<div className="rounded-md border bg-card">
|
|
<div className="p-4">
|
|
<Skeleton className="h-8 w-full" />
|
|
</div>
|
|
<div className="space-y-2 p-4 pt-0">
|
|
{Array.from({ length: 6 }).map((_, idx) => (
|
|
<Skeleton key={idx} className="h-10 w-full" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default async function AllExamsPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<SearchParams>
|
|
}) {
|
|
return (
|
|
<div className="flex h-full flex-col space-y-8 p-8">
|
|
<div className="flex flex-col justify-between space-y-4 md:flex-row md:items-center md:space-y-0">
|
|
<div>
|
|
<h2 className="text-2xl font-bold tracking-tight">All Exams</h2>
|
|
<p className="text-muted-foreground">View and manage all your exams.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<Suspense fallback={<div className="h-10 w-full animate-pulse rounded-md bg-muted" />}>
|
|
<ExamFilters />
|
|
</Suspense>
|
|
|
|
<Suspense fallback={<ExamsResultsFallback />}>
|
|
<ExamsResults searchParams={searchParams} />
|
|
</Suspense>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|