article

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

Related Post