This is the form i made using Shadcn just like it says in the docs:
/app/admin/products/_components/ProductForm.tsx
"use client";
import { addProduct } from "@/app/admin/_actions/products";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { NewProductSchema } from "@/zod/schemas";
import { formatCurrency } from "@/lib/formatters";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
export function ProductForm() {
const form = useForm<z.infer<typeof NewProductSchema>>({
resolver: zodResolver(NewProductSchema),
defaultValues: {
name: "",
description: "",
priceInCents: 200,
file: undefined,
image: undefined,
},
});
const fileRef = form.register("file", { required: true });
const fileRef2 = form.register("image", { required: true });
const [priceInCents, setPriceInCents] = useState<number>(200);
async function onSubmit(values: z.infer<typeof NewProductSchema>) {
console.log(values);
await addProduct(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
//.... more fields
<Button disabled={form.formState.isSubmitting} type="submit">
{form.formState.isSubmitting ? "Saving..." : "Save"}
</Button>
</form>
</Form>
);
}
I tried to trim it down as much as possible. As you can see, this compoentn is a client component becasue of the "use client" at the top. I wrote a separate function that i want to be run on the server since it requires the prisma client and the fs which can only be run on the server:
/app/admin/_actions/products:
"use server";
import fs from "fs/promises";
import { redirect } from "next/navigation";
import { z } from "zod";
import { NewProductSchema } from "@/zod/schemas";
import { prisma } from "@/lib/prismaClient";
export const addProduct = async (values: z.infer<typeof NewProductSchema>) => {
const result = NewProductSchema.safeParse(values);
console.log(result);
if (result.success === false) {
return result.error.formErrors.fieldErrors;
}
const data = result.data;
fs.mkdir("products", { recursive: true });
const filePath = `products/${crypto.randomUUID()}-${data.file.name}`;
await fs.writeFile(filePath, Buffer.from(await data.file.arrayBuffer()));
fs.mkdir("public/products", { recursive: true });
const imagePath = `/products/${crypto.randomUUID()}-${data.image.name}`;
await fs.writeFile(
`public${imagePath}`,
Buffer.from(await data.image.arrayBuffer()),
);
await prisma.product.create({
data: {
name: data.name,
description: data.description,
priceInCents: data.priceInCents,
filePath,
imagePath,
},
});
redirect("/admin/products");
};
If i remove "use server" from the top, i get errors like fs cannot imported etc meaning that the function cannot be run on the client as i said. My problem is that when i click on the submit button on the form, the onSubmit function does run, it logs the values, but it never runs the addProduct function. Does anyone know why this is happening? I admit i maybe am not that good at programming and i probably wrote something stupid but nextjs is pissing me off.
Edit: Here is the zod schema if it makes a difference:
export const NewProductSchema = z.object({
name: z.string().min(2).max(50).trim(),
priceInCents: z.coerce
.number()
.min(200, { message: "The minimum price for a product is 2 dolars" })
.positive({ message: "The price must be a positive number" }),
description: z.string().min(8, {
message: "The description must be at least 8 characters long",
}),
file: z
.any()
.refine((file) => file?.length == 1, "File is required.")
.refine(
(file) => file[0]?.type.startsWith("video/"),
"Must be a png, jpeg or jpg.",
)
.refine((file) => file[0]?.size <= 5000000, `Max file size is 5MB.`),
image: z
.any()
.refine((file) => file?.length == 1, "File is required.")
.refine(
(file) =>
file[0]?.type === "image/png" ||
file[0]?.type === "image/jpeg" ||
file[0]?.type === "image/jpg",
"Must be a png, jpeg or jpg.",
)
.refine((file) => file[0]?.size <= 5000000, `Max file size is 5MB.`),
});
Here is the link to the github repo if you want to recreate it. go to the admin/products/new route.
The issue is with the
onSubmitfunction itself.In your
onSubmitfunction, you're logging the values and then callingawait addProduct(values). However, the addProduct function is a server-side function, and you're trying to call it directly from the client-side component.To fix this, you need to move the call to
addProductto a separate server-side action, which can be done using the Next.jsactionfunction.Create a new file inside the
app/admin/productsdirectory, let's call itaddProduct.tsx,Above code creates a new server-side action that can be triggered by a POST request. It validates the form data using the
NewProductSchema, and if the data is valid, it calls theaddProductfunction from the server-side action file.Then, inside your
ProductFormcomponent, update theonSubmitfunction to use the new server-side action: