diff --git a/src/actions/admin/update-course.ts b/src/actions/admin/update-course.ts new file mode 100644 index 0000000..17ad541 --- /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 + .inputSchema(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..63bd7ce --- /dev/null +++ b/src/app/admin/courses/[courseId]/edit/EditCourseForm.tsx @@ -0,0 +1,119 @@ +"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"; +import { useAction } from "next-safe-action/hooks"; + +// 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(""); + + const {executeAsync: updateCourse} = useAction(updateCourseAction, { + onSuccess: () => { + router.push("/admin/courses"); + }, + onError: ({error}) => { + console.log(error); + setServerError(error.serverError ?? "Something went wrong"); + }, + }); + + // 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 () => { + await updateCourse({ + id: course.id, // pass the course id + ...values, // pass updated form values + }); + }); + }; + + return ( +
+ {/* Server-side error display */} + {serverError && ( +

{serverError}

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

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

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