How to Create a Job Board in NextJS

Amarachi Iheanacho - Mar 31 '22 - - Dev Community

Job boards are a convenient way to share job opportunities. With the advantage of being viewed from our mobile devices and PCs anytime globally, job boards have proven to be a better alternative to traditional means of sharing job opportunities.

What we will be building

This post will discuss creating a simple job board that allows us to create, delete and display job opportunities in a Next.js project. We do not require a custom backend server.

GitHub URL

https://github.com/Iheanacho-ai/appwrite-jobboard-next.js

Prerequisites

To get the most out of this article, we require the following:

  • A basic understanding of CSS, JavaScript, and React.js.
  • Docker Desktop installed on the computer. Run the docker -v command to verify if we have docker desktop installed. If not, install it from here.
  • An Appwrite instance running on our computer. Check this documentation to quickly learn how to create a local Appwrite instance. We will use Appwrite’s powerful database service and experience to manage our board.

Setting up our Next.js app

Next.js is an open-source React framework that enables us to build server-side rendered static web applications.

To create our Next.js app, we navigate to our preferred directory and run the terminal command below:

    npx create-next-app@latest
    # or
    yarn create next-app
Enter fullscreen mode Exit fullscreen mode

After creating our app, we change the directory to our project and start a local development server with:

    cd <name of our project>
    npm run dev
Enter fullscreen mode Exit fullscreen mode

To see our app, we go to http://localhost:3000/.

Installing Appwrite

Appwrite is an open-source, end-to-end, back-end server solution that allows developers to build applications faster.

To use Appwrite in our Next.js application, we install the Appwrite client-side SDK by running this terminal command.

    npm install appwrite
Enter fullscreen mode Exit fullscreen mode

Creating a new Appwrite project

Running a local Appwrite instance gives us access to our console. We go to the local Appwrite instance on whatever port it is started on to create an account. Typically, this is on localhost:80 or as specified during Appwrite’s installation.

On the console, there is a Create Project button. Click on it to start a new project.

Our project dashboard appears once we have created the project. At the top of the page, there is a settings bar. Click it to access our Project ID and API Endpoint.

We copy our Project ID and API Endpoint, which we need to initialize our Web SDK code.
In the root directory of our project, we create a utils folder, which will hold our web-init.js file. This file configures Appwrite in our application.

In our utils/web-init.js file, we initialize our Web SDK with:

    // Init your Web SDK
    import { Appwrite } from "appwrite";
    const sdk = new Appwrite();
    sdk
        .setEndpoint('http://localhost/v1') // Your API Endpoint
        .setProject(projectID) // Your project ID
    ;

    export default sdk;
Enter fullscreen mode Exit fullscreen mode

Creating a collection and attributes

On the left side of our dashboard, we select the Database menu. We create a collection in our database tab by clicking on the Add Collection button. This action redirects us to a Permissions page.

At the Collection Level, we want our Read Access and Write Access to have a value of role:all.

Appwrite Permission Page

On the right of our Permissions page, we copy our collection ID, which we need to perform operations on documents in this collection.

Next, we go to our attributes tab to create the fields we want a document to have. These properties are jobTitle, companyName, place.

Appwrite Database Dashboard

Creating the job entry page

Our job board application will have two routes. One route will lead to a job entry page and another to a job listing page.

Creating our Job Entry User Interface

We will make our job entry page with a form. In our index.js file, we create this form with the code block below.

    import { Appwrite } from 'appwrite';

    const Home = () => {
      return(
        <div className="create-job">
        <h2>Create a Job Post</h2>
        <form action="">
            <div className='txt-field'>
                <input type="text"/>
                <span></span>
                <label htmlFor="input">Job Title</label>
            </div>
            <div className='txt-field'>
                <input type="text" />
                <span></span>
                <label htmlFor="input">Company Name</label>
            </div>
            <div className='txt-field'>
                <input type="text"/>
                <span></span>
                <label htmlFor="input">Place</label>
            </div>
            <button type= "button" className='submit'>Add Job</button>
        </form>
        <p>See your Job Board <Link href="/list-job"><a>here</a></Link></p>
    </div>
      )
    }
Enter fullscreen mode Exit fullscreen mode

Next, we add our form styles.

https://gist.github.com/Iheanacho-ai/65a6ff9f2f372b2be2763482fc0f61bb

Here is our job entry page.

Job Board Index page

Making our Job Entry Page interact with our Database.

Creating an anonymous user session

Appwrite requires a user to sign in before reading or writing to a database to enable safety in our application. However, they allow us to create an anonymous session that we’ll use in this project.

In our index.js file, we import sdk from our web-init.js file.

    import sdk from '../utils/web-init';

Enter fullscreen mode Exit fullscreen mode

Next, we create an anonymous user session once our app is mounted.


    async function createAnonymousSession(){
        try{
            await sdk.account.createAnonymousSession();
        }catch(err){
            console.log(err)
        }

    }
    useEffect(()=> {
        createAnonymousSession()
    }, [])

Enter fullscreen mode Exit fullscreen mode

Creating state variables to hold our form values

In our index.js file, we create state variables to hold the form input values.


    const [job, setJobTitle] = useState('')
    const [companyName, setCompanyName] = useState('')
    const [place, setPlace] = useState('')
Enter fullscreen mode Exit fullscreen mode

In the ìndex.js file, we pass the state variables as the input field values. We then use the onChange event listener to update the state variable values when users type in the input fields.


    <div className="create-job">
        <h2>Create a Job Post</h2>
        <form action="">
            <div className='txt-field'>
                <input type="text" value={job} onChange = {(e) => setJobTitle(e.target.value)}/>
                <span></span>
                <label htmlFor="input">Job Title</label>
            </div>
            <div className='txt-field'>
                <input type="text" value={companyName} onChange = {(e) => setCompanyName(e.target.value)}/>
                <span></span>
                <label htmlFor="input">Company Name</label>
            </div>
            <div className='txt-field'>
                <input type="text" value={place} onChange = {(e) => setPlace(e.target.value)}/>
                <span></span>
                <label htmlFor="input">Place</label>
            </div>
            <button type= "button" className='submit'>Add Job</button>
        </form>
        <p>See your Job Board <Link href="/list-job"><a>here</a></Link></p>
    </div>

Enter fullscreen mode Exit fullscreen mode

Creating database documents.

In our index.js file, we write a handleJobBoard function to create documents in our collection.

    const handleJobBoard = () => {
       let promise = sdk.database.createDocument(collectionID, 'unique()', {
         "jobTitle" : job,
         "companyName": companyName,
         "place": place
      });

      promise.then(function (response) {
          setJobTitle('');
          setCompanyName('');
          setPlace('');

          alert('your job item has been successfully saved'); // Success
      }, function (error) {
          console.log(error)
      });
    }

Enter fullscreen mode Exit fullscreen mode

This handleJobBoard function above does the following :

  • Uses the Appwrite createDocument() method, which creates a document using the collection ID and data fields to be stored. This collection ID is the same ID we copied from our Permissions Page earlier.
  • Alerts us when we have successfully saved our document, then clears the information in our local state variables.

Next, we pass our handleJobBoard() function into an onClick event listener on our button element.

    <button type= "button" className='submit' onClick={handleJobBoard}>Add Job</button>

Enter fullscreen mode Exit fullscreen mode

NOTE: We must use a button with a type= button to override the button’s default submit behavior.

Fill out the form and go to the Documents tab on the Appwrite’s project dashboard to see the saved documents.

Appwrite Dashboard

Our index.js file should look like the code below when we completed this tutorial section.

https://gist.github.com/Iheanacho-ai/d5a13a74774e453a54fa1536d8ddc6bb

Creating our Job Listing Page

Listing documents

In our pages folder, we create a list-job.jsx file. The list-job.jsx file is responsible for creating our job listing page.

In our pages/list-job, we write this code for listing out documents in our collection.

    import sdk from '../utils/web-init';
    import { useEffect, useState } from "react";

    const ListJob = () => {

        const [jobList, setJobList] = useState()

        const listProducts = async () => {
         try {
              let response = await sdk.database.listDocuments(collectionID);
              setJobList(response.documents)

          } catch (error) {
              console.log(error)
          }
      }    
        useEffect(() => {
            listProducts()
        }, [])

        return(
            <div className="list-job">Hello World!</div>
        )
    };
    export default ListJob;

Enter fullscreen mode Exit fullscreen mode

We create a jobList state variable in the code block above. This variable will hold the information in our documents. We then create a listProducts function to display our jobs. We use Appwrite's listDocuments() method to do this.

To specify what collection we want to access, we pass a collection ID parameter to the listDocuments() method. Lastly, we updated our jobList state variable.

Our useEffect() hook runs the listProducts function.

Deleting documents

In our pages/list-job.jsx we create a handleDelete() funtion to delete documents we do not want anymore in our collection.

    import sdk from '../utils/web-init';
    import { useEffect, useState } from "react";

    const ListJob = () => {
      const [jobList, setJobList] = useState()
      const listProducts = async () => {
       try {
          let response = await sdk.database.listDocuments(collectionID);
          setJobList(response.documents)

      } catch (error) {
            console.log(error)
        }
      }

    // deletes our job

      const handleDelete = async (documentid) => {
        try {
            await sdk.database.deleteDocument(collectionID, documentid);
            alert("item have been deleted successfully")
            listProducts()

        } catch (error) {
            console.log(error)
        }
     }   
        useEffect(() => {
            listProducts()
        }, [])

        return(
            <div className="list-job">Hello World!</div>
        )
     };
    export default ListJob;

Enter fullscreen mode Exit fullscreen mode

The handleDelete function above does the following:

  • Finds a document using its collection ID, and the document ID gets passed into the function.
  • Deletes that document using Appwrite deleteDocument() method.
  • Alerts us if we deleted an item.
  • Runs our listProducts function to display our updated job list.
  • Logs an error if deleting the document fails.

Next, in our pages/list-job.jsx file, we use JavaScript's ternary operator to conditionally render our job listing page.

    return(
        <div className="list-job">
            {
                jobList ? <div>Hello World</div> : null
            }
        </div>
    )

Enter fullscreen mode Exit fullscreen mode

Here is what our pages/list-job.jsx file looks like.

    import sdk from '../utils/web-init';
    import { useEffect, useState } from "react";
    import ListJobItem from '../components/list-job-item';

    const ListJob = () => {
        const [jobList, setJobList] = useState()

        const listProducts = async () => {
            let response = await sdk.database.listDocuments(collectionID);
            setJobList(response.documents)
        }

        useEffect(() => {
            listProducts()
        }, [])

        const handleDelete = async (documentid) => {
            await sdk.database.deleteDocument(collectionID, documentid);
            alert("item have been deleted successfully")
            listProducts()
        }


        return(    
            <div className="list-job">
                {
                    jobList ? <div>Hello World!</div> : null
                }
        </div>
        )
    };
    export default ListJob;
Enter fullscreen mode Exit fullscreen mode

Creating a job item template and looping through them

In our root directory, we create a components folder. This folder will contain our list-job-item.jsx file.

In our components/list-job-item.jsx file we create the template for a job item.

    const ListJobItem = () => {
        return (
            <div className="list-job-item">
                <div className="item">
                    <h3>jobTitle goes here</h3>
                    <p>companyName goes heere</p>
                    <p>place goes here</p>
                    <button type= "button" className="delete">delete</button>
                </div>
            </div> 
        )
    }
    export default ListJobItem;

Enter fullscreen mode Exit fullscreen mode

Next, we import the ListJobItem component into the ListJob component in the list-job.jsx file.

Following that, we pass the jobs data and the delete method as props to the rendered ListJobItem component.

    return(
        <div className="list-job">
            {
                jobList ? <ListJobItem  jobList= {jobList} handleDelete={handleDelete}/> : null
            }
        </div>
    )

Enter fullscreen mode Exit fullscreen mode

In our components/list-job-item, we update the document to loop through the jobs passed as props, then render each one.

    const ListJobItem = ({jobList, handleDelete}) => {
        return (
            <div className="list-job-item">
                {
                    jobList.map(({jobTitle, companyName, place, $id}) => (
                        <div className="item" id={$id}>
                            <h3>{jobTitle}</h3>
                            <p>{companyName}</p>
                            <p>{place}</p>
                            <button type= "button" className="delete" onClick={() => handleDelete($id)}>delete</button>
                        </div>
                    ))
                }

            </div> 
        )
    }
    export default ListJobItem;
Enter fullscreen mode Exit fullscreen mode

In the code block above, we do the following:

  • Destructured our props and loop through the jobList variable using the JavaScript map() method.
  • Destructured our jobList variable to get the jobTitle, companyName, place, and $id.
  • Pass in our $id in the handleDelete() method, on the onClick event listener.

Our job listing page is incomplete without the styling. We add these styles in our global.css file.

https://gist.github.com/Iheanacho-ai/81b6adb59a902af2767ced7f7174b4d0

Fill out the form to see how our job board looks.

Appwrite Job board

Conclusion

This article discussed using the Appwrite to quickly create, retrieve and delete data on our database. With this, we created a job board in a Next.js application. The created job board lacks other fields, therefore, improve this project to make a full featured job board.

Resources

Here are some resources that might be helpful:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .