add: more things

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-04 05:55:27 +08:00
parent b54f6cdde0
commit 57fa25b73a
6 changed files with 172 additions and 33 deletions
@@ -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",
+155 -13
View File
@@ -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."
+13 -2
View File
@@ -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>