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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//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
1
2
3
4
5
6
//app\components\LoadingSpinner.tsx
export default function LoadingSpinner() {
    return (
        <h1>Loading...</h1>
    )
}
app\components\Paginationnumber.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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
1
2
3
4
5
6
7
8
9
10
11
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
//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