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`),
|
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`),
|
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`),
|
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),
|
onArchive: (row) => setArchiveTarget(row),
|
||||||
}), [courseId, unitId]);
|
}), [courseId, unitId]);
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export default function UnitsTable({ courseId }) {
|
|||||||
onQuiz: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/quiz/edit`),
|
onQuiz: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/quiz/edit`),
|
||||||
onViewLessons: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/lessons`),
|
onViewLessons: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/lessons`),
|
||||||
onView: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/view`),
|
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),
|
onArchive: (row) => setArchiveTarget(row),
|
||||||
}), [courseId]);
|
}), [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 [
|
return [
|
||||||
{
|
{
|
||||||
key: "view",
|
key: "view",
|
||||||
@@ -8,12 +8,6 @@ export function buildRowActions({ onCreatePage, onViewPage, onView, onEdit, onAr
|
|||||||
icon: <Eye className="h-3.5 w-3.5" />,
|
icon: <Eye className="h-3.5 w-3.5" />,
|
||||||
onClick: (row) => onView(row),
|
onClick: (row) => onView(row),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "edit",
|
|
||||||
label: "Edit Lesson",
|
|
||||||
icon: <Pencil className="h-3.5 w-3.5" />,
|
|
||||||
onClick: (row) => onEdit(row),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "edit",
|
key: "edit",
|
||||||
label: "View Content",
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
key: "view",
|
key: "view",
|
||||||
@@ -8,12 +8,6 @@ export function buildRowActions({ onViewLessons, onView, onEdit, onArchive, onQu
|
|||||||
icon: <Eye className="h-3.5 w-3.5" />,
|
icon: <Eye className="h-3.5 w-3.5" />,
|
||||||
onClick: (row) => onView(row),
|
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",
|
key: "view_units",
|
||||||
label: "View Lessons",
|
label: "View Lessons",
|
||||||
|
|||||||
@@ -44,13 +44,24 @@ const schema = z.object({
|
|||||||
subscription: z.string().min(1, "Subscription is required.").default("free"),
|
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([]),
|
objectives: z.array(z.object({ text: z.string().min(1, "Objective cannot be empty.") })).default([]),
|
||||||
achievement_keys: z.array(z.string()).max(1).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 ─────────────────────────────────────────────────────────────
|
// ─── Steps config ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const STEPS = [
|
const STEPS = [
|
||||||
{ label: "Basic Info", description: "Title, level & objectives" },
|
{ label: "Basic Info", description: "Title, level & objectives" },
|
||||||
{ label: "Rewards", description: "Badge & achievements" },
|
{ label: "First Unit", description: "The course's first unit" },
|
||||||
|
{ label: "First Lesson", description: "A lesson inside that unit" },
|
||||||
|
{ label: "Rewards", description: "Badge & achievements" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
@@ -128,7 +139,7 @@ function StepIndicator({ steps, current, onStepClick }) {
|
|||||||
|
|
||||||
export default function AddCourse() {
|
export default function AddCourse() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createCourse, loading } = useCourses();
|
const { createCourse, createUnit, createLesson, loading } = useCourses();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
@@ -163,12 +174,22 @@ export default function AddCourse() {
|
|||||||
subscription: "free",
|
subscription: "free",
|
||||||
objectives: [],
|
objectives: [],
|
||||||
achievement_keys: [],
|
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 } =
|
const { fields: objectiveFields, append: appendObjective, remove: removeObjective } =
|
||||||
useFieldArray({ control, name: "objectives" });
|
useFieldArray({ control, name: "objectives" });
|
||||||
|
|
||||||
|
const { fields: lessonObjectiveFields, append: appendLessonObjective, remove: removeLessonObjective } =
|
||||||
|
useFieldArray({ control, name: "lesson_objectives" });
|
||||||
|
|
||||||
const currentAchKeys = useWatch({ control, name: "achievement_keys" });
|
const currentAchKeys = useWatch({ control, name: "achievement_keys" });
|
||||||
const watchedTitle = useWatch({ control, name: "title" });
|
const watchedTitle = useWatch({ control, name: "title" });
|
||||||
const watchedLevel = useWatch({ control, name: "level" });
|
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) => {
|
const handleNext = async (e) => {
|
||||||
// The Next button occupies the same DOM position as the eventual
|
// The Next button occupies the same DOM position as the eventual
|
||||||
// type="submit" Create Course button. Advancing the step re-renders
|
// type="submit" Create Course button. Advancing the step re-renders
|
||||||
// that node's type attribute in place *before* the browser evaluates
|
// that node's type attribute in place *before* the browser evaluates
|
||||||
// this click's default action, which would otherwise submit the form.
|
// this click's default action, which would otherwise submit the form.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (currentStep === 0) {
|
const fields = STEP_FIELDS[currentStep];
|
||||||
const valid = await trigger(["title", "subscription", "objectives"]);
|
if (fields?.length) {
|
||||||
|
const valid = await trigger(fields);
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
}
|
}
|
||||||
setCurrentStep((s) => s + 1);
|
setCurrentStep((s) => s + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (values) => {
|
const onSubmit = async (values) => {
|
||||||
const payload = {
|
const coursePayload = {
|
||||||
...values,
|
title: values.title,
|
||||||
objectives: values.objectives.map((o) => o.text),
|
description: values.description,
|
||||||
level: values.level || null,
|
|
||||||
course_code: values.course_code || null,
|
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_color: badgeColor,
|
||||||
badge_asset_id: badgeAssetId ?? null,
|
badge_asset_id: badgeAssetId ?? null,
|
||||||
badge_image_url: badgeImageUrl ?? null,
|
badge_image_url: badgeImageUrl ?? null,
|
||||||
createdBy: user?.user_id ?? null,
|
createdBy: user?.user_id ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await createCourse(payload);
|
const courseResult = await createCourse(coursePayload);
|
||||||
if (!result) return;
|
const newCourse = courseResult?.data?.data ?? null;
|
||||||
navigate("/admin/courses");
|
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 (
|
return (
|
||||||
@@ -366,8 +421,95 @@ export default function AddCourse() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Step 1: Rewards ── */}
|
{/* ── Step 1: First Unit ── */}
|
||||||
{currentStep === 1 && (
|
{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
|
<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."
|
||||||
|
|||||||
@@ -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 AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||||
import CoursesTable from "../../components/courses/CourseTable";
|
import CoursesTable from "../../components/courses/CourseTable";
|
||||||
import { PageMeta } from "@/contexts/MetadataContext";
|
import { PageMeta } from "@/contexts/MetadataContext";
|
||||||
@@ -16,7 +17,17 @@ export default function CourseList() {
|
|||||||
<div className="flex flex-col gap-2 my-6">
|
<div className="flex flex-col gap-2 my-6">
|
||||||
<AppBreadcrumb items={items} />
|
<AppBreadcrumb items={items} />
|
||||||
</div>
|
</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 />
|
<CoursesTable />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user