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 