article

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

Monday, January 22, 2024

Next.js 14 CRUD Create Read Update and Delete - MongoDB Daisyui Tailwind

Next.js 14 CRUD Create Read Update and Delete - MongoDB 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

edit tailwind.config.js Add daisyui to plugins
edit tailwind.config.js
//edit tailwind.config.js
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
app\layout.js
//app\layout.js
import { Inter } from 'next/font/google'
import './globals.css'
import Navbar from "@/components/Navbar";

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

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

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <div className="container mx-auto px-4">
          <Navbar />
          <div className="pb-8">{children}</div>
        </div>
      </body>
    </html>
  )
}
components\Navbar.jsx
//components\Navbar.jsx
import Link from "next/link";

export default function Navbar() {
    return (
        <div className="navbar bg-base-100">
        <div className="flex-1">
            <a className="btn btn-ghost text-xl">Cairocoders</a>
        </div>
        <div className="flex-none">
            <div className="dropdown dropdown-end">
            <div tabIndex={0} role="button" className="btn btn-ghost btn-circle">
                <div className="indicator">
                <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
                <span className="badge badge-sm indicator-item">8</span>
                </div>
            </div>
            <div tabIndex={0} className="mt-3 z-[1] card card-compact dropdown-content w-52 bg-base-100 shadow">
                <div className="card-body">
                <span className="font-bold text-lg">8 Items</span>
                <span className="text-info">Subtotal: $999</span>
                <div className="card-actions">
                    <button className="btn btn-primary btn-block">View cart</button>
                </div>
                </div>
            </div>
            </div>
            <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><a>Logout</a></li>
            </ul>
            </div>
        </div>
        </div>
    );
}
.env
MONGODB_URI=mongodb://127.0.0.1/nextjs14
components\EditProductForm.jsx
//components\EditProductForm.jsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function EditProductForm({ id, name, image, price,category }) {
    const [newName, setNewTitle] = useState(name);
    const [newImage, setNewImage] = useState(image);
    const [newPrice, setNewPrice] = useState(price);
    const [newCategory, setNewCategory] = useState(category);

    const router = useRouter();

    const handleSubmit = async (e) => {
        e.preventDefault();

        try {
            const res = await fetch(`http://localhost:3000/api/products/${id}`, {
                method: "PUT",
                headers: {
                    "Content-type": "application/json",
                },
                body: JSON.stringify({ newName, newImage, newPrice, newCategory }),
            });

            if (!res.ok) {
                throw new Error("Failed to update Product");
            }

            router.refresh();
            router.push("/products");
        } catch (error) {
            console.log(error);
        }
    };

    return (
        <>
        <div className="flex justify-between items-center">
            <h1 className="font-bold py-10 text-2xl">Update Product</h1>
        </div>
        <form onSubmit={handleSubmit} className="flex flex-col gap-3">
            <input
                onChange={(e) => setNewTitle(e.target.value)}
                value={newName}
                className="input input-bordered input-accent w-full max-w-xs"
                type="text"
            />

            <input
                onChange={(e) => setNewImage(e.target.value)}
                value={newImage}
                className="input input-bordered input-accent w-full max-w-xs"
                type="text"
            />
            <input
                onChange={(e) => setNewPrice(e.target.value)}
                value={newPrice}
                className="input input-bordered input-accent w-full max-w-xs"
                type="text"
            />
            <input
                onChange={(e) => setNewCategory(e.target.value)}
                value={newCategory}
                className="input input-bordered input-accent w-full max-w-xs"
                type="text"
            />

            <button className="btn btn-primary w-full max-w-xs">
                Update Product
            </button>
        </form>
        </>
    );
}
components\ProductsList.jsx
//components\EditProductForm.jsx
import Link from "next/link";
import RemoveBtn from "./RemoveBtn";
import Image from 'next/image'

const getProducts = async () => {
    try {
        const res = await fetch("http://localhost:3000/api/products", {
            cache: "no-store",
        });

        if (!res.ok) {
            throw new Error("Failed to fetch products");
        }

        return res.json();
    } catch (error) {
        console.log("Error loading products: ", error);
    }
};

export default async function ProductssList() {
    const { products } = await getProducts();

    return (
        <>
            <div className="overflow-x-auto">
                <div className="flex justify-between items-center">
                    <h1 className="font-bold py-10 text-2xl">Next.js 14 CRUD Crate, Read, Update and Delete - MongoDB Daisyui TailwindCSS</h1>
                </div>
                <div className="text-right">
                    <Link className="btn btn-primary" href={"/addProduct"}>
                        Add Product
                    </Link>
                </div>
            <table className="table">
                <thead>
                <tr>
                    <th>
                    <label>
                        <input type="checkbox" className="checkbox" />
                    </label>
                    </th>
                    <th>Name</th>
                    <th>Price</th>
                    <th>Category</th>
                    <th />
                </tr>
                </thead>
                <tbody>
                    {products.map((rs) => (
                    <tr className="hover" key={rs._id}>
                        <th>
                        <label>
                            <input type="checkbox" className="checkbox" />
                        </label>
                        </th>
                        <td>
                            <div className="flex items-center gap-3">
                                <div className="avatar">
                                <div className="mask mask-squircle w-12 h-12">
                                    <Image
                                            src={rs.image}
                                            alt={rs.name}
                                            width={80}
                                            height={80}
                                            className="rounded-lg"
                                    />
                                </div>
                                </div>
                                <div>
                                <div className="font-bold">{rs.name}</div>
                                </div>
                            </div>
                        </td>
                        <td>
                            ${rs.price}
                        </td>
                        <td>{rs.category}</td>
                        <th>
                            <Link href={`/editProduct/${rs._id}`}>
                                <button className="btn btn-primary">Edit</button>
                            </Link>
                            <RemoveBtn id={rs._id} />
                        </th>
                    </tr>
                    ))}                    
                </tbody>
            </table>
            </div>
        </>
    );
}
components\RemoveBtn.jsx
//components\RemoveBtn.jsx
"use client";

import { useRouter } from "next/navigation";

export default function RemoveBtn({ id }) {
    const router = useRouter();
    const removeProduct= async () => {
        const confirmed = confirm("Are you sure?");

        if (confirmed) {
            const res = await fetch(`http://localhost:3000/api/products?id=${id}`, {
                method: "DELETE",
            });

            if (res.ok) {
                router.refresh();
            }
        }
    };

    return (
        <button onClick={removeProduct} className="btn btn-error ml-2">
            Delete
        </button>
    );
}
libs\mongodb.js
//libs\mongodb.js
import mongoose from "mongoose";

const connectMongoDB = async () => {
    try {
        await mongoose.connect(process.env.MONGODB_URI);
        console.log("Connected to MongoDB.");
    } catch (error) {
        console.log(error);
    }
};

export default connectMongoDB;
models\ProductModel.js
//models\ProductModel.js
import mongoose, { Schema } from "mongoose";

const topicSchema = new Schema(
    {
        name: { type: String, required: true },
        category: { type: String, required: true },
        image: { type: String, required: true },
        price: { type: Number, required: true },
    },
    {
        timestamps: true,
    }
);

const ProductModel = mongoose.models.Product || mongoose.model("Product", topicSchema);

export default ProductModel;
app\products\page.js
//app\products\page.js
import Productlist from "@/components/ProductsList";

export default function Home() {
    return ;
}
app\addProduct\page.jsx
//app\addProduct\page.jsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function AddProduct() {
    const [name, setName] = useState("");
    const [image, setImage] = useState("");
    const [price, setPrice] = useState("");
    const [category, setCategory] = useState("");

    const router = useRouter();

    const handleSubmit = async (e) => {
        e.preventDefault();

        if (!name || !image) {
            alert("Name and image are required.");
            return;
        }

        try {
            const res = await fetch("http://localhost:3000/api/products", {
                method: "POST",
                headers: {
                    "Content-type": "application/json",
                },
                body: JSON.stringify({ name, image, price, category }),
            });

            if (res.ok) {
                router.push("/products");
            } else {
                throw new Error("Failed to create a Product");
            }
        } catch (error) {
            console.log(error);
        }
    };

    return (
        <>
        <div className="flex justify-between items-center">
            <h1 className="font-bold py-10 text-2xl">Add New Product</h1>
        </div>
        <form onSubmit={handleSubmit} className="flex flex-col gap-3">
            <input
                onChange={(e) => setName(e.target.value)}
                value={name}
                    className="input input-bordered input-accent w-full max-w-xs"
                type="text"
                placeholder="Product Name"
            />

            <input
                onChange={(e) => setImage(e.target.value)}
                value={image}
                    className="input input-bordered input-accent w-full max-w-xs"
                type="text"
                placeholder="/images/1.jpg"
                defaultValue="/images/1.jpg"
            />
            <input
                onChange={(e) => setPrice(e.target.value)}
                value={price}
                className="input input-bordered input-accent w-full max-w-xs"
                type="number"
                placeholder="1"
                defaultValue="1"
            />
            <input
                onChange={(e) => setCategory(e.target.value)}
                value={category}
                className="input input-bordered input-accent w-full max-w-xs"
                type="text"
                placeholder="Product Category"
            />

            <button
                type="submit"
                    className="btn btn-primary w-full max-w-xs"
            >
                Add Product
            </button>
        </form>
        </>
    );
}
app\editProduct\[id]\page.js
//app\editProduct\[id]\page.js
import EditProductForm from "@/components/EditProductForm";

const getProductById = async (id) => {
    try {
        const res = await fetch(`http://localhost:3000/api/products/${id}`, {
            cache: "no-store",
        });

        if (!res.ok) {
            throw new Error("Failed to fetch product");
        }

        return res.json();
    } catch (error) {
        console.log(error);
    }
};

export default async function EditProduct({ params }) {
    const { id } = params;
    const { product } = await getProductById(id);
    const { name, image, price,category } = product;

    return <EditProductForm id={id} name={name} image={image} price={price} category={category} />;
}
app\api\products\route.js
//app\api\products\route.js
import connectMongoDB from "@/libs/mongodb";
import Product from "@/models/ProductModel";
import { NextResponse } from "next/server";

export async function GET() {
    await connectMongoDB();
    const products = await Product.find();
    return NextResponse.json({ products });
}

export async function POST(request) {
    const { name, image,price,category } = await request.json();
    await connectMongoDB();
    await Product.create({ name, image, price, category });
    return NextResponse.json({ message: "Product Created" }, { status: 201 });
}

export async function DELETE(request) {
    const id = request.nextUrl.searchParams.get("id");
    await connectMongoDB();
    await Product.findByIdAndDelete(id);
    return NextResponse.json({ message: "Product deleted" }, { status: 200 });
}
app\api\products\[id]\route.js
//app\api\products\[id]\route.js
import connectMongoDB from "@/libs/mongodb";
import Product from "@/models/ProductModel";
import { NextResponse } from "next/server";

export async function PUT(request, { params }) {
    const { id } = params;
    const { newName: name, newImage: image, newPrice: price, newCategory: category } = await request.json();
    await connectMongoDB();
    await Product.findByIdAndUpdate(id, { name, image, price, category});
    return NextResponse.json({ message: "Product updated" }, { status: 200 });
}

export async function GET(request, { params }) {
    const { id } = params;
    await connectMongoDB();
    const product = await Product.findOne({ _id: id });
    return NextResponse.json({ product }, { status: 200 });
}
run C:\nextjs>npm run dev

Wednesday, January 17, 2024

Next.js 14 MongoDB User Registration

Next.js 14 MongoDB User Registration

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

edit tailwind.config.ts 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
app\layout.tsx
//app\layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

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

export const metadata: Metadata = {
  title: 'Next.js 14 MongoDB User Registration',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}
app\register\page.tsx
//app\register\page.tsx
import { Metadata } from 'next'
import Form from './Form'

export const metadata: Metadata = {
    title: 'Register',
}

export default async function Register() {
    return 
}
app\register\Form.tsx
//app\register\Form.tsx
'use client'

import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form' //npm install react-hook-form https://www.npmjs.com/package/react-hook-form
import toast from 'react-hot-toast'

type Inputs = {
    name: string
    email: string
    password: string
    confirmPassword: string
}

const Form = () => {
    const params = useSearchParams()
    const router = useRouter()
    let callbackUrl = params.get('callbackUrl') || '/'
    const {
        register,
        handleSubmit,
        getValues,
        formState: { errors, isSubmitting },
    } = useForm<Inputs>({
        defaultValues: {
            name: '',
            email: '',
            password: '',
            confirmPassword: '',
        },
    })

    const formSubmit: SubmitHandler<Inputs> = async (form) => {
        const { name, email, password } = form

        try {
            const res = await fetch('/api/auth/register', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    name,
                    email,
                    password,
                }),
            })
            console.log(res);
            console.log("Success login");
            if (res.ok) {
                return router.push(
                    `/signin?callbackUrl=${callbackUrl}&success=Account has been created`
                )
            } else {
                const data = await res.json()
                throw new Error(data.message)
            }
        } catch (err: any) {
            const error =
                err.message && err.message.indexOf('E11000') === 0
                    ? 'Email is duplicate'
                    : err.message
            toast.error(error || 'error')
        }
    }
    return (
        <div className="mx-auto max-w-2xl lg:max-w-7xl">
            <div className="flex justify-between items-center">
                <h1 className="font-bold py-10 text-2xl">Next.js 14 MongoDB User Registration</h1>
            </div>
        <div className="max-w-sm  mx-auto card bg-base-300 my-4">
            <div className="card-body">
                <h1 className="card-title">Register</h1>
                <form onSubmit={handleSubmit(formSubmit)}>
                    <div className="my-2">
                        <label className="label" htmlFor="name">
                            Name
                        </label>
                        <input
                            type="text"
                            id="name"
                            {...register('name', {
                                required: 'Name is required',
                            })}
                            className="input input-bordered w-full max-w-sm"
                        />
                        {errors.name?.message && (
                            <div className="text-error">{errors.name.message}</div>
                        )}
                    </div>
                    <div className="my-2">
                        <label className="label" htmlFor="email">
                            Email
                        </label>
                        <input
                            type="text"
                            id="email"
                            {...register('email', {
                                required: 'Email is required',
                                pattern: {
                                    value: /[a-z0-9]+@[a-z]+\.[a-z]{2,3}/,
                                    message: 'Email is invalid',
                                },
                            })}
                            className="input input-bordered w-full max-w-sm"
                        />
                        {errors.email?.message && (
                            <div className="text-error"> {errors.email.message}</div>
                        )}
                    </div>
                    <div className="my-2">
                        <label className="label" htmlFor="password">
                            Password
                        </label>
                        <input
                            type="password"
                            id="password"
                            {...register('password', {
                                required: 'Password is required',
                            })}
                            className="input input-bordered w-full max-w-sm"
                        />
                        {errors.password?.message && (
                            <div className="text-error">{errors.password.message}</div>
                        )}
                    </div>
                    <div className="my-2">
                        <label className="label" htmlFor="confirmPassword">
                            Confirm Password
                        </label>
                        <input
                            type="password"
                            id="confirmPassword"
                            {...register('confirmPassword', {
                                required: 'Confirm Password is required',
                                validate: (value) => {
                                    const { password } = getValues()
                                    return password === value || 'Passwords should match!'
                                },
                            })}
                            className="input input-bordered w-full max-w-sm"
                        />
                        {errors.confirmPassword?.message && (
                            <div className="text-error">{errors.confirmPassword.message}</div>
                        )}
                    </div>
                    <div className="my-2">
                        <button
                            type="submit"
                            disabled={isSubmitting}
                            className="btn btn-primary w-full"
                        >
                            {isSubmitting && (
                                <span className="loading loading-spinner"></span>
                            )}
                            Register
                        </button>
                    </div>
                </form>

                <div className="divider"> </div>
                <div>
                    Already have an account?{' '}
                    <Link className="link" href={`/signin?callbackUrl=${callbackUrl}`}>
                        Login
                    </Link>
                </div>
            </div>
        </div>
        </div>
    )
}

export default Form
app\signin\page.tsx
//app\signin\page.tsx
import { Metadata } from 'next'
import Form from './Form'

export const metadata: Metadata = {
    title: 'Sign in',
}

export default async function Signin() {
    return <Form />
}
app\signin\Form.tsx
//app\signin\Form.tsx
'use client'

import Link from 'next/link'

const Form = () => {
    return (
        <div className="max-w-sm  mx-auto card bg-base-300">
            <div className="card-body">
                <h1 className="card-title">Sign in</h1>
                <form>
                    <div className="my-2">
                        <label className="label" htmlFor="email">
                            Email
                        </label>
                        <input
                            type="text"
                            id="email"
                            className="input input-bordered w-full max-w-sm"
                        />
                    </div>
                    <div className="my-2">
                        <label className="label" htmlFor="password">
                            Password
                        </label>
                        <input
                            type="password"
                            id="password"
                            className="input input-bordered w-full max-w-sm"
                        />
                    </div>
                    <div className="my-4">
                        <button
                            type="submit"
                            className="btn btn-primary w-full"
                        >
                            Sign in
                        </button>
                    </div>
                </form>
                <div>
                    <Link className="link" href={'/register'}>
                        Register
                    </Link>
                </div>
            </div>
        </div>
    )
}
export default Form
app\api\auth\register\route.ts
//app\api\auth\register\Form.ts
import { NextRequest } from 'next/server'
import bcrypt from 'bcryptjs' //npm i bcryptjs https://www.npmjs.com/package/bcryptjs
import dbConnect from '@/lib/dbConnect'
import UserModel from '@/lib/models/UserModel'

export const POST = async (request: NextRequest) => {
    const { name, email, password } = await request.json()
    console.log(name);
    await dbConnect()
    const hashedPassword = await bcrypt.hash(password, 5)
    const newUser = new UserModel({
        name,
        email,
        password: hashedPassword,
    })
    try {
        await newUser.save()
        return Response.json(
            { message: 'User has been created' },
            {
                status: 201,
            }
        )
    } catch (err: any) {
        return Response.json(
            { message: err.message },
            {
                status: 500,
            }
        )
    }
}
lib\dbConnect.ts
//lib\dbConnect.ts
import mongoose from 'mongoose'

async function dbConnect() {
    try {
        await mongoose.connect(process.env.MONGODB_URI!)
    } catch (error) {
        throw new Error('Connection failed!')
    }
}

export default dbConnect
lib\models\UserModel.ts
//lib\models\UserModel.ts
import mongoose from 'mongoose'

export type User = {
    _id: string
    name: string
    email: string
    isAdmin: boolean
}

const UserSchema = new mongoose.Schema(
    {
        name: {
            type: String,
            required: true,
        },
        email: {
            type: String,
            required: true,
            unique: true,
        },
        password: {
            type: String,
            required: true,
        },
        isAdmin: { type: Boolean, required: true, default: false },
    },
    { timestamps: true }
)

const UserModel = mongoose.models?.User || mongoose.model('User', UserSchema)

export default UserModel
run C:\nextjs>npm run dev

Tuesday, January 16, 2024

Next.js 14 Pagination MongoDB

Next.js 14 Pagination MongoDB

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

edit tailwind.config.ts 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/
app\page.tsx
//
import { dbConnect } from "@/lib/connectMongo";
import Link from "next/link";
import Image from 'next/image'

async function getData(perPage, page) {
    try {
        // DB Connect
        const client = await dbConnect();
        const db = client.db("nextjs14");

        // DB Query
        const items = await db
            .collection("products")
            .find({})
            .skip(perPage * (page - 1))
            .limit(perPage)
            .toArray();

        const itemCount = await db.collection("products").countDocuments({});

        const respnse = { items, itemCount };
        //console.log(items);
        console.log(itemCount);
        return respnse;
    } catch (error) {
        throw new Error("Failed to fetch data. Please try again later.");
    }
}

export default async function Page({ searchParams }) {
    let page = parseInt(searchParams.page, 10);
    page = !page || page < 1 ? 1 : page;
    const perPage = 4;
    const data = await getData(perPage, page);

    const totalPages = Math.ceil(data.itemCount / perPage);

    const prevPage = page - 1 > 0 ? page - 1 : 1;
    const nextPage = page + 1;
    const isPageOutOfRange = page > totalPages;

    const pageNumbers = [];
    const offsetNumber = 3;
    for (let i = page - offsetNumber; i <= page + offsetNumber; i++) {
        if (i >= 1 && i <= totalPages) {
            pageNumbers.push(i);
        }
    }

    return (
        <>
            <div className="container mx-auto">
                <div className="flex justify-between items-center">
                    <h1 className="font-bold py-10 text-2xl">Next.js 14 Pagination MongoDB</h1>
                </div>
                <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
                    <thead className="bg-gray-100 dark:bg-gray-700">
                        <tr>
                            <th scope="col" className="p-4">
                                <div className="flex items-center">
                                    <input id="checkbox-all" type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                                    <label htmlFor="checkbox-all" className="sr-only">checkbox</label>
                                </div>
                            </th>
                            <th className="py-3 text-left">Image</th>
                            <th className="py-3 text-left">Product Name</th>
                            <th className="py-3 text-left">Price</th>
                            <th className="py-3 text-left">Category</th>
                            <th className="py-3 text-left">Actions</th>
                        </tr>
                    </thead>
                    <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
                        {data.items.map((item) => (
                            <tr key={item._id} className="hover:bg-gray-100 dark:hover:bg-gray-700">
                                    <td className="p-4 w-4">
                                        <div className="flex items-center">
                                            <input id="checkbox-table-1" type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                                            <label htmlFor="checkbox-table-1" className="sr-only">checkbox</label>
                                        </div>
                                    </td>
                                    <td>
                                        <Image
                                            src={item.image}
                                            alt={item.name}
                                            width={80}
                                            height={80}
                                            className="rounded-lg"
                                        />
                                    </td>
                                    <td>{item.name}</td>
                                    <td>${item.price}</td>
                                    <td>{item.category}</td>
                                    <td>
                                        Delete
                                    </td>
                                </tr>
                        ))}
                    </tbody>
                </table>

                {isPageOutOfRange ? (
                    <div>No more pages...</div>
                ) : (

                    <div className="flex justify-center items-center mt-16">

                        <div className="flex border-[1px] gap-4 rounded-[10px] border-light-green p-4">
                            {page === 1 ? (
                                <div className="opacity-60 py-2 px-5" aria-disabled="true">
                                    Previous
                                </div>
                            ) : (
                                <Link href={`?page=${prevPage}`} className="py-2 px-5" aria-label="Previous Page">
                                    Previous
                                </Link>
                            )}
                                
                            {pageNumbers.map((pageNumber, index) => (
                                <Link
                                    key={index}
                                    className={
                                        page === pageNumber
                                            ? "bg-blue-500 font-bold py-2 px-5 rounded text-white"
                                            : "bg-gray-500 hover:bg-gray-400 font-bold py-2 px-5 rounded text-white"
                                    }
                                    href={`?page=${pageNumber}`}
                                >
                                    {pageNumber}
                                </Link>
                            ))}

                            {page === totalPages ? (
                                <div className="opacity-60 py-2 px-5" aria-disabled="true">
                                    Next
                                </div>
                            ) : (
                                <Link href={`?page=${nextPage}`} className="py-2 px-5" aria-label="Next Page">
                                    Next
                                </Link>
                            )}
                        </div>
                    </div>

                )}

            </div>
        </>
    );
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

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

export const metadata: Metadata = {
  title: 'Next.js 14 MongoDB Pagination',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}
lib\connectMongo.ts
//lib\connectMongo.ts
import { MongoClient } from "mongodb";
const MONGODB_URI = process.env.MONGODB_URI;
let client = null;

export async function dbConnect() {
    if (client) {
        return client;
    }

    if (!MONGODB_URI) {
        console.log("MongoDb URI not found.");
    }

    try {
        client = await MongoClient.connect(MONGODB_URI);
        console.log("Connected to MongoDb successfully.");
        return client;
    } catch (error) {
        console.error("Error connecting to the database:", error);
    }
}
run C:\nextjs>npm run dev

Monday, January 8, 2024

Next.js 14 Server Actions MongoDB - List all data, Create and Delete

Next.js 14 Server Actions MongoDB - List all data, Create and Delete

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

Install the following
npm i mongoose-zod
https://github.com/andreww2012/mongoose-zod
A library which allows to author mongoose ("a MongoDB object modeling tool") schemas using zod ("a TypeScript-first schema declaration and validation library").

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

npm install react-hot-toast
https://www.npmjs.com/package/react-hot-toast
Add beautiful notifications to your React app with react-hot-toast.

edit tailwind.config.ts 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
app\page.tsx
//app\page.tsx
import Image from 'next/image' 
//https://www.npmjs.com/package/react-hot-toast
//https://www.npmjs.com/package/react-daisyui
//https://github.com/andreww2012/mongoose-zod
//https://www.npmjs.com/package/mongoose
//npm i mongoose zod daisyui react-hot-toast 
import { Toaster } from 'react-hot-toast'
import CreateForm from './create-form'
import dbConnect from '@/lib/db-connect'
import ProductModel, { Product } from '@/lib/product-model'
import DeleteForm from './delete-form'

export default async function Home() {
  await dbConnect()
  const products = (await ProductModel.find({}).sort({
    _id: -1,
  })) as Product[]
  return (
    <div className="mx-auto max-w-2xl lg:max-w-7xl">
      <div className="flex justify-between items-center">
        <h1 className="font-bold py-10 text-2xl">Next.js 14 Server Actions MongoDB - List all data, Create Product and Delete</h1>
        <Toaster />
        <CreateForm />
      </div>
      <div className="inline-block min-w-full align-middle">
      <table className="min-w-full divide-y divide-gray-200 table-fixed dark:divide-gray-700">
        <thead className="bg-gray-100 dark:bg-gray-700">
            <tr>
              <th scope="col" className="p-4">
                <div className="flex items-center">
                  <input id="checkbox-all" type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                  <label htmlFor="checkbox-all" className="sr-only">checkbox</label>
                </div>
              </th>
              <th className="py-3 text-left">Image</th>
              <th className="py-3 text-left">Product Name</th>
              <th className="py-3 text-left">Price</th>
              <th className="py-3 text-left">Category</th>
              <th className="py-3 text-left">Actions</th>
            </tr>
        </thead>
        <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
          {products.length === 0 ? (
            <tr>
              <td colSpan={5}>No product found</td>
            </tr>
          ) : (
            products.map((product: Product) => (
              <tr key={product._id} className="hover:bg-gray-100 dark:hover:bg-gray-700"> 
                <td className="p-4 w-4">
                  <div className="flex items-center">
                    <input id="checkbox-table-1" type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                    <label htmlFor="checkbox-table-1" className="sr-only">checkbox</label>
                  </div>
                </td>
                <td>
                  <Image
                    src={product.image}
                    alt={product.name}
                    width={80}
                    height={80}
                    className="rounded-lg"
                  />
                </td>
                <td>{product.name}</td>
                <td>${product.price}</td>
                <td>{product.category}</td>
                <td>
                  <DeleteForm
                    _id={product._id.toString()}
                    name={product.name}
                  />
                </td>
              </tr>
            ))
          )}
        </tbody>
      </table>
    </div>
    </div>
  )
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

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

export const metadata: Metadata = {
  title: 'Next.js 14 Server Actions MongoDB - List all data, Create and Delete',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}
app\create-form.tsx
//
'use client'

import { useFormState, useFormStatus } from 'react-dom'
import { createProduct } from '@/lib/actions'
import { useEffect, useRef } from 'react'
import toast from 'react-hot-toast'

export default function CreateForm() {
    const [state, formAction] = useFormState(createProduct, {
        message: '',
    })
    const { pending } = useFormStatus()
    const ref = useRef<HTMLFormElement>(null)
    useEffect(() => {
        if (state.message.indexOf('Created product') === 0) {
            ; (document.getElementById('my_modal') as any)!.close()
            ref.current?.reset()
            toast(state.message)
        } else if (state.message) {
            toast(state.message)
        }
    }, [state.message])

    return (
        <div>
            <button
                className="btn btn-primary"
                onClick={() =>
                    (document.getElementById('my_modal')! as any).showModal()
                }
            >
                Create Product
            </button>
            <dialog id="my_modal" className="modal">
                <div className="modal-box">
                    <h2 className="tex-2xl font-bold pm-4">Create Product</h2>
                    <form ref={ref} action={formAction}>
                        <div className="form-control w-full max-w-xs py-4">
                            <label htmlFor="name">Name</label>
                            <input
                                type="text"
                                id="name"
                                name="name"
                                className="input input-bordered w-full max-w-xs"
                                required
                            />
                        </div>
                        <div className="form-control w-full max-w-xs py-4">
                            <label htmlFor="image">Image</label>
                            <input
                                type="text"
                                id="image"
                                name="image"
                                className="input input-bordered w-full max-w-xs"
                                required
                                defaultValue="/images/1.jpg"
                            />
                        </div>
                        <div className="form-control w-full max-w-xs py-4">
                            <label htmlFor="price">Price</label>
                            <input
                                type="number"
                                id="price"
                                name="price"
                                className="input input-bordered w-full max-w-xs"
                                required
                                defaultValue="1"
                            />
                        </div>
                        <div className="form-control w-full max-w-xs py-4">
                            <label htmlFor="name">Category</label>
                            <input
                                type="text"
                                id="category"
                                name="category"
                                className="input input-bordered w-full max-w-xs"
                                required
                            />
                        </div>
                        <button
                            className="btn btn-primary mr-3"
                            type="submit"
                            disabled={pending}
                        >
                            Create
                        </button>
                        <button
                            type="button"
                            className="btn btn-ghost"
                            onClick={() =>
                                (document.getElementById('my_modal') as any).close()
                            }
                        >
                            Back
                        </button>
                    </form>
                </div>
            </dialog>
        </div>
    )
}
app\delete-form.tsx
//app\delete-form.tsx
'use client'

import { deleteProduct } from '@/lib/actions'
import { useFormStatus } from 'react-dom'
import toast from 'react-hot-toast'

export default function DeleteForm({
    _id,
    name,
}: {
    _id: string
    name: string
}) {
    const { pending } = useFormStatus()
    return (
        <form
            action={async (formData) => {
                const res = await deleteProduct(formData)
                toast(res.message)
            }}
        >
            <input type="hidden" name="_id" value={_id} />
            <input type="hidden" name="name" value={name} />
            <button type="submit" disabled={pending} className="btn btn-ghost">
                Delete
            </button>
        </form>
    )
}
lib\db-connect.ts
//lib\db-connect.ts
import mongoose from 'mongoose'

export default async function dbConnect() {
    try {
        await mongoose.connect(process.env.MONGODB_URI!)
        console.log("Success Connection");
    } catch (error) {
        throw new Error('Connection failed!')
    }
}
lib\product-model.ts
//lib\product-model.ts
import mongoose from 'mongoose'

export type Product = {
    _id: string
    name: string
    image: string
    price: number
    category: string
}

const productSchema = new mongoose.Schema(
    {
        name: { type: String, required: true, unique: true },
        image: { type: String, required: true },
        price: { type: Number, required: true },
        category: { type: String, required: true },
    },
    {
        timestamps: true,
    }
)

const ProductModel =
    mongoose.models.Product || mongoose.model('Product', productSchema)
export default ProductModel
lib\actions.ts
//lib\actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import ProductModel from './product-model'
import dbConnect from './db-connect'
import { z } from 'zod'

export async function createProduct(prevState: any, formData: FormData) {
    const schema = z.object({
        name: z.string().min(3),
        image: z.string().min(1),
        price: z.number().min(1),
        category: z.string().min(1),
    })
    const parse = schema.safeParse({
        name: formData.get('name'),
        image: formData.get('image'),
        price: Number(formData.get('price')),
        category: formData.get('category'),
    })
    if (!parse.success) {
        console.log(parse.error)
        return { message: 'Form data is not valid' }
    }
    const data = parse.data
    try {
        await dbConnect()
        const product = new ProductModel(data)
        await product.save()
        revalidatePath('/')
        return { message: `Created product ${data.name}` }
    } catch (e) {
        return { message: 'Failed to create product' }
    }
}

export async function deleteProduct(formData: FormData) {
    const schema = z.object({
        _id: z.string().min(1),
        name: z.string().min(1),
    })
    const data = schema.parse({
        _id: formData.get('_id'),
        name: formData.get('name'),
    })

    try {
        await dbConnect()
        await ProductModel.findOneAndDelete({ _id: data._id })
        revalidatePath('/')
        console.log({ message: `Deleted product ${data.name}` })
        return { message: `Deleted product ${data.name}` }
    } catch (e) {
        return { message: 'Failed to delete product' }
    }
}
run C:\nextjs>npm run dev

Saturday, October 30, 2021

Next.js How to call API and display records in table

Next.js How to call API and display records in table

Install axios

https://www.npmjs.com/package/axios

npm i axios

C:\nextjs>npm i axios
C:\nextjs>npm run dev

getInitialProps method to call the API.
getInitialProps method will be called at both server and client side.

pages/index.js
//pages/index.js
import axios from "axios"; //npm i axios

const Index = ({ userList }) => <div style={{ margin: 20 }}>
  <h3>Next.js How to call API and display records in table</h3>
  <table border="1">
    <thead>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Email</th>
      <th>Avatar</th>
    </thead>
    <tbody>
      {userList.data.map((x, i) => <tr key={i}>
        <td>{x.first_name}</td>
        <td>{x.last_name}</td>
        <td>{x.email}</td>
        <td><img src={x.avatar} width="50" height="50" /></td>
      </tr>)}
    </tbody>
  </table>
</div>
 
Index.getInitialProps = async () => {
  const { data } = await axios.get('https://reqres.in/api/users');
  return { userList: data };
}
 
export default Index;

Next.js Simple Shared components

Next.js Simple Shared components

Shared components is to organize pages together.

components/Layout.js
//components/Layout.js
import Link from "next/link";
import Head from "next/head";
 
const Layout = ({ children, title }) => {
  return <div className="p-3">
    <Head>
      <title>{title} - Cairocoders</title>
    </Head>
    <h3>Next.js Simple Shared components </h3>
    <br />
 
    <Link href="/">
      <a style={{ marginRight: 15 }}>Home</a>
    </Link>
    <Link href="/about">
      <a style={{ marginRight: 15 }}>About</a>
    </Link>
    <Link href="/contact">
      <a>Contact</a>
    </Link>
 
    <h1>{title}</h1>
    {children}
 
    <div style={{ marginTop: 30 }}>
      © {new Date().getFullYear()}
    </div>
  </div>
}
 
export default Layout;
pages/index.js
//pages/index.js
import Layout from "../components/Layout";
 
const Index = () => <Layout title="Home">
  <p>Welcome to the Home page!</p>
</Layout>
 
export default Index;
pages/about.js
//pages/about.js
import Layout from "../components/Layout";
 
const About = () => <Layout title="About">
  <p>This is About page!</p>
</Layout>
 
export default About;
pages/contact.js
//pages/contact.js
import Layout from "../components/Layout";
 
const Contact = () => <Layout title="Contact">
  <p>This is Contact page!</p>
</Layout>
 
export default Contact;

Next.js Simple Navigation Page to Page using Link

Next.js Simple Navigation Page to Page using Link

pages/index.js
//pages/index.js
import Link from "next/link";
 
const Index = () => <div>
 
  <h3>Next.js Simple Navigate page to page using link </h3>
  <br />
 
  <Link href="/">
    <a style={{ marginRight: 15 }}>Home</a>
  </Link>
  <Link href="/about">
    <a style={{ marginRight: 15 }}>About</a>
  </Link>
  <Link href="/contact">
    <a>Contact</a>
  </Link>
 
  <h1>Home page!</h1>
</div>
 
export default Index;
pages/about.js
//pages/about.js
import Link from "next/link";
 
const About = () => <div>
 
  <h3>Next.js Simple Navigate page to page using link </h3>
  <br />
 
  <Link href="/">
    <a style={{ marginRight: 15 }}>Home</a>
  </Link>
  <Link href="/about">
    <a style={{ marginRight: 15 }}>About</a>
  </Link>
  <Link href="/contact">
    <a>Contact</a>
  </Link>
 
  <h1>About page!</h1>
</div>
 
export default About;
pages/contact.js
//pages/contact.js
import Link from "next/link";
 
const Contact = () => <div>
 
  <h3>Next.js Simple Navigate page to page using link </h3>
  <br />
 
  <Link href="/">
    <a style={{ marginRight: 15 }}>Home</a>
  </Link>
  <Link href="/about">
    <a style={{ marginRight: 15 }}>About</a>
  </Link>
  <Link href="/contact">
    <a>Contact</a>
  </Link>
 
  <h1>Contact page!</h1>
</div>
 
export default Contact;

Monday, October 25, 2021

Next.js How to setup a project

Next.js How to setup a project

What is Next.js
Next.js is the open source react framework which helps us to create static, dynamic and web applications. It’s used for server rendered applications, SEO website, PWA and many more.

Find more about the Next.js https://nextjs.org/

set up environment node --version

Create package.json and install dependencies : npm init -y

install next, react and react-dom in your project : npm install next react react-dom

Update scripts in package.json

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

dev next – Start Next.js project in development mode.
build next build – Build the application for production usage.
start next start – Start the Next.js production server.

Run project : npm run dev


nextjs/package.json
//nextjs/package.json
{
  "name": "nextjs",
  "version": "1.0.0",
  "description": "This is the first Next.js application",
  "main": "index.js",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },
  "keywords": [],
  "author": "Cairocoders",
  "license": "ISC",
  "dependencies": {
    "next": "^11.1.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}
nodejs/pages/index.js
//nodejs/pages/index.js
const Index = () => <h1>Welcome to the First Next.js Application</h1>
 
export default Index;

Related Post