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
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
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
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;
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.
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.
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>
)
}
Next, we add our form styles.
https://gist.github.com/Iheanacho-ai/65a6ff9f2f372b2be2763482fc0f61bb
Here is our job entry 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';
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()
}, [])
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('')
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>
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)
});
}
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>
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.
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;
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;
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>
)
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;
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;
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>
)
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;
In the code block above, we do the following:
- Destructured our props and loop through the
jobList
variable using the JavaScriptmap()
method. - Destructured our
jobList
variable to get thejobTitle
,companyName
,place
, and$id
. - Pass in our
$id
in thehandleDelete()
method, on theonClick
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.
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:
Top comments (5)
Thank you for writing this up! I also recently made a job board on Next.js - Jobs in JS.
I'd be happy to answer any questions related to building online job boards on Next.js.
Nicely done!
Very well done, Michal Malota!!
I am working on a similar use case, do you have any apis which I can use for companies data?
Cool job board!
What backend, database and RTE did you use?
It's a Next.js website with Postgres and Prisma.io. Hosted on Vercel and Supabase. The RTE is tiptap.dev/.