mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
177 lines
7.1 KiB
React
177 lines
7.1 KiB
React
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>
|
||
);
|
||
}
|