17
Adding Pagination
This is part of the ExamPro Next.js course. Additional content will be added to this lab, such as pagination and job type filtering.
In this lab, we will be adding the pagination feature to the existing application
http://localhost:3000/jobs
would look like this:
This file will be used to set how many jobs will be displayed on one page
- Create
./config/index.js
export const JOBS_PER_PAGE = 4;
- Create
./components/jobs/Pagination.js
import Link from 'next/link';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
import { JOBS_PER_PAGE } from '@/config/index';
export default function Pagination({ currentPage, numJobs, numPages }) {
const isFirst = currentPage === 1;
const isLast = currentPage === numPages;
const prevPage = `/jobs/page/${currentPage - 1}`;
const nextPage = `/jobs/page/${currentPage + 1}`;
const firstJobOfPage = parseInt((currentPage - 1) * JOBS_PER_PAGE + 1);
const lastJobOfPage = parseInt(currentPage * JOBS_PER_PAGE);
if (numPages === 1) return <></>;
return (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
{/* If not first page, display the Previous link */}
{!isFirst && (
<Link href={prevPage}>
<a className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Previous
</a>
</Link>
)}
{/* If not last page, display the Next link */}
{!isLast && (
<Link href={nextPage}>
<a className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Next
</a>
</Link>
)}
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{firstJobOfPage}</span> to{' '}
<span className="font-medium">{lastJobOfPage > numJobs ? numJobs : lastJobOfPage}</span>{' '}
of <span className="font-medium">{numJobs}</span> results
</p>
</div>
<div>
<nav
className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
aria-label="Pagination"
>
{/* If not first page, display the Previous link */}
{!isFirst && (
<Link href={prevPage}>
<a className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span className="sr-only">Previous</span>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
</a>
</Link>
)}
{/* Loop through numPages array */}
{Array.from({ length: numPages }, (_, i) => (
<li key={i} className="list-none">
<Link href={`/jobs/page/${i + 1}`} passHref>
{i == currentPage - 1 ? (
<a
aria-current="page"
className="z-10 bg-orange-50 border-orange-400 text-orange-500 relative inline-flex items-center px-4 py-2 border text-sm font-medium"
>
{i + 1}
</a>
) : (
<a className="bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium">
{i + 1}
</a>
)}
</Link>
</li>
))}
{/* If not last page, display the Next link */}
{!isLast && (
<Link href={nextPage}>
<a className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span className="sr-only">Next</span>
<ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
</a>
</Link>
)}
</nav>
</div>
</div>
</div>
);
}
To add pagination, we need a new dynamic route for our job listings.
- Create
pages/jobs/page/[page_index].js
file
- Import fs and path modules
- Import matter
- Import Job component
- Import Layout component
- Import Pagination
import { promises as fs } from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { JOBS_PER_PAGE } from '@/config/index';
import Job from '@/components/jobs/Job';
import Layout from '@/components/Layout';
import Pagination from '@/components/jobs/Pagination';
- Create getStaticPaths() function
export async function getStaticPaths() {
// Read from the /jobs directory
const files = await fs.readdir(path.join('jobs'));
// Get the number of files and divide by JOBS_PER_PAGE then round up
const numPages = Math.ceil(files.length / JOBS_PER_PAGE);
let paths = [];
for (let i = 1; i <= numPages; i++) {
paths.push({
params: { page_index: i.toString() },
})
}
return {
paths,
fallback: false,
}
}
- Create getStaticProps() function
export async function getStaticProps({ params }) {
const page = parseInt((params && params.page_index) || 1);
// Read from /jobs directory
const files = await fs.readdir(path.join('jobs'));
// Map through jobs directory
const jobs = files.map(async (filename) => {
// Set 'slug' to name of md file
const slug = filename.replace('.md', '');
// Read all markdown from file
const markdown = await fs.readFile(path.join('jobs', filename), 'utf-8');
// Extract data from markdown
const { data } = matter(markdown);
// return slug and data in an array
return {
slug,
data,
};
});
// Get total number of jobs
const numJobs = files.length;
// Get the number of files and divide by JOBS_PER_PAGE then round up
const numPages = Math.ceil(files.length / JOBS_PER_PAGE);
// Get the page index
const pageIndex = page - 1;
// Display only the number of jobs based on JOBS_PER_PAGE
const displayJobs = jobs.slice(pageIndex * JOBS_PER_PAGE, (pageIndex + 1) * JOBS_PER_PAGE);
return {
props: {
jobs: await Promise.all(displayJobs),
numJobs,
numPages,
currentPage: page,
},
};
}
- Create the JobPostings() function
export default function JobPostings({ jobs, numJobs, numPages, currentPage }) {
return (
<Layout title="Jobs | ExamPro">
<div className="px-4 py-4 sm:px-6 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
Job Postings
</h2>
</div>
</div>
<div className="bg-white my-4 shadow overflow-hidden divide-y divide-gray-200 sm:rounded-md">
<ul role="list" className="divide-y divide-gray-200">
{/* Maps through each job */}
{jobs.map((job, index) => (
<Job key={index} job={job} />
))}
</ul>
</div>
<Pagination currentPage={currentPage} numJobs={numJobs} numPages={numPages} />
</Layout>
);
}
- Since we most of the same functionality in
pages/jobs/index.js
andpages/jobs/page/[page_index.js]
, we can just delete everything inpages/jobs/index.js
import like so:
import { getStaticProps } from './page/[page_index]';
import JobPostings from './page/[page_index]';
export { getStaticProps };
export default JobPostings;
17