feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
This commit is contained in:
111
src/pages/student/ExamStatus.tsx
Normal file
111
src/pages/student/ExamStatus.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useExamStatus } from "@/hooks/queries/useExamSession";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export default function ExamStatus() {
|
||||
const { examId: examIdParam } = useParams();
|
||||
const examId = Number(examIdParam);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data, isLoading, isFetching } = useExamStatus(examId, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const s = (data.status ?? "").toLowerCase();
|
||||
if (data.scores_available || s.includes("complete") || s.includes("released")) {
|
||||
navigate(`/student/exam/${examId}/results`, { replace: true });
|
||||
}
|
||||
}, [data, examId, navigate]);
|
||||
|
||||
if (isLoading && !data) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-6">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<p className="text-muted-foreground">Loading status…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const status = (data?.status ?? "").toLowerCase();
|
||||
let view: "scoring" | "partial" | "manual" | "generic" = "generic";
|
||||
if (status.includes("scoring") || status === "processing") view = "scoring";
|
||||
else if (status.includes("manual") || status.includes("approval") || status.includes("under_review"))
|
||||
view = "manual";
|
||||
else if (
|
||||
status.includes("partial") ||
|
||||
status.includes("pending_ws") ||
|
||||
status.includes("lr_scored") ||
|
||||
(status.includes("listening") && status.includes("reading") && status.includes("pending"))
|
||||
)
|
||||
view = "partial";
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex min-h-screen max-w-lg flex-col justify-center gap-6 p-6">
|
||||
{view === "scoring" ? (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<CardTitle>Scores are being calculated</CardTitle>
|
||||
<CardDescription>This page refreshes every few seconds.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center text-xs text-muted-foreground">
|
||||
{isFetching ? "Checking…" : "Waiting for next update…"}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{view === "partial" ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Partial results</CardTitle>
|
||||
<CardDescription>Listening and Reading are scored; Writing and Speaking are still pending review.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>Listening</span>
|
||||
<Badge variant="secondary">Band 7.5</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Reading</span>
|
||||
<Badge variant="secondary">Band 8.0</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Writing</span>
|
||||
<Badge>Pending Review</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Speaking</span>
|
||||
<Badge>Pending Review</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{view === "manual" ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Results under review</CardTitle>
|
||||
<CardDescription>Your institution will release Writing and Speaking results after approval.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button type="button" variant="outline" onClick={() => navigate("/student/dashboard")}>
|
||||
Back to dashboard
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{view === "generic" ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Status</CardTitle>
|
||||
<CardDescription>{data?.status ?? "Unknown"}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user