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:
178
frontend/src/pages/student/ExamResults.tsx
Normal file
178
frontend/src/pages/student/ExamResults.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Radar,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
PolarRadiusAxis,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Award, BookOpen, Download, RefreshCw } from "lucide-react";
|
||||
|
||||
const SKILLS = [
|
||||
{ skill: "Listening", band: 7.5, cefr: "C1", gap: 0.5, target: 8 },
|
||||
{ skill: "Reading", band: 8, cefr: "C1", gap: 0, target: 8 },
|
||||
{ skill: "Writing", band: 6.5, cefr: "B2", gap: 1.5, target: 8 },
|
||||
{ skill: "Speaking", band: 7, cefr: "C1", gap: 1, target: 8 },
|
||||
];
|
||||
|
||||
const RADAR_DATA = SKILLS.map((s) => ({ skill: s.skill, band: s.band }));
|
||||
|
||||
export default function ExamResults() {
|
||||
const { examId } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const practice = searchParams.get("mode") === "practice";
|
||||
const overall = 7.5;
|
||||
const cefr = "C1";
|
||||
const passed = overall >= 7;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl space-y-8 p-6">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">Your overall result</p>
|
||||
<div className="mt-2 flex items-center justify-center gap-3">
|
||||
<Award className="h-12 w-12 text-primary" />
|
||||
<span className="text-5xl font-bold tabular-nums">{overall}</span>
|
||||
<Badge variant="secondary" className="text-lg">
|
||||
Band
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-2 text-lg text-muted-foreground">CEFR equivalent: {cefr}</p>
|
||||
{practice ? <Badge className="mt-2">Practice mode</Badge> : null}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Skill profile</CardTitle>
|
||||
<CardDescription>Per-skill performance vs maximum scale</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-72 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RadarChart data={RADAR_DATA} cx="50%" cy="50%" outerRadius="80%">
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="skill" />
|
||||
<PolarRadiusAxis angle={30} domain={[0, 9]} tickCount={6} />
|
||||
<Radar name="Band" dataKey="band" stroke="hsl(var(--primary))" fill="hsl(var(--primary))" fillOpacity={0.35} />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Skills breakdown</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Skill</TableHead>
|
||||
<TableHead>Band</TableHead>
|
||||
<TableHead>CEFR</TableHead>
|
||||
<TableHead>Gap to Target</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{SKILLS.map((s) => (
|
||||
<TableRow key={s.skill}>
|
||||
<TableCell className="font-medium">{s.skill}</TableCell>
|
||||
<TableCell>{s.band}</TableCell>
|
||||
<TableCell>{s.cefr}</TableCell>
|
||||
<TableCell>{s.gap > 0 ? `−${s.gap}` : "—"}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 text-lg font-semibold">Section feedback</h2>
|
||||
<Accordion type="multiple" className="w-full">
|
||||
{["Listening", "Reading", "Writing", "Speaking"].map((name) => (
|
||||
<AccordionItem key={name} value={name}>
|
||||
<AccordionTrigger>{name}</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
Detailed feedback for {name} will appear here once released by your instructor.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Areas to improve</CardTitle>
|
||||
<CardDescription>Focus recommendations based on this attempt</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-inside list-disc space-y-2 text-sm">
|
||||
<li>Strengthen task response structure in Writing Task 2.</li>
|
||||
<li>Extend range of cohesive devices in argumentative essays.</li>
|
||||
<li>Maintain fluency while reducing hesitation in Speaking Part 2.</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button type="button" variant="outline">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download PDF Report
|
||||
</Button>
|
||||
<Button type="button" asChild>
|
||||
<Link to={`/student/course/generate?from=exam&examId=${examId ?? ""}`}>
|
||||
<BookOpen className="mr-2 h-4 w-4" />
|
||||
View Recommended Course
|
||||
</Link>
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" asChild>
|
||||
<Link to={`/student/exam/${examId}/session?mode=practice`}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Retake Exam
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card className={passed ? "border-green-600/40 bg-green-500/5" : ""}>
|
||||
<CardHeader>
|
||||
<CardTitle>Pass path</CardTitle>
|
||||
<CardDescription>Congratulations — you are on track for your goal.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm">Consider registering for the official test while skills are strong.</p>
|
||||
<Button type="button" size="sm">
|
||||
Register for official exam
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className={!passed ? "border-amber-600/40 bg-amber-500/5" : ""}>
|
||||
<CardHeader>
|
||||
<CardTitle>Below threshold</CardTitle>
|
||||
<CardDescription>Close the gap with targeted training.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm">Start adaptive training tailored to your weakest skills.</p>
|
||||
<Button type="button" size="sm" variant="secondary" asChild>
|
||||
<Link to={`/student/course/generate?from=exam&examId=${examId ?? ""}`}>Start adaptive training</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user