NextJS 14 Image upload with Server actions.

DeepM
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",
    };
  }
}


Comments

  • DannyFromCloudinary
    DannyFromCloudinary Member, Cloudinary Staff Posts: 150

    Hi @DeepM. Thanks for sharing this! Hopefully it helps someone out with a similar problem in the future! :)