- {/* ── Bundle question ──────────────────────────────────────────── */}
{loading ? (
-
-
+
- ) : (
-
-
- Bundle {subscription} units with this plan?
-
-
-
-
-
-
- )}
-
- {/* ── Bundle all summary ───────────────────────────────────────── */}
- {!loading && bundleAll && total > 0 && (
-
- All {total} {subscription} unit{total !== 1 ? "s" : ""} will be included.
-
- )}
-
- {/* ── Already-assigned-elsewhere warning ──────────────────────── */}
- {!loading && conflicts.length > 0 && (
-
-
-
-
- {conflicts.length} unit{conflicts.length !== 1 ? "s" : ""} already belong{conflicts.length === 1 ? "s" : ""} to another plan.
-
-
- A unit can only belong to one plan at a time. Assigning {conflicts.length === 1 ? "it" : "them"} here will move {conflicts.length === 1 ? "it" : "them"} out of{" "}
- {conflictsByPlan.map(([label, count], i) => (
-
- {label} ({count}){i < conflictsByPlan.length - 1 ? ", " : ""}
-
- ))}. Uncheck them below if that's not what you want.
-
-
-
- )}
-
- {/* ── No units in tier ─────────────────────────────────────────── */}
- {!loading && total === 0 && (
+ ) : total === 0 ? (
No {subscription} units found. Add units with this subscription first.
- )}
-
- {/* ── Specific picker (Popover) ─────────────────────────────────── */}
- {!loading && !bundleAll && total > 0 && (
+ ) : (
@@ -199,10 +87,7 @@ export function UnitPicker({ subscription, selectedIds, onChange, isPreloaded =
type="button"
variant="outline"
size="sm"
- className={cn(
- "w-full justify-between gap-2",
- selectedCount === 0 && "border-destructive text-destructive hover:border-destructive"
- )}
+ className="w-full justify-between gap-2"
>
{selectedCount === 0
? "No units selected"
@@ -227,7 +112,6 @@ export function UnitPicker({ subscription, selectedIds, onChange, isPreloaded =
{filtered.map((unit) => {
const id = String(unit.unit_id);
const checked = selectedIds.has(id);
- const conflict = isConflict(unit);
return (
)}
- {conflict && (
-
-
- In "{unit.assigned_plan.label}"
-
- )}
);
@@ -282,8 +160,7 @@ export function UnitPicker({ subscription, selectedIds, onChange, isPreloaded =
{selectedCount === 0 && (
-
-
+
Select at least one unit to bundle with this plan.
)}
diff --git a/src/modules/admin/config/users/rowActions.config.jsx b/src/modules/admin/config/users/rowActions.config.jsx
index d072bb2..843dfe1 100644
--- a/src/modules/admin/config/users/rowActions.config.jsx
+++ b/src/modules/admin/config/users/rowActions.config.jsx
@@ -45,7 +45,7 @@ export function buildRowActions({ navigate, onArchive, onBan, onUnban, onMakeAdm
className: "text-destructive focus:text-destructive",
icon:
,
onClick: (row) => onBan(row),
- hidden: (row) => !!row.is_banned || !row.is_active,
+ hidden: (row) => !!row.is_banned || !row.is_active || row.user_id === currentUserId,
separator: true,
},
{
@@ -63,7 +63,7 @@ export function buildRowActions({ navigate, onArchive, onBan, onUnban, onMakeAdm
className: "text-destructive focus:text-destructive",
icon:
,
onClick: (row) => onArchive(row),
- hidden: (row) => !row.is_active,
+ hidden: (row) => !row.is_active || row.user_id === currentUserId,
},
];
}
\ No newline at end of file
diff --git a/src/modules/admin/pages/advertisements/AddAdvertisement.jsx b/src/modules/admin/pages/advertisements/AddAdvertisement.jsx
index e75304a..4bc264f 100644
--- a/src/modules/admin/pages/advertisements/AddAdvertisement.jsx
+++ b/src/modules/admin/pages/advertisements/AddAdvertisement.jsx
@@ -1,8 +1,8 @@
// modules/admin/pages/advertisements/AddAdvertisement.jsx
-import { useMemo, useState } from "react";
+import { useState } from "react";
import { useNavigate } from "react-router-dom";
-import { useForm, useFieldArray } from "react-hook-form";
+import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import {
@@ -30,58 +30,33 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { DateTimePicker } from "@/components/ui/date-time-picker";
import { AssetPickerSheet } from "@/components/generic/AssetPickerSheet";
import { PlacementSkeleton } from "@/components/generic/PlacementSkeleton";
+import { AdvertisementPreview } from "@/components/admin/advertisements/AdvertisementPreview";
-import { MAX_CTAS, MAX_BADGE_LABELS, CONTENT_MODES } from "@/data/advertisement.data";
+import { MAX_BADGE_LABELS } from "@/data/advertisement.data";
import { PLACEMENTS, PLACEMENT_MAP } from "@/data/placement.data";
// ─── Schema ─────────────────────────────────────────────────────────────────
+// Every ad now carries the same mandatory shape — badge label(s), headline,
+// description, image, and a single link — no more Image Only / Text with
+// Image split (that toggle produced misleading results: Banner's "Text with
+// Image" mode didn't even show the image).
const schema = z.object({
placement: z.string().min(1, "Placement is required."),
- content_mode: z.enum(["image", "content"]).default("image"),
- badge_labels: z.array(z.string().min(1)).max(MAX_BADGE_LABELS, `A maximum of ${MAX_BADGE_LABELS} badges is allowed.`).default([]),
- headline: z.string().optional(),
- description: z.string().max(100, "Description must be 100 characters or fewer.").optional(),
- image_asset_id: z.union([z.string(), z.number()]).nullable().optional(),
- // Hard cap of 2 CTAs per advertisement (max() backs up the UI-level append guard)
- ctas: z.array(z.object({
- label: z.string().min(1, "Label is required."),
- link: z.string().min(1, "Link is required.").refine(isValidLink, LINK_ERROR),
- variant: z.enum(["default", "outline"]).default("default"),
- })).max(MAX_CTAS, `A maximum of ${MAX_CTAS} buttons is allowed.`).default([]),
- redirect_link: z.string().optional(),
- landing_page: z.object({
- title: z.string().optional(),
- description: z.string().optional(),
- body: z.string().optional(),
- links: z.array(z.object({
- label: z.string().optional(),
- link: z.string().optional(),
- })).default([]),
- }).default({}),
+ badge_labels: z.array(z.string().min(1)).min(1, "At least one badge label is required.").max(MAX_BADGE_LABELS, `A maximum of ${MAX_BADGE_LABELS} badges is allowed.`).default([]),
+ headline: z.string().min(1, "Headline is required."),
+ description: z.string().min(1, "Description is required.").max(100, "Description must be 100 characters or fewer."),
+ image_asset_id: z.union([z.string(), z.number()]).refine((v) => v !== null && v !== undefined && v !== "", "Image is required."),
+ redirect_link: z.string().min(1, "Link is required.").refine(isValidLink, LINK_ERROR),
start_date: z.string().optional(),
end_date: z.string().optional(),
is_active: z.boolean().default(true),
-}).superRefine((data, ctx) => {
- // Image Only ads have no other click-through — the Link is their only
- // destination, so it's required (Text with Image ads click through via
- // their own CTA links instead, see redirect_link comment above).
- if (data.content_mode !== "image") return;
- if (!data.redirect_link?.trim()) {
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["redirect_link"], message: "Link is required." });
- } else if (!isValidLink(data.redirect_link)) {
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["redirect_link"], message: LINK_ERROR });
- }
});
// ─── Steps ────────────────────────────────────────────────────────────────────
-// The Page Builder step only shows up when no redirect_link was given — it's
-// the alternative click-through destination (an internally-authored landing
-// page) for ads that don't link straight out to a URL.
const ALL_STEPS = [
{ id: "placement", label: "Placement", icon: MapPin, description: "Choose where this ad appears — Dashboard, Tier Plans, or Course Details." },
- { id: "content", label: "Type", icon: FileText, description: "Image Only, or Text with Image with badges, headline, description, and links." },
- // { id: "pageBuilder", label: "Page Builder", icon: LayoutTemplate, description: "No redirect link? Build an internal landing page for this ad instead.", skippable: true },
+ { id: "content", label: "Content", icon: FileText, description: "Badge labels, headline, description, image, and link for this ad." },
{ id: "scheduling", label: "Scheduling & Display", icon: CalendarClock, description: "Start/end dates and draft/active status." },
{ id: "review", label: "Review", icon: Check, description: "Confirm everything before creating this ad." },
];
@@ -208,195 +183,86 @@ function StepImagePicker({ selectedAsset, imageUrl, setPickerOpen }) {
function StepContent({
register, errors, setValue, watch, description,
- selectedAsset, imageUrl, setPickerOpen,
- ctaFields, appendCta, removeCta,
+ selectedAsset, imageUrl, setPickerOpen, format,
}) {
- const contentMode = watch("content_mode");
const badgeLabelFields = watch("badge_labels") ?? [];
const appendBadgeLabel = () => setValue("badge_labels", [...badgeLabelFields, ""], { shouldValidate: true, shouldDirty: true });
const removeBadgeLabel = (index) => setValue("badge_labels", badgeLabelFields.filter((_, i) => i !== index), { shouldValidate: true, shouldDirty: true });
return (
-
-
-
- {CONTENT_MODES.map((m) => (
-
+
+
+
+ {badgeLabelFields.map((_, index) => (
+
+
+
+
+
+
+
))}
+ {badgeLabelFields.length < MAX_BADGE_LABELS ? (
+
+ ) : (
+
Maximum of {MAX_BADGE_LABELS} badges reached.
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100 ? "text-destructive" : "text-muted-foreground"}`}>
+ {description?.length ?? 0}/100
+
+
- {contentMode === "image" && (
-
-
-
-
- Internal label shown in the Ads list — not displayed on the ad itself.
-
-
- )}
+
+
+
+
+
+ Where clicking this ad goes to.
+
+
- {contentMode === "content" && (
-
-
-
-
- {badgeLabelFields.map((_, index) => (
-
-
-
-
-
-
-
- ))}
- {badgeLabelFields.length < MAX_BADGE_LABELS ? (
-
- ) : (
-
Maximum of {MAX_BADGE_LABELS} badges reached.
- )}
-
-
-
-
-
-
-
-
-
-
-
- 100 ? "text-destructive" : "text-muted-foreground"}`}>
- {description?.length ?? 0}/100
-
-
-
+
-
-
-
- Up to {MAX_CTAS} buttons. The first is styled Primary, the second Outline.
-
-
- {ctaFields.map((field, index) => (
-
-
-
-
-
-
-
-
-
-
-
- ))}
- {ctaFields.length < MAX_CTAS ? (
-
- ) : (
-
Maximum of {MAX_CTAS} buttons reached.
- )}
-
-
-
- )}
-
- {contentMode === "image" && (
- <>
-
-
-
-
-
-
- Where clicking the image goes to.
-
-
- >
- )}
+
);
}
-// ─── Step 3: Page Builder ───────────────────────────────────────────────────
-
-function StepPageBuilder({ register, linkFields, appendLink, removeLink }) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-// ─── Step 4: Scheduling & Display ───────────────────────────────────────────
+// ─── Step 3: Scheduling & Display ───────────────────────────────────────────
function StepScheduling({ watch, setValue }) {
const isActive = watch("is_active");
@@ -451,8 +317,6 @@ function SummaryRow({ label, value }) {
function StepReview({ data, selectedAsset, imageUrl }) {
const { fmtDateTime } = useDateFormat();
const placementMeta = PLACEMENT_MAP[data.placement];
- const ctas = (data.ctas ?? []).filter((c) => c.label || c.link);
- const hasLandingPage = !data.redirect_link && (data.landing_page?.title || data.landing_page?.body);
return (
@@ -467,14 +331,9 @@ function StepReview({ data, selectedAsset, imageUrl }) {
Content
-
- {data.content_mode === "content" && (
- <>
-
-
-
- >
- )}
+
+
+
@@ -490,30 +349,10 @@ function StepReview({ data, selectedAsset, imageUrl }) {
)}
- {ctas.length > 0 && (
-
-
Links
- {ctas.map((c, i) => (
-
- ))}
-
- )}
-
- {data.content_mode === "image" && (
-
-
Click-through
- {data.redirect_link ? (
-
- ) : hasLandingPage ? (
- <>
-
-
l.label || l.link).length || null} />
- >
- ) : (
- No link or landing page set — this ad won't link anywhere when clicked.
- )}
-
- )}
+
Scheduling & display
@@ -540,7 +379,6 @@ export default function AddAdvertisement() {
const {
register,
handleSubmit,
- control,
trigger,
getValues,
watch,
@@ -550,23 +388,17 @@ export default function AddAdvertisement() {
resolver: zodResolver(schema),
defaultValues: {
placement: undefined,
- content_mode: "image",
badge_labels: [],
headline: "",
description: "",
image_asset_id: null,
- ctas: [],
redirect_link: "",
- landing_page: { title: "", description: "", body: "", links: [] },
start_date: "",
end_date: "",
is_active: true,
},
});
- const { fields: ctaFields, append: appendCta, remove: removeCta } = useFieldArray({ control, name: "ctas" });
- const { fields: linkFields, append: appendLink, remove: removeLink } = useFieldArray({ control, name: "landing_page.links" });
-
// selectedAsset lives outside the form and its setValue() call doesn't pass
// shouldDirty, so isDirty alone would miss it.
const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(
@@ -575,21 +407,12 @@ export default function AddAdvertisement() {
const placement = watch("placement");
const description = watch("description");
- const redirectLink = watch("redirect_link");
- const contentMode = watch("content_mode");
const format = PLACEMENT_MAP[placement]?.format;
- const steps = useMemo(
- () => ALL_STEPS.filter((s) => !s.skippable || !redirectLink?.trim()),
- [redirectLink]
- );
+ const steps = ALL_STEPS;
const stepIndex = Math.min(step, steps.length - 1);
const current = steps[stepIndex];
- // Image Only ads require a valid Link before advancing past the Type step
- const linkStepInvalid = current.id === "content" && contentMode === "image"
- && (!redirectLink?.trim() || !isValidLink(redirectLink));
-
const breadcrumbItems = [
{ label: "Home", icon:
, to: "/admin" },
{ label: "Ads", to: "/admin/advertisements" },
@@ -600,7 +423,7 @@ export default function AddAdvertisement() {
const handleNext = async () => {
let fields = [];
if (current.id === "placement") fields = ["placement"];
- else if (current.id === "content") fields = watch("content_mode") === "content" ? ["headline", "description", "badge_labels", "ctas"] : ["redirect_link"];
+ else if (current.id === "content") fields = ["badge_labels", "headline", "description", "image_asset_id", "redirect_link"];
const valid = fields.length ? await trigger(fields) : true;
if (valid) setStep((s) => Math.min(s + 1, steps.length - 1));
@@ -612,9 +435,6 @@ export default function AddAdvertisement() {
const handleCreate = handleSubmit(async (values) => {
const payload = {
...values,
- image_asset_id: values.image_asset_id || null,
- redirect_link: values.redirect_link || null,
- landing_page: values.redirect_link ? null : values.landing_page,
start_date: values.start_date || null,
end_date: values.end_date || null,
createdBy: user?.user_id ?? null,
@@ -663,17 +483,7 @@ export default function AddAdvertisement() {
selectedAsset={selectedAsset}
imageUrl={imagePreviewUrl}
setPickerOpen={setPickerOpen}
- ctaFields={ctaFields}
- appendCta={appendCta}
- removeCta={removeCta}
- />
- )}
- {current.id === "pageBuilder" && (
-
)}
{current.id === "scheduling" && (
@@ -701,7 +511,7 @@ export default function AddAdvertisement() {
Create advertisement
) : (
-
{/* Objectives */}
diff --git a/src/modules/admin/pages/courses/units/AddUnit.jsx b/src/modules/admin/pages/courses/units/AddUnit.jsx
index e807799..8d81d7b 100644
--- a/src/modules/admin/pages/courses/units/AddUnit.jsx
+++ b/src/modules/admin/pages/courses/units/AddUnit.jsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams, useLocation } 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 { ChevronLeft, ChevronRight, Check, FileText, ListChecks, Link2, Plus } from "lucide-react";
@@ -15,6 +15,9 @@ 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";
@@ -23,6 +26,7 @@ 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 = [
@@ -49,11 +53,21 @@ export default function AddUnit() {
const [requirements, setRequirements] = useState([]);
const [attachOpen, setAttachOpen] = useState(false);
- const { register, trigger, getValues, 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, trigger, getValues, control, setValue, formState: { errors, isDirty } } = useForm({
resolver: zodResolver(schema),
- defaultValues: { title: "", description: "", order: 0 },
+ 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(() => {
@@ -69,7 +83,8 @@ export default function AddUnit() {
// click at Requirements (the last step), which is purely a draft form
// until now.
const handleCreate = async () => {
- const result = await createUnit(courseId, { ...getValues(), createdBy: user?.user_id });
+ 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;
@@ -178,6 +193,28 @@ export default function AddUnit() {
+
+
+
+
+
+ Optional. Gates this unit directly, independent of the course it's attached to.
+
+
)}
diff --git a/src/modules/admin/pages/courses/units/EditUnit.jsx b/src/modules/admin/pages/courses/units/EditUnit.jsx
index c5eb928..af5d949 100644
--- a/src/modules/admin/pages/courses/units/EditUnit.jsx
+++ b/src/modules/admin/pages/courses/units/EditUnit.jsx
@@ -1,6 +1,6 @@
import { useState, useEffect } 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, House } from "lucide-react";
@@ -8,11 +8,15 @@ import { ArrowLeft, House } from "lucide-react";
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";
@@ -20,6 +24,7 @@ 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(),
});
function FieldError({ message }) {
@@ -34,11 +39,21 @@ export default function EditUnit() {
const { user } = useAuth();
const navigate = useNavigate();
- 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: "", order: 0 },
+ defaultValues: { title: "", description: "", order: 0, subscription: "free" },
});
+ const watchedSubscr = useWatch({ control, name: "subscription" });
+ const defaultTierSlug = tierCategories.find((c) => c.is_default)?.slug || "free";
+
useEffect(() => {
(async () => {
const res = await fetchUnit(courseId, unitId);
@@ -48,6 +63,7 @@ export default function EditUnit() {
title: unit.title ?? "",
description: unit.description ?? "",
order: unit.order ?? 0,
+ subscription: unit.subscription || defaultTierSlug,
});
setUnitTitle(unit.title ?? "");
})();
@@ -61,7 +77,7 @@ export default function EditUnit() {
const onSubmit = async (data) => {
if (!isDirty) { bypassOnce(); return navigate(`/admin/courses/${courseId}/units/${unitId}/view`); }
- const result = await updateUnit(courseId, unitId, { ...data, updatedBy: user?.user_id });
+ const result = await updateUnit(courseId, unitId, { ...data, subscription: data.subscription || defaultTierSlug, updatedBy: user?.user_id });
if (!result) return;
bypassOnce();
navigate(`/admin/courses/${courseId}/units/${unitId}/view`);
@@ -105,6 +121,28 @@ export default function EditUnit() {
+
+
+
+
+ Optional. Gates this unit directly, independent of the course it's attached to.
+
+
+
diff --git a/src/modules/admin/pages/library/lessons/AddLibraryLesson.jsx b/src/modules/admin/pages/library/lessons/AddLibraryLesson.jsx
index 90f5f94..24ee415 100644
--- a/src/modules/admin/pages/library/lessons/AddLibraryLesson.jsx
+++ b/src/modules/admin/pages/library/lessons/AddLibraryLesson.jsx
@@ -44,7 +44,7 @@ const schema = z.object({
blocks: z.array(z.any()).optional(),
});
-const DEFAULT_VALUES = { title: "", description: "", subscription: "", blocks: [] };
+const DEFAULT_VALUES = { title: "", description: "", subscription: "free", blocks: [] };
const STEPS = [
{ id: 0, label: "Lesson", icon: FileText },
@@ -65,6 +65,7 @@ function FieldError({ message }) {
// ─── Step 1 — Lesson ───────────────────────────────────────────────────────────
function StepLesson({ register, errors, control, setValue, tierCategories }) {
const watchedSubscr = useWatch({ control, name: "subscription" });
+ const defaultTierSlug = tierCategories.find((c) => c.is_default)?.slug || "free";
return (
@@ -82,14 +83,13 @@ function StepLesson({ register, errors, control, setValue, tierCategories }) {
-
+
{attachUnitId &&
}
@@ -289,7 +289,7 @@ export default function AddLibraryLesson() {
const result = await createLesson({
title: data.title,
description: data.description || null,
- subscription: data.subscription || null,
+ subscription: data.subscription || (tierCategories.find((c) => c.is_default)?.slug || "free"),
...(attachUnitId ? { unit_id: attachUnitId } : {}),
createdBy: user?.user_id,
});
diff --git a/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx b/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx
index 7814284..aab8481 100644
--- a/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx
+++ b/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx
@@ -49,12 +49,13 @@ export default function EditLibraryLesson() {
const { register, handleSubmit, reset, control, setValue, formState: { errors, isDirty } } = useForm({
resolver: zodResolver(schema),
- defaultValues: { title: "", description: "", subscription: "" },
+ 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);
@@ -62,12 +63,12 @@ export default function EditLibraryLesson() {
useEffect(() => {
if (lesson && String(lesson.lesson_id) === String(lessonId)) {
- reset({ title: lesson.title ?? "", description: lesson.description ?? "", subscription: lesson.subscription ?? "" });
+ 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 || null, updatedBy: user?.user_id });
+ const result = await updateLesson(lessonId, { ...data, subscription: data.subscription || defaultTierSlug, updatedBy: user?.user_id });
if (!result) return;
bypassOnce();
navigate(`/admin/lessons/${lessonId}/view`);
@@ -108,14 +109,13 @@ export default function EditLibraryLesson() {