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 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> ); } |
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 | //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 |
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-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; |
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 | //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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //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() } |
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 | //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; } |