article

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

Friday, February 21, 2025

Nextjs 15 Laravel 11 Rest API CRUD (Create, Read, Update and Delete) with Pagination and View | Tanstack Query Tailwind CSS

Nextjs 15 Laravel 11 Rest API CRUD (Create, Read, Update and Delete) with Pagination and View | Tanstack Query Tailwind CSS

Install nextjs npx create-next-app@latest nextjs_org/docs/getting-started/installation

Install react-query

npm i @tanstack/react-query
tanstack com/query/latest/docs/framework/react/installation
app\page.tsx
//app\page.tsx
"use client";

import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import CustomerList from "./components/CustomerList";
import { getCustomerList } from "./api/customer";
import Paginationnumber from "./components/Paginationnumber";
import LoadingSpinner from "./components/LoadingSpinner";
import { useSearchParams } from 'next/navigation'
import Link from "next/link";

export default function Home() {
    const searchParams = useSearchParams()
    const page = searchParams.get('page')

    const currentPage = Number(page) || 1;

    const { isLoading, data, isError, isFetching,  error } = useQuery({
        queryKey: ["customers", currentPage],
        queryFn: () => getCustomerList(currentPage)
    });
    //console.log(data);
    if (isLoading) return "Loading...";
    if (isError) return `Error: ${error.message}`;
    
    //const perpage = 3;
    const totalPages = Math.ceil(Number(data.totalpage) / Number(data.perpage));
  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 pl-10 pr-10">
            <h1 className="text-4xl font-bold">Nextjs 15 Laravel 11 Rest API CRUD (Create, Read, Update and Delete) with Pagination and View | Tanstack Query Tailwind CSS</h1>
        </div> 
        <div className="overflow-x-auto pt-10">
            <div className="mb-2 w-full text-right">
                <Link
                href="/customer/create"
                className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
                Add New
                </Link>
            </div>
            <CustomerList customerlist={data.customerlist} />
            <div className="flex items-center justify-between my-5">
                <Paginationnumber totalPages={totalPages} />
                {isFetching ? <LoadingSpinner /> : null}
            </div>
        </div>
    </div>
  );
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import QueryProvider from "./components/QueryProvider";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  );
}
app\components\QueryProvider.tsx
//app\components\QueryProvider.tsx
"use client";

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState } from "react";

interface Props {
  children: React.ReactNode;
}

export default function QueryProvider({ children }: Props) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
  );
}
app\components\CustomerList.tsx
//app\components\CustomerList.tsx
import Link from "next/link";
import { useMutation, useQueryClient } from "@tanstack/react-query"; //npm i @tanstack/react-query 
import { deleteCustomer } from "../api/customer";

const CustomerList = ({ customerlist }) => { 
    const queryClient = useQueryClient();

    const deleteCustomerMutation = useMutation({
      mutationFn: deleteCustomer,
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['customers']});
      }
    });
 
    const handleDelete = (id) => {
        deleteCustomerMutation.mutate(id)
    }

  return (
    <>
        <table className="table table-zebra">
            <thead className="text-sm text-gray-700 uppercase bg-gray-50">
                <tr>
                    <th className="py-3 px-6">ID</th>
                    <th className="py-3 px-6">Name</th>
                    <th className="py-3 px-6">Price</th>
                    <th className="py-3 px-6 text-center">Actions</th>
                </tr>
            </thead>
            <tbody>
                {customerlist.map((item) => (
                    <tr key={item.id} className="bg-white border-b text-black">
                        <td className="py-3 px-6">
                        {item.id}
                        </td>
                        <td className="py-3 px-6">{item.name}</td>   
                        <td className="py-3 px-6">{item.email}</td>
                        <td className="flex justify-center gap-1 py-3">
                            <Link className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
                                href={`/customer/${item.id}`}>Read</Link> 
                             
                            <Link className="focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:focus:ring-yellow-900"
                                href={`/customer/edit/${item.id}/`}>
                                Edit
                            </Link>
                            <button onClick={() => handleDelete(item.id)} className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
                                Delete</button>
                        </td>
                    </tr>
                ))}
            </tbody>
        </table>
    </>
  )
}
 
export default CustomerList
app\components\LoadingSpinner.tsx
//app\components\LoadingSpinner.tsx
export default function LoadingSpinner() {
    return (
        <h1>Loading...</h1>
    )
} 
app\customer\create\page.tsx
//app\customer\create\page.tsx
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query"
import CustomertForm from "../../components/CustomertForm"
import { createCustomer } from "../../api/customer"
import { useRouter } from 'next/navigation'

const AddCustomer= () => {
  const queryClient = useQueryClient();
  const router = useRouter();

  const createUserMutation = useMutation({
    mutationFn: createCustomer,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['customers']});
      //console.log("success!")
      router.push('/')
    }
  });
 
  const handleAddPost = (customer) => {
    //console.log(customer)
    createUserMutation.mutate({
      ...customer
    })
  }
   
  return (
    <div className="max-w-md mx-auto mt-5 mb-5">
      <h1 className="text-2xl text-center mb-2">Add New User</h1>
      <CustomertForm onSubmit={handleAddPost} initialValue={{}} />
    </div>
  )
}
  
export default AddCustomer
app\components\CustomertForm.tsx
//app\components\CustomertForm.tsx
"use client";

import { useState } from "react"
  
const CustomertForm = ({ onSubmit, initialValue }) => {
  const [customer, setCustomer] = useState({
    name: initialValue.name || "",
    email: initialValue.email || ""
  });
  //console.log(customer);
  const handleChangeInput = (e) => {
    setCustomer({
      ...customer,
      [e.target.name]: e.target.value
    })
  }
  
  const renderField = (label) => (
    <div>
      <label className="block text-sm font-medium text-gray-900">{label}</label>
      <input onChange={handleChangeInput} type="text" name={label.toLowerCase()} value={customer[label.toLowerCase()]} 
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500blue-500"
      />
    </div>
  );
  
  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(customer);
    setCustomer({
      name: "",
      email: ""
    })
  
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <div className="mb-5">{renderField('Name')}</div>
      <div className="mb-5">{renderField('Email')}</div>
      <button type="submit"
        className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
      >
        Submit</button>
    </form>
  )
}
  
export default CustomertForm
app\customer\[id]\page.tsx
//app\customer\[id]\page.tsx
"use client";
 
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { useParams } from 'next/navigation'
import { fetchCustomer } from "../../api/customer";

const ViewCustomer = () => {
 
  const {id}=useParams();
 
  const {
    isLoading,
    isError,
    data: customer,
    error,
  } = useQuery({
    queryKey: ["customer", id],
    queryFn: () => fetchCustomer(id),
  });
  
  if (isLoading) return "loading...";
  if (isError) return `Error: ${error.message}`;
  
  //console.log(customer)
  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-100">
      <div className="bg-white rounded-sm shadow p-8 text-black">
      <div className="max-w-2xl mx-auto mt-5">
        <h1 className="text-2xl text-center mb-2">View Customer</h1>
        <h2>{customer.results.id}</h2>
        <h2>{customer.results.name}</h2>
        <h2>{customer.results.email}</h2>
      </div>
      </div>
    </div>
  )
}
  
export default ViewCustomer
app\customer\edit\[id]\page.tsx
//app\customer\edit\[id]\page.tsx
"use client";
 
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useParams, useRouter } from 'next/navigation'
import { fetchCustomer, updateCustomer } from "../../../api/customer";
import CustomertForm from "../../../components/CustomertForm"
 
const EditCustomer = () => {
  const {id}=useParams();
  //console.log(id);
 
  const queryClient = useQueryClient();
  const router = useRouter();
 
  const {
    isLoading,
    isError,
    data: customer,
    error,
  } = useQuery({
    queryKey: ["customers", id],
    queryFn: () => fetchCustomer(id),
  });
 
  //console.log(customer);

  const updateCustomerMutation = useMutation({
    mutationFn: updateCustomer,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['customers']});
      console.log("success");
      router.push('/')
    }
  })
 
  if (isLoading) return "loading...";
  if (isError) return `Error: ${error.message}`;
 
  const handleSubmit = (updatedCustomer) => {
    updateCustomerMutation.mutate({id, ...updatedCustomer})
  }
 
  return (
    <div className="max-w-md mx-auto mt-5">
        <div className="flex items-center justify-between gap-1 mb-5">
            <h1 className="text-4xl font-bold">{customer.name}</h1>
        </div> 
        <CustomertForm onSubmit={handleSubmit} initialValue={customer.results} />
    </div>
  )
}
  
export default EditCustomer
app\components\Paginationnumber.tsx
//app\components\Paginationnumber.tsx
"use client";
 
import Link from "next/link";
import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; //npm install react-icons --save npmjs com/package/react-icons
import { usePathname, useSearchParams } from "next/navigation";
import clsx from "clsx"; //npm i clsx npmjs com/package/clsx
 
export const generatePagination = (currentPage: number, totalPages: number) => {
  if (totalPages <= 7) {
    return Array.from({ length: totalPages }, (_, i) => i + 1);
  }
  
  if (currentPage <= 3) {
    return [1, 2, 3, "...", totalPages - 1, totalPages];
  }
 
  if (currentPage >= totalPages - 2) {
    return [1, 2, 3, "...", totalPages - 2, totalPages - 1, totalPages];
  }
 
  return [
    1,
    "...",
    currentPage - 1,
    currentPage,
    currentPage + 1,
    "...",
    totalPages,
  ];
};

const Paginationnumber = ({ totalPages }: { totalPages: number }) => {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get("page")) || 1;
 
  const createPageURL = (pageNumber: string | number) => {
    const params = new URLSearchParams(searchParams);
    params.set("page", pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };
 
  const allPages = generatePagination(currentPage, totalPages);
 
  const PaginationNumber = ({
    page,
    href,
    position,
    isActive,
  }: {
    page: number | string;
    href: string;
    position?: "first" | "last" | "middle" | "single";
    isActive: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "rounded-l-sm": position === "first" || position === "single",
        "rounded-r-sm": position === "last" || position === "single",
        "z-10 bg-blue-700 border-blue-500 text-white bg-blue-700": isActive,
        "hover:bg-blue-700": !isActive && position !== "middle",
        "text-gray-300 pointer-events-none": position === "middle",
      }
    );
 
    return isActive && position === "middle" ? (
      <div className={className}>{page}</div>
    ) : (
      <Link href={href} className={className}>
        {page}
      </Link>
    );
  };
   
  const PaginationArrow = ({
    href,
    direction,
    isDisabled,
  }: {
    href: string;
    direction: "left" | "right";
    isDisabled?: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "pointer-events-none text-blue-300": isDisabled,
        "hover:bg-blue-700": !isDisabled,
        "mr-2": direction === "left",
        "ml-2": direction === "right",
      }
    );
 
    const icon =
      direction === "left" ? (
        <HiChevronLeft size={20} />
      ) : (
        <HiChevronRight size={20} />
      );
 
    return isDisabled ? (
      <div className={className}>{icon}</div>
    ) : (
      <Link href={href} className={className}>
        {icon}
      </Link>
    );
  };
 
  return (
    <div className="inline-flex">
      <PaginationArrow
        direction="left"
        href={createPageURL(currentPage - 1)}
        isDisabled={currentPage <= 1}
      />
      <div className="flex -space-x-px">
        {allPages.map((page, index) => {
          let position: "first" | "last" | "single" | "middle" | undefined;
 
          if (index === 0) position = "first";
          if (index === allPages.length - 1) position = "last";
          if (allPages.length === 1) position = "single";
          if (page === "...") position = "middle";
 
          return (
            <PaginationNumber
              key={index}
              href={createPageURL(page)}
              page={page}
              position={position}
              isActive={currentPage === page} 
            />
          );
        })}
      </div>
      <PaginationArrow
        direction="right"
        href={createPageURL(currentPage + 1)}
        isDisabled={currentPage >= totalPages}
      />
    </div>
  );
};
 
export default Paginationnumber;
app\api\customer.tsx
//app\api\customer.tsx
export async function getCustomerList(page) {
  const response = await fetch(`http://127.0.0.1:8000/api/customers?page=${page}`);
  const data = await response.json();
  //console.log(data.data);
  return {
    customerlist: data.results.data,
    totalpage: data.results.total, 
    perpage: data.results.per_page
  }
}

export async function fetchCustomer(id) {
  const response = await fetch("http://127.0.0.1:8000/api/customers/"+id);
  return response.json()
}

export async function createCustomer(newCustomer) {
  const response = await fetch("http://127.0.0.1:8000/api/customerssave", {
    method: "POST",
    headers: { 
      "Content-Type": "application/json"
    },
    body: JSON.stringify(newCustomer)
  });
  console.log(response);
  return response.json()
}
 
export async function updateCustomer(updatedCustomer) {
  const response = await fetch(`http://127.0.0.1:8000/api/customersupdate/${updatedCustomer.id}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(updatedCustomer)
  });
  console.log(response)
  return response.json()
}
 
export async function deleteCustomer(id) {
  const response = await fetch(`http://127.0.0.1:8000/api/customers/${id}`, {
    method: "DELETE",
  });
  return response.json()
}
run C:\nextjs>npm run dev
Download Laravel App

https://laravel.com/docs/11.x/installation

Connecting our Database

open .env file root directory.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=laravel11dev
DB_USERNAME=root
DB_PASSWORD=root

Create Model and Migration

C:\xampp\htdocs\laravel\laravelproject>php artisan make:model Customer -m

A new file named Product.php will be created in the app directory and database/migrations directory to generate the table in our database
app/Models/Customer.php
//app/Models/Customer.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    protected $fillable = [
        'name',
        'email',
    ];
}
database\migrations\create_customers_table.php
//database\migrations\create_customers_table.ph
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customers');
    }
};
Database Migration
php artisan migrate

C:\xampp\htdocs\laravel\laravel10project>php artisan migrate
Migration table created successfully.
check database table
Create Controller
C:\xampp\htdocs\laravel\laravel10project>php artisan make:controller CustomerController

app\Http\Controllers\CustomerController.php
//app\Http\Controllers\CustomerController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Customer;
use Illuminate\Support\Facades\Validator;

class CustomerController extends Controller
{
    public function index()
    {
        //$customer = Customer::all(); // All Product
        $customer = Customer::paginate(3);

        // Return Json Response
        return response()->json([
            'results' => $customer
        ], 200);
    }

    public function show($id)
    {
        $customer = Customer::find($id);
        if (!$customer) {
            return response()->json([
                'message' => 'Product Not Found.'
            ], 404);
        }

        // Return Json Response
        return response()->json([
            'results' => $customer
        ], 200);
    }

    public function save(Request $request)
    {

        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|unique:customers|max:255',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'status' => false,
                'message' => 'Validation error',
                'errors' => $validator->errors()
            ], 422);
        }

        $customer = Customer::create([
            'name' => $request->name,
            'email' => $request->email
        ]);

        return response()->json([
            'status' => true,
            'message' => 'Customer created successfully',
            'data' => $customer
        ], 201);
    }

    public function update(Request $request, $id)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:customers,email,' . $id,
        ]);

        if ($validator->fails()) {
            return response()->json([
                'status' => false,
                'message' => 'Validation error',
                'errors' => $validator->errors()
            ], 422);
        }

        $customer = Customer::findOrFail($id);
        $customer->update($request->all());
        
        return response()->json([
            'status' => true,
            'message' => 'Customer updated successfully',
            'data' => $customer
        ], 200);
    }

    public function destroy($id)
    {
        $customer = Customer::findOrFail($id);
        $customer->delete();

        return response()->json([
            'status' => true,
            'message' => 'Customer deleted successfully'
        ], 204);
    }
}
Routes
install
php artisan install:api
All API requests will need the header Accept: application/json.
open routes/api.php and update the following code
routes\api.php
//routes\api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CustomerController;

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');


Route::get('/customers', [CustomerController::class, 'index']);
Route::get('/customers/{id}', [CustomerController::class, 'show']);
Route::post('/customers', [CustomerController::class, 'store']);
Route::post('/customerssave', [CustomerController::class, 'save']);
Route::put('/customersupdate/{id}', [CustomerController::class, 'update']);
Route::delete('/customers/{id}', [CustomerController::class, 'destroy']);
Run C:\xampp\htdocs\laravel\myapp>php artisan serve
Starting Laravel development server: http://127.0.0.1:8000

Monday, February 10, 2025

Nextjs 15 Laravel 11 Rest API Pagination View Product | Tanstack Query Tailwind CSS

Nextjs 15 Laravel 11 Rest API Pagination View Product | Tanstack Query Tailwind CSS

Install nextjs npx create-next-app@latest nextjs_org/docs/getting-started/installation

Install react-query

npm i @tanstack/react-query
tanstack com/query/latest/docs/framework/react/installation
app\page.tsx
//app\page.tsx
"use client";

import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query tanstack com/query/latest/docs/framework/react/installation
import ProductList from "./components/ProductList";
import { getProductList } from "./api/product";
import LoadingSpinner from "./components/LoadingSpinner";
import Paginationnumber from "./components/Paginationnumber";
import { useSearchParams } from 'next/navigation'

export default function Home() {

    const searchParams = useSearchParams()
    const page = searchParams.get('page')

    const currentPage = Number(page) || 1;
    //const currentPage = 1;

    const { isLoading, data, isError, isFetching,  error } = useQuery({
        queryKey: ["products", currentPage],
        queryFn: () => getProductList(currentPage)
    });
    //console.log(data);
    if (isLoading) return "Loading...";
    if (isError) return `Error: ${error.message}`;

    
    const totalPages = Math.ceil(Number(data.totalpage) / Number(data.perpage));
    //console.log(data)
  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 pl-10 pr-5">
            <h1 className="text-4xl font-bold">Nextjs 15 Laravel 11 Rest API Pagination View Product | Tanstack Query Tailwind CSS</h1>
        </div> 
        <div className="overflow-x-auto py-10">
            <ProductList productlist={data.postlist} />
            <div className="flex items-center justify-between my-5">
                <Paginationnumber totalPages={totalPages} />
                {isFetching ? <LoadingSpinner /> : null}
            </div>
        </div>
    </div>
  );
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import QueryProvider from "./components/QueryProvider";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  );
}
app\components\QueryProvider.tsx
//app\components\QueryProvider.tsx
"use client";

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState } from "react";

interface Props {
  children: React.ReactNode;
}

export default function QueryProvider({ children }: Props) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
  );
}
app\components\ProductList.tsx
//app\components\ProductList.tsx
import Image from 'next/image'

const ProductList = ({ productlist }) => {

  return (
    <>
      <div className="grid md:grid-cols-3 gap-5 mt-10">
        {productlist.map((item) => (
          <div key={item.id} className="bg-white rounded-lg shadow-lg p-8">
              <div className="relative overflow-hidden">
                <Image
                  src={`http://127.0.0.1:8000/storage/${item.image}`}
                  width={400}
                  height={400}
                  alt="Photo"
                  />
                <div className="absolute inset-0 bg-black opacity-40" />
                <div className="absolute inset-0 flex items-center justify-center">
                  <a className="bg-white text-gray-900 py-2 px-6 rounded-full font-bold hover:bg-gray-300"
                    href={`/product/${item.id}`}>
                    View Product
                  </a>
                </div>
              </div>
              <h3 className="text-xl font-bold text-gray-900 mt-4">{item.name}</h3>
              <p className="text-gray-500 text-sm mt-2">Description: {item.name}</p>
              <div className="flex items-center justify-between mt-4">
                <span className="text-gray-900 font-bold text-lg">${item.price}.99</span>
                <button className="bg-gray-900 text-white py-2 px-4 rounded-full font-bold hover:bg-gray-800">Add to Cart</button>
            </div>
          </div>
        ))}
      </div>
    </>
  )
}
 
export default ProductList
app\components\LoadingSpinner.tsx
//app\components\LoadingSpinner.tsx
export default function LoadingSpinner() {
    return (
        <h1>Loading...</h1>
    )
}
app\components\Paginationnumber.tsx
//app\components\Paginationnumber.tsx
"use client";
 
import Link from "next/link";
import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; //npm install react-icons --save npmjs com/package/react-icons
import { usePathname, useSearchParams } from "next/navigation";
import clsx from "clsx"; //npm i clsx npmjs com/package/clsx
 
export const generatePagination = (currentPage: number, totalPages: number) => {
  if (totalPages <= 7) {
    return Array.from({ length: totalPages }, (_, i) => i + 1);
  }
 
  if (currentPage <= 3) {
    return [1, 2, 3, "...", totalPages - 1, totalPages];
  }
 
  if (currentPage >= totalPages - 2) {
    return [1, 2, 3, "...", totalPages - 2, totalPages - 1, totalPages];
  }
 
  return [
    1,
    "...",
    currentPage - 1,
    currentPage,
    currentPage + 1,
    "...",
    totalPages,
  ];
};

const Paginationnumber = ({ totalPages }: { totalPages: number }) => {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get("page")) || 1;
 
  const createPageURL = (pageNumber: string | number) => {
    const params = new URLSearchParams(searchParams);
    params.set("page", pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };
 
  const allPages = generatePagination(currentPage, totalPages);
 
  const PaginationNumber = ({
    page,
    href,
    position,
    isActive,
  }: {
    page: number | string;
    href: string;
    position?: "first" | "last" | "middle" | "single";
    isActive: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "rounded-l-sm": position === "first" || position === "single",
        "rounded-r-sm": position === "last" || position === "single",
        "z-10 bg-blue-100 border-blue-500 text-white bg-blue-700": isActive,
        "hover:bg-blue-700": !isActive && position !== "middle",
        "text-gray-300 pointer-events-none": position === "middle",
      }
    );
 
    return isActive && position === "middle" ? (
      <div className={className}>{page}</div>
    ) : (
      <Link href={href} className={className}>
        {page}
      </Link>
    );
  };
   
  const PaginationArrow = ({
    href,
    direction,
    isDisabled,
  }: {
    href: string;
    direction: "left" | "right";
    isDisabled?: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "pointer-events-none text-blue-300": isDisabled,
        "hover:bg-blue-700": !isDisabled,
        "mr-2": direction === "left",
        "ml-2": direction === "right",
      }
    );
 
    const icon =
      direction === "left" ? (
        <HiChevronLeft size={20} />
      ) : (
        <HiChevronRight size={20} />
      );
 
    return isDisabled ? (
      <div className={className}>{icon}</div>
    ) : (
      <Link href={href} className={className}>
        {icon}
      </Link>
    );
  };
 
  return (
    <div className="inline-flex">
      <PaginationArrow
        direction="left"
        href={createPageURL(currentPage - 1)}
        isDisabled={currentPage <= 1}
      />
      <div className="flex -space-x-px">
        {allPages.map((page, index) => {
          let position: "first" | "last" | "single" | "middle" | undefined;
 
          if (index === 0) position = "first";
          if (index === allPages.length - 1) position = "last";
          if (allPages.length === 1) position = "single";
          if (page === "...") position = "middle";
 
          return (
            <PaginationNumber
              key={index}
              href={createPageURL(page)}
              page={page}
              position={position}
              isActive={currentPage === page} 
            />
          );
        })}
      </div>
      <PaginationArrow
        direction="right"
        href={createPageURL(currentPage + 1)}
        isDisabled={currentPage >= totalPages}
      />
    </div>
  );
};
 
export default Paginationnumber;
app\product\[id]\page.tsx
//app\product\[id]\page.tsx
"use client";
 
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { useParams } from 'next/navigation'
import { fetchProduct } from "../../api/product";
import Image from 'next/image'

const Viewproduct = () => {
 
  const {id}=useParams();
 
  const {
    isLoading,
    isError,
    data: product,
    error,
  } = useQuery({
    queryKey: ["product", id],
    queryFn: () => fetchProduct(id),
  });
  
  if (isLoading) return "loading...";
  if (isError) return `Error: ${error.message}`;
  
  console.log(product)
  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-100">
      <div className="bg-white rounded-sm shadow p-8 text-black">
      <div className="max-w-2xl mx-auto mt-5">
        <h1 className="text-2xl text-center mb-2">View Product</h1>
        <table className="table table-zebra">
            <thead className="text-sm text-gray-700 uppercase bg-gray-50">
              <tr>
                <th>S No.</th>
                <th>Product Name</th>
                <th>Price</th>             
              </tr>
            </thead>
            <tbody>
              <tr>
                  <td>{product.product.id}</td>
                  <td>{product.product.name}</td>
                  <td>{product.product.price}</td>
              </tr>
            </tbody>
        </table>
        <p className="text-center mt-6">
          <Image
          src={`http://127.0.0.1:8000/storage/${product.product.image}`}
          width={200}
          height={200}
          alt="Photo"
          style={{width:'400px', height: "auto" }}
        />
        </p>
      </div>
      </div>
    </div>
  )
}
  
export default Viewproduct
app\api\product.tsx
//app\api\product.tsx
export async function getProductList(page) {
  const response = await fetch(`http://127.0.0.1:8000/api/products?page=${page}`);
  const data = await response.json();
  //console.log(data.results.total);
  return {
    postlist: data.results.data,
    totalpage: data.results.total,
    perpage: data.results.per_page
  }
}

export async function fetchProduct(id) {
  const response = await fetch("http://127.0.0.1:8000/api/products/"+id);
  return response.json()
}
next.config
//next.config
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactStrictMode: true,
  images : {
    domains : ['localhost', 'cairocdoers-ednalan.com', '127.0.0.1'] // == Domain name
  }
};

export default nextConfig;
run C:\nextjs>npm run dev
Download Laravel App

https://laravel.com/docs/11.x/installation

Connecting our Database

open .env file root directory.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=laravel11dev
DB_USERNAME=root
DB_PASSWORD=root

Create Model and Migration

C:\xampp\htdocs\laravel\laravelproject>php artisan make:model Product -m

A new file named Product.php will be created in the app directory and database/migrations directory to generate the table in our database
app/Models/Product.php
//app/Models/Product.php
<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Product extends Model
{
    use HasFactory;
 
    protected $fillable = [
        'name', 
        'image', 
        'price'
    ];
}
database\migrations\create_products_table.php
//database\migrations\create_products_table.ph
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('image');
            $table->integer('price');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};
Database Migration
php artisan migrate

C:\xampp\htdocs\laravel\laravel10project>php artisan migrate
Migration table created successfully.
check database table
Create Controller and Request
C:\xampp\htdocs\laravel\laravel10project>php artisan make:controller ProductController

app\Http\Controllers\ProductController.php
//app\Http\Controllers\ProductController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Product;
use Illuminate\Support\Facades\Storage; //php artisan storage:link = php artisan storage:link = http://127.0.0.1:8000/storage/1.jpg

class ProductController extends Controller
{
    public function index()
    {
        //$products = Product::all(); // All Product

        $products = Product::paginate(3);

        // Return Json Response
        return response()->json([
            'results' => $products
        ], 200);
    }

    public function show($id)
    {
        // Product Detail 
        $product = Product::find($id);
        if (!$product) {
            return response()->json([
                'message' => 'Product Not Found.'
            ], 404);
        }

        // Return Json Response
        return response()->json([
            'product' => $product
        ], 200);
    }
}
Routes
install
php artisan install:api
All API requests will need the header Accept: application/json.
open routes/api.php and update the following code
routes\api.php
//routes\api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

Route::get('products', [ProductController::class, 'index']);
Route::get('products/{id}', [ProductController::class, 'show']);
Run C:\xampp\htdocs\laravel\myapp>php artisan serve
Starting Laravel development server: http://127.0.0.1:8000

Sunday, February 9, 2025

Nextjs 15 WordPress Rest API Pagination View Post | Tanstack Query Tailwind CSS

Nextjs 15 WordPress Rest API Pagination View Post | Tanstack Query Tailwind CSS

Install nextjs npx create-next-app@latest nextjs_org/docs/getting-started/installation

Install react-query

npm i @tanstack/react-query
tanstack com/query/latest/docs/framework/react/installation
app\page.tsx
//app\page.tsx
"use client";

import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import PostList from "./components/PostList";
import { getPostList } from "./api/post";
import LoadingSpinner from "./components/LoadingSpinner";
import Paginationnumber from "./components/Paginationnumber";
import { useSearchParams } from 'next/navigation'

export default function Home() {

    const searchParams = useSearchParams()
    const page = searchParams.get('page')

    const currentPage = Number(page) || 1;
    //const currentPage = 1;

    const { isLoading, data, isError, isFetching,  error } = useQuery({
        queryKey: ["posts", currentPage],
        queryFn: () => getPostList(currentPage)
    });
    //console.log(data);
    if (isLoading) return "Loading...";
    if (isError) return `Error: ${error.message}`;

    const ITEMS_PER_PAGE = 6;
    const totalPages = Math.ceil(Number(data.totalData) / ITEMS_PER_PAGE);
    //console.log(totalPages)
  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 pl-10 pr-5">
            <h1 className="text-4xl font-bold">Nextjs 15 WordPress Rest API Pagination View Post | Tanstack Query Tailwind CSS</h1>
        </div> 
        <div className="overflow-x-auto py-10">
            <PostList postlist={data.postlist} />
            <div className="flex items-center justify-between my-5">
                <Paginationnumber totalPages={totalPages} />
                {isFetching ? <LoadingSpinner /> : null}
            </div>
        </div>
    </div>
  );
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import QueryProvider from "./components/QueryProvider";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  );
}
app\components\QueryProvider.tsx
//app\components\QueryProvider.tsx
"use client";

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState } from "react";

interface Props {
  children: React.ReactNode;
}

export default function QueryProvider({ children }: Props) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
  );
}
app\components\PostList.tsx
//app\components\PostList.tsx
const PostList = ({ postlist }) => {

  return (
    <>
      <div className="grid md:grid-cols-3 gap-5 mt-10">
        {postlist.map((post) => (
          <div key={post.id} className="max-w-sm border border-gray-200 rounded-md shadow">
            <div className="relative aspect-video">
              <img src={post._embedded["wp:featuredmedia"][0].media_details.sizes.full.source_url}
                alt={post.title.rendered}
                sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
                className="rounded-t-md object-cover"/>
            </div>
            <div className="p-5">
              <h1>
                <a
                  href={`/post/${post.slug}`}>
                  {post.title.rendered}
                </a>         
              </h1>
            </div>
          </div>
        ))}
      </div>
    </>
  )
}
 
export default PostList
app\components\LoadingSpinner.tsx
//app\components\LoadingSpinner.tsx
export default function LoadingSpinner() {
    return (
        <h1>Loading...</h1>
    )
}
app\components\Paginationnumber.tsx
//app\components\Paginationnumber.tsx
"use client";
 
import Link from "next/link";
import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; //npm install react-icons --save npmjs com/package/react-icons
import { usePathname, useSearchParams } from "next/navigation";
import clsx from "clsx"; //npm i clsx npmjs com/package/clsx
 
export const generatePagination = (currentPage: number, totalPages: number) => {
  if (totalPages <= 7) {
    return Array.from({ length: totalPages }, (_, i) => i + 1);
  }
 
  if (currentPage <= 3) {
    return [1, 2, 3, "...", totalPages - 1, totalPages];
  }
 
  if (currentPage >= totalPages - 2) {
    return [1, 2, 3, "...", totalPages - 2, totalPages - 1, totalPages];
  }
 
  return [
    1,
    "...",
    currentPage - 1,
    currentPage,
    currentPage + 1,
    "...",
    totalPages,
  ];
};

const Paginationnumber = ({ totalPages }: { totalPages: number }) => {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get("page")) || 1;
 
  const createPageURL = (pageNumber: string | number) => {
    const params = new URLSearchParams(searchParams);
    params.set("page", pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };
 
  const allPages = generatePagination(currentPage, totalPages);
 
  const PaginationNumber = ({
    page,
    href,
    position,
    isActive,
  }: {
    page: number | string;
    href: string;
    position?: "first" | "last" | "middle" | "single";
    isActive: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "rounded-l-sm": position === "first" || position === "single",
        "rounded-r-sm": position === "last" || position === "single",
        "z-10 bg-blue-100 border-blue-500 text-white bg-blue-700": isActive,
        "hover:bg-blue-700": !isActive && position !== "middle",
        "text-gray-300 pointer-events-none": position === "middle",
      }
    );
 
    return isActive && position === "middle" ? (
      <div className={className}>{page}</div>
    ) : (
      <Link href={href} className={className}>
        {page}
      </Link>
    );
  };
   
  const PaginationArrow = ({
    href,
    direction,
    isDisabled,
  }: {
    href: string;
    direction: "left" | "right";
    isDisabled?: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "pointer-events-none text-gray-300": isDisabled,
        "hover:bg-gray-100": !isDisabled,
        "mr-2": direction === "left",
        "ml-2": direction === "right",
      }
    );
 
    const icon =
      direction === "left" ? (
        <HiChevronLeft size={20} />
      ) : (
        <HiChevronRight size={20} />
      );
 
    return isDisabled ? (
      <div className={className}>{icon}</div>
    ) : (
      <Link href={href} className={className}>
        {icon}
      </Link>
    );
  };
 
  return (
    <div className="inline-flex">
      <PaginationArrow
        direction="left"
        href={createPageURL(currentPage - 1)}
        isDisabled={currentPage <= 1}
      />
      <div className="flex -space-x-px">
        {allPages.map((page, index) => {
          let position: "first" | "last" | "single" | "middle" | undefined;
 
          if (index === 0) position = "first";
          if (index === allPages.length - 1) position = "last";
          if (allPages.length === 1) position = "single";
          if (page === "...") position = "middle";
 
          return (
            <PaginationNumber
              key={index}
              href={createPageURL(page)}
              page={page}
              position={position}
              isActive={currentPage === page} 
            />
          );
        })}
      </div>
      <PaginationArrow
        direction="right"
        href={createPageURL(currentPage + 1)}
        isDisabled={currentPage >= totalPages}
      />
    </div>
  );
};
 
export default Paginationnumber;
app\post\[slug]\page.tsx
//app\post\[slug]\page.tsx
"use client";
 
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { useParams } from 'next/navigation'
import { fetchPost } from "../../api/post";
  
const Post = () => {
 
  const {slug}=useParams();
 
  const {
    isLoading,
    isError,
    data: post,
    error,
  } = useQuery({
    queryKey: ["post", slug],
    queryFn: () => fetchPost(slug),
  });
  
  if (isLoading) return "loading...";
  if (isError) return `Error: ${error.message}`;
  
  
  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-100">
      <div className="bg-white rounded-sm shadow p-8 text-black">
            {
                post.length ? (
                <div>
                  <h1 className="text-2xl font-bold mb-5">{post[0].title.rendered}</h1>
                    <div className="mb-4">            
                      <div 
                        dangerouslySetInnerHTML={{ __html: post[0]['content']['rendered'] }}
                      />   
                    </div>
                </div>
                ) : ('Loading....')
             
            }
      </div>
    </div>
  )
}
  
export default Post
app\api\post.tsx
//app\api\post.tsx
const ITEM_PER_PAGE = 6;
export async function getPostList(page) {
  const response = await fetch(`http://localhost:8888/cairocoders/wp-json/wp/v2/posts?per_page=${ITEM_PER_PAGE}&page=${page}&_embed`);
  const totalData = response.headers.get('X-WP-Total');
  const data = await response.json();
  //console.log(data);
  return {
    postlist: data,
    totalData
  }
}

export async function fetchPost(slug) {
  const response = await fetch("http://localhost:8888/cairocoders/wp-json/wp/v2/posts?_embed&slug="+slug);
  return response.json()
}
themes\cairocoders\functions.php
//themes\cairocoders\functions.php
<?php
/**
 * Theme functions and definitions
 *
 * @package cairocoders
 */

add_action('rest_api_init', 'register_rest_images' );

function register_rest_images(){
    register_rest_field( array('post'),
        'fimg_url',
        array(
            'get_callback'    => 'get_rest_featured_image',
            'update_callback' => null,
            'schema'          => null,
        )
    );
}

function get_rest_featured_image( $object, $field_name, $request ) {
    if( $object['featured_media'] ){
        $img = wp_get_attachment_image_src( $object['featured_media'], 'app-thumb' );
        return $img[0];
    }
    return false;
}
run C:\nextjs>npm run dev

Friday, February 7, 2025

Nextjs 15 CRUD (Create, Read, Update and Delete) with Pagination Tanstack Query | Node Express MongoDB Atlas

Nextjs 15 CRUD (Create, Read, Update and Delete) with Pagination Tanstack Query | Node Express MongoDB Atlas

expressjs_com/
Express JS
Fast, unopinionated, minimalist web framework for Node.js

$ npm install express --savev PS C:\nodeproject> npm install express --save
expressjs_com/en/starter/hello-world.html

mongoose
mongoosejs/docs/
npm install mongoose --save

cors
CORS is a node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options.
npmjs-com/package/cors
PS C:\nodeproject>npm i cors

run PS C:\nodeproject> node index.js
index.js
//index.js
const express = require('express')
const mongoose = require('mongoose')
const cors = require('cors')
const UserModel = require('./User')
   
const app = express()
const port = 3001
  
app.use(cors())
app.use(express.json())
   
main().catch(err => console.log(err));
  
async function main() {
    try {                     //"mongodb+srv://"username":"password"@cluster0.x45tgvn.mongodb.net/"databasename"?retryWrites=true&w=majority&appName=Cluster0"
        await mongoose.connect('mongodb+srv://cairocoders:123456@cluster0.x45tgvn.mongodb.net/expressdb?retryWrites=true&w=majority&appName=Cluster0', {});
        console.log("CONNECTED TO DATABASE SUCCESSFULLY");
    } catch (error) {
        console.error('COULD NOT CONNECT TO DATABASE:', error.message);
    }
}

app.get('/', (req, res) => {
  res.send('Hello World!')
})

//Pagination
app.get("/pagination", async (req, res) => {
  let { page, limit } = req.query;
  if (!page) page = 1;
  if (!limit) limit = 2;

  const skip = (page - 1) * 2;
  const users = await UserModel.find().sort({ "_id":-1})
    .skip(skip)
    .limit(limit);

  const total = await UserModel.find().countDocuments()  
  res.header('Access-Control-Expose-Headers', 'X-Total-Count')
  res.header('X-Total-Count', total)
  res.send({ page: page, limit: limit, users: users });
});

app.get('/users', (req, res) => {
    UserModel.find()
    .then(users => res.json(users))
    .catch(err => res.json(err))
})

app.post('/users/create', (req, res) => {
    UserModel.create(req.body)
    .then(user => res.json(user))
    .catch(err => res.json(err))
})

app.get('/users/get/:id', (req, res) => {
    const id = req.params.id
    UserModel.findById({_id: id})
    .then(post => res.json(post))
    .catch(err => console.log(err))
})

app.put('/users/update/:id', (req, res) => {
    const id = req.params.id;
    UserModel.findByIdAndUpdate({_id: id}, {
        name: req.body.name,
        email: req.body.email,
        age: req.body.age
    }).then(user => res.json(user))
    .catch(err => res.json(err))
})

app.delete('/users/delete/:id', (req, res) => {
    const id = req.params.id;
    UserModel.findByIdAndDelete({_id: id})
    .then(response => res.json(response))
    .catch(err => res.json(err))
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})
User.js
//User.js
const mongoose = require('mongoose')
   
const UserSchema = new mongoose.Schema({
    name: String,
    email: String,
    age: Number
})
   
const UserModel = mongoose.model("users", UserSchema)
   
module.exports = UserModel;
Install nextjs npx create-next-app@latest nextjs_org/docs/getting-started/installation

Install react-query

npm i @tanstack/react-query
tanstack_com/query/latest/docs/framework/react/installation
app\page.tsx
//app\page.tsx
"use client";

import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query tanstack_com/query/latest/docs/framework/react/installation
import UserList from "./components/UserList";
import { getUserList } from "./api/users";
import AddUser from './components/AddUser'
import LoadingSpinner from "./components/LoadingSpinner";
import Paginationnumber from "./components/Paginationnumber";
import { useSearchParams } from 'next/navigation'

export default function Home() {

    const searchParams = useSearchParams()
    const page = searchParams.get('page')

    //console.log(page);
    //const page = 1 
    const currentPage = Number(page) || 1;

    const { isLoading, data, isError, isFetching,  error } = useQuery({
        queryKey: ["users", currentPage],
        queryFn: () => getUserList(currentPage)
    });
    //console.log(data);
    if (isLoading) return "Loading...";
    if (isError) return `Error: ${error.message}`;

    const ITEMS_PER_PAGE = 3;
    const totalPages = Math.ceil(Number(data.totalData) / ITEMS_PER_PAGE);
  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 pl-10 pr-5">
            <h1 className="text-4xl font-bold">Nextjs 15 CRUD (Create, Read, Update and Delete) with Pagination Tanstack Query | Node Express MongoDB Atlas</h1>
        </div> 
        <div className="overflow-x-auto py-10">
            <AddUser/>
            <UserList userlist={data.userlist} />
            <div className="flex items-center justify-between my-5">
                <Paginationnumber totalPages={totalPages} />
                {isFetching ? <LoadingSpinner /> : null}
            </div>
        </div>
    </div>
  );
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import QueryProvider from "./components/QueryProvider";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  );
}
app\components\QueryProvider.tsx
//app\components\QueryProvider.tsx
"use client";

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState } from "react";

interface Props {
  children: React.ReactNode;
}

export default function QueryProvider({ children }: Props) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
  );
}
app\components\AddUser.tsx
//app\components\AddUser.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query"
import UserForm from "./UserForm"
import { v4 as uuidv4 } from 'uuid'; //npm i uuid 
import { createUser } from "../api/users"

const AddUser = () => {
  const queryClient = useQueryClient();

  const createUserMutation = useMutation({
    mutationFn: createUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users']});
      console.log("success!")
    }
  });

  const handleAddPost = (user) => {
    createUserMutation.mutate({
      id: uuidv4(),
      ...user
    })
  }
  
  return (
    <div className="max-w-md mx-auto mt-5 mb-5">
      <h1 className="text-2xl text-center mb-2">Add New User</h1>
      <UserForm onSubmit={handleAddPost} initialValue={{}} />
    </div>
  )
}
 
export default AddUser
app\components\UserForm.tsx
//app\components\UserForm.tsx
import { useState } from "react"
 
const UserForm = ({ onSubmit, initialValue }) => {
  const [user, setUser] = useState({
    name: initialValue.name || "",
    email: initialValue.email || "",
    age: initialValue.age || "" 
  });
 
  const handleChangeInput = (e) => {
    setUser({
      ...user,
      [e.target.name]: e.target.value
    })
  }
 
  const renderField = (label) => (
    <div>
      <label className="block text-sm font-medium text-gray-900">{label}</label>
      <input onChange={handleChangeInput} type="text" name={label.toLowerCase()} value={user[label.toLowerCase()]} 
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500blue-500"
      />
    </div>
  );
 
  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(user);
    setUser({
      name: "",
      email: "",
      age: ""
    })
 
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <div className="mb-5">{renderField('Name')}</div>
      <div className="mb-5">{renderField('Email')}</div>
      <div className="mb-5">{renderField('Age')}</div>
      <button type="submit"
        className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
      >
        Submit</button>
    </form>
  )
}
 
export default UserForm
app\components\UserList.tsx
//app\components\UserList.tsx
import Link from "next/link";
import { useMutation, useQueryClient } from "@tanstack/react-query"; //npm i @tanstack/react-query 
import { deleteUser } from "../api/users";

const UserList = ({ userlist }) => {
    const queryClient = useQueryClient();

    const deleteUserMutation = useMutation({
      mutationFn: deleteUser,
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['users']});
      }
    });

    const handleDelete = (id) => {
      deleteUserMutation.mutate(id)
    }

  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">Age</th>
                    <th className="py-3 px-6 text-center">Actions</th>
                </tr>
            </thead>
            <tbody>
                {userlist.map((user) => (
                    <tr key={user._id} className="bg-white border-b text-black">
                        <td className="py-3 px-6">{user._id}</td>
                        <td className="py-3 px-6">{user.name}</td>   
                        <td className="py-3 px-6">{user.email}</td>
                        <td className="py-3 px-6">{user.age}</td>
                        <td className="flex justify-center gap-1 py-3">
                            <Link className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
                                href={`/user/${user._id}`}>Read</Link> 
                            
                            <Link className="focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:focus:ring-yellow-900"
                                href={`/user/edit/${user._id}/`}>
                                Edit
                            </Link>
                            <button onClick={() => handleDelete(user._id)} className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
                                Delete</button>
                        </td>
                    </tr>
                ))}
            </tbody>
        </table>
  )
}
 
export default UserList
app\components\LoadingSpinner.tsx
//app\components\LoadingSpinner.tsx
export default function LoadingSpinner() {
    return (
        <h1>Loading...</h1>
    )
}
app\components\Paginationnumber.tsx
//app\components\Paginationnumber.tsx
"use client";
 
import Link from "next/link";
import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; //npm install react-icons --save npmjs_com/package/react-icons
import { usePathname, useSearchParams } from "next/navigation";
import clsx from "clsx"; //npm i clsx npmjs_com/package/clsx
 
export const generatePagination = (currentPage: number, totalPages: number) => {
  if (totalPages <= 7) {
    return Array.from({ length: totalPages }, (_, i) => i + 1);
  }
 
  if (currentPage <= 3) {
    return [1, 2, 3, "...", totalPages - 1, totalPages];
  }
 
  if (currentPage >= totalPages - 2) {
    return [1, 2, 3, "...", totalPages - 2, totalPages - 1, totalPages];
  }
 
  return [
    1,
    "...",
    currentPage - 1,
    currentPage,
    currentPage + 1,
    "...",
    totalPages,
  ];
};

const Paginationnumber = ({ totalPages }: { totalPages: number }) => {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get("page")) || 1;
 
  const createPageURL = (pageNumber: string | number) => {
    const params = new URLSearchParams(searchParams);
    params.set("page", pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };
 
  const allPages = generatePagination(currentPage, totalPages);
 
  const PaginationNumber = ({
    page,
    href,
    position,
    isActive,
  }: {
    page: number | string;
    href: string;
    position?: "first" | "last" | "middle" | "single";
    isActive: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "rounded-l-sm": position === "first" || position === "single",
        "rounded-r-sm": position === "last" || position === "single",
        "z-10 bg-blue-100 border-blue-500 text-white bg-blue-700": isActive,
        "hover:bg-blue-700": !isActive && position !== "middle",
        "text-gray-300 pointer-events-none": position === "middle",
      }
    );
 
    return isActive && position === "middle" ? (
      <div className={className}>{page}</div>
    ) : (
      <Link href={href} className={className}>
        {page}
      </Link>
    );
  };
   
  const PaginationArrow = ({
    href,
    direction,
    isDisabled,
  }: {
    href: string;
    direction: "left" | "right";
    isDisabled?: boolean;
  }) => {
    const className = clsx(
      "flex h-10 w-10 items-center justify-center text-sm border",
      {
        "pointer-events-none text-gray-300": isDisabled,
        "hover:bg-gray-100": !isDisabled,
        "mr-2": direction === "left",
        "ml-2": direction === "right",
      }
    );
 
    const icon =
      direction === "left" ? (
        <HiChevronLeft size={20} />
      ) : (
        <HiChevronRight size={20} />
      );
 
    return isDisabled ? (
      <div className={className}>{icon}</div>
    ) : (
      <Link href={href} className={className}>
        {icon}
      </Link>
    );
  };
 
  return (
    <div className="inline-flex">
      <PaginationArrow
        direction="left"
        href={createPageURL(currentPage - 1)}
        isDisabled={currentPage <= 1}
      />
      <div className="flex -space-x-px">
        {allPages.map((page, index) => {
          let position: "first" | "last" | "single" | "middle" | undefined;
 
          if (index === 0) position = "first";
          if (index === allPages.length - 1) position = "last";
          if (allPages.length === 1) position = "single";
          if (page === "...") position = "middle";
 
          return (
            <PaginationNumber
              key={index}
              href={createPageURL(page)}
              page={page}
              position={position}
              isActive={currentPage === page} 
            />
          );
        })}
      </div>
      <PaginationArrow
        direction="right"
        href={createPageURL(currentPage + 1)}
        isDisabled={currentPage >= totalPages}
      />
    </div>
  );
};
 
export default Paginationnumber;
app\user\[id]\page.tsx
//app\user\[id]\page.tsx
"use client";

import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { useParams } from 'next/navigation'
import { fetchUser } from "../../api/users";
 
const User = () => {

  const {id}=useParams();

  const {
    isLoading,
    isError,
    data: user,
    error,
  } = useQuery({
    queryKey: ["users", id],
    queryFn: () => fetchUser(id),
  });
 
  if (isLoading) return "loading...";
  if (isError) return `Error: ${error.message}`;
 
 
  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">{user.name}</h1>
        </div> 
        <div className="overflow-x-auto py-10">
          <h2>{user.email}</h2>
          <h3>{user.age}</h3>
        </div>
    </div>
  )
}
 
export default User
app\user\edit\[id]\page.tsx
//app\user\edit\[id]\page.tsx
"use client";

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useParams, useRouter } from 'next/navigation'
import { fetchUser, updateUser } from "../../../api/users";
import UserForm from "../../../components/UserForm"

const EditUser = () => {
  const {id}=useParams();
  //console.log(id);

  const queryClient = useQueryClient();
  const router = useRouter();

  const {
    isLoading,
    isError,
    data: user,
    error,
  } = useQuery({
    queryKey: ["users", id],
    queryFn: () => fetchUser(id),
  });

  const updateUserMutation = useMutation({
    mutationFn: updateUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users']});
      router.push('/')
    }
  })

  if (isLoading) return "loading...";
  if (isError) return `Error: ${error.message}`;

  const handleSubmit = (updatedUser) => {
    updateUserMutation.mutate({id, ...updatedUser})
  }

  return (
    <div className="max-w-md mx-auto mt-5">
        <div className="flex items-center justify-between gap-1 mb-5">
            <h1 className="text-4xl font-bold">{user.name}</h1>
        </div> 
        <UserForm onSubmit={handleSubmit} initialValue={user} />
    </div>
  )
}
 
export default EditUser
app\api\page.tsx
//app\api\page.tsx
const ITEM_PER_PAGE = 3;
export async function getUserList(page) {
  const response = await fetch(`http://localhost:3001/pagination?page=${page}&limit=${ITEM_PER_PAGE}`);
  const totalData = response.headers.get('X-Total-Count');
  const data = await response.json();
  //console.log(totalData);
  return {
    userlist: data.users,
    totalData
  }
}

export async function fetchUsers() {
  const response = await fetch('http://localhost:3001/users');
  return response.json()
}

export async function createUser(newUser) {
  const response = await fetch(`http://localhost:3001/users/create`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(newUser)
  });
  return response.json()
}

export async function fetchUser(id) {
  const response = await fetch(`http://localhost:3001/users/get/${id}`);
  return response.json()
}

export async function updateUser(updatedUser) {
  const response = await fetch(`http://localhost:3001/users/update/${updatedUser.id}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(updatedUser)
  });
  return response.json()
}

export async function deleteUser(id) {
  const response = await fetch(`http://localhost:3001/users/delete/${id}`, {
    method: "DELETE",
  });
  return response.json()
}
run C:\nextjs>npm run dev

Tuesday, February 4, 2025

Nextjs 15 Tanstack Query | Node Express MongoDB Atlas

Nextjs 15 Tanstack Query | Node Express MongoDB Atlas
https://expressjs_com/
Express JS
Fast, unopinionated, minimalist web framework for Node.js

$ npm install express --savev PS C:\nodeproject> npm install express --save
https://expressjs.com/en/starter/hello-world.html

mongoose
mongoosejs/docs/
npm install mongoose --save

cors
CORS is a node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options.
npmjs-com/package/cors
PS C:\nodeproject>npm i cors

run PS C:\nodeproject> node index.js
index.js
//index.js
const express = require('express')
const mongoose = require('mongoose')
const cors = require('cors')
const UserModel = require('./User')
   
const app = express()
const port = 3001
  
app.use(cors())
app.use(express.json())
   
main().catch(err => console.log(err));
  
async function main() {
    try {                     //"mongodb+srv://"username":"password"@cluster0.x45tgvn.mongodb.net/"databasename"?retryWrites=true&w=majority&appName=Cluster0"
        await mongoose.connect('mongodb+srv://cairocoders:123456@cluster0.x45tgvn.mongodb.net/expressdb?retryWrites=true&w=majority&appName=Cluster0', {});
        console.log("CONNECTED TO DATABASE SUCCESSFULLY");
    } catch (error) {
        console.error('COULD NOT CONNECT TO DATABASE:', error.message);
    }
}

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.get('/users', (req, res) => {
    UserModel.find()
    .then(users => res.json(users))
    .catch(err => res.json(err))
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})
User.js
//User.js
const mongoose = require('mongoose')
  
const UserSchema = new mongoose.Schema({
    name: String,
    email: String,
    age: Number
})
  
const UserModel = mongoose.model("users", UserSchema)
  
module.exports = UserModel;
Install nextjs npx create-next-app@latest https://nextjs.org/docs/getting-started/installation

Install react-query

npm i @tanstack/react-query
tanstack_com/query/latest/docs/framework/react/installation
app\page.tsx
//app\page.tsx
"use client";
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query https://tanstack.com/query/latest/docs/framework/react/installation

type User = {
  id: number;
  name: string;
  email: string;
};

async function fetchUsers(): Promise<User[]> {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  return response.json();
}

export default function Home() {
  const { data, error, isLoading, isError } = useQuery({
    queryKey: ["users"],
    queryFn: fetchUsers,
  });

  if (isLoading) return <p> Loading...</p>;

  if (isError) return <p> Error: {(error as Error).message}</p>;

  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">Nextjs 15 Tanstack Query | Node Express MongoDB Atlas</h1>
        </div> 
        <div className='max-w-[1100px] mx-auto mt-10 pb-10 px-4'>
          <div className="grid grid-cols-3 gap-x-3 gap-y-3">
            {data?.map((user, key) => (
              <div key={key} className="bg-primary p-10 rounded-lg shadow-md border border-gray-200">
                {" "}
                <h4> Name: {user.name}</h4> <p> Email: {user.email}</p>{" "}
              </div>
            ))}
        </div>
        </div>
    </div>
  );
}
app\node-express-api\page.tsx
//app\node-express-api\page.tsx
"use client";
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query https://tanstack.com/query/latest/docs/framework/react/installation

type User = {
  id: number;
  name: string;
  email: string;
};

async function fetchUsers(): Promise<User[]> {
const response = await fetch("http://localhost:3001/users");
  return response.json();
}

export default function Home() {
  const { data, error, isLoading, isError } = useQuery({
    queryKey: ["users"],
    queryFn: fetchUsers,
  });

  if (isLoading) return <p> Loading...</p>;

  if (isError) return <p> Error: {(error as Error).message}</p>;

  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">Nextjs 15 Tanstack Query | Node Express MongoDB Atlas</h1>
        </div> 
        <div className='max-w-[1100px] mx-auto mt-10 pb-10 px-4'>
          <div className="grid grid-cols-3 gap-x-3 gap-y-3">
            {data?.map((user, key) => (
              <div key={key} className="bg-primary p-10 rounded-lg shadow-md border border-gray-200">
                {" "}
                <h4> Name: {user.name}</h4> <p> Email: {user.email}</p>{" "}
              </div>
            ))}
        </div>
        </div>
    </div>
  );
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import QueryProvider from "./components/QueryProvider";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
       <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  );
}
app\components\QueryProvider.tsx
//app\components\QueryProvider.tsx
"use client";

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState } from "react";

interface Props {
  children: React.ReactNode;
}

export default function QueryProvider({ children }: Props) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
  );
}
run C:\nextjs>npm run dev

Monday, January 13, 2025

Nextjs Data fetching using SWR

Nextjs Data fetching using SWR

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

Install the following

SWR React Hooks for Data Fetching
npm i swr
https://swr.vercel.app/docs/getting-started
app\page.tsx
//app\page.tsx
import Post from "./components/post";

export default function Home() {
  return (
    <div className="max-w-7xl flex flex-col gap-10 mx-auto p-10">
      <div className="flex justify-between items-center">
        <h1 className="text-4xl font-bold">Nextjs Data fetching using SWR</h1>
      </div>
      <Post />
    </div>
  );
}
app\components\post.tsx
//app\components\post.tsx
"use client";

import useSWR from "swr"; //npm i swr https://swr.vercel.app/docs/getting-started

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Home() {
  const {
    data: posts,
    isLoading,
    isError: error,
  } = useSWR(
    'https://jsonplaceholder.typicode.com/posts',
    fetcher,
    { revalidateOnFocus: false, revalidateOnReconnect: false }
  );

  if (error) {
    return <p className='text-secondary'>Failed to fetch</p>;
  }

  return (
    <>
      <header className='p-7 text-center'>
        <h1 className='text-secondary font-bold text-xl'>Basic</h1>
      </header>
      <main className='max-w-[1100px] mx-auto mt-10 pb-10 px-4'>
        {isLoading ? (
          <p className='text-secondary'>Loading posts</p>
        ) : (
          <ul className='grid grid-cols-3 gap-x-3 gap-y-3'>
            {posts.map((post, index) => (
              <li className='bg-primary p-10 rounded-lg shadow-md border border-gray-200' key={index}>
                <span className='block mb-4 font-bold text-lg text-secondary capitalize'>
                  {post.title}
                </span>
                <span className='text-secondary'>{post.body}</span>
                <h2>{post.id}</h2>
              </li>
            ))}
          </ul>
        )}
      </main>
    </>
  );
}
app\infinite-loading\page.tsx
//app\infinite-loading\page.tsx
"use client";

import useSWRInfinite from 'swr/infinite';

const fetcher = (url) => fetch(url).then((res) => res.json());

const getKey = (pageIndex, previousPageData) => {
  if (pageIndex && !previousPageData.length) return null;
  return `https://jsonplaceholder.typicode.com/posts?_page=${pageIndex}&_limit=6`;
};

export default function Home() {
  const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher);

  return (
    <div className="max-w-7xl flex flex-col gap-10 mx-auto p-10">
      <div className="flex justify-between items-center">
        <h1 className="text-4xl font-bold">Nextjs Data fetching using SWR</h1>
      </div>
      <header className='p-7 text-center'>
        <h1 className='text-secondary font-bold text-xl'>Infinite Loading</h1>
      </header>
      <main className='max-w-[1100px] mx-auto mt-10 pb-10 px-4'>
        {isLoading ? (
          <p className='text-secondary'>Loading posts</p>
        ) : (
          <ul className='grid grid-cols-3 gap-x-3 gap-y-3'>
            {data.map((posts) => {
              return posts.map((post, index) => (
                <li
                  className='bg-primary p-10 rounded-lg shadow-md border border-gray-200'
                  key={index}
                >
                  <span className='block mb-4 font-bold text-lg text-secondary capitalize'>
                    {post.title}
                  </span>
                  <span className='text-secondary'>{post.body}</span>
                </li>
              ));
            })}
          </ul>
        )}
        <div className='max-w-[600px] mx-auto my-10 flex justify-center'>
          <button
            className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
            onClick={() => setSize(size + 1)}
          >
            Load more
          </button>
        </div>
      </main>
    </div>
  );
}
app\prevnext-pagination\page.tsx
//app\prevnext-pagination\page.tsx
"use client";

import { useState } from 'react';
import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());
const Page = ({ pageIndex }) => {
    const { data: posts, isLoading } = useSWR(
      `https://jsonplaceholder.typicode.com/posts?_page=${pageIndex}&_limit=6`,
      fetcher
    );

  return (
    isLoading ? (
          <p className='text-secondary'>Loading posts</p>
        ) : (
          <ul className='grid grid-cols-3 gap-x-3 gap-y-3'>
            {posts.map((post, index) => (
              <li className='bg-primary p-10 rounded-lg shadow-md border border-gray-200' key={index}>
                <span className='block mb-4 font-bold text-lg text-secondary capitalize'>
                  {post.title}
                </span>
                <span className='text-secondary'>{post.body}</span>
                <h2>{post.id}</h2>
              </li>
            ))}
          </ul>
        )
  )
}

export default function Home() {
  const [pageIndex, setPageIndex] = useState(1);

  return (
    <div className="max-w-7xl flex flex-col gap-10 mx-auto p-10">
      <div className="flex justify-between items-center">
        <h1 className="text-4xl font-bold">Nextjs Data fetching using SWR</h1>
      </div>
      <header className="p-7 text-center">
        <h1 className="text-secondary font-bold text-xl">Next Prev Pagination</h1>
      </header>
      <main className="max-w-[1100px] mx-auto mt-10 pb-10 px-4">
        <Page pageIndex={pageIndex} />
        <div className="hidden">
          <Page pageIndex={pageIndex + 1} />
        </div>
        <div className="max-w-[600px] mx-auto my-10 grid grid-cols-2 gap-x-3">
          <button
            className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
            onClick={() => setPageIndex((_currentPage) => _currentPage - 1)}
          >
            Prev
          </button>
          <button
            className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
            onClick={() => setPageIndex((_currentPage) => _currentPage + 1)}
          >
            Next
          </button>
        </div>
      </main>
    </div>
  );
}
run C:\nextjs>npm run dev

Wednesday, December 11, 2024

Nextjs How to connect mongoDB Atlas using mongoose | Insert Data Server Actions

Nextjs How to connect mongoDB Atlas using mongoose | Insert Data
Install nextjs npx create-next-app@latest https://nextjs.org/docs/getting-started/installation

Install the following

Mongoose
npm install mongoose
https://www.npmjs.com/package/mongoose
app\addProduct\page.tsx
//app\addProduct\page.tsx
"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("/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="max-w-screen-lg mx-auto py-14">
        <h1 className="text-4xl font-bold">Nextjs How to connect mongoDB Atlas using mongoose | Insert Data</h1>
        <form onSubmit={handleSubmit} className="max-w-sm mx-auto">
        <div className="mb-5">
            <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
            <input
                onChange={(e) => setName(e.target.value)}
                value={name}
                className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                type="text"
                placeholder="Product Name"
            />
        </div>
        <div className="mb-5">
            <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Image</label>
            <input
                onChange={(e) => setImage(e.target.value)}
                value={image}
                className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                type="text"
                placeholder="/images/1.jpg"
                defaultValue="/images/1.jpg"
            />
        </div>
        <div className="mb-5">
            <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Price</label>
            <input
                onChange={(e) => setPrice(e.target.value)}
                value={price}
                className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                type="number"
                placeholder="1"
                defaultValue="1"
            />
        </div>
        <div className="mb-5">
            <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Category</label>
            <input
                onChange={(e) => setCategory(e.target.value)}
                value={category}
                className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                type="text"
                placeholder="Product Category"
            />
        </div>
        <button type="submit" className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Add Product</button>
        </form>
        </div>
    );
}
app\api\products\route.ts
//app\api\products\route.ts
import connectMongoDB from "@/libs/mongodb";
import Product from "@/models/ProductModel";
import { NextResponse } from "next/server";
 
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 });
}
libs\mongodb.ts
//libs\mongodb.ts
import mongoose from "mongoose"; //npm install mongoose https://www.npmjs.com/package/mongoose
 
const connectMongoDB = async () => { //mongodb+srv://<username>:<password>@firstcluster.4rc4s.mongodb.net/<dbname>?retryWrites=true&w=majority
    try {
        await mongoose
        .connect(process.env.MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
        })
        .then(() => {
            console.log('Connected to the Database.');
        })
        .catch(err => console.error(err));
        //console.log("Connected to MongoDB.");
    } catch (error) {
        console.log(error);
    }
};
 
export default connectMongoDB;
models\ProductModel.ts
//models\ProductModel.ts
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;
.env
//.env
MONGODB_URI= "mongodb+srv://"username":"password"@cluster0.x45tgvn.mongodb.net/"databasename"?retryWrites=true&w=majority&appName=Cluster0"
run C:\nextjs>npm run dev

Related Post