import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { format } from "date-fns"; import { useNavigate } from "react-router-dom"; import { Info } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Switch } from "@/components/ui/switch"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Separator } from "@/components/ui/separator"; import { useCreateIeltsExam } from "@/hooks/queries/useExamTemplates"; import type { IELTSSkill, IELTSExamType, AssemblyMode, ExamDifficulty } from "@/types"; const skillOptions: { id: IELTSSkill; label: string }[] = [ { id: "listening", label: "Listening" }, { id: "reading", label: "Reading" }, { id: "writing", label: "Writing" }, { id: "speaking", label: "Speaking" }, ]; const formSchema = z.object({ exam_type: z.enum(["academic", "general_training"]), skills: z.array(z.enum(["listening", "reading", "writing", "speaking"])).min(1), title: z.string().min(2, "Title is required"), target_band: z.number().min(4).max(9), difficulty: z.enum(["easy", "medium", "hard"]), randomize: z.boolean(), assembly_mode: z.enum(["auto", "manual", "hybrid"]), results_release_mode: z.enum(["auto", "manual_approval"]), }); type FormValues = z.infer; function bandSteps(): number[] { const out: number[] = []; for (let v = 4; v <= 9; v += 0.5) out.push(Math.round(v * 10) / 10); return out; } const BAND_STEPS = bandSteps(); function nearestBand(x: number): number { let best = BAND_STEPS[0]; let d = Math.abs(x - best); for (const b of BAND_STEPS) { const nd = Math.abs(x - b); if (nd < d) { d = nd; best = b; } } return best; } export default function IeltsExamCreate() { const navigate = useNavigate(); const createExam = useCreateIeltsExam(); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { exam_type: "academic", skills: ["listening", "reading", "writing", "speaking"], title: "", target_band: 7, difficulty: "medium", randomize: true, assembly_mode: "hybrid", results_release_mode: "auto", }, }); const examType = form.watch("exam_type"); const targetBand = form.watch("target_band"); const suggestedTitle = useMemo(() => { const label = examType === "academic" ? "IELTS Academic" : "IELTS General Training"; const band = targetBand.toFixed(1); const dateStr = format(new Date(), "yyyy-MM-dd"); return `${label} — Band ${band} — ${dateStr}`; }, [examType, targetBand]); useEffect(() => { const cur = form.getValues("title"); if (!cur.trim()) { form.setValue("title", suggestedTitle); } }, [form, suggestedTitle]); const onSuggestTitle = () => { form.setValue("title", suggestedTitle); }; const onSubmit = async (values: FormValues) => { const payload = { exam_type: values.exam_type as IELTSExamType, skills: values.skills as IELTSSkill[], title: values.title, target_band: values.target_band, difficulty: values.difficulty as ExamDifficulty, randomize: values.randomize, assembly_mode: values.assembly_mode as AssemblyMode, results_release_mode: values.results_release_mode, }; const raw = await createExam.mutateAsync(payload); const res = raw as { exam_id?: number; data?: { exam_id?: number } }; const examId = res.exam_id ?? res.data?.exam_id; if (examId != null) { sessionStorage.setItem(`ielts_assembly_${examId}`, values.assembly_mode); navigate(`/admin/exam/ielts/${examId}/skills`); } }; return (

IELTS exam — Phase 1

Initialization and assembly preferences.

{["Initialization", "Skills", "Content", "Validate"].map((label, i) => (
{i + 1}. {label}
))}
Exam type Select the official IELTS paper type. ( field.onChange(v as IELTSExamType)} value={field.value} className="grid gap-3 sm:grid-cols-2" > )} /> Target skills Include at least one skill in this build. (
{skillOptions.map((s) => ( ( { const next = new Set(field.value ?? []); if (checked) next.add(s.id); else next.delete(s.id); field.onChange(Array.from(next) as IELTSSkill[]); }} /> {s.label} )} /> ))}
)} />
Exam details ( Exam title
Uses type, band, and today’s date.
)} /> ( Target band ({field.value.toFixed(1)}) field.onChange(nearestBand(v[0] ?? 7))} className="max-w-md" /> )} /> ( Difficulty )} /> (
Randomization Shuffle order within allowed constraints.
)} />
Assembly mode How items are chosen from the content pool. ( )} /> Score release ( )} /> IELTS structure (reference)
  • Listening: 4 parts, 10 questions each (approx. 30 minutes plus transfer time).
  • Reading: 3 passages, about 13–14 questions each (60 minutes).
  • Writing: 2 tasks (60 minutes).
  • Speaking: 3 parts (11–14 minutes).
); }