This project was bootstrapped with Create React App.
- Test Live Demo!
In this tutorial, we will be exploring a Job Listing App. Developed with React.js, alongside Firebase and Material UI touch. The key features that we will be exploring:
- Local State Management using Hooks
- Writing/Fetching of data from Firebase
- Saving Credentials using .ENV
_ Hooks are involved in every opeartion that is taking place in the App _
- Basics Knowledge about HTML, CSS & JavaScript
- Node.js must be installed
- Basic Knowledge about Firebase
Job Finder :)
Let's take a peak at our project structure for better understanding
Overall Project Structure
UI Components
- Header
- Post a Job
- Filter Component
- Job Card
- Job Details
Includes the 'Heading' and a Button Post a Job
.
Filter can be applied based on 2 types:
- Job Type
- Part-time, Full-time, Contract etc.
- Working Type
- Remote or Office etc.
Sub-divided into Three parts:
- Job Title & Company Tag
- Required Skills
- Job Details
- Posted Time
- Job Type
- Working Type
Check Details
Button
- Hooks are new to React.js
v16.8
- Let's you use State & features like Lifecycle Methods without writing a
class
- Reduce the number of concepts you need to juggle while writing React apps.
- Reduce the constant switching between functions, classes, higher-order components & allows the preference of
functions
.
We are using Hooks to managing the local state of our app, which includes very minimilist features like:
- Managing the state of loader
- State in filtration
- State in Job Details Component etc.
useState()
Hook for initializing the Local State inmain
componentApp.js
//State to save data of jobs from firebase
const [jobs,setJobs]=useState([]);
//state for the loader data.
const [loading,setLoading]=useState(true);
//State for the custom Search or Filteration
const [customSearch,setCustomSearch]=useState(false);
//State for the Apply for a Job Modal
const [openModal,setOpenModal]=useState(false);
//State for the View Job Modal
const [viewJob,setViewJob]=useState({});
useEffect()
Hook
The idea to implement useEffect
hook is to execute code that needs happens during lifecycle of the component instead of on specific user interactions or DOM
events.
useEffect(() => {
fetchJobs(); // We'll take a look into `fetchJobs()` in few moments
},[]);
-Config.js
- We are accessing the credentials via
.ENV
file.
import app from 'firebase/app';
import 'firebase/firestore';
var firebaseConfig = {
apiKey: process.env.REACT_APP_apiKey,
authDomain: process.env.REACT_APP_authDomain,
databaseURL: process.env.REACT_APP_databaseURL,
projectId: process.env.REACT_APP_projectId,
storageBucket: process.env.REACT_APP_storageBucket,
messagingSenderId: process.env.REACT_APP_messagingSenderId,
appId: process.env.REACT_APP_appId,
};
// Initialize Firebase
const firebase=app.initialize
- How we setup
.ENV
file- Create an .env in the root of your project
- The structure will be something like this:
.env file structure
- Your
.gitignore
file must contain.env
Post a Job Form
Setup your App with Cloud Firestore by following the official Documentation
Steps are being done in View Job Model
component
- Declaring an
Object
const initState = {
title: "",
type: "Full Time",
companyName: "",
companyUrl: "",
location: "Remote",
link: "",
description: "",
skills: [],
}
- Initalizing the State
// State for the Job Details
const [jobDetails,setJobDetails] = useState(initState);
// State for the loader of the data
const [loading,setLoading]=useState(false);
- Handling the change in Forms
const handleChange = (e) => {
e.persist();
setJobDetails(oldState => ({
...oldState,
[e.target.name]:e.target.value
}));
}
- Handling the change in Skills Selection
const addRemoveSkill = (skill) => {
jobDetails.skills.includes(skill)
//Removing Skill
? setJobDetails(oldState => ({
...oldState,
skills:oldState.skills.filter(s=> s!==skill)
}))
//Adding Skill
:setJobDetails(oldState => ({
...oldState,
skills:oldState.skills.concat(skill)
}))
}
- Handling the change on
Submit
const handleSubmit = async () => {
for (const field in jobDetails) {
if(typeof jobDetails[field] === "string" && !jobDetails[field] ) {
alert("Fill all the Required Fileds");
return;
}
}
if (!jobDetails.skills.length) {
alert("Fill all the Required Fileds");
return;
}
setLoading(true);
// function for sending all the details to Firebase
await props.PostJob(jobDetails);
closeModal();
}
closeModal()
is actually:
const closeModal = () =>{
setJobDetails(initState);
setLoading(false);
// Function from another component using props
props.closeJobModal();
}
Passing the function from main
component as a prop
in which we use the state openModal
for opening or closing the modal.
closeJobModal = {( ) => setOpenModal(false)}
Post a Job
: Loader Working
On submission of Forms
this is how the Loader works in the Post a Job
Button onClick:
<Button
onClick = {handleSubmit}
variant = "contained"
disableElevation
color = "primary"
disabled = {loading}>
{loading ? (<CircularProgress color="secondary" size={22} />
) : (
"Post Job"
)}
</Button>
Now, we have all the data which is perfectly ready to be uploaded on Firebase
- Function for sending the data to Firebase (in
main
componentApp.js
)
const PostJob = async (jobDetails) => {
await firestore.collection("jobs").add({
...jobDetails,
postedOn:app.firestore.FieldValue.serverTimestamp()
});
}
Getting data from Firestore and showing in the cards
- Function that will do the actual job of fetching the data from
cloud_firestore
const fetchJobs = async () => {
setCustomSearch(false);
//Setting the state of loader
setLoading(true);
const req = await firestore
//selection the collection from firestore
.collection('jobs')
//Sorting the jobs by posting time
.orderBy('postedOn','desc')
//getting the data
.get();
//mapping on the retrieved data
const tempData = req.docs.map((job) => ({
...job.data(),
//Adding the id to the data
id : job.id,
//setting the posted date
postedOn:job.data().postedOn.toDate(),
}));
//Push all the data to the Jobs Local State
setJobs(tempData);
//Setting the loader state to stop it
setLoading(false);
}
- Designing the Job Cards
<Box p={2} >
<Grid container mb={2} alignItems="center">
{/* Grid Item for the Title of job and the Company Name */}
<Grid item xs>
<Typography variant="subtitle1">{props.title}</Typography>
<Typography className={classes.companyName} variant="subtitle1">{props.companyName}</Typography>
</Grid>
{/* Grid Item for displaying the Required Skill set for the job */}
<Grid item container xs>
{/* Mapping over the skills and show each skill in the skillChip form */}
{props.skills.map(skill => <Grid className={classes.skillChip} key={skill}>
{skill}
</Grid>)}
</Grid>
<Grid item container direction="column" alignItems="flex-end" xs>
<Grid item>
{/* Foramt the Posted Date time in (count) Days Ago etc */}
<typography variant="caption">{formatDistance(Date.now(),props.postedOn)} ago | {props.type} | {props.location}</typography>
</Grid>
<Grid item>
<Box mt={2}>
<Button onClick={props.open} variant="outlined">Check</Button>
</Box>
</Grid>
</Grid>
</Grid>
</Box>
- Rendering Job Card component in
main
componentApp.js
<Box>
<Grid container justify="center">
<Grid item xs={10}>
{
{/* Setting the loader */}
loading ? (
<Box display="flex" justifyContent="center">
<CircularProgress />
</Box>
) : (
{/* Displaying all the jobs in jobCard */}
{jobs.map((job)=> (
<JobCard open={()=>setViewJob(job)} key={job.id} {...job} />
))}
)}
</Grid>
</Grid>
</Box>
Applying Filter to various Jobs
- Function for Filtering the data (jobs) from Firestore
const fetchCustomJobs = async (jobSearch) => {
//Setting the Loader
setLoading(true);
//Setting the Custom Search
setCustomSearch(true);
//Fetching data from Firestore
const req = await firestore
//Selecting the Collection from Firestore
.collection('jobs')
//Sorting the Data on posted Time
.orderBy('postedOn','desc')
//Filtering the Data
//comparing the Working Type
.where("location", "==" ,jobSearch.location)
//Comparing the Job Type
.where("type", "==" ,jobSearch.type)
//Getting the data
.get();
//<apping the retrieved data
const tempData=req.docs.map((job) => ({
...job.data(),
id : job.id,
postedOn:job.data().postedOn.toDate(),
}));
//Set the Data in local State
setJobs(tempData);
//Stop the Loader
setLoading(false);
}
- Rendering the Filtered Job Cards
<Box>
<Grid container justify="center">
<Grid item xs={10}>
{/* passing the Fetching Function */}
<SearchBar fetchCustomJobs={fetchCustomJobs} />
{
loading ? (
//Displaying the Loader
<Box display="flex" justifyContent="center">
<CircularProgress />
</Box>
) : (
<>
{/* adding the Custom Search button after filteration*/}
{customSearch && (
<Box my={2} display="flex" justifyContent="flex-end">
<Button onClick={fetchJobs}>
<CloseIcon size={20} />
Custom Search
</Button>
</Box>
)}
{/* Mapping the Jobs */}
{jobs.map((job)=> (
<JobCard open={()=>setViewJob(job)} key={job.id} {...job} />
))}
</>
)}
</Grid>
</Grid>
</Box>
Details of a Job
- Designing the Job Details Modal
<Dialog
{/* passing the jobs data as prop */}
open={!!Object.keys(props.job).length}
fullWidth>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
{/* Displaynig the Main heading */}
{props.job.title} @ {props.job.companyName}
{/* Setting the Closing button for Modal */}
<IconButton onClick={props.closeModal}>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<Box>
{/* Displaying all the Details about the job */}
<Box className={classes.info} display="flex">
<Typography variant="caption" size={20}>Posted on: </Typography>
<Typography variant="body2" size={20}>{props.job.postedOn && format(props.job.postedOn,"dd/MM/yyyy HH:MM")}</Typography>
</Box>
<Box className={classes.info} display="flex">
<Typography variant="caption">Job Type: </Typography>
<Typography variant="body2" size={20}>{props.job.type}</Typography>
</Box>
<Box className={classes.info} display="flex">
<Typography variant="caption">Work Type: </Typography>
<Typography variant="body2" size={20}>{props.job.location}</Typography>
</Box>
<Box className={classes.info} display="flex">
<Typography variant="caption">Description: </Typography>
<Typography variant="body2" size={20}>{props.job.description}</Typography>
</Box>
<Box className={classes.info} display="flex">
<Typography variant="caption">Comapny Name: </Typography>
<Typography variant="body2" size={20}>{props.job.companyName}</Typography>
</Box>
<Box className={classes.info} display="flex">
<Typography variant="caption">Comapny Website : </Typography>
<Typography variant="body2" size={20}>{props.job.companyUrl}</Typography>
</Box>
<Box className={classes.info} >
<Grid container alignItems="center">
{/* Mapping from the Skills */}
{props.job.skills &&
props.job.skills.map((skill)=>(
<Grid item key={skill} className={classes.skillChip}>
{skill}
</Grid>
))}
</Grid>
</Box>
</Box>
</DialogContent>
<DialogActions>
<Button
className={classes.included}
variant="outlined"
component="a"
rel={'external'}
href={props.job.link}
target="_blank">
Apply
</Button>
</DialogActions>
</Dialog>
so these are all the code snippets which are shared with you to understand the working of project more clearly.
Hosting a React.js App via Netlify
β‘ Start getting reviews on your projects from experts and your friends and what's the best way other than hosting it live!
π Read the documentation & Good Luck!
π Read the documentation here.
You can also follow my GitHub Profile to stay updated about my latest projects:
If you liked the repo then kindly support it by giving it a star β!
Copyright (c) 2020 CB