- 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
123 lines
5.5 KiB
TypeScript
123 lines
5.5 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Loader2, Lock, CheckCircle2, PlayCircle, Circle, ArrowRight, Sparkles } from "lucide-react";
|
|
import { useLearningPlan, useGenerateLearningPlan } from "@/hooks/queries";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import type { PlanItemStatus } from "@/types";
|
|
|
|
const statusIcons: Record<PlanItemStatus, ReactNode> = {
|
|
locked: <Lock className="h-4 w-4 text-muted-foreground" />,
|
|
available: <Circle className="h-4 w-4 text-blue-500" />,
|
|
in_progress: <PlayCircle className="h-4 w-4 text-yellow-500" />,
|
|
completed: <CheckCircle2 className="h-4 w-4 text-green-500" />,
|
|
};
|
|
|
|
const statusColors: Record<PlanItemStatus, string> = {
|
|
locked: "bg-muted text-muted-foreground",
|
|
available: "bg-blue-50 text-blue-700 border-blue-200",
|
|
in_progress: "bg-yellow-50 text-yellow-700 border-yellow-200",
|
|
completed: "bg-green-50 text-green-700 border-green-200",
|
|
};
|
|
|
|
export default function LearningPlanPage() {
|
|
const { subjectId } = useParams<{ subjectId: string }>();
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const { data: plan, isLoading } = useLearningPlan({ subject_id: Number(subjectId) });
|
|
const generateMutation = useGenerateLearningPlan();
|
|
|
|
const handleGenerate = () => {
|
|
generateMutation.mutate(
|
|
{ subject_id: Number(subjectId) },
|
|
{
|
|
onSuccess: () => toast({ title: "Plan Generated", description: "Your personalized learning plan is ready." }),
|
|
onError: () => toast({ title: "Error", description: "Failed to generate plan", variant: "destructive" }),
|
|
}
|
|
);
|
|
};
|
|
|
|
if (isLoading) return <div className="flex items-center justify-center min-h-[400px]"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" /></div>;
|
|
|
|
if (!plan) {
|
|
return (
|
|
<div className="max-w-lg mx-auto text-center py-16 space-y-6">
|
|
<Sparkles className="h-16 w-16 text-primary mx-auto opacity-60" />
|
|
<h1 className="text-2xl font-bold">No Learning Plan Yet</h1>
|
|
<p className="text-muted-foreground">Complete a diagnostic assessment first, then generate your personalized learning plan.</p>
|
|
<div className="flex gap-3 justify-center">
|
|
<Button variant="outline" onClick={() => navigate(`/student/diagnostic/${subjectId}`)}>Take Diagnostic</Button>
|
|
<Button onClick={handleGenerate} disabled={generateMutation.isPending}>
|
|
{generateMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
Generate Plan
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const items = plan.items.sort((a, b) => a.sequence - b.sequence);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Learning Plan: {plan.subject_name}</h1>
|
|
<p className="text-muted-foreground">{plan.ai_summary}</p>
|
|
</div>
|
|
<Badge variant={plan.status === "active" ? "default" : "secondary"}>{plan.status}</Badge>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm text-muted-foreground">Overall Progress</span>
|
|
<span className="text-sm font-medium">{Math.round(plan.overall_progress)}%</span>
|
|
</div>
|
|
<Progress value={plan.overall_progress} className="h-3" />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="space-y-3">
|
|
{items.map((item, index) => (
|
|
<Card key={item.id} className={`border ${statusColors[item.status]}`}>
|
|
<CardContent className="py-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-xs text-muted-foreground font-mono">{index + 1}</div>
|
|
{statusIcons[item.status]}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">{item.topic_name}</span>
|
|
<Badge variant="outline" className="text-xs">{item.domain_name}</Badge>
|
|
</div>
|
|
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
|
|
<span>~{item.estimated_hours}h estimated</span>
|
|
{item.actual_hours > 0 && <span>{item.actual_hours}h spent</span>}
|
|
<span>Mastery: {Math.round(item.mastery)}%</span>
|
|
</div>
|
|
{item.status !== "locked" && (
|
|
<Progress value={item.mastery} className="h-1 mt-2" />
|
|
)}
|
|
</div>
|
|
{(item.status === "available" || item.status === "in_progress") && (
|
|
<Button size="sm" onClick={() => navigate(`/student/topic/${item.topic_id}`)}>
|
|
{item.status === "in_progress" ? "Continue" : "Start"} <ArrowRight className="ml-1 h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
{item.status === "completed" && (
|
|
<Button variant="ghost" size="sm" onClick={() => navigate(`/student/topic/${item.topic_id}`)}>Review</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|