article

Monday, February 26, 2024

Nextjs14 CRUD Postgresql Prisma | TailwindCSS DaisyUI

Nextjs14 CRUD Postgresql Prisma | TailwindCSS DaisyUI

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 Employee {
id String @id @default(cuid())
name String
email String
phone 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/employee/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//app/employee/page.tsx
import Link from "next/link";
import { getEmployeelist } from "@/lib/action";
import { formatDate } from "@/lib/utils";
import { DeleteButton } from "@/components/delete";
 
//const Employee = async () => {
const Employee = async ({
  query
}: {
  query: string;
}) => {
    const employees = await getEmployeelist(query);
    return (
    <div className="w-screen py-20 flex justify-center flex-col items-center">
      <div className="flex items-center justify-between gap-1 mb-5">
        <h1 className="text-4xl font-bold">Next.js 14 CRUD Create, Read, Update and Delete <br/>Prisma PostgreSQL | TailwindCSS DaisyUI</h1>
      </div>
    <div className="overflow-x-auto">
        <div className="mb-2 w-full text-right">
          <Link
            href="/employee/create"
            className="btn btn-primary">
            Create
          </Link>
        </div>
        <table className="table table-zebra">
        <thead className="text-sm text-gray-700 uppercase bg-gray-50">
            <tr>
            <th className="py-3 px-6">#</th>
            <th className="py-3 px-6">Name</th>
            <th className="py-3 px-6">Email</th>
            <th className="py-3 px-6">Phone Number</th>
            <th className="py-3 px-6">Created At</th>
            <th className="py-3 px-6 text-center">Actions</th>
            </tr>
        </thead>
        <tbody>
            {employees.map((rs, index) => (
            <tr key={rs.id} className="bg-white border-b">
                <td className="py-3 px-6">{index + 1}</td>
                <td className="py-3 px-6">{rs.name}</td>
                <td className="py-3 px-6">{rs.email}</td>
                <td className="py-3 px-6">{rs.phone}</td>
                <td className="py-3 px-6">
                {formatDate(rs.createdAt.toString())}
                </td>
                <td className="flex justify-center gap-1 py-3">
                    <Link
                        href={`/employee/edit/${rs.id}`}
                        className="btn btn-info"
                        >
                        Edit
                    </Link>
                     <DeleteButton id={rs.id} />
                </td>
            </tr>
            ))}
        </tbody>
        </table>
    </div>     
    </div>
  );
};
 
export default Employee;
app/employee/create/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//app/employee/create/page.tsx
"use client";
 
import { useFormState } from "react-dom";
import { saveEmployee } from "@/lib/action";
 
const CreateEmployeePage = () => {
    const [state, formAction] = useFormState(saveEmployee, null);
    return (
    <div className="max-w-md mx-auto mt-5">
        <h1 className="text-2xl text-center mb-2">Add New Employee</h1>
        <div>
        <form action={formAction}>
        <div className="mb-5">
          <label htmlFor="name" className="block text-sm font-medium text-gray-900">
            Full Name
          </label>
          <input
            type="text"
            name="name"
            id="name"
            className="input input-bordered input-primary w-full max-w-xs"
            placeholder="Full Name..."
          />
          <div id="name-error" aria-live="polite" aria-atomic="true">
            <p className="mt-2 text-sm text-red-500">{state?.Error?.name}</p>
          </div>
        </div>
        <div className="mb-5">
          <label htmlFor="email" className="block text-sm font-medium text-gray-900">
            Email
          </label>
          <input
            type="email"
            name="email"
            id="email"
            className="input input-bordered input-primary w-full max-w-xs"
            placeholder="Email..."
          />
          <div id="email-error" aria-live="polite" aria-atomic="true">
            <p className="mt-2 text-sm text-red-500">{state?.Error?.email}</p>
          </div>
        </div>
        <div className="mb-5">
          <label
            htmlFor="phone" className="block text-sm font-medium text-gray-900">
            Phone Number
          </label>
          <input
            type="text"
            name="phone"
            id="phone"
            className="input input-bordered input-primary w-full max-w-xs"
            placeholder="Phone Number..."
          />
          <div id="phone-error" aria-live="polite" aria-atomic="true">
            <p className="mt-2 text-sm text-red-500">{state?.Error?.phone}</p>
          </div>
        </div>
         
        <button className="btn btn-primary">Save</button>
      </form>
    </div>
    </div>
  );
};
 
export default CreateEmployeePage;
app/employee/edit/[id]/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//app/employee/edit/[id]/page.tsx
import UpdateForm from "@/components/editform";
import { getEmployeeById } from "@/lib/action";
import { notFound } from "next/navigation";
 
const UpdateEmployeePage = async ({ params }: { params: { id: string } }) => {
    const id = params.id;
    const employee = await getEmployeeById(id);
    //console.log(employee);
 
    if (!employee) {
        notFound();
    }
 
    return (
    <div className="max-w-md mx-auto mt-5">
      <h1 className="text-2xl text-center mb-2">Update Employee</h1>
        <UpdateForm employee={employee} />
    </div>
  );
};
 
export default UpdateEmployeePage;
components/editform.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//components/editform.tsx
"use client";
 
import { updateEmployee } from "@/lib/action";
import { useFormState } from "react-dom";
import type { Employee } from "@prisma/client";
 
const UpdateForm = ({ employee }: { employee: Employee }) => {
    const UpdateEmployeeWithId = updateEmployee.bind(null, employee.id);
    const [state, formAction] = useFormState(UpdateEmployeeWithId, null);
 
    return (
    <div>
      <form action={formAction}>
        <div className="mb-5">
          <label htmlFor="name" className="block text-sm font-medium text-gray-900">
            Full Name
          </label>
          <input
            type="text"
            name="name"
            id="name"
            className="input input-bordered input-primary w-full max-w-xs"
            placeholder="Full Name..."
            defaultValue={employee.name}
          />
          <div id="name-error" aria-live="polite" aria-atomic="true">
            <p className="mt-2 text-sm text-red-500">{state?.Error?.name}</p>
          </div>
        </div>
        <div className="mb-5">
          <label htmlFor="email" className="block text-sm font-medium text-gray-900">
            Email
          </label>
          <input
            type="text"
            name="email"
            id="email"
            className="input input-bordered input-primary w-full max-w-xs"
            placeholder="Email..."
            defaultValue={employee.email}
          />
          <div id="email-error" aria-live="polite" aria-atomic="true">
            <p className="mt-2 text-sm text-red-500">{state?.Error?.email}</p>
          </div>
        </div>
        <div className="mb-5">
          <label htmlFor="phone" className="block text-sm font-medium text-gray-900">
            Phone Number
          </label>
          <input
            type="text"
            name="phone"
            id="phone"
            className="input input-bordered input-primary w-full max-w-xs"
            placeholder="Phone Number..."
            defaultValue={employee.phone}
          />
          <div id="phone-error" aria-live="polite" aria-atomic="true">
            <p className="mt-2 text-sm text-red-500">{state?.Error?.phone}</p>
          </div>
        </div>
        <div id="message-error" aria-live="polite" aria-atomic="true">
          <p className="mt-2 text-sm text-red-500">{state?.message}</p>
        </div>
        <button className="btn btn-primary">Update</button>
      </form>
    </div>
  );
};
 
export default UpdateForm;
components/delete.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
//components/delete.tsx
import { deleteEmployee } from "@/lib/action";
 
export const DeleteButton = ({ id }: { id: string }) => {
  const DeleteEmployeetWithId = deleteEmployee.bind(null, id);
  return (
    <form action={DeleteEmployeetWithId}>
      <button className="btn btn-error">
        Delete
      </button>
    </form>
  );
};
lib\action.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//lib\action.ts
"use server";
 
import { z } from "zod"; //npm i zod https://www.npmjs.com/package/zod
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
 
const EmployeeSchema = z.object({
  name: z.string().min(6),
  email: z.string().min(6),
  phone: z.string().min(11),
});
 
export const saveEmployee = async (prevSate: any, formData: FormData) => {
  const validatedFields = EmployeeSchema.safeParse(
    Object.fromEntries(formData.entries())
  );
 
  if (!validatedFields.success) {
    return {
      Error: validatedFields.error.flatten().fieldErrors,
    };
  }
 
  try {
    await prisma.employee.create({
      data: {
        name: validatedFields.data.name,
        email: validatedFields.data.email,
        phone: validatedFields.data.phone,
      },
    });
     
  } catch (error) {
    return { message: "Failed to create new employee" };
  }
 
  revalidatePath("/employee");
  redirect("/employee");
};
 
export const getEmployeelist = async (query: string) => {
  try {
    const employees = await prisma.employee.findMany({
      select: {
        id: true,
        name: true,
        email: true,
        phone: true,
        createdAt: true,
      },
      orderBy: {
        createdAt: "desc",
      },
    });
    return employees;
  } catch (error) {
    throw new Error("Failed to fetch employees data");
  }
};
 
export const getEmployeeById = async (id: string) => {
  try {
    const employee = await prisma.employee.findUnique({
      where: { id },
    });
    return employee;
  } catch (error) {
    throw new Error("Failed to fetch contact data");
  }
};
 
export const updateEmployee = async (
  id: string,
  prevSate: any,
  formData: FormData
) => {
  const validatedFields = EmployeeSchema.safeParse(
    Object.fromEntries(formData.entries())
  );
 
  if (!validatedFields.success) {
    return {
      Error: validatedFields.error.flatten().fieldErrors,
    };
  }
 
  try {
    await prisma.employee.update({
      data: {
        name: validatedFields.data.name,
        email: validatedFields.data.email,
        phone: validatedFields.data.phone,
      },
      where: { id },
    });
  } catch (error) {
    return { message: "Failed to update employee" };
  }
 
  revalidatePath("/employee");
  redirect("/employee");
};
 
export const deleteEmployee = async (id: string) => {
  try {
    await prisma.employee.delete({
      where: { id },
    });
  } catch (error) {
    return { message: "Failed to delete employee" };
  }
 
  revalidatePath("/employee");
};
lib\prisma.ts
1
2
3
4
5
6
7
8
9
10
//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\utils.ts
1
2
3
4
5
6
7
8
9
//lib\utils.ts
export const formatDate = (dateStr: string) => {
  const date = new Date(dateStr);
  const formatter = new Intl.DateTimeFormat("id-ID", {
    dateStyle: "medium",
    timeStyle: "short",
  });
  return formatter.format(date);
};
prisma\schema.prisma
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//prisma\schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
 
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
 
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model Employee {
  id String @id @default(cuid())
  name String
  email String
  phone 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