article

Monday, October 7, 2024

Nextjs 14 CRUD Create,Read,Update and Delete with upload and delete image Server-Side | Postgresql Prisma

Nextjs 14 CRUD Create,Read,Update and Delete with upload and delete image Server-Side | Postgresql Prisma

Next.js

Install requirements
Zod
Zod is a TypeScript-first schema declaration and validation library.
https://www.npmjs.com/package/zod

Prisma is an open-source next-generation ORM. It consists of the following parts: Prisma Client: Auto-generated and type-safe query builder
https://www.prisma.io

Install prisma/client
npm install @prisma/client
https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/generating-prisma-client

Generate Prisma Client with the following command:
npx prisma generate

Install prisma
npm install prisma --save-dev
https://www.prisma.io/docs/getting-started/quickstart

set up Prisma with the init command of the Prisma CLI:
npx prisma init --datasource-provider sqlite
to npx prisma init --datasource-provider postgres

Model data in the Prisma schema
prisma/schema.prisma

model Upload {
id String @id @default(cuid())
title String
image String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

Run a migration to create your database tables with Prisma Migrate
npx prisma migrate dev --name init

.env
DATABASE_URL="postgresql://postgres:admin@localhost:5432/postgresDB?schema=public"

https://www.prisma.io/docs/orm/prisma-client/queries/crud

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

app\page.tsx
//app\page.tsx
import Image from "next/image";
import Link from "next/link";
import { DeleteButton, EditButton } from "@/components/button";
import { getImages } from "@/lib/data";

export default async function Home() {
  const getimages = await getImages();

  return (
    <div className="max-w-screen-lg mx-auto py-14">
      <h1 className="text-4xl font-bold">Nextjs 14 CRUD Create,Read,Update and Delete with upload and delete image Server-Side | Postgresql Prisma</h1>
      <div className="flex items-end justify-between m-12">
        <h1 className="text-4xl font-bold">Images</h1>
        <Link
          href="/create"
          className="py-3 px-6 bg-green-700 hover:bg-green-800 text-white"
        >
          Upload New Image
        </Link>
      </div>
      <div className="grid md:grid-cols-3 gap-5 mt-10">
        {getimages.map((item) => (
          <div key={item.id} className="max-w-sm border border-gray-200 rounded-md shadow">
            <div className="relative aspect-video">
              <Image
                src={`http://localhost:3000/assets/${item.image}`}
                alt={item.title}
                fill
                priority
                sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
                className="rounded-t-md object-cover"
              />
            </div>
            <div className="p-5">
              <h1 className="text-2xl font-bold text-gray-900 truncate">
                {item.title}
              </h1>
            </div>
            <div className="flex items-center justify-between">
              <EditButton id={item.id} />
              <DeleteButton id={item.id}/>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
components\button.tsx
//components\button.tsx
"use client";

import { useFormStatus } from "react-dom";
import { clsx } from "clsx";
import { deleteData } from "@/lib/actions";
import Link from "next/link";

export const SubmitButton = ({ label }: { label: string }) => {
  const { pending } = useFormStatus();
  return (
    <button
      className={clsx(
        "bg-blue-700 text-white w-full font-medium py-2.5 px-6 text-base rounded-sm hover:bg-blue-600",
        {
          "opacity-50 cursor-progress": pending,
        }
      )}
      type="submit"
      disabled={pending}
    >
      {label === "upload" ? (
        <>{pending ? "Uploading..." : "Upload"}</>
      ) : (
        <>{pending ? "Updating..." : "Update"}</>
      )}
    </button>
  );
};

export const EditButton = ({ id }: { id: string }) => {
  return (
    <Link
      href={`edit/${id}`}
      className="py-3 text-sm text-white bg-blue-700 rounded-bl-md w-full hover:bg-blue-800 text-center"
    >
      Edit
    </Link>
  );
};

export const DeleteButton = ({ id }: { id: string }) => {
  const deleteDataWithId = deleteData.bind(null, id);
  return (
    <form
      action={deleteDataWithId}
      className="py-3 text-sm text-white bg-red-700 rounded-br-md w-full hover:bg-red-800 text-center"
    >
      <DeleteBtn />
    </form>
  );
};

const DeleteBtn = () => {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Deleting..." : "Delete"}
    </button>
  );
};
lib\data.ts
//lib\data.ts
import { prisma } from "@/lib/prisma";

export const getImages = async () => {
  try {
    const result = await prisma.upload.findMany({
      orderBy: { createdAt: "desc" },
    });
    return result;
  } catch (error) {
    throw new Error("Failed to fetch data");
  }
};

export const getDataById = async (id: string) => {
  try {
    const result = await prisma.upload.findUnique({
      where: { id },
    });
    return result;
  } catch (error) {
    throw new Error("Failed to fetch data");
  }
};
lib\data.ts
//lib\prisma.ts
import { PrismaClient } from "@prisma/client";

declare global {
  var prisma: PrismaClient | undefined;
}

export const prisma = globalThis.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
lib\actions.ts
//lib\actions.ts
"use server";

import { z } from "zod"; //https://www.npmjs.com/package/zod
import { prisma } from "@/lib/prisma";
import { writeFile } from "fs/promises";
import path from "path";
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import { getDataById } from "@/lib/data";
import fs from 'fs';

const UploadSchema = z.object({
  title: z.string().min(1),
  image: z
    .instanceof(File)
    .refine((file) => file.size > 0, { message: "Image is required" })
    .refine((file) => file.size === 0 || file.type.startsWith("image/"), {
      message: "Only images are allowed",
    })
    .refine((file) => file.size < 4000000, {
      message: "Image must less than 4MB",
    }),
});

const EditSchema = z.object({
  title: z.string().min(1),
  image: z
    .instanceof(File)
    .refine((file) => file.size === 0 || file.type.startsWith("image/"), {
      message: "Only images are allowed",
    })
    .refine((file) => file.size < 4000000, {
      message: "Image must less than 4MB",
    })
    .optional(),
});

export const CreateData = async (prevState: unknown, formData: FormData) => {
    const validatedFields = UploadSchema.safeParse(
      Object.fromEntries(formData.entries())
    );

    if (!validatedFields.success) {
      return {
        error: validatedFields.error.flatten().fieldErrors,
      };
    }
    const file = formData.get("image");
    const { title } = validatedFields.data;

    try {
      const buffer = Buffer.from(await file.arrayBuffer());
      const filename =  file.name.replaceAll(" ", "_");
      console.log(filename);

      await writeFile(
        path.join(process.cwd(), "public/assets/" + filename),
        buffer
      );

      await prisma.upload.create({
          data: {
            title,
            image: filename,
          },
      });

      //return { message: "Success" };
    } catch (error) {
      console.log("Error occured ", error);
      return { message: "Failed" };
    }

  revalidatePath("/");
  redirect("/");
};

// Update
export const updateData = async (
  id: string,
  prevState: unknown,
  formData: FormData
) => {
  const validatedFields = EditSchema.safeParse(
    Object.fromEntries(formData.entries())
  );

  if (!validatedFields.success) {
    return {
      error: validatedFields.error.flatten().fieldErrors,
    };
  }

  const data = await getDataById(id);
  if (!data) return { message: "No Data Found" };

  const file = formData.get("image");
  const { title, image } = validatedFields.data;
  let imageFilename;
  
  if (!image || image.size <= 0) {
    imageFilename = data.image;
  } else {
    console.log(data.image);
    fs.unlink("public/assets/" + data.image, (err) => {
      if (err) {
        console.error('An error occurred:', err);
      } else {
        console.log('File deleted successfully!');
      }
    });

    const buffer = Buffer.from(await file.arrayBuffer());
    const filename =  file.name.replaceAll(" ", "_");
    console.log(filename);

    await writeFile(
      path.join(process.cwd(), "public/assets/" + filename),
      buffer
    );
    
    imageFilename = filename;
  }

  try {
    await prisma.upload.update({
      data: {
        title,
        image: imageFilename,
      },
      where: { id },
    });
  } catch (error) {
    return { message: "Failed to update data" };
  }

  revalidatePath("/");
  redirect("/");
};

export const deleteData = async (id: string) => {
  const data = await getDataById(id);
  if (!data) return { message: "No data found" };

  console.log(data.image);
  try { 
    fs.unlink("public/assets/" + data.image, (err) => {
      if (err) {
        console.error('An error occurred:', err);
      } else {
        console.log('File deleted successfully!');
      }
    });

    await prisma.upload.delete({
      where: { id },
    });
  } catch (error) {
    return { message: "Failed to delete data" };
  }

  revalidatePath("/");
};
app\create\page.tsx
//app\create\page.tsx
import UploadForm from "@/components/upload-form";

const UploadPage = () => {
  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-100">
      <div className="bg-white rounded-sm shadow p-8">
        <h1 className="text-2xl font-bold mb-5">Upload Image</h1>
        <UploadForm />
      </div>
    </div>
  );
};

export default UploadPage;
components\upload-form.tsx
//components\upload-form.tsx
"use client";

import React from "react";
import { CreateData } from "@/lib/actions";
import { useFormState } from "react-dom";
import { SubmitButton } from "@/components/button";

const UploadForm = () => {
  const [state, formAction] = useFormState(CreateData, null);

  return (
    <form action={formAction}>   
      {/* Alert */}
      {state?.message ? (
        <div
          className="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50"
          role="alert"
        >
          <div className="font-medium">{state?.message}</div>
        </div>
      ) : null}
      <div className="mb-4 pt-2">
        <input
          type="text"
          name="title"
          className="py-2 px-4 rounded-sm border border-gray-400 w-full"
          placeholder="Title..."
        />
        <div aria-live="polite" aria-atomic="true">
          <p className="text-sm text-red-500 mt-2">{state?.error?.title}</p>
        </div>
      </div>
      <div className="mb-4 pt-2">
        <input
          type="file"
          name="image"
          className="file:py-2 file:px-4 file:mr-4 file:rounded-sm file:border-0 file:bg-gray-200 hover:file:bg-gray-300 file:cursor-pointer border border-gray-400 w-full"
        />
        <div aria-live="polite" aria-atomic="true">
          <p className="text-sm text-red-500 mt-2">{state?.error?.image}</p>
        </div>
      </div>
      <div className="mb-4 pt-4">
        <SubmitButton label="upload" />
      </div>
    </form>
  );
};

export default UploadForm;
app\edit\[id]\page.tsx
//app\edit\[id]\page.tsx
import EditForm from "@/components/edit-form";
import { getDataById } from "@/lib/data";
import { notFound } from "next/navigation";

const EditPage = async ({ params }: { params: { id: string } }) => {
  const data = await getDataById(params.id);
  if (!data) return notFound();

  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-100">
      <div className="bg-white rounded-sm shadow p-8">
        <h1 className="text-2xl font-bold mb-5">Update Image</h1>
        <EditForm data={data} />
      </div>
    </div>
  );
};

export default EditPage;
components\edit-form.tsx
//components\edit-form.tsx
"use client";
import React from "react";
import { updateData } from "@/lib/actions";
import { useFormState } from "react-dom";
import { SubmitButton } from "@/components/button";
import type { Upload } from "@prisma/client";
import Image from "next/image";

const EditForm = ({ data }: { data: Upload }) => {
  const [state, formAction] = useFormState(
    updateData.bind(null, data.id),
    null
  );

  return (
    <form action={formAction}>
      {state?.message ? (
        <div
          className="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50"
          role="alert"
        >
          <div className="font-medium">{state?.message}</div>
        </div>
      ) : null}

      <div className="mb-4 pt-2">
        <input
          type="text"
          name="title"
          className="py-2 px-4 rounded-sm border border-gray-400 w-full"
          placeholder="Title..."
          defaultValue={data.title}
        />
        <div aria-live="polite" aria-atomic="true">
          <p className="text-sm text-red-500 mt-2">{state?.error?.title}</p>
        </div>
              <Image
                src={`http://localhost:3000/assets/${data.image}`}
                alt={data.title}
                width={200}
                height={200}
              />
      </div>
      <div className="mb-4 pt-2">
        <input
          type="file"
          name="image"
          className="file:py-2 file:px-4 file:mr-4 file:rounded-sm file:border-0 file:bg-gray-200 hover:file:bg-gray-300 file:cursor-pointer border border-gray-400 w-full"
        />
        <div aria-live="polite" aria-atomic="true">
          <p className="text-sm text-red-500 mt-2">{state?.error?.image}</p>
        </div>
      </div>
      <div className="mb-4 pt-4">
        <SubmitButton label="update" />
      </div>
    </form>
  );
};

export default EditForm;
prisma\schema.prisma
//prisma\schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider  = "postgresql"
  url      = "postgresql://postgres:admin@localhost:5432/postgresDB?schema=public" 
}

model Upload {
  id        String   @id @default(cuid())
  title     String
  image     String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
.env DATABASE_URL="postgresql://postgres:admin@localhost:5432/postgresDB?schema=public"

run C:\nextjs>npm run dev

Related Post