assets and tier plans revamp

This commit is contained in:
rgrgogu
2026-08-01 17:44:12 +08:00
parent 4f738a691d
commit a34c6feb84
45 changed files with 2172 additions and 273 deletions
@@ -1,6 +1,6 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useForm } from "react-hook-form";
import { useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeft } from "lucide-react";
@@ -9,17 +9,23 @@ 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 }) {
@@ -30,29 +36,38 @@ function FieldError({ message }) {
export default function EditLibraryLesson() {
const navigate = useNavigate();
const { lessonId } = useParams();
const { fetchLesson, updateLesson, lesson, loading } = useLibrary();
const { fetchLesson, updateLesson, lesson, loading, fetchLessonProduct, saveLessonProduct, removeLessonProduct } = useLibrary();
const { fetchLessonRequirements, syncLessonRequirements } = useCourses();
const { user } = useAuth();
const { register, handleSubmit, reset, formState: { errors, isDirty } } = useForm({
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: "" },
defaultValues: { title: "", description: "", subscription: "" },
});
const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty);
const watchedSubscr = useWatch({ control, name: "subscription" });
useEffect(() => {
fetchLesson(lessonId);
}, [lessonId]);
useEffect(() => {
if (lesson && String(lesson.lesson_id) === String(lessonId)) {
reset({ title: lesson.title ?? "", description: lesson.description ?? "" });
reset({ title: lesson.title ?? "", description: lesson.description ?? "", subscription: lesson.subscription ?? "" });
}
}, [lesson, lessonId, reset]);
const onSubmit = async (data) => {
const result = await updateLesson(lessonId, { ...data, updatedBy: user?.user_id });
const result = await updateLesson(lessonId, { ...data, subscription: data.subscription || null, updatedBy: user?.user_id });
if (!result) return;
bypassOnce();
navigate(`/admin/lessons/${lessonId}/view`);
@@ -90,6 +105,29 @@ export default function EditLibraryLesson() {
<Textarea id="description" placeholder="Optional description" rows={3} {...register("description")} />
</div>
<div className="space-y-1.5">
<Label>Subscription</Label>
<Select
value={watchedSubscr || "__open"}
onValueChange={(val) => setValue("subscription", val === "__open" ? "" : val, { shouldDirty: true })}
>
<SelectTrigger>
<SelectValue placeholder="No tier gate" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__open">No tier gate (open)</SelectItem>
{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">
@@ -115,6 +153,20 @@ export default function EditLibraryLesson() {
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>