Creating the Basic Job Listing

ExamPro Markdown Lab Part 1
This is part of the ExamPro Next.js course. Preview of complete lab deployed on Vercel
In this lab, we will be creating the Job Listings portion of ExamPro using the following stack:
Technology Stack
  • Node.js (12.22.0 or later)
  • Next.js (12.0.4)
  • React (17.0.2)
  • TailwindCSS (3.0.0)
  • gray-matter (4.0.3)
  • marked (4.0.3)
  • Application Screenshots
    localhost:3000/jobs/ display a list of all jobs
    localhost:3000/jobs/[slug] displays individual jobs
    Getting Started
    You may choose to start a new repository or continue with the current exampro-nextjs project
    If you're starting from scratch, proceed to Step 1.
    Setting Up Next.js
  • Create a new Next.js app called exampro-markdown
  • npx create-next-app@latest exampro-markdown
  • Change to the exampro-markdown directory
  • cd exampro-markdown
    Setting Up TailwindCSS
  • Install TailwindCSS, its peer-dependencies, plugins, and other Tailwind Labs tools
  • npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
    npm install @headlessui/react @heroicons/react
  • Initialize your Tailwind configuration files
  • npx tailwindcss init -p
  • Include Tailwind in your CSS by replacing the original content with the following lines in your ./styles/globals.css file
  • @tailwind base;
    @tailwind components;
    @tailwind utilities;
  • In tailwind.config.js, add orange to your colors by adding the following line at the top of the file
  • const colors = require('tailwindcss/colors');
    and extending the color palette to including orange
    module.exports = {
      content: ['./components/**/*.js', './pages/**/*.js'],
      theme: {
        extend: {
          colors: {
            orange: colors.orange,
          },
        },
      },
      variants: {
        extend: {},
      },
      plugins: [],
    };
    Setting Up Prettier and Husky Hooks (optional)
  • Install Prettier, Husky, and lint-staged
  • npm install --save-dev --save-exact prettier
    npm install --save-dev husky lint-staged
    npx husky install
    npm set-script prepare "husky install"
    npx husky add .husky/pre-commit "npx lint-staged"
  • Create .prettierrc.json in the root directory
  • {
      "arrowParens": "always",
      "bracketSpacing": true,
      "embeddedLanguageFormatting": "auto",
      "endOfLine": "lf",
      "htmlWhitespaceSensitivity": "css",
      "insertPragma": false,
      "bracketSameLine": false,
      "jsxSingleQuote": false,
      "proseWrap": "preserve",
      "quoteProps": "as-needed",
      "requirePragma": false,
      "singleQuote": true,
      "tabWidth": 2,
      "trailingComma": "es5",
      "useTabs": false,
      "vueIndentScriptAndStyle": false,
      "printWidth": 100
    }
  • Create .prettierignore in the root directory
  • package.json
    package-lock.json
    node_modules/
    .cache
    .next
  • In the package.json, add the following scripts and lint-staged:
  • "scripts": {
        ...
        "prettier": "prettier --write \"./**/*.{md,json,html,css,js,yml}\"",
        "prettier-check": "prettier --check \"./**/*.{md,json,html,css,js,yml}\"",
        ...
      },
      ...
        "lint-staged": {
        "**/*": "prettier --write --ignore-unknown"
      }
    Install gray-matter and marked
    npm install --save gray-matter
    npm install marked
    Removing unnecessary file and code
  • Delete the styles/Home.module.css file
  • Remove everything inside the parent <div> element in ./pages/index.js and the import lines
  • import Head from 'next/head'
    import Image from 'next/image'
    import styles from '../styles/Home.module.css'
    Setting up jsconfig.json
    This specifies path mapping to be computed relative to baseUrl option.
  • Create jsconfig.json file
  • {
      "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "baseUrl": ".",
        "paths": {
          "@/components/*": ["components/*"],
          "@/config/*": ["config/*"],
          "@/styles/*": ["styles/*"]
        }
      }
    }
    Using provided components and stylesheets
  • Copy the following components and stylesheet into your project. These are React components that have been styled using TailwindCSS. Markdown.module.css is used to style the Markdown content
  • Footer from ./components/Footer.js
  • Header from ./components/Header.js
  • Layout from ./components/Layout.js
  • Main from ./components/Main.js
  • Job from ./components/jobs/Job.js
  • JobsHeader from ./components/jobs/JobsHeader.js
  • TypeLabel from ./components/jobs/TypeLabel.js
  • TypeList from ./components/jobs/TypeList.js
  • ./styles/Markdown.module.css
  • Update the ./pages/index.js file to include the Layout and Main components
  • import Main from '@/components/Main';
    import Layout from '@/components/Layout';
    
    export default function Home() {
      return (
        <Layout>
          <Main />
        </Layout>
      );
    }
  • Run npm run dev to start the server, you should see
  • Markdown Implementation
    Job Postings
  • Create /jobs directory and fill it with job postings in markdown (.md files).
  • You can copy the .md files in the /jobs of the repository or create your own using Lorem Markdownum. Make sure to include frontmatter above your markdown. Frontmatter looks like:
  • ---
    title: 'Cloud Support Engineer'
    type: 'Part-Time'
    location: 'Remote'
    category: 'Operations, IT and Support Engineering'
    ---
    JobPostings Component (Page component that shows list of all jobs)
  • Create pages/jobs/index.js file
  • Import the fs and path modules
  • Import matter from gray-matter
  • Import the Job Component
  • Import the Layout component
  • import { promises as fs } from 'fs';
    import path from 'path';
    import matter from 'gray-matter';
    
    import Job from '@/components/jobs/Job';
    import Layout from '@/components/Layout';
  • Create the getStaticProps() function
  • export async function getStaticProps() {
      // 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,
        };
      });
    
      return {
        props: {
          jobs: await Promise.all(jobs),
        },
      };
    }
  • Your JobPostings() function will take the jobs prop from the getStaticProps() function and map through each of the job markdown files in /jobs
  • // Takes the `jobs` prop from the getStaticProps() function
    export default function JobPostings({ jobs }) {
      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>
        </Layout>
      );
    }
    Markdown Component (For parsing markdown)
    This component handles the parsing of the markdown content to html so we can style it using Markdown.module.css
  • Create ./components/Markdown.js file
  • import { marked } from 'marked';
    
    import styles from '@/styles/Markdown.module.css';
    
    // Takes content (for example from ./pages/jobs/[slug].js)
    export default function Markdown({ content }) {
      return (
        // Uses marked to parse markdown to html
        <div className={styles.markdown} dangerouslySetInnerHTML={{ __html: marked(content) }}></div>
      );
    }
    JobPage Component (Individual job posting)
  • Create ./pages/jobs/[slug].js file
  • Import the following
  • import { promises as fs } from 'fs';
    import path from 'path';
    import Link from 'next/link';
    import matter from 'gray-matter';
    import { BriefcaseIcon, LocationMarkerIcon, UsersIcon } from '@heroicons/react/solid';
    
    import Markdown from '@/components/Markdown';
    import Layout from '@/components/Layout';
  • Create a getStaticPaths() function
  • export async function getStaticPaths() {
      // Read from the /jobs directory
      const files = await fs.readdir(path.join('jobs'));
      // Map through the files
      const paths = await Promise.all(
        files.map(async (filename) => ({
          params: {
            // Create a slug using the name of the file without the .md extension at the end
            slug: filename.replace('.md', ''),
          },
        }))
      );
    
      return {
        paths,
        fallback: false,
      };
    }
  • Create a getStaticProps() function
  • // This function takes the slug from getStaticPaths()
    export async function getStaticProps({ params: { slug } }) {
      // Read file with name of slug + .md extension in the /jobs directory
      const markdown = await fs.readFile(path.join('jobs', slug + '.md'), 'utf-8');
      // Use `matter` to extract the content and data from each file
      // content is the body of the markdown file
      // data is the frontmatter of the markdown file
      const { content, data } = matter(markdown);
    
      // Return content, data, and slug as props
      return {
        props: {
          content,
          data,
          slug,
        },
      };
    }
  • Your JobPage() function will take content and data as props from getStaticProps() and will display them as React Components
  • export default function JobPage({ content, data }) {
      return (
        <Layout title={`${data.title} | ExamPro`}>
          <div className="px-4 py-4 sm:px-6 md:flex md:items-center md:justify-between lg:flex lg:items-center lg: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">
                {data.title}
              </h2>
              <div className="mt-1 flex flex-col sm:flex-row sm:flex-wrap sm:mt-0 sm:space-x-6">
                <div className="mt-2 flex items-center text-sm text-gray-500">
                  <UsersIcon
                    className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
                    aria-hidden="true"
                  />
                  {data.category}
                </div>
                <div className="mt-2 flex items-center text-sm text-gray-500">
                  <LocationMarkerIcon
                    className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
                    aria-hidden="true"
                  />
                  {data.location}
                </div>
                <div className="mt-2 flex items-center text-sm text-gray-500">
                  <BriefcaseIcon
                    className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
                    aria-hidden="true"
                  />
                  {data.type}
                </div>
              </div>
            </div>
            <div className="mt-5 flex lg:mt-0 lg:ml-4">
              <span className="sm:ml-3">
                <Link href="/jobs" passHref>
                  <button
                    type="button"
                    className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-400"
                  >
                    Back to Jobs
                  </button>
                </Link>
              </span>
            </div>
          </div>
          <div>
            <Markdown content={content} />
          </div>
        </Layout>
      );
    }

    36

    This website collects cookies to deliver better user experience

    Creating the Basic Job Listing