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:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
parent a3e12f62fa
commit f1c4953a63
731 changed files with 67205 additions and 139 deletions

View 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>
);
}