feat: connect academic pages to backend APIs

- Generation page: exam structure dropdown loads from backend
- Generation page: selecting structure auto-selects its modules
- Rubrics page: loads rubrics from backend API with fallback to demo data

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-12 11:25:02 +04:00
parent 1e688107fa
commit c3ae379870
2 changed files with 48 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useMutation } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -52,6 +52,7 @@ import {
import AiTipBanner from "@/components/ai/AiTipBanner"; import AiTipBanner from "@/components/ai/AiTipBanner";
import { generationService } from "@/services/generation.service"; import { generationService } from "@/services/generation.service";
import { mediaService, type Avatar } from "@/services/media.service"; import { mediaService, type Avatar } from "@/services/media.service";
import { examsService } from "@/services/exams.service";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
type ModuleKey = "reading" | "listening" | "writing" | "speaking" | "level" | "industry"; type ModuleKey = "reading" | "listening" | "writing" | "speaking" | "level" | "industry";
@@ -73,6 +74,7 @@ const MODULES: ModuleInfo[] = [
{ key: "industry", label: "Industry", icon: <Briefcase className="h-4 w-4" />, color: "text-amber-700", bgColor: "bg-amber-50 border-amber-200" }, { key: "industry", label: "Industry", icon: <Briefcase className="h-4 w-4" />, color: "text-amber-700", bgColor: "bg-amber-50 border-amber-200" },
]; ];
const MODULE_KEYS: ModuleKey[] = MODULES.map((m) => m.key);
const CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"]; const CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"];
const READING_EXERCISE_TYPES = [ const READING_EXERCISE_TYPES = [
@@ -197,6 +199,12 @@ export default function GenerationPage() {
const [moduleStates, setModuleStates] = useState<Record<string, ModuleState>>({}); const [moduleStates, setModuleStates] = useState<Record<string, ModuleState>>({});
const [previewOpen, setPreviewOpen] = useState(false); const [previewOpen, setPreviewOpen] = useState(false);
const structuresQ = useQuery({
queryKey: ["exam-structures"],
queryFn: () => examsService.listStructures({}),
});
const structures = structuresQ.data?.items ?? [];
const getModuleState = useCallback((mod: ModuleKey): ModuleState => { const getModuleState = useCallback((mod: ModuleKey): ModuleState => {
return moduleStates[mod] ?? defaultModuleState(mod); return moduleStates[mod] ?? defaultModuleState(mod);
}, [moduleStates]); }, [moduleStates]);
@@ -990,13 +998,27 @@ export default function GenerationPage() {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Label>Exam Structure</Label> <Label>Exam Structure</Label>
<Select value={examStructure} onValueChange={setExamStructure}> <Select value={examStructure} onValueChange={(val) => {
setExamStructure(val);
const s = structures.find((st) => String(st.id) === val);
if (s) {
const mods = (s as Record<string, unknown>).modules as string[] | undefined;
if (Array.isArray(mods) && mods.length) {
const next = new Set<ModuleKey>();
mods.forEach((m) => { if (MODULE_KEYS.includes(m as ModuleKey)) next.add(m as ModuleKey); });
setSelectedModules(next);
if (next.size > 0) setActiveModule([...next][0]);
}
}
}}>
<SelectTrigger><SelectValue placeholder="Select an exam structure" /></SelectTrigger> <SelectTrigger><SelectValue placeholder="Select an exam structure" /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="standard_ielts">Standard IELTS Academic</SelectItem> {structures.map((s) => (
<SelectItem value="corporate">Corporate English Assessment</SelectItem> <SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>
<SelectItem value="hospitality">Hospitality English Test</SelectItem> ))}
<SelectItem value="medical">Medical English Proficiency</SelectItem> {structures.length === 0 && (
<SelectItem value="_none" disabled>No structures available</SelectItem>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -8,11 +9,21 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Search, Plus } from "lucide-react"; import { Search, Plus, Loader2 } from "lucide-react";
import AiTipBanner from "@/components/ai/AiTipBanner"; import AiTipBanner from "@/components/ai/AiTipBanner";
import AiCreationAssistant from "@/components/ai/AiCreationAssistant"; import AiCreationAssistant from "@/components/ai/AiCreationAssistant";
import { examsService } from "@/services/exams.service";
const rubrics = [ interface RubricItem {
id: number;
name: string;
levels: string[];
criteria: number;
created: string;
skill?: string;
}
const FALLBACK_RUBRICS: RubricItem[] = [
{ id: 1, name: "IELTS Writing Task 2", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 4, created: "2025-01-05" }, { id: 1, name: "IELTS Writing Task 2", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 4, created: "2025-01-05" },
{ id: 2, name: "Speaking Fluency", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 3, created: "2025-01-10" }, { id: 2, name: "Speaking Fluency", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 3, created: "2025-01-10" },
{ id: 3, name: "Reading Comprehension", levels: ["A1","A2","B1","B2","C1"], criteria: 5, created: "2025-02-01" }, { id: 3, name: "Reading Comprehension", levels: ["A1","A2","B1","B2","C1"], criteria: 5, created: "2025-02-01" },
@@ -26,6 +37,13 @@ const rubricGroups = [
export default function RubricsPage() { export default function RubricsPage() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const rubricsQ = useQuery({
queryKey: ["rubrics"],
queryFn: () => examsService.listRubrics({}),
});
const backendRubrics = (rubricsQ.data?.items ?? []) as RubricItem[];
const rubrics = backendRubrics.length > 0 ? backendRubrics : FALLBACK_RUBRICS;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">