Adding Pagination
Tags: nextjs, tailwindcss, markdown, react
Published: 2021-11-24 15:46:18

# ExamPro Markdown Lab Part 2

This is part of the [ExamPro](https://www.exampro.co/) 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:

![](https://exampro-support.s3.amazonaws.com/Nextjs/job_listing_with_pagination.png)

#### Config file

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

1. Create `./config/index.js`

```javascript
export const JOBS_PER_PAGE = 4;
```

#### Pagination Component

1. Create `./components/jobs/Pagination.js`

```javascript
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

```javascript
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';
```

2. Create getStaticPaths() function

```javascript
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,
  }
}
```

3. Create getStaticProps() function

```javascript
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,
    },
  };
}
```

4. Create the JobPostings() function

```javascript
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>
  );
}
```

5. 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:

```javascript
import { getStaticProps } from './page/[page_index]';
import JobPostings from './page/[page_index]';

export { getStaticProps };
export default JobPostings;
```