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> ); } |
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> ); } |
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> ); } |
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 |
1 2 3 4 5 6 | //app\components\LoadingSpinner.tsx export default function LoadingSpinner() { return ( <h1>Loading...</h1> ) } |
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; |
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 |
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) { return response.json() } |
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; |
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' ]; } |
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' ); } }; |
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); } } |
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' ]); |
Starting Laravel development server: http://127.0.0.1:8000