Adding Pagination

ExamPro Markdown Lab Part 2

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

Adding Pagination Feature

http://localhost:3000/jobs would look like this:

Config file

This file will be used to set how many jobs will be displayed on one page

  1. Create ./config/index.js
export const JOBS_PER_PAGE = 4;

Pagination Component

  1. 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.

  1. 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';
  1. 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,
  }
}
  1. 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,
    },
  };
}
  1. 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>
  );
}
  1. Since we most of the same functionality in pages/jobs/index.js and pages/jobs/page/[page_index.js], we can just delete everything in pages/jobs/index.js import like so:
import { getStaticProps } from './page/[page_index]';
import JobPostings from './page/[page_index]';

export { getStaticProps };
export default JobPostings;

17