mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -52,7 +52,6 @@ export default function LessonsTable({ courseId, unitId }) {
|
||||
onViewPage: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/page/view`),
|
||||
onCreatePage: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/page`),
|
||||
onView: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/view`),
|
||||
onEdit: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/edit`),
|
||||
onArchive: (row) => setArchiveTarget(row),
|
||||
}), [courseId, unitId]);
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ export default function UnitsTable({ courseId }) {
|
||||
onQuiz: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/quiz/edit`),
|
||||
onViewLessons: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/lessons`),
|
||||
onView: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/view`),
|
||||
onEdit: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/edit`),
|
||||
onArchive: (row) => setArchiveTarget(row),
|
||||
}), [courseId]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Eye, Pencil, Archive, SquarePlus, SquareChartGantt } from "lucide-react";
|
||||
import { Eye, Archive, SquarePlus, SquareChartGantt } from "lucide-react";
|
||||
|
||||
export function buildRowActions({ onCreatePage, onViewPage, onView, onEdit, onArchive }) {
|
||||
export function buildRowActions({ onCreatePage, onViewPage, onView, onArchive }) {
|
||||
return [
|
||||
{
|
||||
key: "view",
|
||||
@@ -8,12 +8,6 @@ export function buildRowActions({ onCreatePage, onViewPage, onView, onEdit, onAr
|
||||
icon: <Eye className="h-3.5 w-3.5" />,
|
||||
onClick: (row) => onView(row),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "Edit Lesson",
|
||||
icon: <Pencil className="h-3.5 w-3.5" />,
|
||||
onClick: (row) => onEdit(row),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "View Content",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Eye, Pencil, Archive, BookCheck, NotebookPen, ClipboardList, PlusCircle } from "lucide-react";
|
||||
import { Eye, Archive, BookCheck, NotebookPen, ClipboardList, PlusCircle } from "lucide-react";
|
||||
|
||||
export function buildRowActions({ onViewLessons, onView, onEdit, onArchive, onQuiz, onViewQuiz }) {
|
||||
export function buildRowActions({ onViewLessons, onView, onArchive, onQuiz, onViewQuiz }) {
|
||||
return [
|
||||
{
|
||||
key: "view",
|
||||
@@ -8,12 +8,6 @@ export function buildRowActions({ onViewLessons, onView, onEdit, onArchive, onQu
|
||||
icon: <Eye className="h-3.5 w-3.5" />,
|
||||
onClick: (row) => onView(row),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "Edit Info",
|
||||
icon: <Pencil className="h-3.5 w-3.5" />,
|
||||
onClick: (row) => onEdit(row),
|
||||
},
|
||||
{
|
||||
key: "view_units",
|
||||
label: "View Lessons",
|
||||
|
||||
@@ -44,13 +44,24 @@ const schema = z.object({
|
||||
subscription: z.string().min(1, "Subscription is required.").default("free"),
|
||||
objectives: z.array(z.object({ text: z.string().min(1, "Objective cannot be empty.") })).default([]),
|
||||
achievement_keys: z.array(z.string()).max(1).default([]),
|
||||
|
||||
unit_title: z.string().min(1, "Unit title is required."),
|
||||
unit_description: z.string().optional(),
|
||||
unit_order: z.coerce.number().min(0).default(0),
|
||||
|
||||
lesson_title: z.string().min(1, "Lesson title is required."),
|
||||
lesson_description: z.string().optional(),
|
||||
lesson_order: z.coerce.number().min(0).default(0),
|
||||
lesson_objectives: z.array(z.object({ text: z.string().min(1, "Objective cannot be empty.") })).default([]),
|
||||
});
|
||||
|
||||
// ─── Steps config ─────────────────────────────────────────────────────────────
|
||||
|
||||
const STEPS = [
|
||||
{ label: "Basic Info", description: "Title, level & objectives" },
|
||||
{ label: "Rewards", description: "Badge & achievements" },
|
||||
{ label: "Basic Info", description: "Title, level & objectives" },
|
||||
{ label: "First Unit", description: "The course's first unit" },
|
||||
{ label: "First Lesson", description: "A lesson inside that unit" },
|
||||
{ label: "Rewards", description: "Badge & achievements" },
|
||||
];
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -128,7 +139,7 @@ function StepIndicator({ steps, current, onStepClick }) {
|
||||
|
||||
export default function AddCourse() {
|
||||
const navigate = useNavigate();
|
||||
const { createCourse, loading } = useCourses();
|
||||
const { createCourse, createUnit, createLesson, loading } = useCourses();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
@@ -163,12 +174,22 @@ export default function AddCourse() {
|
||||
subscription: "free",
|
||||
objectives: [],
|
||||
achievement_keys: [],
|
||||
unit_title: "",
|
||||
unit_description: "",
|
||||
unit_order: 0,
|
||||
lesson_title: "",
|
||||
lesson_description: "",
|
||||
lesson_order: 0,
|
||||
lesson_objectives: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields: objectiveFields, append: appendObjective, remove: removeObjective } =
|
||||
useFieldArray({ control, name: "objectives" });
|
||||
|
||||
const { fields: lessonObjectiveFields, append: appendLessonObjective, remove: removeLessonObjective } =
|
||||
useFieldArray({ control, name: "lesson_objectives" });
|
||||
|
||||
const currentAchKeys = useWatch({ control, name: "achievement_keys" });
|
||||
const watchedTitle = useWatch({ control, name: "title" });
|
||||
const watchedLevel = useWatch({ control, name: "level" });
|
||||
@@ -189,34 +210,68 @@ export default function AddCourse() {
|
||||
}
|
||||
};
|
||||
|
||||
const STEP_FIELDS = [
|
||||
["title", "subscription", "objectives"],
|
||||
["unit_title", "unit_order"],
|
||||
["lesson_title", "lesson_order", "lesson_objectives"],
|
||||
[],
|
||||
];
|
||||
|
||||
const handleNext = async (e) => {
|
||||
// The Next button occupies the same DOM position as the eventual
|
||||
// type="submit" Create Course button. Advancing the step re-renders
|
||||
// that node's type attribute in place *before* the browser evaluates
|
||||
// this click's default action, which would otherwise submit the form.
|
||||
e.preventDefault();
|
||||
if (currentStep === 0) {
|
||||
const valid = await trigger(["title", "subscription", "objectives"]);
|
||||
const fields = STEP_FIELDS[currentStep];
|
||||
if (fields?.length) {
|
||||
const valid = await trigger(fields);
|
||||
if (!valid) return;
|
||||
}
|
||||
setCurrentStep((s) => s + 1);
|
||||
};
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
const payload = {
|
||||
...values,
|
||||
objectives: values.objectives.map((o) => o.text),
|
||||
level: values.level || null,
|
||||
const coursePayload = {
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
course_code: values.course_code || null,
|
||||
order_index: values.order_index,
|
||||
level: values.level || null,
|
||||
subscription: values.subscription,
|
||||
objectives: values.objectives.map((o) => o.text),
|
||||
achievement_keys: values.achievement_keys,
|
||||
badge_color: badgeColor,
|
||||
badge_asset_id: badgeAssetId ?? null,
|
||||
badge_image_url: badgeImageUrl ?? null,
|
||||
createdBy: user?.user_id ?? null,
|
||||
};
|
||||
|
||||
const result = await createCourse(payload);
|
||||
if (!result) return;
|
||||
navigate("/admin/courses");
|
||||
const courseResult = await createCourse(coursePayload);
|
||||
const newCourse = courseResult?.data?.data ?? null;
|
||||
if (!newCourse) return;
|
||||
|
||||
const unitResult = await createUnit(newCourse.course_id, {
|
||||
title: values.unit_title,
|
||||
description: values.unit_description,
|
||||
order: values.unit_order,
|
||||
createdBy: user?.user_id,
|
||||
});
|
||||
const newUnit = unitResult?.data?.data ?? null;
|
||||
if (!newUnit) {
|
||||
navigate(`/admin/courses/${newCourse.course_id}/view`);
|
||||
return;
|
||||
}
|
||||
|
||||
await createLesson(newCourse.course_id, newUnit.unit_id, {
|
||||
title: values.lesson_title,
|
||||
description: values.lesson_description,
|
||||
order: values.lesson_order,
|
||||
objectives: values.lesson_objectives.map((o) => o.text),
|
||||
createdBy: user?.user_id,
|
||||
});
|
||||
|
||||
navigate(`/admin/courses/${newCourse.course_id}/view`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -366,8 +421,95 @@ export default function AddCourse() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Step 1: Rewards ── */}
|
||||
{/* ── Step 1: First Unit ── */}
|
||||
{currentStep === 1 && (
|
||||
<SectionCard title="First Unit" description="Every course needs at least one unit to hold its lessons.">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="unit_title">
|
||||
Title <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="unit_title" placeholder="e.g. Getting Started" {...register("unit_title")} />
|
||||
<FieldError message={errors.unit_title?.message} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="unit_description">Description</Label>
|
||||
<Textarea id="unit_description" placeholder="Optional unit description" rows={3} {...register("unit_description")} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-w-[120px]">
|
||||
<Label htmlFor="unit_order">Order</Label>
|
||||
<Input id="unit_order" type="number" min={0} {...register("unit_order")} />
|
||||
</div>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* ── Step 2: First Lesson ── */}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<SectionCard title="First Lesson" description="Add the first lesson inside that unit.">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="lesson_title">
|
||||
Title <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="lesson_title" placeholder="e.g. Welcome to the Course" {...register("lesson_title")} />
|
||||
<FieldError message={errors.lesson_title?.message} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="lesson_description">Description</Label>
|
||||
<Textarea id="lesson_description" placeholder="Optional lesson description" rows={3} {...register("lesson_description")} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-w-[120px]">
|
||||
<Label htmlFor="lesson_order">Order</Label>
|
||||
<Input id="lesson_order" type="number" min={0} {...register("lesson_order")} />
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
title="Lesson Objectives"
|
||||
description="What will learners be able to do after this lesson?"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{lessonObjectiveFields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-start gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Input
|
||||
placeholder={`Objective ${index + 1}`}
|
||||
{...register(`lesson_objectives.${index}.text`)}
|
||||
/>
|
||||
<FieldError message={errors.lesson_objectives?.[index]?.text?.message} />
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mt-0.5 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeLessonObjective(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full mt-1"
|
||||
onClick={() => appendLessonObjective({ text: "" })}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Objective
|
||||
</Button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Step 3: Rewards ── */}
|
||||
{currentStep === 3 && (
|
||||
<SectionCard
|
||||
title="Rewards"
|
||||
description="Badge and achievements awarded to learners who complete this course."
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { House } from "lucide-react";
|
||||
import { House, Layers } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||
import CoursesTable from "../../components/courses/CourseTable";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
@@ -16,7 +17,17 @@ export default function CourseList() {
|
||||
<div className="flex flex-col gap-2 my-6">
|
||||
<AppBreadcrumb items={items} />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="w-full space-y-4">
|
||||
<div className="flex items-start gap-3 rounded-lg border bg-card p-4 text-sm">
|
||||
<Layers className="h-4 w-4 mt-0.5 shrink-0 text-muted-foreground" />
|
||||
<p className="text-muted-foreground">
|
||||
Courses are organized under <span className="font-medium text-foreground">Tier Plans</span> — each course's{" "}
|
||||
<span className="font-medium text-foreground">Subscription</span> value determines which plan bundle it belongs to.{" "}
|
||||
<Link to="/admin/tiers/plans" className="text-primary hover:underline font-medium">
|
||||
Manage Tier Plans
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<CoursesTable />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user