mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
158 lines
5.9 KiB
React
158 lines
5.9 KiB
React
import { useEffect } from "react";
|
||
import { useNavigate, useParams } from "react-router-dom";
|
||
import { useForm, useFieldArray } from "react-hook-form";
|
||
import { z } from "zod";
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { ArrowLeft, Plus, Trash2 } from "lucide-react";
|
||
|
||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||
import { useAuth } from "@/contexts/AuthContext";
|
||
import { PageMeta } from "@/contexts/MetadataContext";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { Spinner } from "@/components/ui/spinner";
|
||
|
||
const schema = z.object({
|
||
title: z.string().min(1, "Title is required."),
|
||
description: z.string().optional(),
|
||
order: z.coerce.number().min(0).default(0),
|
||
objectives: z.array(
|
||
z.object({ value: z.string().min(1, "Objective cannot be empty.") })
|
||
).optional(),
|
||
});
|
||
|
||
function FieldError({ message }) {
|
||
if (!message) return null;
|
||
return <p className="text-xs text-destructive mt-1">{message}</p>;
|
||
}
|
||
|
||
export default function AddLesson() {
|
||
const navigate = useNavigate();
|
||
const { courseId, unitId } = useParams();
|
||
const { createLesson, fetchUnit, course, unit, loading } = useCourses();
|
||
const { user } = useAuth();
|
||
|
||
const { register, handleSubmit, control, formState: { errors } } = useForm({
|
||
resolver: zodResolver(schema),
|
||
defaultValues: { title: "", description: "", order: 0, objectives: [] },
|
||
});
|
||
|
||
const { fields, append, remove } = useFieldArray({ control, name: "objectives" });
|
||
|
||
useEffect(() => {
|
||
fetchUnit(courseId, unitId);
|
||
}, [courseId, unitId]);
|
||
|
||
const onSubmit = async (data) => {
|
||
const payload = {
|
||
...data,
|
||
objectives: data.objectives?.map((o) => o.value) ?? [],
|
||
createdBy: user?.user_id,
|
||
};
|
||
const result = await createLesson(courseId, unitId, payload);
|
||
if (!result) return;
|
||
navigate(`/admin/courses/${courseId}/units/${unitId}/lessons`);
|
||
};
|
||
|
||
return (
|
||
<section className="bg-muted h-full">
|
||
<PageMeta title={unit ? `Add Lesson – ${unit.title} - STARR` : undefined} />
|
||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-center px-4 py-10">
|
||
|
||
<div className="w-full max-w-2xl mx-auto">
|
||
<div className="flex items-center gap-3 mb-6">
|
||
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Button>
|
||
<div>
|
||
<h1 className="text-xl font-semibold">Create Lesson</h1>
|
||
<p className="text-sm text-muted-foreground">Add a new lesson to this unit.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="title">Title <span className="text-destructive">*</span></Label>
|
||
<Input id="title" placeholder="Lesson title" {...register("title")} />
|
||
<FieldError message={errors.title?.message} />
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="description">Description</Label>
|
||
<Textarea id="description" placeholder="Optional description" rows={3} {...register("description")} />
|
||
</div>
|
||
|
||
<div className="space-y-1.5 max-w-[120px]">
|
||
<Label htmlFor="order">Order</Label>
|
||
<Input id="order" type="number" min={0} {...register("order")} />
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{/* Objectives */}
|
||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm font-medium">Objectives</p>
|
||
<p className="text-xs text-muted-foreground">What learners will achieve from this lesson.</p>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => append({ value: "" })}
|
||
>
|
||
<Plus className="h-4 w-4 mr-1" />
|
||
Add
|
||
</Button>
|
||
</div>
|
||
|
||
{fields.length === 0 && (
|
||
<p className="text-sm text-muted-foreground text-center py-4">
|
||
No objectives yet. Click Add to get started.
|
||
</p>
|
||
)}
|
||
|
||
<div className="space-y-3">
|
||
{fields.map((field, index) => (
|
||
<div key={field.id} className="flex items-start gap-2">
|
||
<div className="flex-1 space-y-1">
|
||
<Input
|
||
placeholder={`Objective ${index + 1}`}
|
||
{...register(`objectives.${index}.value`)}
|
||
/>
|
||
<FieldError message={errors.objectives?.[index]?.value?.message} />
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => remove(index)}
|
||
className="text-muted-foreground hover:text-destructive mt-0.5"
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-3">
|
||
<Button type="button" variant="outline" onClick={() => navigate(-1)} disabled={loading}>
|
||
Cancel
|
||
</Button>
|
||
<Button type="submit" disabled={loading}>
|
||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||
Create Lesson
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
} |