28
Creating the Basic Job Listing
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:
- 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)
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.
- 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
- 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
, addorange
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"
}
npm install --save gray-matter
npm install marked
- Delete the
styles/Home.module.css
file - Remove everything inside the parent
<div>
element in./pages/index.js
and theimport
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/*"]
}
}
}
- 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
- 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'
---
- Create
pages/jobs/index.js
file
- Import the
fs
andpath
modules - Import
matter
fromgray-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 thejobs
prop from thegetStaticProps()
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>
);
}
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>
);
}
- 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
anddata
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>
);
}
28