mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
units,lesson as standalone
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
// modules/admin/components/courses/CreateLessonDialog.jsx
|
||||
// Lightweight "create a brand-new lesson and attach it to this unit" dialog —
|
||||
// the create-new counterpart to AttachLessonsDialog's attach-existing flow.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
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.") })).default([]),
|
||||
});
|
||||
|
||||
export default function CreateLessonDialog({ open, onOpenChange, courseId, unitId, nextOrder = 0, onCreated, draftMode = false }) {
|
||||
const { createLesson, loading } = useCourses();
|
||||
const { user } = useAuth();
|
||||
|
||||
const { register, handleSubmit, reset, control, formState: { errors } } = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { title: "", description: "", order: nextOrder, objectives: [] },
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: "objectives" });
|
||||
|
||||
useEffect(() => {
|
||||
if (open) reset({ title: "", description: "", order: nextOrder, objectives: [] });
|
||||
}, [open, nextOrder, reset]);
|
||||
|
||||
const onValid = async (values) => {
|
||||
const objectives = values.objectives.map((o) => o.value);
|
||||
|
||||
if (draftMode) {
|
||||
onCreated?.({
|
||||
lesson_id: null,
|
||||
key: crypto.randomUUID(),
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
order_index: values.order,
|
||||
objectives,
|
||||
});
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await createLesson(courseId, unitId, {
|
||||
...values,
|
||||
objectives,
|
||||
createdBy: user?.user_id,
|
||||
});
|
||||
const lesson = result?.data?.data ?? null;
|
||||
if (!lesson) return;
|
||||
onCreated?.(lesson);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[440px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Lesson</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onValid)} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="lesson_title">
|
||||
Title <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="lesson_title" placeholder="e.g. Welcome to the Course" {...register("title")} />
|
||||
{errors.title && <p className="text-sm text-destructive">{errors.title.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="lesson_description">Description</Label>
|
||||
<Textarea id="lesson_description" placeholder="Optional lesson description" rows={3} {...register("description")} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-w-[120px]">
|
||||
<Label htmlFor="lesson_order">Order</Label>
|
||||
<Input id="lesson_order" type="number" min={0} {...register("order")} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">Objectives</Label>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => append({ value: "" })}>
|
||||
<Plus className="h-3.5 w-3.5 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
{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`)} />
|
||||
{errors.objectives?.[index]?.value && (
|
||||
<p className="text-xs text-destructive">{errors.objectives[index].value.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button" variant="ghost" size="icon"
|
||||
className="mt-0.5 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline" disabled={loading}>Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Create Lesson
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user