import { useEffect, useState } from "react"; import { useNavigate, useParams, useLocation } from "react-router-dom"; import { useForm, useWatch } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { ChevronLeft, ChevronRight, Check, FileText, ListChecks, Link2, Plus } from "lucide-react"; import { useCourses } from "@/contexts/AdminCoursesContext"; import { useAuth } from "@/contexts/AuthContext"; import { PageMeta } from "@/contexts/MetadataContext"; import { cn } from "@/lib/utils"; import api from "@/utils/api.util"; 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"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard"; import DraftRequirementsEditor from "@/modules/admin/components/courses/DraftRequirementsEditor"; import AttachUnitsDialog from "@/modules/admin/components/library/AttachUnitsDialog"; const schema = z.object({ title: z.string().min(1, "Title is required."), description: z.string().optional(), order: z.coerce.number().min(0).default(0), subscription: z.string().optional(), }); const STEPS = [ { id: 0, label: "Details", icon: FileText }, { id: 1, label: "Completion Requirements", icon: ListChecks }, ]; function FieldError({ message }) { if (!message) return null; return
{message}
; } export default function AddUnit() { const navigate = useNavigate(); const location = useLocation(); const { courseId } = useParams(); const { createUnit, fetchCourse, course, loading, syncUnitRequirements } = useCourses(); const { user } = useAuth(); const backTarget = location.state?.returnTo ?? `/admin/courses/${courseId}/units`; const [mode, setMode] = useState(null); // null | "create" — the Details/Requirements form only shows once "Create" is chosen const [step, setStep] = useState(0); const [requirements, setRequirements] = useState([]); const [attachOpen, setAttachOpen] = useState(false); const [tierCategories, setTierCategories] = useState([]); useEffect(() => { api.get("/admin/tiers/categories") .then(({ data }) => setTierCategories((data.data ?? []).filter((c) => c.is_active))) .catch(() => {}); }, []); const { register, trigger, getValues, control, setValue, formState: { errors, isDirty } } = useForm({ resolver: zodResolver(schema), defaultValues: { title: "", description: "", order: 0, subscription: "free" }, }); const watchedSubscr = useWatch({ control, name: "subscription" }); const defaultTierSlug = tierCategories.find((c) => c.is_default)?.slug || "free"; const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty || requirements.length > 0); useEffect(() => { fetchCourse(courseId); }, [courseId]); const handleNext = async () => { const valid = await trigger(); if (valid) setStep(1); }; // The one and only persistence point — nothing is created until this final // click at Requirements (the last step), which is purely a draft form // until now. const handleCreate = async () => { const values = getValues(); const result = await createUnit(courseId, { ...values, subscription: values.subscription || defaultTierSlug, createdBy: user?.user_id }); const newUnitId = result?.data?.data?.unit_id; if (!newUnitId) return; if (requirements.length > 0) { const clean = requirements.map(({ _key, ...r }) => r); await syncUnitRequirements(courseId, newUnitId, clean); } bypassOnce(); navigate(backTarget); }; return (Creates a new unit in the shared Units Library and attaches it to this course.
Attach an existing library unit to reuse its content, or create a new one from scratch.
Optional. Gates this unit directly, independent of the course it's attached to.
Configure how learners complete this unit — optional, sensible defaults apply automatically. This is created together with the rest of the unit when you finish.