Files
encoach_backend_new_v2/frontend/src/pages/student/PlacementBriefing.tsx
Yamen Ahmad f1c4953a63 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
2026-04-10 17:26:42 +04:00

133 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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">2030 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>
);
}