Build a Simple Next.js Interactive Job Board App

Written by

in

In today’s fast-paced digital landscape, the ability to build dynamic, interactive web applications is a crucial skill. Next.js, a powerful React framework, provides developers with the tools to create performant, SEO-friendly, and user-friendly web experiences. This article will guide you through building a simple, yet functional, interactive Job Board application using Next.js. This project is ideal for beginners and intermediate developers looking to expand their skillset and understand the core concepts of Next.js.

Why Build a Job Board App?

Creating a Job Board application is an excellent way to learn and practice several key web development concepts. It allows you to:

  • Understand Data Fetching: Learn how to retrieve data from APIs or databases.
  • Master Dynamic Routing: Create different routes for each job listing.
  • Handle User Interactions: Implement search functionality, filtering, and potentially user authentication.
  • Explore UI Components: Build reusable components for job listings, search bars, and other UI elements.
  • Grasp State Management (Optional): Implement state management for search queries, filtered results, etc.

Beyond the technical skills, building a Job Board app offers a practical project to showcase your abilities to potential employers. It demonstrates your proficiency in front-end development, data handling, and user interface design.

Prerequisites

Before we begin, ensure you have the following:

  • Node.js and npm (or yarn): Installed on your computer.
  • A Code Editor: Such as Visual Studio Code, Sublime Text, or Atom.
  • Basic Knowledge of JavaScript and React: Familiarity with these technologies is helpful.

Setting Up Your Next.js Project

Let’s get started by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app job-board-app

This command will create a new directory named “job-board-app” with all the necessary files for your Next.js project. Navigate into the project directory:

cd job-board-app

Now, start the development server:

npm run dev

Your application will be running on http://localhost:3000. Open this in your browser to see the default Next.js welcome page.

Project Structure

Let’s briefly examine the project structure. Next.js has a specific file and directory structure that makes development organized:

  • pages/: This directory is the heart of your Next.js application. Each file inside this directory becomes a route. For example, `pages/index.js` becomes the route `/`, and `pages/about.js` becomes the route `/about`.
  • components/: This directory is where you’ll store your reusable React components.
  • public/: This directory holds static assets like images and fonts.
  • styles/: This directory is for your CSS or other styling files.
  • package.json: Contains project dependencies and scripts.

Creating the Job Listing Component

First, let’s create a component to display individual job listings. Create a new file named `JobListing.js` inside the `components` directory:

// components/JobListing.js
import React from 'react';

function JobListing({ job }) {
  return (
    <div className="job-listing">
      <h3>{job.title}</h3>
      <p>Company: {job.company}</p>
      <p>Location: {job.location}</p>
      <p>Description: {job.description}</p>
      <a href={job.applyUrl} target="_blank" rel="noopener noreferrer">Apply Now</a>
    </div>
  );
}

export default JobListing;

This component accepts a `job` object as a prop and renders the job details. We’ll use this component later to display each job listing.

Important: Note the use of `target=”_blank” rel=”noopener noreferrer”` in the `<a>` tag. This is crucial for security when opening links in a new tab. It prevents the new page from having access to your page’s `window` object.

Fetching Job Data

Now, we need to fetch job data. For simplicity, we’ll use a local JSON file to simulate an API response. Create a new file named `jobs.json` in the `public` directory:

// public/jobs.json
[
  {
    "id": 1,
    "title": "Software Engineer",
    "company": "Acme Corp",
    "location": "San Francisco, CA",
    "description": "We are looking for a skilled software engineer...",
    "applyUrl": "https://www.acmecorp.com/careers/software-engineer"
  },
  {
    "id": 2,
    "title": "Frontend Developer",
    "company": "Beta Solutions",
    "location": "New York, NY",
    "description": "Join our team as a frontend developer...",
    "applyUrl": "https://www.betasolutions.com/careers/frontend-developer"
  },
  {
    "id": 3,
    "title": "UI/UX Designer",
    "company": "Gamma Design",
    "location": "Austin, TX",
    "description": "Create beautiful and functional user interfaces...",
    "applyUrl": "https://www.gammadesign.com/careers/ui-ux-designer"
  }
]

This JSON file contains an array of job objects. In a real-world application, you would fetch this data from a database or a third-party API.

Next, let’s modify `pages/index.js` to fetch and display the job listings. Replace the content of `pages/index.js` with the following:

// pages/index.js
import React, { useState, useEffect } from 'react';
import JobListing from '../components/JobListing';

function Home() {
  const [jobs, setJobs] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchJobs() {
      try {
        const res = await fetch('/jobs.json');
        const data = await res.json();
        setJobs(data);
        setLoading(false);
      } catch (error) {
        console.error('Error fetching jobs:', error);
        setLoading(false);
      }
    }

    fetchJobs();
  }, []);

  if (loading) {
    return <p>Loading...</p>;
  }

  return (
    <div className="container">
      <h1>Job Board</h1>
      <div className="job-listings">
        {jobs.map((job) => (
          <JobListing key={job.id} job={job} />
        ))}
      </div>
    </div>
  );
}

export default Home;

Let’s break down the changes:

  • Import `useState` and `useEffect`: We import these React hooks to manage state and handle side effects (like fetching data).
  • `jobs` state: We use `useState` to create a `jobs` state variable, initialized as an empty array. This will hold the job data.
  • `loading` state: A `loading` state is added to indicate when the data is being fetched.
  • `useEffect` hook: This hook runs after the component renders. Inside, we define an `async` function `fetchJobs` to fetch the data from `/jobs.json`.
  • `fetch` API: The `fetch` API is used to get the data.
  • Data Parsing: The response is parsed to JSON and the `setJobs` function updates the `jobs` state.
  • Loading State: The `loading` state is used to display a “Loading…” message while the data is being fetched.
  • Mapping Job Listings: The `jobs.map()` function iterates through the `jobs` array and renders a `JobListing` component for each job. The `key` prop is essential for React to efficiently update the list.

Styling the Application

To make our job board look presentable, let’s add some basic styling. Open `styles/globals.css` and replace its content with the following:

/* styles/globals.css */
body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f4f4f4;
}

.container {
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  color: #333;
}

.job-listings {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
}

.job-listing {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background-color: #f9f9f9;
}

.job-listing h3 {
  margin-top: 0;
  color: #0070f3;
}

.job-listing p {
  margin-bottom: 5px;
}

.job-listing a {
  display: inline-block;
  margin-top: 10px;
  padding: 10px 15px;
  background-color: #0070f3;
  color: white;
  text-decoration: none;
  border-radius: 4px;
}

This CSS provides basic layout and styling for the job board. You can customize this to match your desired design.

Adding Search Functionality

Let’s add a search bar to filter job listings. First, create a new component called `Search.js` in the `components` directory:


// components/Search.js
import React, { useState } from 'react';

function Search({ onSearch }) {
  const [searchTerm, setSearchTerm] = useState('');

  const handleChange = (event) => {
    setSearchTerm(event.target.value);
    onSearch(event.target.value);
  };

  return (
    <div className="search-bar">
      <input
        type="text"
        placeholder="Search jobs..."
        value={searchTerm}
        onChange={handleChange}
      />
    </div>
  );
}

export default Search;

This component takes an `onSearch` prop, which is a function to be called when the search term changes. It also maintains the `searchTerm` state.

Next, modify `pages/index.js` to include the `Search` component and implement the search functionality:


// pages/index.js
import React, { useState, useEffect } from 'react';
import JobListing from '../components/JobListing';
import Search from '../components/Search';

function Home() {
  const [jobs, setJobs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    async function fetchJobs() {
      try {
        const res = await fetch('/jobs.json');
        const data = await res.json();
        setJobs(data);
        setLoading(false);
      } catch (error) {
        console.error('Error fetching jobs:', error);
        setLoading(false);
      }
    }

    fetchJobs();
  }, []);

  const handleSearch = (term) => {
    setSearchTerm(term);
  };

  const filteredJobs = jobs.filter(job =>
    job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
    job.company.toLowerCase().includes(searchTerm.toLowerCase())
  );

  if (loading) {
    return <p>Loading...</p>
  }

  return (
    <div className="container">
      <h1>Job Board</h1>
      <Search onSearch={handleSearch} />
      <div className="job-listings">
        {filteredJobs.map((job) => (
          <JobListing key={job.id} job={job} />
        ))}
      </div>
    </div>
  );
}

export default Home;

Here’s what changed:

  • Import `Search`: We imported the `Search` component.
  • `searchTerm` state: We added `searchTerm` to manage the search input.
  • `handleSearch` function: This function is passed as a prop to the `Search` component and updates the `searchTerm` state whenever the search input changes.
  • `filteredJobs`: We added `filteredJobs` which filters the `jobs` array based on the `searchTerm`. The `toLowerCase()` method makes the search case-insensitive.
  • Rendering `Search`: The `Search` component is rendered above the job listings, passing the `handleSearch` function as a prop.
  • Mapping `filteredJobs`: We now map over `filteredJobs` to display the filtered job listings.

Now, your job board should have a working search bar!

Adding Dynamic Routes for Job Details (Optional)

To create a more comprehensive job board, you could add dynamic routes for each job listing. This would allow users to click on a job title and view a dedicated page with more details. This is more advanced but is a good way to improve the application.

First, create a new file in the `pages` directory named `jobs/[id].js`. This is a dynamic route. The `[id]` part will be replaced with the job ID.


// pages/jobs/[id].js
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import JobListing from '../../components/JobListing';

function JobDetail() {
  const router = useRouter();
  const { id } = router.query;
  const [job, setJob] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchJob() {
      if (!id) return;
      try {
        const res = await fetch(`/jobs.json`);
        const data = await res.json();
        const job = data.find(job => job.id === parseInt(id));
        setJob(job);
        setLoading(false);
      } catch (error) {
        console.error('Error fetching job:', error);
        setLoading(false);
      }
    }

    fetchJob();
  }, [id]);

  if (loading) {
    return <p>Loading...</p>
  }

  if (!job) {
    return <p>Job not found.</p>
  }

  return (
    <div className="container">
      <h1>Job Detail</h1>
      <JobListing job={job} />
    </div>
  );
}

export default JobDetail;

Here’s what this code does:

  • Import `useRouter`: This hook from `next/router` allows us to access the route parameters.
  • `router.query.id`: This retrieves the `id` from the URL (e.g., in the URL `/jobs/1`, `id` would be “1”).
  • Fetching the Job Data: The code fetches the job data from `jobs.json` and finds the job that matches the ID in the URL.
  • Conditional Rendering: The component handles loading and “Job not found” states.
  • Displaying Job Details: It renders the `JobListing` component with the specific job details.

Now, modify the `JobListing` component in `components/JobListing.js` to link to the job detail page. Replace the `h3` element with a link:


// components/JobListing.js
import React from 'react';
import Link from 'next/link';

function JobListing({ job }) {
  return (
    <div className="job-listing">
      <Link href={`/jobs/${job.id}`}>
        <h3>{job.title}</h3>
      </Link>
      <p>Company: {job.company}</p>
      <p>Location: {job.location}</p>
      <p>Description: {job.description}</p>
      <a href={job.applyUrl} target="_blank" rel="noopener noreferrer">Apply Now</a>
    </div>
  );
}

export default JobListing;

The `Link` component from `next/link` is used to create a client-side navigation to the job detail page. We construct the URL using a template literal: `/jobs/${job.id}`.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect File Paths: Double-check your file paths when importing components or fetching data. Typos in paths are a common source of errors.
  • Data Fetching Errors: Ensure your API endpoint or local JSON file is accessible and returns the correct data format. Use `console.log()` to debug the response. Make sure to handle errors appropriately with try/catch blocks.
  • Missing Keys in `map()`: Always provide a unique `key` prop when rendering lists of elements using `map()`. This helps React efficiently update the DOM.
  • CSS Specificity Issues: If your styles aren’t applying as expected, check for CSS specificity conflicts. You might need to adjust your CSS selectors or use more specific selectors.
  • Not Using `target=”_blank” rel=”noopener noreferrer”`: When linking to external websites, always include `target=”_blank” rel=”noopener noreferrer”` to prevent potential security vulnerabilities.

Key Takeaways

  • Next.js is a Powerful Framework: Next.js simplifies building modern web applications with its features like server-side rendering, static site generation, and routing.
  • Component-Based Architecture: React and Next.js encourage a component-based approach, making your code modular and reusable.
  • Data Fetching is Crucial: Understanding data fetching techniques is essential for building dynamic web applications.
  • User Interface is Important: Pay attention to user experience by designing a clear and intuitive interface.
  • Practice Makes Perfect: Building projects like this job board app is a great way to learn and improve your skills.

Optional: FAQ

Here are some frequently asked questions about this project:

  1. Can I use a real API instead of a local JSON file? Yes, absolutely! Replace the `fetch(‘/jobs.json’)` call with a call to your API endpoint. Make sure to handle potential errors and data transformation as needed.
  2. How can I deploy this application? You can deploy your Next.js application to platforms like Vercel (which is recommended), Netlify, or other hosting providers. Vercel has excellent integration with Next.js.
  3. How can I add more features? You can add features like:
    • Filtering by location, salary, or job type.
    • User authentication.
    • Saving favorite jobs.
    • Implementing a database to store job data.
  4. Why is `key` prop important in `map()`? The `key` prop helps React efficiently update the DOM when the list changes. It allows React to identify which items have changed, been added, or removed, resulting in better performance.

This tutorial provides a solid foundation for building a job board application with Next.js. By understanding the core concepts and practicing with this project, you’ll be well on your way to building more complex and feature-rich web applications. Remember to experiment, explore, and expand upon this foundation to create your own unique projects. Happy coding!