NextJS 14 Image upload with Server actions.
DeepM
Member Posts: 1 ✭
Hi there folks.I've had some struggle with getting my code working in NextJS with Server actions and TypeScript. Now that I solved it. I would like to share my code in case somebody else stumbled upon the same problem.
Lets first start with the page.tsx. I use Shadcn for my forms and custom hooks for passing some props to avoid prop drilling. I hope still will help somebody :)
Good luck! And never give up!
import { usePhoneForm } from "@/lib/hooks"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; import PhoneFormBtn from "./phone-form-btn"; import { createUpload, addPhoneToDatabase } from "@/actions/actions"; import { FormEvent } from "react"; type AddPhoneProps = { closeForm: () => void; }; export function AddPhoneForm({ closeForm }: AddPhoneProps) { const { register, errors } = usePhoneForm(); // start submit function async function formSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); // Get the file from the form const form = event.currentTarget; const fileInput = form.querySelector( "input[type=file]" ) as HTMLInputElement; if (!fileInput || !fileInput.files || fileInput.files.length === 0) return; const file = fileInput.files[0]; // Create a FormData object and append the file to it const formData = new FormData(form); formData.append("file", file); try { // Upload the file to Cloudinary const uploadResult = await createUpload(formData); if (!uploadResult) return; // After successful upload, add the phone to the database with the returned URL const newPhoneData = new FormData(); newPhoneData.append("name", formData.get("name") as string); newPhoneData.append("brand", formData.get("brand") as string); newPhoneData.append("price", formData.get("price") as string); newPhoneData.append("image", uploadResult as string); await addPhoneToDatabase(newPhoneData); closeForm(); } catch (error) { console.error("Form submission failed:", error); } } return ( <form onSubmit={formSubmit} className="flex flex-col space-y-4"> <div className="space-y-3"> <div className="space-y-1"> <Label htmlFor="name">Naam</Label> <Input id="name" {...register("name")} /> {errors.name && <p className="text-red-500">{errors.name.message}</p>} </div> <div className="space-y-1"> <Label htmlFor="brand">Merk</Label> <Input id="brand" {...register("brand")} /> {errors.brand && ( <p className="text-red-500">{errors.brand.message}</p> )} </div> <div className="space-y-1"> <Label htmlFor="price">Prijs</Label> <Input id="price" {...register("price", { valueAsNumber: true })} type="number" step="0.01" min="20" /> {errors.price && ( <p className="text-red-500">{errors.price.message}</p> )} </div> <div className="space-y-1"> <Label htmlFor="image">Afbeeldingen</Label> <Input id="image" {...register("image")} type="file" name="image" /> {errors.image && ( <p className="text-red-500">{String(errors.image.message)}</p> )} </div> </div> <PhoneFormBtn actionType="add" /> </form> ); }
My actions.ts:
"use server"; import { getPhoneId } from "@/lib/server-utils"; import prisma from "@/lib/db"; import { revalidatePath } from "next/cache"; import { v2 as cloudinary } from "cloudinary"; import { NextResponse } from "next/server"; cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, secure: true, }); // Get signature export async function getSignature() { const timestamp = Math.round(Date.now() / 1000); const params = { timestamp, folder: "phones" }; const api_secret = process.env.CLOUDINARY_API_SECRET; const signature = cloudinary.utils.api_sign_request( params, api_secret as string ); return { timestamp, signature, api_key: cloudinary.config().api_key }; } // Create upload export async function createUpload(req: FormData) { const data = req; const image = data.get("image"); const fileBuffer = await (image instanceof Blob ? image.arrayBuffer() : null); const mime = (image as File)?.type; const encoding = "base64"; const base64Data = fileBuffer ? Buffer.from(fileBuffer).toString("base64") : ""; const fileUri = "data:" + mime + ";" + encoding + "," + base64Data; try { const uploadToCloudinary = () => { return new Promise((resolve, reject) => { cloudinary.uploader .upload(fileUri, { invalidate: true, }) .then((result) => { // Create a plain object with only the properties you need const simplifiedResult = { public_id: result.public_id, version: result.version, signature: result.signature, width: result.width, height: result.height, format: result.format, resource_type: result.resource_type, created_at: result.created_at, bytes: result.bytes, type: result.type, url: result.url, secure_url: result.secure_url, }; console.log(simplifiedResult); resolve(simplifiedResult); }) .catch((error) => { console.log(error); reject(error); }); }); }; const result: { secure_url: string } = (await uploadToCloudinary()) as { secure_url: string; }; if (!result) { throw new Error("Upload failed"); } return result.secure_url; } catch (error) { console.log("server err", error); return NextResponse.json({ err: "Internal Server Error" }, { status: 500 }); } } export async function addPhoneToDatabase(newPhoneData: FormData) { try { await prisma.phone.create({ data: { name: newPhoneData.get("name") as string, brand: newPhoneData.get("brand") as string, price: Number(newPhoneData.get("price")), image: newPhoneData.get("image") as string, }, }); revalidatePath("/app/dashboard", "layout"); } catch (error) { return { message: "Error updating phone", }; } }
Tagged:
7
Comments
-
Hi @DeepM. Thanks for sharing this! Hopefully it helps someone out with a similar problem in the future! :)
1