mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
fix some issues
This commit is contained in:
@@ -1,22 +1,27 @@
|
|||||||
// modules/admin/components/courses/CreateUnitDialog.jsx
|
// modules/admin/components/courses/CreateUnitDialog.jsx
|
||||||
// Lightweight "create a brand-new unit and attach it to this course" dialog —
|
// Lightweight "create a brand-new unit and attach it to this course" dialog —
|
||||||
// the create-new counterpart to AttachUnitsDialog's attach-existing flow.
|
// the create-new counterpart to AttachUnitsDialog's attach-existing flow.
|
||||||
|
// Two steps (Details -> Completion Requirements) to match the other two
|
||||||
|
// create-unit surfaces (AddUnit.jsx, AddLibraryUnit.jsx) — same capability,
|
||||||
|
// just condensed into a dialog since this one runs inline in a wizard.
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import DraftRequirementsEditor from "./DraftRequirementsEditor";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
title: z.string().min(1, "Title is required."),
|
title: z.string().min(1, "Title is required."),
|
||||||
@@ -25,19 +30,33 @@ const schema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrder = 0, onCreated, draftMode = false }) {
|
export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrder = 0, onCreated, draftMode = false }) {
|
||||||
const { createUnit, loading } = useCourses();
|
const { createUnit, syncUnitRequirements, loading } = useCourses();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { register, handleSubmit, reset, formState: { errors } } = useForm({
|
const [step, setStep] = useState(0);
|
||||||
|
const [requirements, setRequirements] = useState([]);
|
||||||
|
|
||||||
|
const { register, handleSubmit, trigger, reset, formState: { errors } } = useForm({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: { title: "", description: "", order: nextOrder },
|
defaultValues: { title: "", description: "", order: nextOrder },
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) reset({ title: "", description: "", order: nextOrder });
|
if (open) {
|
||||||
|
reset({ title: "", description: "", order: nextOrder });
|
||||||
|
setStep(0);
|
||||||
|
setRequirements([]);
|
||||||
|
}
|
||||||
}, [open, nextOrder, reset]);
|
}, [open, nextOrder, reset]);
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
const valid = await trigger();
|
||||||
|
if (valid) setStep(1);
|
||||||
|
};
|
||||||
|
|
||||||
const onValid = async (values) => {
|
const onValid = async (values) => {
|
||||||
|
const clean = requirements.map(({ _key, ...r }) => r);
|
||||||
|
|
||||||
if (draftMode) {
|
if (draftMode) {
|
||||||
onCreated?.({
|
onCreated?.({
|
||||||
unit_id: null,
|
unit_id: null,
|
||||||
@@ -46,6 +65,7 @@ export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrd
|
|||||||
description: values.description,
|
description: values.description,
|
||||||
order_index: values.order,
|
order_index: values.order,
|
||||||
lessons: [],
|
lessons: [],
|
||||||
|
requirements: clean,
|
||||||
});
|
});
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
return;
|
return;
|
||||||
@@ -54,6 +74,11 @@ export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrd
|
|||||||
const result = await createUnit(courseId, { ...values, createdBy: user?.user_id });
|
const result = await createUnit(courseId, { ...values, createdBy: user?.user_id });
|
||||||
const unit = result?.data?.data ?? null;
|
const unit = result?.data?.data ?? null;
|
||||||
if (!unit) return;
|
if (!unit) return;
|
||||||
|
|
||||||
|
if (clean.length > 0) {
|
||||||
|
await syncUnitRequirements(courseId, unit.unit_id, clean);
|
||||||
|
}
|
||||||
|
|
||||||
onCreated?.(unit);
|
onCreated?.(unit);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
@@ -63,35 +88,63 @@ export default function CreateUnitDialog({ open, onOpenChange, courseId, nextOrd
|
|||||||
<DialogContent className="sm:max-w-[440px]">
|
<DialogContent className="sm:max-w-[440px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>New Unit</DialogTitle>
|
<DialogTitle>New Unit</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Creates a new unit in the shared Units Library and attaches it to this course.
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onValid)} className="space-y-4">
|
<form onSubmit={handleSubmit(onValid)} className="space-y-4">
|
||||||
<div className="space-y-1.5">
|
{step === 0 && (
|
||||||
<Label htmlFor="unit_title">
|
<>
|
||||||
Title <span className="text-destructive">*</span>
|
<div className="space-y-1.5">
|
||||||
</Label>
|
<Label htmlFor="unit_title">
|
||||||
<Input id="unit_title" placeholder="e.g. Getting Started" {...register("title")} />
|
Title <span className="text-destructive">*</span>
|
||||||
{errors.title && <p className="text-sm text-destructive">{errors.title.message}</p>}
|
</Label>
|
||||||
</div>
|
<Input id="unit_title" placeholder="e.g. Getting Started" {...register("title")} />
|
||||||
|
{errors.title && <p className="text-sm text-destructive">{errors.title.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="unit_description">Description</Label>
|
<Label htmlFor="unit_description">Description</Label>
|
||||||
<Textarea id="unit_description" placeholder="Optional unit description" rows={3} {...register("description")} />
|
<Textarea id="unit_description" placeholder="Optional unit description" rows={3} {...register("description")} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5 max-w-[120px]">
|
<div className="space-y-1.5 max-w-[120px]">
|
||||||
<Label htmlFor="unit_order">Order</Label>
|
<Label htmlFor="unit_order">Order</Label>
|
||||||
<Input id="unit_order" type="number" min={0} {...register("order")} />
|
<Input id="unit_order" type="number" min={0} {...register("order")} />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Configure how learners complete this unit — optional, sensible defaults apply automatically.
|
||||||
|
</p>
|
||||||
|
<DraftRequirementsEditor entityType="unit" items={requirements} onChange={setRequirements} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
{step === 0 ? (
|
||||||
<Button type="button" variant="outline" disabled={loading}>Cancel</Button>
|
<>
|
||||||
</DialogClose>
|
<DialogClose asChild>
|
||||||
<Button type="submit" disabled={loading}>
|
<Button type="button" variant="outline" disabled={loading}>Cancel</Button>
|
||||||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
</DialogClose>
|
||||||
Create Unit
|
<Button type="button" onClick={handleNext}>Next</Button>
|
||||||
</Button>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setStep(0)} disabled={loading}>
|
||||||
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={loading}>
|
||||||
|
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||||||
|
Create Unit
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { buildRowActions } from "../../config/courses/units/rowActions.config";
|
|||||||
|
|
||||||
import { getTimestamp } from "@/utils/timestamp.util";
|
import { getTimestamp } from "@/utils/timestamp.util";
|
||||||
|
|
||||||
export default function UnitsTable({ courseId }) {
|
export default function UnitsTable({ courseId, returnTo }) {
|
||||||
const [archiveTarget, setArchiveTarget] = useState(null);
|
const [archiveTarget, setArchiveTarget] = useState(null);
|
||||||
const [archiveIds, setArchiveIds] = useState(null);
|
const [archiveIds, setArchiveIds] = useState(null);
|
||||||
const [attachOpen, setAttachOpen] = useState(false);
|
const [attachOpen, setAttachOpen] = useState(false);
|
||||||
@@ -68,6 +68,7 @@ export default function UnitsTable({ courseId }) {
|
|||||||
exportConfig,
|
exportConfig,
|
||||||
navigate,
|
navigate,
|
||||||
courseId,
|
courseId,
|
||||||
|
returnTo,
|
||||||
getFilters: () => tableRefsRef.current.getFilters(),
|
getFilters: () => tableRefsRef.current.getFilters(),
|
||||||
getSort: () => tableRefsRef.current.getSort(),
|
getSort: () => tableRefsRef.current.getSort(),
|
||||||
getTableInstance: () => tableRefsRef.current.tableInstance,
|
getTableInstance: () => tableRefsRef.current.tableInstance,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function buildToolbarActions({
|
|||||||
exportConfig,
|
exportConfig,
|
||||||
navigate,
|
navigate,
|
||||||
courseId,
|
courseId,
|
||||||
|
returnTo,
|
||||||
getFilters,
|
getFilters,
|
||||||
getSort,
|
getSort,
|
||||||
getTableInstance,
|
getTableInstance,
|
||||||
@@ -42,7 +43,7 @@ export function buildToolbarActions({
|
|||||||
label: "New Unit",
|
label: "New Unit",
|
||||||
icon: <Plus className="h-3.5 w-3.5" />,
|
icon: <Plus className="h-3.5 w-3.5" />,
|
||||||
variant: "default",
|
variant: "default",
|
||||||
onClick: () => navigate(`/admin/courses/${courseId}/units/add`),
|
onClick: () => navigate(`/admin/courses/${courseId}/units/add`, returnTo ? { state: { returnTo } } : undefined),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "archived-units",
|
key: "archived-units",
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ export default function AddCourse() {
|
|||||||
unit_id: u.unit_id,
|
unit_id: u.unit_id,
|
||||||
title: u.title,
|
title: u.title,
|
||||||
description: u.description,
|
description: u.description,
|
||||||
|
requirements: u.requirements ?? [],
|
||||||
lessons: u.lessons.map((l) => ({
|
lessons: u.lessons.map((l) => ({
|
||||||
lesson_id: l.lesson_id,
|
lesson_id: l.lesson_id,
|
||||||
title: l.title,
|
title: l.title,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
import { useForm, useFieldArray, useWatch } from "react-hook-form";
|
import { useForm, useFieldArray, useWatch } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -14,6 +14,7 @@ import { TIER_COLOR_OPTIONS } from "@/utils/tierColors";
|
|||||||
import { AssetPickerSheet } from "@/components/generic/AssetPickerSheet";
|
import { AssetPickerSheet } from "@/components/generic/AssetPickerSheet";
|
||||||
import { PageMeta } from "@/contexts/MetadataContext";
|
import { PageMeta } from "@/contexts/MetadataContext";
|
||||||
import CourseInstructorPicker from "@/modules/admin/components/courses/CourseInstructorPicker";
|
import CourseInstructorPicker from "@/modules/admin/components/courses/CourseInstructorPicker";
|
||||||
|
import UnitsTable from "@/modules/admin/components/courses/UnitsTable";
|
||||||
import CoursePrerequisiteBuilder from "@/modules/admin/components/courses/CoursePrerequisiteBuilder";
|
import CoursePrerequisiteBuilder from "@/modules/admin/components/courses/CoursePrerequisiteBuilder";
|
||||||
import { useCategories } from "@/contexts/AdminCategoriesContext";
|
import { useCategories } from "@/contexts/AdminCategoriesContext";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
@@ -61,6 +62,7 @@ const schema = z.object({
|
|||||||
|
|
||||||
const STEPS = [
|
const STEPS = [
|
||||||
{ label: "Basic Info", description: "Title, level & objectives" },
|
{ label: "Basic Info", description: "Title, level & objectives" },
|
||||||
|
{ label: "Units", description: "Roadmap for this course" },
|
||||||
{ label: "Categories", description: "Tags & instructors" },
|
{ label: "Categories", description: "Tags & instructors" },
|
||||||
{ label: "Rewards", description: "Badge & achievements" },
|
{ label: "Rewards", description: "Badge & achievements" },
|
||||||
{ label: "Requirements", description: "Prerequisites & completion" },
|
{ label: "Requirements", description: "Prerequisites & completion" },
|
||||||
@@ -157,7 +159,12 @@ export default function EditCourse() {
|
|||||||
const { categories: allCategories, fetchCategories } = useCategories();
|
const { categories: allCategories, fetchCategories } = useCategories();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [searchParams] = useSearchParams();
|
||||||
|
const initialStep = Math.min(
|
||||||
|
Math.max(Number(searchParams.get("step")) || 0, 0),
|
||||||
|
STEPS.length - 1,
|
||||||
|
);
|
||||||
|
const [currentStep, setCurrentStep] = useState(initialStep);
|
||||||
|
|
||||||
// ─── Tier categories ──────────────────────────────────────────────────────
|
// ─── Tier categories ──────────────────────────────────────────────────────
|
||||||
const [tierCategories, setTierCategories] = useState([]);
|
const [tierCategories, setTierCategories] = useState([]);
|
||||||
@@ -751,8 +758,18 @@ export default function EditCourse() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Step 1: Categories & Instructors ── */}
|
{/* ── Step 1: Units ── */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
|
<SectionCard
|
||||||
|
title="Units"
|
||||||
|
description="This course's roadmap. Units live independently in the shared Units Library — creating one here attaches it automatically, and attaching an existing one reuses it without copying."
|
||||||
|
>
|
||||||
|
<UnitsTable courseId={courseId} returnTo={`/admin/courses/${courseId}/edit?step=1`} />
|
||||||
|
</SectionCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Step 2: Categories & Instructors ── */}
|
||||||
|
{currentStep === 2 && (
|
||||||
<>
|
<>
|
||||||
<SectionCard title="Categories" description="Assign this course to one or more categories for browsing.">
|
<SectionCard title="Categories" description="Assign this course to one or more categories for browsing.">
|
||||||
{selectedCategoryIds.length > 0 && (
|
{selectedCategoryIds.length > 0 && (
|
||||||
@@ -900,8 +917,8 @@ export default function EditCourse() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Step 2: Rewards ── */}
|
{/* ── Step 3: Rewards ── */}
|
||||||
{currentStep === 2 && (
|
{currentStep === 3 && (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="Rewards"
|
title="Rewards"
|
||||||
description="Badge and achievements awarded to learners who complete this course."
|
description="Badge and achievements awarded to learners who complete this course."
|
||||||
@@ -1109,8 +1126,8 @@ export default function EditCourse() {
|
|||||||
</SectionCard>
|
</SectionCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Step 3: Prerequisites & Completion Requirements ── */}
|
{/* ── Step 4: Prerequisites & Completion Requirements ── */}
|
||||||
{currentStep === 3 && (
|
{currentStep === 4 && (
|
||||||
<>
|
<>
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="Prerequisites"
|
title="Prerequisites"
|
||||||
@@ -1153,8 +1170,8 @@ export default function EditCourse() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Step 4: Pricing ── */}
|
{/* ── Step 5: Pricing ── */}
|
||||||
{currentStep === 4 && (
|
{currentStep === 5 && (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="Product Listing"
|
title="Product Listing"
|
||||||
description="Allow learners to purchase this course individually via PayPal."
|
description="Allow learners to purchase this course individually via PayPal."
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams, useLocation } from "react-router-dom";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -35,10 +35,13 @@ function FieldError({ message }) {
|
|||||||
|
|
||||||
export default function AddUnit() {
|
export default function AddUnit() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const { createUnit, fetchCourse, course, loading, syncUnitRequirements } = useCourses();
|
const { createUnit, fetchCourse, course, loading, syncUnitRequirements } = useCourses();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const backTarget = location.state?.returnTo ?? `/admin/courses/${courseId}/units`;
|
||||||
|
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [requirements, setRequirements] = useState([]);
|
const [requirements, setRequirements] = useState([]);
|
||||||
|
|
||||||
@@ -72,7 +75,7 @@ export default function AddUnit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bypassOnce();
|
bypassOnce();
|
||||||
navigate(`/admin/courses/${courseId}/units`);
|
navigate(backTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,13 +89,15 @@ export default function AddUnit() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => (step === 0 ? navigate(`/admin/courses/${courseId}/units`) : setStep(0))}
|
onClick={() => (step === 0 ? navigate(backTarget) : setStep(0))}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold">Create Unit</h1>
|
<h1 className="text-xl font-semibold">Create Unit</h1>
|
||||||
<p className="text-sm text-muted-foreground">Add a new unit to this course.</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Creates a new unit in the shared Units Library and attaches it to this course.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -169,7 +174,7 @@ export default function AddUnit() {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => (step === 0 ? navigate(`/admin/courses/${courseId}/units`) : setStep(0))}
|
onClick={() => (step === 0 ? navigate(backTarget) : setStep(0))}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
|
|||||||
Reference in New Issue
Block a user