- 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
120 lines
5.7 KiB
TypeScript
120 lines
5.7 KiB
TypeScript
import { useState } from "react";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { useCreateCourse } from "@/hooks/queries";
|
|
|
|
const steps = ["Basic Info", "Schedule", "Review"];
|
|
|
|
export default function CourseBuilder() {
|
|
const [step, setStep] = useState(0);
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const createMut = useCreateCourse();
|
|
|
|
const [form, setForm] = useState({
|
|
title: "", code: "", description: "",
|
|
level: "Beginner" as "Beginner" | "Intermediate" | "Advanced",
|
|
start_date: "", end_date: "", max_capacity: "",
|
|
});
|
|
|
|
function handlePublish() {
|
|
createMut.mutate(
|
|
{
|
|
title: form.title,
|
|
code: form.code,
|
|
description: form.description,
|
|
level: form.level,
|
|
instructor_id: 0,
|
|
max_capacity: Number(form.max_capacity) || 30,
|
|
start_date: form.start_date,
|
|
end_date: form.end_date,
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
toast({ title: "Course Created", description: "Your new course has been saved." });
|
|
navigate("/admin/courses");
|
|
},
|
|
onError: (err: Error) => toast({ title: "Error", description: err.message, variant: "destructive" }),
|
|
},
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-3xl">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Course Builder</h1>
|
|
<p className="text-muted-foreground">Create a new course in {steps.length} steps.</p>
|
|
</div>
|
|
|
|
<div className="flex gap-2 mb-6">
|
|
{steps.map((s, i) => (
|
|
<div key={s} className={`flex-1 h-2 rounded-full ${i <= step ? "bg-primary" : "bg-muted"}`} />
|
|
))}
|
|
</div>
|
|
<p className="text-sm font-medium mb-4">Step {step + 1}: {steps[step]}</p>
|
|
|
|
<Card>
|
|
<CardContent className="pt-6 space-y-4">
|
|
{step === 0 && (
|
|
<>
|
|
<div className="space-y-2"><Label>Course Title</Label><Input value={form.title} onChange={(e) => setForm(f => ({ ...f, title: e.target.value }))} placeholder="e.g. IELTS Academic Writing" /></div>
|
|
<div className="space-y-2"><Label>Course Code</Label><Input value={form.code} onChange={(e) => setForm(f => ({ ...f, code: e.target.value }))} placeholder="e.g. IELTS-W101" /></div>
|
|
<div className="space-y-2"><Label>Description</Label><Textarea value={form.description} onChange={(e) => setForm(f => ({ ...f, description: e.target.value }))} placeholder="Describe the course..." rows={4} /></div>
|
|
<div className="space-y-2">
|
|
<Label>Level</Label>
|
|
<Select value={form.level} onValueChange={(v) => setForm(f => ({ ...f, level: v as typeof form.level }))}>
|
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Beginner">Beginner</SelectItem>
|
|
<SelectItem value="Intermediate">Intermediate</SelectItem>
|
|
<SelectItem value="Advanced">Advanced</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
{step === 1 && (
|
|
<>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2"><Label>Start Date</Label><Input type="date" value={form.start_date} onChange={(e) => setForm(f => ({ ...f, start_date: e.target.value }))} /></div>
|
|
<div className="space-y-2"><Label>End Date</Label><Input type="date" value={form.end_date} onChange={(e) => setForm(f => ({ ...f, end_date: e.target.value }))} /></div>
|
|
</div>
|
|
<div className="space-y-2"><Label>Max Capacity</Label><Input type="number" value={form.max_capacity} onChange={(e) => setForm(f => ({ ...f, max_capacity: e.target.value }))} placeholder="30" /></div>
|
|
</>
|
|
)}
|
|
{step === 2 && (
|
|
<div className="space-y-3 py-4">
|
|
<p className="font-medium text-lg text-center">Review & Publish</p>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
<p className="text-muted-foreground">Title:</p><p className="font-medium">{form.title || "—"}</p>
|
|
<p className="text-muted-foreground">Code:</p><p className="font-medium">{form.code || "—"}</p>
|
|
<p className="text-muted-foreground">Level:</p><p className="font-medium">{form.level}</p>
|
|
<p className="text-muted-foreground">Capacity:</p><p className="font-medium">{form.max_capacity || "30"}</p>
|
|
<p className="text-muted-foreground">Dates:</p><p className="font-medium">{form.start_date || "—"} — {form.end_date || "—"}</p>
|
|
</div>
|
|
{form.description && <p className="text-sm mt-2 text-muted-foreground">{form.description}</p>}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="flex justify-between">
|
|
<Button variant="outline" disabled={step === 0} onClick={() => setStep(s => s - 1)}>Previous</Button>
|
|
{step < steps.length - 1 ? (
|
|
<Button onClick={() => setStep(s => s + 1)} disabled={step === 0 && !form.title}>Next</Button>
|
|
) : (
|
|
<Button onClick={handlePublish} disabled={createMut.isPending || !form.title}>
|
|
{createMut.isPending ? "Publishing..." : "Publish Course"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|