article

Showing posts with label Next-JS. Show all posts
Showing posts with label Next-JS. Show all posts

Thursday, April 4, 2024

Next.js 14 Python Flask Mysql List All Data | TailwindCSS DaisyUI

Next.js 14 Python Flask Mysql List All Data | TailwindCSS DaisyUI

https://python-guide-es.readthedocs.io/es/guide-es/dev/virtualenvs.html

Create an environment
ednalan@Cairocoders myapp % pip install virtualenv
ednalan@Cairocoders myapp % pip install virtualenv

Activate the environment
ednalan@Cairocoders myapp % source venv/bin/activate
(venv) ednalan@Cairocoders myapp %

Install Flask
https://pypi.org/project/Flask/
(venv) ednalan@Cairocoders myapp % pip install -U Flask
(venv) ednalan@Cairocoders myapp % flask run

Install requirements
pip install -U flask-cors
https://pypi.org/project/Flask-Cors/

Flask-SQLAlchemy
Flask-SQLAlchemy is an extension for Flask that adds support for SQLAlchemy to your application.
https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/

(venv) PS C:\flask_dev\myapp>pip install -U Flask-SQLAlchemy

Flask + marshmallow for beautiful APIs
https://pypi.org/project/flask-marshmallow/

(venv) PS C:\flask_dev\myapp>pip install flask-marshmallow

python3 -m pip install
https://pypi.org/project/pymysql/
app.py
 
//app.py
from flask import Flask, jsonify, request
from flask_marshmallow import Marshmallow #ModuleNotFoundError: No module named 'flask_marshmallow' = pip install flask-marshmallow 
from flask_cors import CORS, cross_origin #ModuleNotFoundError: No module named 'flask_cors' = pip install Flask-Cors
from models import db, Users
 
app = Flask(__name__)
 
app.config['SECRET_KEY'] = 'cairocoders-ednalan'
#app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///flaskdb.db'
# Databse configuration mysql                             Username:password@hostname/databasename
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:root@localhost:8889/flasknextjs' #python3 -m pip install PyMySQL https://pypi.org/project/pymysql/
                                                        
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = True
  
CORS(app, supports_credentials=True)
 
db.init_app(app)
        
with app.app_context():
    db.create_all()
 
ma=Marshmallow(app)
 
class UserSchema(ma.Schema):
    class Meta:
        fields = ('id','name','email','password')
  
user_schema = UserSchema()
users_schema = UserSchema(many=True)

@app.route("/")
def hello():
    return "Hello, World!"
 
@app.route('/users', methods=['GET']) 
def listusers():
    all_users = Users.query.all()
    results = users_schema.dump(all_users)
    return jsonify(results)

if __name__ == "__main__":
    app.run(debug=True)
models.py
 
//models.py
from flask_sqlalchemy import SQLAlchemy #pip install -U Flask-SQLAlchemy
         
db = SQLAlchemy()
         
class Users(db.Model):
    __tablename__ = "tblusers"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(150), index=True, unique=True)
    email = db.Column(db.String(150), index=True, unique=True)
    password = db.Column(db.String(255), index=True, unique=True)
run (venv) C:\flask_dev\myapp>flask run

Next.js Install requirements
npm install axios
https://www.npmjs.com/package/axios

app\page.tsx
//app\page.tsx
"use client";
  
import React, { useEffect, useState } from "react";
import axios from 'axios' //npm install axios https://www.npmjs.com/package/axios
import Link from "next/link";

export default function Home() {
    const [userData, setUSerData] = useState([]);
    useEffect(() => {
        fetchData();
    }, [])
 
    const fetchData = async () => {
        try {
            const result = await axios("http://127.0.0.1:5000/users");
            console.log(result.data);
            setUSerData(result.data)
        } catch (err) {
            console.log("somthing Wrong");
        }
    }

  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 Python Flask Mysql List All Data | TailwindCSS DaisyUI</h1>
      </div>    
      <div className="overflow-x-auto">
          <div className="mb-2 w-full text-right">
            <Link
              href="#"
              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">Password</th>
            <th className="py-3 px-6 text-center">Actions</th>
            </tr>
        </thead>
        <tbody>
            {userData.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.password}</td>
                <td className="flex justify-center gap-1 py-3">
                    <button className="btn btn-active btn-primary">Edit</button>
                    <button className="btn btn-active btn-secondary">Delete</button>
                </td>
            </tr>
            ))}
        </tbody>
        </table>
      </div>  
    </div>
  );
}
run C:\nextjs>npm run dev
Github - Next.js 14 Python Flask Mysql List All Data | TailwindCSS DaisyUI

Wednesday, April 3, 2024

Next.Js Python Flask

Next.Js Python Flask

https://python-guide-es.readthedocs.io/es/guide-es/dev/virtualenvs.html

Create an environment
ednalan@Cairocoders myapp % pip install virtualenv
ednalan@Cairocoders myapp % pip install virtualenv

Activate the environment
ednalan@Cairocoders myapp % source venv/bin/activate
(venv) ednalan@Cairocoders myapp %

Install Flask
https://pypi.org/project/Flask/
(venv) ednalan@Cairocoders myapp % pip install -U Flask
(venv) ednalan@Cairocoders myapp % flask run

Install requirements
pip install -U flask-cors
https://pypi.org/project/Flask-Cors/

app.py
//app.py
from flask import Flask, jsonify
from flask_cors import CORS #pip install -U flask-cors https://pypi.org/project/Flask-Cors/

# app instance
app = Flask(__name__)
CORS(app)

@app.route("/")
def hello():
    return "Hello, World!"

# /api/user
@app.route("/api/user", methods=['GET'])
def return_home():
    return jsonify({
        'name': ['cairocoders', 'clydey', 'caitlyn'],
        'email': ['cairocoders@gmail.com', 'clydey@gmail.com', 'caitlyn@gmail.com']
    })


if __name__ == "__main__":
    app.run(debug=True, port=8080)
Install Nextjs 14
https://nextjs.org/docs/getting-started/installation
app\page.tsx
//app\page.tsx
"use client";
  
import React, { useEffect, useState } from "react";

export default function Home() {
  const [message, setMessage] = useState("Loading");
  const [userData, setUSerData] = useState([]);
            
    fetch("http://127.0.0.1:5000/api/user")
      .then((response) => response.json())
      .then((data) => {
        console.log(data);
        setMessage(data.message);
        setUSerData(data.name)
      });
  }, []);

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <h3>Next js Python Flask</h3>
      <div>{message}</div>
      {userData.map((user, index) => (
        <div key={index}>{user}</div>
      ))}
    </main>
  );
}
app\page.tsx axios
"use client";
  
import React, { useEffect, useState } from "react";
import axios from 'axios' //npm install axios https://www.npmjs.com/package/axios

export default function Home() {
    const [userData, setUSerData] = useState([]);
    useEffect(() => {
        fetchData();
    }, [])
 
    const fetchData = async () => {
        try {
            const result = await axios("http://127.0.0.1:5000/api/user");
            console.log(result.data);
            setUSerData(result.data.name)
        } catch (err) {
            console.log("somthing Wrong");
        }
    }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <h3>Next js Python Flask</h3>
      {userData.map((user, index) => (
        <div key={index}>{user}</div>
      ))}
    </main>
  );
}

Wednesday, March 13, 2024

Next.js 14 CRUD and Search with Prisma Mysql | TailwindCSS DaisyUI

Next.js 14 CRUD and Search with 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 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="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\employee\page.tsx
//app\employee\page.tsx
import Link from "next/link";
import TableData from "@/components/tabledata";
import { Suspense } from "react";
import { Spinner } from "@/components/spinner";
import Search from "@/components/search";

const Home = async ({
        searchParams,
    }: {
        searchParams?: {
            query?: string;
        };
    }) => {
    const query = searchParams?.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 and Search with Prisma Mysql | 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>
          <Search />
          <Suspense key={query} fallback={<Spinner />}>
            <TableData query={query}/>
          </Suspense>
      </div>  
    </div>
  ); 
};
 
export default Home;
app\employee\create\page.tsx
//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
//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\tabledata.tsx
//components\tabledata.tsx
import Link from "next/link";
//import { getEmployeelist } from "@/lib/action";
import { getData } from "@/lib/action";
import { formatDate } from "@/lib/utils";
import { DeleteButton } from "@/components/delete";

const Employee = async ({
    query
    }: {
    query: string;
    }) => {
    const employees = await getData(query);
    //const employees = await getEmployeelist(query);
    return (
        <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>
  );
};
 
export default Employee;
components\editform.tsx
//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\search.tsx
//components\search.tsx
"use client";
 
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useDebouncedCallback } from "use-debounce"; //npm i use-debounce https://www.npmjs.com/package/use-debounce
 
const Search = () => {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const { replace } = useRouter();
 
  const handleSearch = useDebouncedCallback((term: string) => {
    console.log(term);
    const params = new URLSearchParams(searchParams);
    if (term) {
        params.set("query", term);
    } else {
      params.delete("query");
    }
    replace(`${pathname}?${params.toString()}`);
  }, 300);
 
  return (
    <div className="relative flex flex-1 mb-5 ml-5 mr-5">
      <input
        type="text"
        className="input input-bordered input-accent w-full"
        placeholder="Search..."
        onChange={(e) => handleSearch(e.target.value)}
        defaultValue={searchParams.get("query")?.toString()}
      />
    </div>
  );
};
 
export default Search;
components\delete.tsx
//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>
  );
};
components\spinner.tsx
//components\spinner.tsx
export const Spinner = () => {
  return (
    <span className="loading loading-spinner loading-lg"></span>
  );
};
lib\action.ts
//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 getData = async (query: string) => {
  try {
    const employees = await prisma.employee.findMany({
        where: {
            name: {
            contains: query,
            },
        },
        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
//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
//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
// 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 = "mysql"
  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="mysql://root:root@localhost:8889/nextjsdb"

run C:\nextjs>npm run dev

Thursday, February 29, 2024

Next.js 14 Search with Prisma PostgreSQL | TailwindCSS DaisyUI

Next.js 14 Search with Prisma PostgreSQL | 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
//app\employee\page.tsx
import Link from "next/link";
import TableData from "@/components/tabledata";
import { Suspense } from "react";
import { Spinner } from "@/components/spinner";
import Search from "@/components/search";

const Home = async ({
  searchParams,
}: {
  searchParams?: {
    query?: string;
  };
}) => {
    const query = searchParams?.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 Search with 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>
          <Search />
          <Suspense key={query} fallback={<Spinner />}>
            <TableData query={query}/>
          </Suspense>
      </div>      
    </div>
  );
};

export default Home;
components\tabledata.tsx
//components\tabledata.tsx
import { getData } from "@/lib/action";

const TableData = async ({
  query,
}: {
  query: string;
}) => {
  const employees = await getData(query);
    return (
        <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">
                    July 11, 2023
                </td>
                <td className="flex justify-center gap-1 py-3">
                    <button className="btn btn-info">View</button>
                    <button className="btn btn-success">Edit</button>
                    <button className="btn btn-warning">Delete</button>
                </td>
            </tr>
            ))}
        </tbody>
        </table>
  );
};

export default TableData;
components\search.tsx
//components\search.tsx
"use client";

import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useDebouncedCallback } from "use-debounce"; //npm i use-debounce https://www.npmjs.com/package/use-debounce

const Search = () => {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const { replace } = useRouter();

  const handleSearch = useDebouncedCallback((term: string) => {
    console.log(term);
    const params = new URLSearchParams(searchParams);
    if (term) {
        params.set("query", term);
    } else {
      params.delete("query");
    }
    replace(`${pathname}?${params.toString()}`);
  }, 300);

  return (
    <div className="relative flex flex-1 mb-5 ml-5 mr-5">
      <input
        type="text"
        className="input input-bordered input-accent w-full"
        placeholder="Search..."
        onChange={(e) => handleSearch(e.target.value)}
        defaultValue={searchParams.get("query")?.toString()}
      />
    </div>
  );
};

export default Search;
components\spinner.tsx
//components\spinner.tsx
export const Spinner = () => {
  return (
    <span className="loading loading-spinner loading-lg"></span>
  );
};
lib\action.ts
//lib\action.ts
"use server";

import { prisma } from "@/lib/prisma";

export const getData = async (query: string) => {
  try {
    const employees = await prisma.employee.findMany({
      where: {
        OR: [
          {
            name: {
              contains: query,
              mode: "insensitive",
            },
          },
          {
            phone: {
              contains: query,
              mode: "insensitive",
            },
          },
        ],
      },
    });
    return employees;
  } catch (error) {
    throw new Error("Failed to fetch data");
  }
};
lib\action.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;
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

Wednesday, February 28, 2024

Next.js 14 React Suspense for data loading using Postgresql Prisma | TailwindCSS DaisyUI Add and List all data

Next.js 14 React Suspense for data loading using Postgresql Prisma | TailwindCSS DaisyUI Add and List all data

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/react-suspense/page.tsx
//app/react-suspense/page.tsx
import Link from "next/link";
import TableData from "@/components/employeetable";
import { Suspense } from "react";
import { Spinner } from "@/components/spinner";

export default function Home() {
    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 How to use React Suspense for data loading <br/>Prisma PostgreSQL | TailwindCSS DaisyUI</h1>
      </div>
      <div className="overflow-x-auto">
          <div className="mb-2 w-full text-right">
            <Link
              href="/react-suspense/create"
              className="btn btn-primary">
              Create
            </Link>
          </div>
        <Suspense fallback={<Spinner />}>
          <TableData />
        </Suspense>
      </div>      
    </div>
  );
};
app/react-suspense/create/page.tsx
//app/react-suspense/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;
components\employeetable.tsx
//components\employeetable.tsx
import { getEmployeelist } from "@/lib/action";
import { formatDate } from "@/lib/utils";

const TableEmployee = async ({
  query,
}: {
  query: string;
}) => {
  const employees = await getEmployeelist(query);

    return (
        <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">
                date format
                </td>
                <td className="flex justify-center gap-1 py-3">
                    <button className="btn btn-info">View</button>
                    <button className="btn btn-success">Edit</button>
                    <button className="btn btn-warning">Delete</button>
                </td>
            </tr>
            ))}
        </tbody>
        </table>
  );
};

export default TableEmployee;
components\spinner.tsx
//components\spinner.tsx
export const Spinner = () => {
  return (
    <span className="loading loading-spinner loading-lg"></span>
  );
};
lib/action.ts
//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("/react-suspense");
  redirect("/react-suspense");
};

export const getEmployeelist = async (query: string) => {
  try {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    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");
  }
};
lib/prisma.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;
prisma\schema.prisma
//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

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
//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
//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
//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
//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
//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
//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
//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
//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
//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

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

Friday, February 9, 2024

NextJS 14 CRUD REST API using Prisma with MYSQL

NextJS 14 CRUD REST API using Prisma with MYSQL

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 User {
id Int @id @default(autoincrement())
email String @unique
name String?
}

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

Create Folder and Files
app\users\route.js
//app\users\route.js
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

export async function POST(request) {
    try {
        const data = await request.json();
        console.log(data);
        const { name, email } = data

        const newUser = await prisma.user.create({
            data: {
                name,
                email
            }
        });
        return NextResponse.json(newUser);
    } catch (error) {
        console.log("Error creating user:", error)
        return NextResponse.error("Internal Server Error", 500)
    }
}

export async function GET() {
    try {
        const users = await prisma.user.findMany();
        return NextResponse.json(users);
    } catch (error) {
        console.error("Error fetching users:", error);
        return NextResponse.error("Internal Server Error", 500);
    }
}
app\users\[id]\route.js
//app\users\[id]\route.js
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

export async function GET(request, { params }) {
    const id = parseInt(params.id);
    try {
        const users = await prisma.user.findUnique({
        where: {
            id: id,
        },
        })
        return NextResponse.json(users);
    } catch (error) {
        console.error("Error fetching users:", error);
        return NextResponse.error("Internal Server Error", 500);
    }
}

export async function PUT(request, { params }) {
    try {
        const data = await request.json();
        console.log(data);
        const { name, email } = data
        const id = parseInt(params.id);

        const updatedUser = await prisma.user.update({
            where: { id },
            data: {
                name,
                email
            }
        });
        return NextResponse.json(updatedUser);
    } catch (error) {
        console.log("Error updating user:", error)
        return NextResponse.error("Internal Server Error", 500)
    }
}

export async function DELETE(request, { params }) {
    try {
        const id = parseInt(params.id);
        const deleteUser = await prisma.user.delete({
            where: { id }
        });
        return NextResponse.json(deleteUser);
    } catch (error) {
        console.error("error deleting user:", error);
        return NextResponse.error("Internal server Error", 500);
    }
}
run C:\nextjs>npm run dev

Thursday, February 8, 2024

Next.js 14 How to set up Prisma with Mysql

Next.js 14 How to set up Prisma with Mysql

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 User {
id Int @id @default(autoincrement())
email String @unique
name String?
}

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

run C:\nextjs>npm run dev

Wednesday, January 31, 2024

Next.js 14 Authentication with Clerk

Next.js 14 Authentication with Clerk

Install nextjs npx create-next-app@latest https://nextjs.org/docs/getting-started/installation

Install the following
Clerk
npm install @clerk/nextjs
https://clerk.com/docs/quickstarts/nextjs


.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_Zmlyc3Qtd2lsZGNhdC03LmNsZXJrLmFjY291bnRzLmRldiQ
CLERK_SECRET_KEY=sk_test_yZkAi9ZB6GF5hZxasIvd0sqBoxbfgyi1Wkc4KyZ9Nd
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
app\layout.tsx
//app\layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { ClerkProvider } from '@clerk/nextjs';
import Header from '@/components/header';

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    
      
        
          
{children} ) }
app\page.tsx
//app\page.tsx
import Image from 'next/image'

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      HOmepage
    </main>
  )
}
components\header.tsx
//components\header.tsx
import { UserButton, auth } from '@clerk/nextjs';
import Link from 'next/link';

export default async function Header() {
    const { userId } = auth();

    return (
        <div className='bg-gray-600 text-neutral-100'>
            <div className='container mx-auto flex items-center justify-between py-4'>
                <Link href='/'>Home</Link>
                <div>
                    {userId ? (
                        <div className='flex gap-4 items-center'>
                            <Link href='/dashboard'>Dashboard</Link>
                            <UserButton afterSignOutUrl='/' />
                        </div>
                    ) : (
                        <div className='flex gap-4 items-center'>
                            <Link href='/sign-up'>Sign up</Link>
                            <Link href='/sign-in'>Sign In</Link>
                        </div>
                    )}
                </div>
            </div>
        </div>
    );
}
app\sign-up\[[...sign-up]]\page.tsx
//app\sign-up\[[...sign-up]]\page.tsx
import { SignUp } from "@clerk/nextjs";

export default function Page() {
    return (
        <div className='flex items-center justify-center flex-col gap-10'>
            <h1 className='text-4xl font-bold mt-20'>Sign UP</h1>
            <SignUp />
        </div>
    );
}
app\sign-in\[[...sign-in]]\page.tsx
//app\sign-in\[[...sign-in]]\page.tsx
import { SignIn } from "@clerk/nextjs";

export default function Page() {
    return (
        <div className='flex items-center justify-center flex-col gap-10'>
            <h1 className='text-4xl font-bold mt-20'>Sign In</h1>
            <SignIn />
        </div>
    );
}
app\dashboard\page.tsx
//dashboard\page.tsx
import { auth, currentUser } from '@clerk/nextjs';

export default async function DashboardPage() {
    const { userId } = auth();
    const user = await currentUser();

    if (!userId || !user) {
        return <div>You are not logged in</div>;
    }

    return (
        <div className='mt-10 text-start max-w-xl mx-auto bg-neutral-200 p-5 rounded'>
            <h1 className='text-4xl font-bold'>Welcome</h1>
            <ul className='list-none mt-10'>
                <li className='mb-2'>
                    <span className='font-semibold'>First Name:</span> {user.firstName}
                </li>
                <li className='mb-2'>
                    <span className='font-semibold'>Last Name:</span> {user.lastName}
                </li>
                <li className='mb-2'>
                    <span className='font-semibold'>Email:</span>{' '}
                    {user.emailAddresses[0].emailAddress}
                </li>
            </ul>
        </div>
    );
}
app\about\page.tsx
//app\about\page.tsx
export default function AboutPage() {
    return <div>AboutPage</div>;
}
run C:\nextjs>npm run dev

Monday, January 29, 2024

Next.js 14 Login Register MongoDB Next-Auth Login Authentication Daisyui Tailwind

Next.js 14 Login Register MongoDB Next-Auth Login Authentication Daisyui Tailwind

Install nextjs npx create-next-app@latest https://nextjs.org/docs/getting-started/installation

Install the following

npm install react-daisyui
https://www.npmjs.com/package/react-daisyui
daisyUI components built with React, Typescript and TailwindCSS

Mongoose
npm install mongoose
https://www.npmjs.com/package/mongoose

NextAuth.js
npm install next-auth
https://www.npmjs.com/package/next-auth/v/4.23.1
NextAuth.js is a complete open source authentication solution for Next.js applications.

bcrypt.js
npm install bcryptjs
https://www.npmjs.com/package/bcryptjs

edit tailwind.config.js Add daisyui to plugins
edit tailwind.config.ts
//edit tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
        'gradient-conic':
          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
      },
    },
  },
  plugins: [require('daisyui')], //https://www.npmjs.com/package/react-daisyui
}
export default config
.env
MONGODB_URI=mongodb://127.0.0.1/nextjs14
NEXTAUTH_SECRET=cairocoders
app\layout.tsx
//app\layout.js
import Navbar from "@/components/Navbar";
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";

import { getServerSession } from "next-auth";
import SessionProvider from "@/utils/SessionProvider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Next.js 14 Login Register MongoDB Next-Auth Login Authentication Daisyui Tailwind",
  description: "Generated by create next app",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getServerSession();
  return (
    <html lang="en">
      <body className={inter.className}>
        <SessionProvider session={session}>
          <div className="container mx-auto px-4">
            <Navbar />
            {children}
          </div>
        </SessionProvider>
      </body>
    </html>
  );
}
components\Navbar.tsx
//components\Navbar.jsx
"use client";
import React from "react";
import Link from "next/link";
import { signOut, useSession } from "next-auth/react";

const Navbar = () => {
    const { data: session }: any = useSession();
    return (
        <div className="navbar bg-base-100">
            <div className="flex-1">
                <a className="btn btn-ghost text-xl">Cairocoders</a>
                <Link href="/">
                    Home
                </Link>
            </div>
            <div className="flex-none">
                {!session ? (
                    <>
                        <Link href="/login" className="btn btn-primary">
                            Login
                        </Link>
                        <Link href="/register" className="btn btn-secondary ml-2">
                            Register
                        </Link>
                    </>
                ) : (
                    <>
                        {session.user?.email}
                            <div className="dropdown dropdown-end">
                                <div tabIndex={0} role="button" className="btn btn-ghost btn-circle avatar">
                                    <div className="w-10 rounded-full">
                                        <img alt="Tailwind CSS Navbar component" src="https://daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg" />
                                    </div>
                                </div>
                                <ul tabIndex={0} className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
                                    <li>
                                        <a className="justify-between">
                                            Profile
                                            <span className="badge">New</span>
                                        </a>
                                    </li>
                                    <li><a>Settings</a></li>
                                    <li>
                                        <button
                                            onClick={() => {
                                                signOut();
                                            }}
                                        >
                                            Logout
                                        </button>
                                    </li>
                                </ul>
                            </div>
                    </>
                )}
            </div>
        </div>
    );
};

export default Navbar;
models\User.js
//models\User.js
import mongoose from "mongoose";

const { Schema } = mongoose;

const userSchema = new Schema(
    {
        name: {
            type: String,
            required: true,
        },
        email: {
            type: String,
            unique: true,
            required: true,
        },
        password: {
            type: String,
            required: false,
        },
    },
    { timestamps: true }
);

export default mongoose.models.User || mongoose.model("User", userSchema);
utils\db.js
//utils\db.js
import mongoose from "mongoose";

const connect = async () => {
    if (mongoose.connections[0].readyState) return;

    try {
        await mongoose.connect(process.env.MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
        });
        console.log("Mongo Connection successfully established.");
    } catch (error) {
        throw new Error("Error connecting to Mongoose");
    }
};

export default connect;
utils\SessionProvider.tsx
//utils\SessionProvider.tsx
"use client";
import React from "react";
import { SessionProvider } from "next-auth/react"; //https://www.npmjs.com/package/next-auth/v/4.23.1

const AuthProvider = ({ children }: any) => {
    return {children};
};

export default AuthProvider;
app\pag.tsx
//app\pag.tsx
export default function Home() {
  return (
    <main>
      <div className="carousel w-full">
        <div id="item1" className="carousel-item w-full">
          <img src="https://daisyui.com/images/stock/photo-1625726411847-8cbb60cc71e6.jpg" className="w-full" />
        </div>
        <div id="item2" className="carousel-item w-full">
          <img src="https://daisyui.com/images/stock/photo-1609621838510-5ad474b7d25d.jpg" className="w-full" />
        </div>
        <div id="item3" className="carousel-item w-full">
          <img src="https://daisyui.com/images/stock/photo-1414694762283-acccc27bca85.jpg" className="w-full" />
        </div>
        <div id="item4" className="carousel-item w-full">
          <img src="https://daisyui.com/images/stock/photo-1665553365602-b2fb8e5d1707.jpg" className="w-full" />
        </div>
      </div>
      <div className="flex justify-center w-full py-2 gap-2">
        <a href="#item1" className="btn btn-xs">1</a>
        <a href="#item2" className="btn btn-xs">2</a>
        <a href="#item3" className="btn btn-xs">3</a>
        <a href="#item4" className="btn btn-xs">4</a>
      </div>
    </main>
  );
}
app\login\page.tsx
//app\login\page.tsx
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { signIn, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";

const Login = () => {
    const router = useRouter();
    const [error, setError] = useState("");
    const { data: session, status: sessionStatus } = useSession();

    useEffect(() => {
        if (sessionStatus === "authenticated") {
            router.replace("/dashboard");
        }
    }, [sessionStatus, router]);

    const isValidEmail = (email: string) => {
        const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
        return emailRegex.test(email);
    };

    const handleSubmit = async (e: any) => {
        e.preventDefault();
        const email = e.target[0].value;
        const password = e.target[1].value;

        if (!isValidEmail(email)) {
            setError("Email is invalid");
            return;
        }

        if (!password || password.length < 8) {
            setError("Password is invalid");
            return;
        }

        const res = await signIn("credentials", {
            redirect: false,
            email,
            password,
        });

        if (res?.error) {
            setError("Invalid email or password");
            if (res?.url) router.replace("/dashboard");
        } else {
            setError("");
        }
    };

    if (sessionStatus === "loading") {
        return <h1>Loading...</h1>;
    }

    return (
        sessionStatus !== "authenticated" && (
        <div className="justify-center mt-16">
        <div className="w-full p-6 m-auto bg-white rounded-md shadow-md lg:max-w-lg">
            <h1 className="text-3xl font-semibold text-center text-purple-700">Login</h1>
            <form onSubmit={handleSubmit} className="space-y-4">
            <div>
                <label className="label">
                <span className="text-base label-text">Email</span>
                </label>
                <input type="text" placeholder="Email Address" required className="w-full input input-bordered input-primary" />
            </div>
            <div>
                <label className="label">
                <span className="text-base label-text">Password</span>
                </label>
                <input type="password" placeholder="Enter Password" required className="w-full input input-bordered input-primary" />
            </div>
            <a href="#" className="text-xs text-gray-600 hover:underline hover:text-blue-600">Forget Password?</a>
            <div>
            <button
                type="submit"
                className="btn btn-primary"
            >
            {" "}
            Sign In
            </button>
            <p className="text-red-600 text-[16px] mb-4">{error && error}</p>
            </div>
            </form>
                <Link
                    className="block text-center text-blue-500 hover:underline mt-2"
                    href="/register"
                 >
                Register Here
                </Link>
            </div>
        </div>
        )
    );
};

export default Login;
app\register\page.tsx
//app\register\page.tsx
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";

const Register = () => {
    const [error, setError] = useState("");
    const router = useRouter();
    const { data: session, status: sessionStatus } = useSession();

    useEffect(() => {
        if (sessionStatus === "authenticated") {
            router.replace("/dashboard");
        }
    }, [sessionStatus, router]);

    const isValidEmail = (email: string) => {
        const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
        return emailRegex.test(email);
    };
    const handleSubmit = async (e: any) => {
        e.preventDefault();
        const name = e.target[0].value;
        const email = e.target[1].value;
        const password = e.target[2].value;

        if (!isValidEmail(email)) {
            setError("Email is invalid");
            return;
        }

        if (!password || password.length < 8) {
            setError("Password is invalid");
            return;
        }

        try {
            const res = await fetch("/api/register", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    name,
                    email,
                    password,
                }),
            });
            if (res.status === 400) {
                setError("This email is already registered");
            }
            if (res.status === 200) {
                setError("");
                router.push("/login");
            }
        } catch (error) {
            setError("Error, try again");
            console.log(error);
        }
    };

    if (sessionStatus === "loading") {
        return <h1>Loading...</h1>;
    }

    return (
        sessionStatus !== "authenticated" && (
            <div className="justify-center mt-16">
                <div className="w-full p-6 m-auto bg-white rounded-md shadow-md lg:max-w-lg">
                    <h1 className="text-3xl font-semibold text-center text-purple-700">Register</h1>
                    <form onSubmit={handleSubmit} className="space-y-4">
                        <div>
                            <label className="label">
                                <span className="text-base label-text">Name</span>
                            </label>
                            <input type="text" placeholder="Name" required className="w-full input input-bordered input-primary" />
                        </div>
                        <div>
                            <label className="label">
                                <span className="text-base label-text">Email</span>
                            </label>
                            <input type="text" placeholder="Email Address" required className="w-full input input-bordered input-primary" />
                        </div>
                        <div>
                            <label className="label">
                                <span className="text-base label-text">Password</span>
                            </label>
                            <input type="password" placeholder="Enter Password" required className="w-full input input-bordered input-primary" />
                        </div>
                        <button
                            type="submit"
                            className="btn btn-primary"
                        >
                            {" "}
                            Register
                        </button>
                        <p className="text-red-600 text-[16px] mb-4">{error && error}</p>
                    </form>
                    <div className="text-center text-gray-500 mt-4">- OR -</div>
                    <Link
                        className="block text-center text-blue-500 hover:underline mt-2"
                        href="/login"
                    >
                        Login with an existing account
                    </Link>
                </div>
            </div>
        )
    );
};

export default Register;
app\api\register\route.ts
//app\api\register\route.ts
import User from "@/models/User";
import connect from "@/utils/db";
import bcrypt from "bcryptjs"; //https://www.npmjs.com/package/bcryptjs npm install bcryptjs
import { NextResponse } from "next/server";

export const POST = async (request: any) => {
    const { name, email, password } = await request.json();

    await connect();

    const existingUser = await User.findOne({ email });

    if (existingUser) {
        return new NextResponse("Email is already in use", { status: 400 });
    }

    const hashedPassword = await bcrypt.hash(password, 5);
    const newUser = new User({
        name,
        email,
        password: hashedPassword,
    });

    try {
        await newUser.save();
        return new NextResponse("user is registered", { status: 200 });
    } catch (err: any) {
        return new NextResponse(err, {
            status: 500,
        });
    }
};
app\api\auth\[...nextauth]\route.ts
//app\api\auth\[...nextauth]\route.ts
import NextAuth from "next-auth";
import { Account, User as AuthUser } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import User from "@/models/User";
import connect from "@/utils/db";

export const authOptions: any = {
    // Configure one or more authentication providers
    secret: process.env.NEXTAUTH_SECRET,
    providers: [
        CredentialsProvider({
            id: "credentials",
            name: "Credentials",
            credentials: {
                email: { label: "Email", type: "text" },
                password: { label: "Password", type: "password" },
            },
            async authorize(credentials: any) {
                await connect();
                try {
                    const user = await User.findOne({ email: credentials.email });
                    if (user) {
                        const isPasswordCorrect = await bcrypt.compare(
                            credentials.password,
                            user.password
                        );
                        if (isPasswordCorrect) {
                            return user;
                        }
                    }
                } catch (err: any) {
                    throw new Error(err);
                }
            },
        }),
    ],
    callbacks: {
        async signIn({ user, account }: { user: AuthUser; account: Account }) {
            if (account?.provider == "credentials") {
                return true;
            }
        },
    },
};

export const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
run C:\nextjs>npm run dev

Related Post