From 08a04dce8e360dd52ee4971537d13a23843df67d Mon Sep 17 00:00:00 2001 From: brandon Date: Sat, 29 Nov 2025 21:36:25 -0600 Subject: [PATCH 1/2] Made admin edit course feature --- src/actions/admin/update-course.ts | 62 ++++++++++ .../[courseId]/edit/EditCourseForm.tsx | 117 ++++++++++++++++++ .../admin/courses/[courseId]/edit/page.tsx | 44 ++++++- src/components/admin/CourseTable.tsx | 11 +- 4 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 src/actions/admin/update-course.ts create mode 100644 src/app/admin/courses/[courseId]/edit/EditCourseForm.tsx diff --git a/src/actions/admin/update-course.ts b/src/actions/admin/update-course.ts new file mode 100644 index 0000000..0b76db9 --- /dev/null +++ b/src/actions/admin/update-course.ts @@ -0,0 +1,62 @@ +"use server"; + +import { adminClient } from "@/lib/safe-action"; +import { db } from "@/db"; +import { courses, coursesTags, tags } from "@/db/schema"; +import { eq, inArray } from "drizzle-orm"; +import { z } from "zod"; + +// validation schema for updating a course +const updateCourseSchema = z.object({ + id: z.number(), + title: z.string().min(1), + description: z.string().optional(), + difficulty: z.enum(["beginner", "intermediate", "advanced"]), + tags: z.array(z.string()).optional(), // safe to ignore for now +}); + +export const updateCourseAction = adminClient + .schema(updateCourseSchema) + .action(async ({ parsedInput }) => { + const { id, title, description, difficulty, tags: tagNames } = parsedInput; + + // make sure the course exists + const existing = await db + .select() + .from(courses) + .where(eq(courses.id, id)); + + if (existing.length === 0) { + return { success: false, serverError: "Course not found." }; + } + + // update the course fields + await db + .update(courses) + .set({ + title, + description, + difficulty, + updatedAt: new Date(), + }) + .where(eq(courses.id, id)); + + // tag updating logic + if (tagNames && tagNames.length > 0) { + await db.delete(coursesTags).where(eq(coursesTags.courseId, id)); + + const dbTags = await db + .select() + .from(tags) + .where(inArray(tags.tagName, tagNames)); + + for (const tag of dbTags) { + await db.insert(coursesTags).values({ + courseId: id, + tagId: tag.id, + }); + } + } + + return { success: true, message: "Course updated successfully." }; + }); diff --git a/src/app/admin/courses/[courseId]/edit/EditCourseForm.tsx b/src/app/admin/courses/[courseId]/edit/EditCourseForm.tsx new file mode 100644 index 0000000..ef7e072 --- /dev/null +++ b/src/app/admin/courses/[courseId]/edit/EditCourseForm.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { updateCourseAction } from "@/actions/admin/update-course"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { useRouter } from "next/navigation"; + +// zod form validation schema +const formSchema = z.object({ + title: z.string().min(1, "Title is required"), + description: z.string().optional(), + difficulty: z.enum(["beginner", "intermediate", "advanced"]), +}); + +export default function EditCourseForm({ course }: { course: any }) { + const router = useRouter(); + + // `useTransition` lets us show a pending state during async work + const [isPending, startTransition] = useTransition(); + + // holds any server side validation errors from the action + const [serverError, setServerError] = useState(""); + + // react-hook-form setup with Zod resolver and default values + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + title: course.title, + description: course.description ?? "", + difficulty: course.difficulty ?? "beginner", + }, + }); + + // handle form submission + const onSubmit = (values: z.infer) => { + setServerError(""); + + // run async update inside startTransition for smoother UX + startTransition(async () => { + const result = await updateCourseAction({ + id: course.id, // pass the course id + ...values, // pass updated form values + }); + + // server error handling + if (result?.serverError) { + setServerError(result.serverError); + return; + } + + // on success, redirect back to admin course list + router.push("/admin/courses"); + }); + }; + + return ( +
+ {/* Server-side error display */} + {serverError && ( +

{serverError}

+ )} + + {/* course title Input*/} +
+ + + {form.formState.errors.title && ( +

+ {form.formState.errors.title.message} +

+ )} +
+ + {/* course description input */} +
+ +