Files
starr-philproperties/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx
T
2026-08-05 04:32:29 +08:00

177 lines
7.1 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeft } from "lucide-react";
import { useLibrary } from "@/contexts/AdminLibraryContext";
import { useCourses } from "@/contexts/AdminCoursesContext";
import { useAuth } from "@/contexts/AuthContext";
import { PageMeta } from "@/contexts/MetadataContext";
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 CompletionRequirementBuilder from "@/modules/admin/components/courses/CompletionRequirementBuilder";
import ProductPricingCard from "@/modules/admin/components/products/ProductPricingCard";
const schema = z.object({
title: z.string().min(1, "Title is required."),
description: z.string().optional(),
subscription: z.string().optional(),
});
function FieldError({ message }) {
if (!message) return null;
return <p className="text-xs text-destructive mt-1">{message}</p>;
}
export default function EditLibraryLesson() {
const navigate = useNavigate();
const { lessonId } = useParams();
const { fetchLesson, updateLesson, lesson, loading, fetchLessonProduct, saveLessonProduct, removeLessonProduct } = useLibrary();
const { fetchLessonRequirements, syncLessonRequirements } = useCourses();
const { user } = useAuth();
const [tierCategories, setTierCategories] = useState([]);
useEffect(() => {
api.get("/admin/tiers/categories")
.then(({ data }) => setTierCategories((data.data ?? []).filter((c) => c.is_active)))
.catch(() => {});
}, []);
const { register, handleSubmit, reset, control, setValue, formState: { errors, isDirty } } = useForm({
resolver: zodResolver(schema),
defaultValues: { title: "", description: "", subscription: "free" },
});
const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty);
const watchedSubscr = useWatch({ control, name: "subscription" });
const defaultTierSlug = tierCategories.find((c) => c.is_default)?.slug || "free";
useEffect(() => {
fetchLesson(lessonId);
}, [lessonId]);
useEffect(() => {
if (lesson && String(lesson.lesson_id) === String(lessonId)) {
reset({ title: lesson.title ?? "", description: lesson.description ?? "", subscription: lesson.subscription || defaultTierSlug });
}
}, [lesson, lessonId, reset]);
const onSubmit = async (data) => {
const result = await updateLesson(lessonId, { ...data, subscription: data.subscription || defaultTierSlug, updatedBy: user?.user_id });
if (!result) return;
bypassOnce();
navigate(`/admin/lessons/${lessonId}/view`);
};
return (
<section className="bg-muted h-full">
<PageMeta title={lesson ? `Edit Lesson – ${lesson.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(`/admin/lessons/${lessonId}/view`)}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-semibold">Edit Lesson</h1>
<p className="text-sm text-muted-foreground">
Changes apply everywhere this lesson is attached.
</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">
<Label>Subscription</Label>
<Select
value={watchedSubscr || defaultTierSlug}
onValueChange={(val) => setValue("subscription", val, { shouldDirty: true })}
>
<SelectTrigger>
<SelectValue placeholder="No tier gate" />
</SelectTrigger>
<SelectContent>
{tierCategories.map((c) => (
<SelectItem key={c.slug} value={c.slug}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Optional. Gates this lesson directly, independent of any unit it may later be attached to.
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<Button type="button" variant="outline" onClick={() => navigate(`/admin/lessons/${lessonId}/view`)} disabled={loading}>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading && <Spinner className="h-4 w-4 mr-2" />}
Save Changes
</Button>
</div>
</form>
<div className="rounded-lg border bg-card p-6 space-y-4 mt-6">
<div>
<h2 className="text-sm font-semibold">Completion Requirements</h2>
<p className="text-xs text-muted-foreground">What a learner must do for this lesson to count as complete, wherever it's attached.</p>
</div>
<CompletionRequirementBuilder
entityType="lesson"
fetchFn={fetchLessonRequirements}
syncFn={syncLessonRequirements}
args={[null, null, lessonId]}
/>
</div>
<div className="rounded-lg border bg-card p-6 space-y-4 mt-6">
<div>
<h2 className="text-sm font-semibold">Pricing</h2>
<p className="text-xs text-muted-foreground">Optional individual-purchase listing for this lesson.</p>
</div>
<ProductPricingCard
label="this lesson"
fetchFn={fetchLessonProduct}
saveFn={saveLessonProduct}
removeFn={removeLessonProduct}
args={[lessonId]}
/>
</div>
</div>
</div>
{unsavedChangesDialog}
</section>
);
}