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:
132
frontend/src/pages/student/PlacementBriefing.tsx
Normal file
132
frontend/src/pages/student/PlacementBriefing.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Loader2, Clock, Layers, Sparkles } from "lucide-react";
|
||||
import { usePlacementStart } from "@/hooks/queries/usePlacement";
|
||||
import type { PlacementSubject } from "@/types";
|
||||
import { ApiError } from "@/lib/api-client";
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
const dimensions = [
|
||||
{ name: "Grammar", detail: "~30 MCQ", time: "~8 min" },
|
||||
{ name: "Vocabulary", detail: "~20 items", time: "~5 min" },
|
||||
{ name: "Reading", detail: "~10 Q / 2 passages", time: "~10 min" },
|
||||
{ name: "Speaking", detail: "3 prompts", time: "~5 min" },
|
||||
];
|
||||
|
||||
export default function PlacementBriefing() {
|
||||
const navigate = useNavigate();
|
||||
const start = usePlacementStart();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleBegin = async () => {
|
||||
setError(null);
|
||||
const subject: PlacementSubject = "english";
|
||||
try {
|
||||
const res = await start.mutateAsync(subject);
|
||||
sessionStorage.setItem(
|
||||
`placement_session_${res.session_id}`,
|
||||
JSON.stringify({ question: res.first_question }),
|
||||
);
|
||||
navigate(`/student/placement/test?session=${encodeURIComponent(res.session_id)}`, {
|
||||
state: { question: res.first_question, sessionId: res.session_id },
|
||||
});
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : "Could not start the test");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-4xl mx-auto py-6 px-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Placement test</h1>
|
||||
<p className="text-muted-foreground mt-1">Adaptive English placement across four skills.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card className="border-0 shadow-md bg-gradient-to-br from-primary/10 to-transparent">
|
||||
<CardHeader className="pb-2">
|
||||
<Clock className="h-5 w-5 text-primary mb-1" />
|
||||
<CardTitle className="text-base">Duration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold tabular-nums">20–30 min</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Pace yourself; timer tracks elapsed time.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<Layers className="h-5 w-5 text-primary mb-1" />
|
||||
<CardTitle className="text-base">Sections</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">4</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Grammar, vocabulary, reading, speaking.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<Sparkles className="h-5 w-5 text-primary mb-1" />
|
||||
<CardTitle className="text-base">Adaptive</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Badge variant="secondary" className="mb-1">
|
||||
CAT-style
|
||||
</Badge>
|
||||
<p className="text-xs text-muted-foreground">Difficulty adjusts based on your responses.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="border-0 shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle>What to expect</CardTitle>
|
||||
<CardDescription>Approximate structure per dimension</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Dimension</TableHead>
|
||||
<TableHead>Format</TableHead>
|
||||
<TableHead className="text-right">Time</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dimensions.map((row) => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell className="font-medium">{row.name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{row.detail}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{row.time}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button size="lg" className="min-w-[160px]" onClick={() => void handleBegin()} disabled={start.isPending}>
|
||||
{start.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Starting
|
||||
</>
|
||||
) : (
|
||||
"Begin Test"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user