article

Tuesday, February 20, 2024

Next.js 14 Todo Server Actions Prisma Mysql | TailwindCSS DaisyUI

Next.js 14 Todo Server Actions Prisma Mysql | 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 mysql

Model data in the Prisma schema
prisma/schema.prisma
model Todo {
id Int @id @default(autoincrement())
title String?
isCompleted Boolean @default(false)
updatedAt DateTime? @updatedAt
createdAt DateTime @default(now())
}

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

.env
DATABASE_URL="mysql://root:root@localhost:8889/nextjsdb"

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 { PrismaClient } from "@prisma/client";
import AddTodo from "@/components/AddTodo";
import Todo from "@/components/Todo";

const prisma = new PrismaClient();
async function getData() {
  const data = await prisma.todo.findMany({
    select: {
      title: true,
      id: true,
      isCompleted: true,
    },
    orderBy: {
      createdAt: "desc",
    },
  });

  return data;
}
const Home = async () => {
  const data = await getData();
  return (
    <div className="w-screen py-20 flex justify-center flex-col items-center">
      <h1 className=" text-3xl font-extrabold mb-5">
        Next.js 14 Todo Server Actions
      </h1>

      <div className="flex justify-center flex-col items-center w-[1000px] ">
        <AddTodo />
        <div className=" flex flex-col gap-5 items-center justify-center mt-10 w-full">
          {data.map((todo, id) => (
            <div className="w-full card bg-base-100 shadow-xl" key={id}>
              <div className="card-body">
              <Todo todo={todo} />
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default Home;
app/actions/route.ts
//app/actions/route.ts
"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/utils/prisma";

export async function create(formData: FormData) {
  const input = formData.get("input") as string;

  if (!input.trim()) {
    return;
  }

  await prisma.todo.create({
    data: {
      title: input,
    },
  });

  revalidatePath("/");
}

export async function edit(formData: FormData) {
  const input = formData.get("newTitle") as string;
  const inputId = formData.get("inputId") as string;

  const id = parseInt(inputId);

  await prisma.todo.update({
    where: {
      id: id,
    },
    data: {
      title: input,
    },
  });

  revalidatePath("/");
}

export async function deleteTodo(formData: FormData) {
  const inputId = formData.get("inputId") as string;

  const id = parseInt(inputId);

  await prisma.todo.delete({
    where: {
      id: id,
    },
  });

  revalidatePath("/");
}


export async function todoStatus(formData: FormData) {
  const inputId = formData.get("inputId") as string;

  const id = parseInt(inputId);

  const todo = await prisma.todo.findUnique({
    where: {
      id: id,
    },
  });

  if (!todo) {
    return;
  }

  const updatedStatus = !todo.isCompleted;

  await prisma.todo.update({
    where: {
      id: id,
    },
    data: {
      isCompleted: updatedStatus,
    },
  });

  revalidatePath("/");

  return updatedStatus;
}
components\AddTodo.tsx
//components\AddTodo.tsx
import { create } from "@/app/actions/route";
import Form from "@/components/Form";

const AddTodo = () => {
  return (
    <Form action={create} className="w-1/2 m-auto">
      <div className="flex">
        <input type="text" name="input" placeholder="Add Todo..." className="input input-bordered input-success w-full max-w-xs" />
        <button className="btn btn-primary">Add New Todo</button>
      </div>
    </Form>
  );
};

export default AddTodo;
components\ChangeTodo.tsx
//components\ChangeTodo.tsx
import { todoStatus } from "@/app/actions/route";
import Form from "@/components/Form";
import { todoType } from "@/types/todoTypes";

const ChangeTodo = ({ todo }: { todo: todoType }) => {
  return (
    <Form action={todoStatus}>
      <input
        name="inputId"
        value={todo.id}
        className="border-gray-700 border"
        type="hidden"
      />
      <button type="submit" className="btn btn-info">Done</button>
    </Form>
  );
};

export default ChangeTodo;
components\DeleteTodo.tsx
//components\DeleteTodo.tsx
"use client";

import { deleteTodo } from "@/app/actions/route";
import Form from "@/components/Form";
import { todoType } from "@/types/todoTypes";

const DeleteTodo = ({ todo }: { todo: todoType }) => {
  return (
    <Form action={deleteTodo}>
      <input type="hidden" name="inputId" value={todo.id} />
      <button type="submit" className="btn btn-error">Delete</button>
    </Form>
  );
};

export default DeleteTodo;
components\EditTodo.tsx
//components\EditTodo.tsx
"use client";

import { edit } from "@/app/actions/route";
import Form from "@/components/Form";
import { useState } from "react";
import { todoType } from "@/types/todoTypes";

const EditTodo = ({ todo }: { todo: todoType }) => {
  const [editTodo, setEditTodo] = useState(false);

  const handleEdit = () => {
    setEditTodo(!editTodo);
  };

  const handleSubmit = () => {
    setEditTodo(false);
  };
  return (
     <div className="flex gap-5 items-center">
       <button onClick={handleEdit} className="btn btn-success">Edit </button>

      {editTodo ? (
         <Form action={edit} onSubmit={handleSubmit}>
           <input
            name="inputId"
            value={todo.id}
            type="hidden"
          />

           <div className="flex justify-center">
             <input type="text" placeholder="Edit Todo..." 
              name="newTitle"
              className="input input-bordered input-success w-full max-w-xs" />
             <button type="submit" className="btn btn-success">Save </button>
           </div>
         </Form>
      ) : null}
     </div>
  );
};

export default EditTodo;
components\Form.tsx
//components\Form.tsx
"use client";

import { useRef } from "react";
import { ReactNode } from "react";

interface FormProps {
  children: ReactNode;
  action: (formData: FormData) => Promise<void | boolean>;
  className?: string;
  onSubmit?: () => void;
}

const Form = ({
  children,
  action,
  className,
  onSubmit,
}: FormProps) => {
  const ref = useRef<HTMLFormElement>(null);
  return (
    <form
      className={className}
      onSubmit={onSubmit}
      ref={ref}
      action={async (formData) => {
        await action(formData);
        ref.current?.reset();
      }}
    >
      {children}
    </form>
  );
};

export default Form;
components\Todo.tsx
//components\Todo.tsx
import ChangeTodo from "./ChangeTodo";
import EditTodo from "./EditTodo";
import DeleteTodo from "./DeleteTodo";
import { todoType } from "@/types/todoTypes";

const Todo = ({ todo }: { todo: todoType }) => {
  const todoStyle = {
    textDecoration:
      todo.isCompleted === true ? "line-through" : "none",
    opacity: todo.isCompleted === true ? 0.5 : 1,
  };

  return (
    <div
      className="w-full  flex items-center justify-between py-3 px-20 rounded-2xl"
      style={todoStyle}
    >
      <h2 className="card-title">{todo.title}</h2>
      <div className="flex items-center gap-5">
        <ChangeTodo todo={todo} />
        <EditTodo todo={todo} />
        <DeleteTodo todo={todo} />
      </div>
    </div>
  );
};

export default Todo;
prisma\schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = "mysql://root:root@localhost:8889/nextjsdb"
}

model Todo {
  id Int @id @default(autoincrement())
  title       String?
  isCompleted Boolean   @default(false)
  updatedAt   DateTime? @updatedAt
  createdAt   DateTime  @default(now())
}
types\todoTypes.ts
//types\todoTypes.ts
export type todoType = {
  id: string;
  title?: string | null;
  isCompleted: boolean;
  updatedAt?: Date | null;
  createdAt?: Date;
};
utils\prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
run C:\nextjs>npm run dev

Related Post