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:
88
src/pages/OfficialExamAccess.tsx
Normal file
88
src/pages/OfficialExamAccess.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Loader2, Clock, AlertCircle, ClipboardList } from "lucide-react";
|
||||
import { API_BASE_URL } from "@/lib/api-client";
|
||||
import { examsService } from "@/services/exams.service";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export default function OfficialExamAccess() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const token = searchParams.get("token");
|
||||
|
||||
const { data: examInfo, isLoading, error } = useQuery({
|
||||
queryKey: ["exam", "access", token],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${API_BASE_URL}/exam/access/${token}`);
|
||||
if (!res.ok) throw new Error(res.status === 404 ? "Invalid or expired token" : "Failed to load exam");
|
||||
return res.json() as Promise<{ id: number; name: string; duration_minutes: number; start_time: string; module: string }>;
|
||||
},
|
||||
enabled: !!token,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const handleStart = () => {
|
||||
if (examInfo) {
|
||||
navigate(`/exam/${examInfo.module}/${examInfo.id}?token=${token}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
{!token ? (
|
||||
<CardContent className="py-12 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-3" />
|
||||
<h2 className="text-lg font-semibold">No Access Token</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">Please use the link provided by your institution to access the exam.</p>
|
||||
</CardContent>
|
||||
) : isLoading ? (
|
||||
<CardContent className="py-12 text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
|
||||
<p className="text-sm text-muted-foreground mt-3">Loading exam information...</p>
|
||||
</CardContent>
|
||||
) : error ? (
|
||||
<CardContent className="py-12 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-3" />
|
||||
<h2 className="text-lg font-semibold">Access Denied</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">{(error as Error).message}</p>
|
||||
</CardContent>
|
||||
) : examInfo ? (
|
||||
<>
|
||||
<CardHeader className="text-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/10 flex items-center justify-center mx-auto mb-2">
|
||||
<ClipboardList className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
<CardTitle>{examInfo.name}</CardTitle>
|
||||
<CardDescription>Official Exam Access</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3 p-4 rounded-lg border bg-muted/30">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Duration</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span className="font-medium">{examInfo.duration_minutes} minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Start Time</span>
|
||||
<span className="font-medium">{new Date(examInfo.start_time).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Module</span>
|
||||
<Badge variant="outline">{examInfo.module}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full" size="lg" onClick={handleStart}>
|
||||
Start Exam
|
||||
</Button>
|
||||
</CardContent>
|
||||
</>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user