userSlice.js
export const updateUser = createAsyncThunk(
'user/updateUser',
async (user, thunkAPI) => {
try {
const resp = await customFetch.patch('/auth/updateUser', user, {
headers: {
// authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
authorization: `Bearer `,
},
});
return resp.data;
} catch (error) {
// console.log(error.response);
if (error.response.status === 401) {
thunkAPI.dispatch(logoutUser());
return thunkAPI.rejectWithValue('Unauthorized! Logging Out...');
}
return thunkAPI.rejectWithValue(error.response.data.msg);
}
}
);
// logoutUser
logoutUser: (state) => {
state.user = null;
state.isSidebarOpen = false;
toast.success('Logout Successful!');
removeUserFromLocalStorage();
},
- features/user/userThunk.js
import customFetch from '../../utils/axios';
import { logoutUser } from './userSlice';
export const registerUserThunk = async (url, user, thunkAPI) => {
try {
const resp = await customFetch.post(url, user);
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
export const loginUserThunk = async (url, user, thunkAPI) => {
try {
const resp = await customFetch.post(url, user);
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
export const updateUserThunk = async (url, user, thunkAPI) => {
try {
const resp = await customFetch.patch(url, user, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
return resp.data;
} catch (error) {
// console.log(error.response);
if (error.response.status === 401) {
thunkAPI.dispatch(logoutUser());
return thunkAPI.rejectWithValue('Unauthorized! Logging Out...');
}
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
userSlice.js
import {
loginUserThunk,
registerUserThunk,
updateUserThunk,
} from './userThunk';
export const registerUser = createAsyncThunk(
'user/registerUser',
async (user, thunkAPI) => {
return registerUserThunk('/auth/register', user, thunkAPI);
}
);
export const loginUser = createAsyncThunk(
'user/loginUser',
async (user, thunkAPI) => {
return loginUserThunk('/auth/login', user, thunkAPI);
}
);
export const updateUser = createAsyncThunk(
'user/updateUser',
async (user, thunkAPI) => {
return updateUserThunk('/auth/updateUser', user, thunkAPI);
}
);
- features/job/jobSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { toast } from 'react-toastify';
import customFetch from '../../utils/axios';
import { getUserFromLocalStorage } from '../../utils/localStorage';
const initialState = {
isLoading: false,
position: '',
company: '',
jobLocation: '',
jobTypeOptions: ['full-time', 'part-time', 'remote', 'internship'],
jobType: 'full-time',
statusOptions: ['interview', 'declined', 'pending'],
status: 'pending',
isEditing: false,
editJobId: '',
};
const jobSlice = createSlice({
name: 'job',
initialState,
});
export default jobSlice.reducer;
store.js
import jobSlice from './features/job/jobSlice';
export const store = configureStore({
reducer: {
user: userSlice,
job: jobSlice,
},
});
AddJob.js
import { FormRow } from '../../components';
import Wrapper from '../../assets/wrappers/DashboardFormPage';
import { useSelector, useDispatch } from 'react-redux';
import { toast } from 'react-toastify';
const AddJob = () => {
const {
isLoading,
position,
company,
jobLocation,
jobType,
jobTypeOptions,
status,
statusOptions,
isEditing,
editJobId,
} = useSelector((store) => store.job);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!position || !company || !jobLocation) {
toast.error('Please Fill Out All Fields');
return;
}
};
const handleJobInput = (e) => {
const name = e.target.name;
const value = e.target.value;
};
return (
<Wrapper>
<form className="form">
<h3>{isEditing ? 'edit job' : 'add job'}</h3>
<div className="form-center">
{/* position */}
<FormRow
type="text"
name="position"
value={position}
handleChange={handleJobInput}
/>
{/* company */}
<FormRow
type="text"
name="company"
value={company}
handleChange={handleJobInput}
/>
{/* location */}
<FormRow
type="text"
labelText="job location"
name="jobLocation"
value={jobLocation}
handleChange={handleJobInput}
/>
{/* job status */}
{/* job type */}
{/* btn container */}
<div className="btn-container">
<button
type="button"
className="btn btn-block clear-btn"
onClick={() => console.log('clear values')}
>
clear
</button>
<button
type="submit"
className="btn btn-block submit-btn"
onClick={handleSubmit}
disabled={isLoading}
>
submit
</button>
</div>
</div>
</form>
</Wrapper>
);
};
export default AddJob;
// job status
return (
<div className="form-row">
<label htmlFor="status" className="form-label">
status
</label>
<select
name="status"
value={status}
onChange={handleJobInput}
className="form-select"
>
{statusOptions.map((itemValue, index) => {
return (
<option key={index} value={itemValue}>
{itemValue}
</option>
);
})}
</select>
</div>
);
- FormRowSelect.js
const FormRowSelect = ({ labelText, name, value, handleChange, list }) => {
return (
<div className="form-row">
<label htmlFor={name} className="form-label">
{labelText || name}
</label>
<select
name={name}
value={value}
id={name}
onChange={handleChange}
className="form-select"
>
{list.map((itemValue, index) => {
return (
<option key={index} value={itemValue}>
{itemValue}
</option>
);
})}
</select>
</div>
);
};
export default FormRowSelect;
AddJob.js
/* job status */
<FormRowSelect
name='status'
value={status}
handleChange={handleJobInput}
list={statusOptions}
/>
<FormRowSelect
name='jobType'
labelText='job type'
value={jobType}
handleChange={handleJobInput}
list={jobTypeOptions}
/>
jobSlice.js
// reducers
handleChange: (state, { payload: { name, value } }) => {
state[name] = value;
},
export const { handleChange } = jobSlice.actions;
AddJob.js
import { handleChange } from '../../features/job/jobSlice';
const handleJobInput = (e) => {
const name = e.target.name;
const value = e.target.value;
dispatch(handleChange({ name, value }));
};
// reducers
clearValues: () => {
return {
...initialState
};
return initialState
},
export const { handleChange, clearValues } = jobSlice.actions;
AddJob.js
import { clearValues, handleChange } from '../../features/job/jobSlice';
return (
<button
type="button"
className="btn btn-block clear-btn"
onClick={() => dispatch(clearValues())}
>
clear
</button>
);
- POST /jobs
- { position:'position', company:'company', jobLocation:'location', jobType:'full-time', status:'pending' }
- authorization header : 'Bearer token'
- sends back the job object
export const createJob = createAsyncThunk(
'job/createJob',
async (job, thunkAPI) => {
try {
const resp = await customFetch.post('/jobs', job, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
thunkAPI.dispatch(clearValues());
return resp.data;
} catch (error) {
// basic setup
return thunkAPI.rejectWithValue(error.response.data.msg);
// logout user
if (error.response.status === 401) {
thunkAPI.dispatch(logoutUser());
return thunkAPI.rejectWithValue('Unauthorized! Logging Out...');
}
return thunkAPI.rejectWithValue(error.response.data.msg);
}
}
);
// extra reducers
extraReducers: {
[createJob.pending]: (state) => {
state.isLoading = true;
},
[createJob.fulfilled]: (state, action) => {
state.isLoading = false;
toast.success('Job Created');
},
[createJob.rejected]: (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
},
}
AddJob.js
import {
clearValues,
handleChange,
createJob,
} from '../../features/job/jobSlice';
const handleSubmit = (e) => {
e.preventDefault();
if (!position || !company || !jobLocation) {
toast.error('Please Fill Out All Fields');
return;
}
dispatch(createJob({ position, company, jobLocation, jobType, status }));
};
AddJob.js
const { user } = useSelector((store) => store.user);
useEffect(() => {
// eventually will check for isEditing
if (!isEditing) {
dispatch(handleChange({ name: 'jobLocation', value: user.location }));
}
}, []);
jobSlice.js
// reducers
clearValues: () => {
return {
...initialState,
jobLocation: getUserFromLocalStorage()?.location || '',
};
},
userSlice.js
logoutUser: (state,{payload}) => {
state.user = null;
state.isSidebarOpen = false;
removeUserFromLocalStorage();
if(payload){
toast.success(payload)
}
},
Navbar.js
<button
type="button"
className="dropdown-btn"
onClick={() => dispatch(logoutUser('Logging out...'))}
>
logout
</button>
- features/allJobs/allJobsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { toast } from 'react-toastify';
import customFetch from '../../utils/axios';
const initialFiltersState = {
search: '',
searchStatus: 'all',
searchType: 'all',
sort: 'latest',
sortOptions: ['latest', 'oldest', 'a-z', 'z-a'],
};
const initialState = {
isLoading: false,
jobs: [],
totalJobs: 0,
numOfPages: 1,
page: 1,
stats: {},
monthlyApplications: [],
...initialFiltersState,
};
const allJobsSlice = createSlice({
name: 'allJobs',
initialState,
});
export default allJobsSlice.reducer;
store.js
import { configureStore } from '@reduxjs/toolkit';
import userSlice from './features/user/userSlice';
import jobSlice from './features/job/jobSlice';
import allJobsSlice from './features/allJobs/allJobsSlice';
export const store = configureStore({
reducer: {
user: userSlice,
job: jobSlice,
allJobs: allJobsSlice,
},
});
- create
- components/SearchContainer.js
- components/JobsContainer.js
- components/Job.js
- import/export
AllJobs.js
import { JobsContainer, SearchContainer } from '../../components';
const AllJobs = () => {
return (
<>
<SearchContainer />
<JobsContainer />
</>
);
};
export default AllJobs;
import { useEffect } from 'react';
import Job from './Job';
import Wrapper from '../assets/wrappers/JobsContainer';
import { useSelector, useDispatch } from 'react-redux';
const JobsContainer = () => {
const { jobs, isLoading } = useSelector((store) => store.allJobs);
const dispatch = useDispatch();
if (isLoading) {
return (
<Wrapper>
<h2>Loading...</h2>
</Wrapper>
);
}
if (jobs.length === 0) {
return (
<Wrapper>
<h2>No jobs to display...</h2>
</Wrapper>
);
}
return (
<Wrapper>
<h5>jobs info</h5>
<div className="jobs">
{jobs.map((job) => {
return <Job key={job._id} {...job} />;
})}
</div>
</Wrapper>
);
};
export default JobsContainer;
Loading.js
const Loading = ({ center }) => {
return <div className={center ? 'loading loading-center' : 'loading'}></div>;
};
export default Loading;
JobsContainer.js
import Loading from './Loading';
if (isLoading) {
return <Loading center />;
}
- GET /jobs
- authorization header : 'Bearer token'
- returns {jobs:[],totalJobs:number, numOfPages:number }
allJobsSlice.js
export const getAllJobs = createAsyncThunk(
'allJobs/getJobs',
async (_, thunkAPI) => {
let url = `/jobs`;
try {
const resp = await customFetch.get(url, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
}
);
// extra reducers
extraReducers: {
[getAllJobs.pending]: (state) => {
state.isLoading = true;
},
[getAllJobs.fulfilled]: (state, { payload }) => {
state.isLoading = false;
state.jobs = payload.jobs;
},
[getAllJobs.rejected]: (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
},
}
JobsContainer.js
import { getAllJobs } from '../features/allJobs/allJobsSlice';
useEffect(() => {
dispatch(getAllJobs());
}, []);
Job.js
import { FaLocationArrow, FaBriefcase, FaCalendarAlt } from 'react-icons/fa';
import { Link } from 'react-router-dom';
import Wrapper from '../assets/wrappers/Job';
import { useDispatch } from 'react-redux';
const Job = ({
_id,
position,
company,
jobLocation,
jobType,
createdAt,
status,
}) => {
const dispatch = useDispatch();
return (
<Wrapper>
<header>
<div className="main-icon">{company.charAt(0)}</div>
<div className="info">
<h5>{position}</h5>
<p>{company}</p>
</div>
</header>
<div className="content">
<div className="content-center">
<h4>more content</h4>
<div className={`status ${status}`}>{status}</div>
</div>
<footer>
<div className="actions">
<Link
to="/add-job"
className="btn edit-btn"
onClick={() => {
console.log('edit job');
}}
>
Edit
</Link>
<button
type="button"
className="btn delete-btn"
onClick={() => {
console.log('delete job');
}}
>
Delete
</button>
</div>
</footer>
</div>
</Wrapper>
);
};
export default Job;
- components/JobInfo.js
import Wrapper from '../assets/wrappers/JobInfo';
const JobInfo = ({ icon, text }) => {
return (
<Wrapper>
<span className="icon">{icon}</span>
<span className="text">{text}</span>
</Wrapper>
);
};
export default JobInfo;
Job.js
const date = createdAt
<div className='content-center'>
<JobInfo icon={<FaLocationArrow />} text={jobLocation} />
<JobInfo icon={<FaCalendarAlt />} text={date} />
<JobInfo icon={<FaBriefcase />} text={jobType} />
<div className={`status ${status}`}>{status}</div>
</div>
npm install moment
Job.js
const date = moment(createdAt).format('MMM Do, YYYY');
allJobsSlice.js
reducers: {
showLoading: (state) => {
state.isLoading = true;
},
hideLoading: (state) => {
state.isLoading = false;
},
}
export const {
showLoading,
hideLoading,
} = allJobsSlice.actions;
- DELETE /jobs/jobId
- authorization header : 'Bearer token'
jobSlice.js
import { showLoading, hideLoading, getAllJobs } from '../allJobs/allJobsSlice';
export const deleteJob = createAsyncThunk(
'job/deleteJob',
async (jobId, thunkAPI) => {
thunkAPI.dispatch(showLoading());
try {
const resp = await customFetch.delete(`/jobs/${jobId}`, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
thunkAPI.dispatch(getAllJobs());
return resp.data;
} catch (error) {
thunkAPI.dispatch(hideLoading());
return thunkAPI.rejectWithValue(error.response.data.msg);
}
}
);
Job.js
<button
type="button"
className="btn delete-btn"
onClick={() => {
dispatch(deleteJob(_id));
}}
>
Delete
</button>
jobSlice.js
reducers:{
setEditJob: (state, { payload }) => {
return { ...state, isEditing: true, ...payload };
},
}
export const { handleChange, clearValues, setEditJob } = jobSlice.actions;
Job.js
import { setEditJob, deleteJob } from '../features/job/jobSlice';
<Link
to="/add-job"
className="btn edit-btn"
onClick={() => {
dispatch(
setEditJob({
editJobId: _id,
position,
company,
jobLocation,
jobType,
status,
})
);
}}
>
Edit
</Link>;
AddJob.js
useEffect(() => {
if (!isEditing) {
dispatch(handleChange({ name: 'jobLocation', value: user.location }));
}
}, []);
- PATCH /jobs/jobId
- { position:'position', company:'company', jobLocation:'location', jobType:'full-time', status:'pending' }
- authorization header : 'Bearer token'
- sends back the updated job object
jobSlice.js
export const editJob = createAsyncThunk(
'job/editJob',
async ({ jobId, job }, thunkAPI) => {
try {
const resp = await customFetch.patch(`/jobs/${jobId}`, job, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
thunkAPI.dispatch(clearValues());
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
}
);
extraReducers:{
[editJob.pending]: (state) => {
state.isLoading = true;
},
[editJob.fulfilled]: (state) => {
state.isLoading = false;
toast.success('Job Modified...');
},
[editJob.rejected]: (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
},
}
AddJob.js
import {
clearValues,
handleChange,
createJob,
editJob,
} from '../../features/job/jobSlice';
if (isEditing) {
dispatch(
editJob({
jobId: editJobId,
job: {
position,
company,
jobLocation,
jobType,
status,
},
})
);
return;
}
- features/job/jobThunk.js
import customFetch from '../../utils/axios';
import { showLoading, hideLoading, getAllJobs } from '../allJobs/allJobsSlice';
import { clearValues } from './jobSlice';
export const createJobThunk = async (job, thunkAPI) => {
try {
const resp = await customFetch.post('/jobs', job, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
thunkAPI.dispatch(clearValues());
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
export const deleteJobThunk = async (jobId, thunkAPI) => {
thunkAPI.dispatch(showLoading());
try {
const resp = await customFetch.delete(`/jobs/${jobId}`, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
thunkAPI.dispatch(getAllJobs());
return resp.data;
} catch (error) {
thunkAPI.dispatch(hideLoading());
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
export const editJobThunk = async ({ jobId, job }, thunkAPI) => {
try {
const resp = await customFetch.patch(`/jobs/${jobId}`, job, {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
});
thunkAPI.dispatch(clearValues());
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
jobSlice.js
import { createJobThunk, deleteJobThunk, editJobThunk } from './jobThunk';
export const createJob = createAsyncThunk('job/createJob', createJobThunk);
export const deleteJob = createAsyncThunk('job/deleteJob', deleteJobThunk);
export const editJob = createAsyncThunk('job/editJob', editJobThunk);
jobThunk.js
const authHeader = (thunkAPI) => {
return {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
};
};
export const createJobThunk = async (job, thunkAPI) => {
try {
const resp = await customFetch.post('/jobs', job, authHeader(thunkAPI));
thunkAPI.dispatch(clearValues());
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
- create utils/authHeader.js
const authHeader = (thunkAPI) => {
return {
headers: {
authorization: `Bearer ${thunkAPI.getState().user.user.token}`,
},
};
};
export default authHeader;
jobThunk.js
import authHeader from '../../utils/authHeader';
- utils/axios.js
import axios from 'axios';
import { getUserFromLocalStorage } from './localStorage';
const customFetch = axios.create({
baseURL: 'https://jobify-prod.herokuapp.com/api/v1/toolkit',
});
customFetch.interceptors.request.use(
(config) => {
const user = getUserFromLocalStorage();
if (user) {
config.headers['Authorization'] = `Bearer ${user.token}`;
// in the latest version "common" returns undefined
// config.headers.common['Authorization'] = `Bearer ${user.token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default customFetch;
- remove auth header
- email : testUser@test.com
- password : secret
- read only!
- dummy data
Register.js
<button
type="button"
className="btn btn-block btn-hipster"
disabled={isLoading}
onClick={() => {
dispatch(loginUser({ email: 'testUser@test.com', password: 'secret' }));
}}
>
{isLoading ? 'loading...' : 'demo'}
</button>
-
GET /jobs/stats
-
authorization header : 'Bearer token'
-
returns { defaultStats:{pending:24,interview:27,declined:24}, monthlyApplications:[{date:"Nov 2021",count:5},{date:"Dec 2021",count:4} ] }
-
last six months
allJobsSlice.js
export const showStats = createAsyncThunk(
'allJobs/showStats',
async (_, thunkAPI) => {
try {
const resp = await customFetch.get('/jobs/stats');
console.log(resp.data));
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
}
);
// extraReducers
[showStats.pending]: (state) => {
state.isLoading = true;
},
[showStats.fulfilled]: (state, { payload }) => {
state.isLoading = false;
state.stats = payload.defaultStats;
state.monthlyApplications = payload.monthlyApplications;
},
[showStats.rejected]: (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
},
- create
- components/StatsContainer.js
- components/ChartsContainer.js
- import/export
Stats.js
import { useEffect } from 'react';
import { StatsContainer, Loading, ChartsContainer } from '../../components';
import { useDispatch, useSelector } from 'react-redux';
import { showStats } from '../../features/allJobs/allJobsSlice';
const Stats = () => {
const { isLoading, monthlyApplications } = useSelector(
(store) => store.allJobs
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(showStats());
// eslint-disable-next-line
}, []);
if (isLoading) {
return <Loading center />;
}
return (
<>
<StatsContainer />
{monthlyApplications.length > 0 && <ChartsContainer />}
</>
);
};
export default Stats;
- create components/StatItem.js
StatsContainer.js
import StatItem from './StatItem';
import { FaSuitcaseRolling, FaCalendarCheck, FaBug } from 'react-icons/fa';
import Wrapper from '../assets/wrappers/StatsContainer';
import { useSelector } from 'react-redux';
const StatsContainer = () => {
const { stats } = useSelector((store) => store.allJobs);
const defaultStats = [
{
title: 'pending applications',
count: stats.pending || 0,
icon: <FaSuitcaseRolling />,
color: '#e9b949',
bcg: '#fcefc7',
},
{
title: 'interviews scheduled',
count: stats.interview || 0,
icon: <FaCalendarCheck />,
color: '#647acb',
bcg: '#e0e8f9',
},
{
title: 'jobs declined',
count: stats.declined || 0,
icon: <FaBug />,
color: '#d66a6a',
bcg: '#ffeeee',
},
];
return (
<Wrapper>
{defaultStats.map((item, index) => {
return <StatItem key={index} {...item} />;
})}
</Wrapper>
);
};
export default StatsContainer;
StatItem.js
import Wrapper from '../assets/wrappers/StatItem';
const StatItem = ({ count, title, icon, color, bcg }) => {
return (
<Wrapper color={color} bcg={bcg}>
<header>
<span className="count">{count}</span>
<span className="icon">{icon}</span>
</header>
<h5 className="title">{title}</h5>
</Wrapper>
);
};
export default StatItem;
- create
- components/AreaChart.js
- components/BarChart.js
ChartsContainer.js
import React, { useState } from 'react';
import BarChart from './BarChart';
import AreaChart from './AreaChart';
import Wrapper from '../assets/wrappers/ChartsContainer';
import { useSelector } from 'react-redux';
const ChartsContainer = () => {
const [barChart, setBarChart] = useState(true);
const { monthlyApplications: data } = useSelector((store) => store.allJobs);
return (
<Wrapper>
<h4>Monthly Applications</h4>
<button type="button" onClick={() => setBarChart(!barChart)}>
{barChart ? 'Area Chart' : 'Bar Chart'}
</button>
{barChart ? <BarChart data={data} /> : <AreaChart data={data} />}
</Wrapper>
);
};
export default ChartsContainer;
npm install recharts
- For now does not work with React 18
npm install react@17 react-dom@17
npm install recharts
npm install react@18 react-dom@18
AreaChart.js
import {
ResponsiveContainer,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
} from 'recharts';
const AreaChartComponent = ({ data }) => {
return (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ top: 50 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis allowDecimals={false} />
<Tooltip />
<Area type="monotone" dataKey="count" stroke="#1e3a8a" fill="#3b82f6" />
</AreaChart>
</ResponsiveContainer>
);
};
export default AreaChartComponent;
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
const BarChartComponent = ({ data }) => {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data} margin={{ top: 50 }}>
<CartesianGrid strokeDasharray="3 3 " />
<XAxis dataKey="date" />
<YAxis allowDecimals={false} />
<Tooltip />
<Bar dataKey="count" fill="#3b82f6" barSize={75} />
</BarChart>
</ResponsiveContainer>
);
};
export default BarChartComponent;
SearchContainer.js
import { FormRow, FormRowSelect } from '.';
import Wrapper from '../assets/wrappers/SearchContainer';
import { useSelector, useDispatch } from 'react-redux';
const SearchContainer = () => {
const { isLoading, search, searchStatus, searchType, sort, sortOptions } =
useSelector((store) => store.allJobs);
const { jobTypeOptions, statusOptions } = useSelector((store) => store.job);
const dispatch = useDispatch();
const handleSearch = (e) => {};
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<Wrapper>
<form className="form">
<h4>search form</h4>
<div className="form-center">
{/* search position */}
<FormRow
type="text"
name="search"
value={search}
handleChange={handleSearch}
/>
{/* search by status */}
<FormRowSelect
labelText="status"
name="searchStatus"
value={searchStatus}
handleChange={handleSearch}
list={['all', ...statusOptions]}
/>
{/* search by type */}
<FormRowSelect
labelText="type"
name="searchType"
value={searchType}
handleChange={handleSearch}
list={['all', ...jobTypeOptions]}
/>
{/* sort */}
<FormRowSelect
name="sort"
value={sort}
handleChange={handleSearch}
list={sortOptions}
/>
<button
className="btn btn-block btn-danger"
disabled={isLoading}
onClick={handleSubmit}
>
clear filters
</button>
</div>
</form>
</Wrapper>
);
};
export default SearchContainer;
allJobsSlice.js
reducers:{
handleChange: (state, { payload: { name, value } }) => {
// state.page = 1;
state[name] = value;
},
clearFilters: (state) => {
return { ...state, ...initialFiltersState };
},
}
export const { showLoading, hideLoading, handleChange, clearFilters } =
allJobsSlice.actions;
SearchContainer.js
import { handleChange, clearFilters } from '../features/allJobs/allJobsSlice';
const handleSearch = (e) => {
if (isLoading) return;
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch(clearFilters());
};
-
server returns 10 jobs per page
-
create
-
components/PageBtnContainer.js
allJobsSlice.js
extraReducers:{
[getAllJobs.fulfilled]: (state, { payload }) => {
state.isLoading = false;
state.jobs = payload.jobs;
state.numOfPages = payload.numOfPages;
state.totalJobs = payload.totalJobs;
},
}
JobsContainer
const { jobs, isLoading, page, totalJobs, numOfPages } = useSelector(
(store) => store.allJobs
);
return (
<Wrapper>
<h5>
{totalJobs} job{jobs.length > 1 && 's'} found
</h5>
<div className="jobs">
{jobs.map((job) => {
return <Job key={job._id} {...job} />;
})}
</div>
{numOfPages > 1 && <PageBtnContainer />}
</Wrapper>
);
import { HiChevronDoubleLeft, HiChevronDoubleRight } from 'react-icons/hi';
import Wrapper from '../assets/wrappers/PageBtnContainer';
import { useSelector, useDispatch } from 'react-redux';
const PageBtnContainer = () => {
const { numOfPages, page } = useSelector((store) => store.allJobs);
const dispatch = useDispatch();
const pages = Array.from({ length: numOfPages }, (_, index) => {
return index + 1;
});
const nextPage = () => {};
const prevPage = () => {};
return (
<Wrapper>
<button className="prev-btn" onClick={prevPage}>
<HiChevronDoubleLeft />
prev
</button>
<div className="btn-container">
{pages.map((pageNumber) => {
return (
<button
type="button"
className={pageNumber === page ? 'pageBtn active' : 'pageBtn'}
key={pageNumber}
onClick={() => console.log('change page')}
>
{pageNumber}
</button>
);
})}
</div>
<button className="next-btn" onClick={nextPage}>
next
<HiChevronDoubleRight />
</button>
</Wrapper>
);
};
export default PageBtnContainer;
allJobsSlice.js
reducers:{
changePage: (state, { payload }) => {
state.page = payload;
},
}
export const {
showLoading,
hideLoading,
handleChange,
clearFilters,
changePage,
} = allJobsSlice.actions;
PageBtnContainer.js
import { changePage } from '../features/allJobs/allJobsSlice';
const nextPage = () => {
let newPage = page + 1;
if (newPage > numOfPages) {
newPage = 1;
}
dispatch(changePage(newPage));
};
const prevPage = () => {
let newPage = page - 1;
if (newPage < 1) {
newPage = numOfPages;
}
dispatch(changePage(newPage));
};
return (
<div className="btn-container">
{pages.map((pageNumber) => {
return (
<button
type="button"
className={pageNumber === page ? 'pageBtn active' : 'pageBtn'}
key={pageNumber}
onClick={() => dispatch(changePage(pageNumber))}
>
{pageNumber}
</button>
);
})}
</div>
);
allJobsSlice
export const getAllJobs = createAsyncThunk(
'allJobs/getJobs',
async (_, thunkAPI) => {
const { page, search, searchStatus, searchType, sort } =
thunkAPI.getState().allJobs;
let url = `/jobs?status=${searchStatus}&jobType=${searchType}&sort=${sort}&page=${page}`;
if (search) {
url = url + `&search=${search}`;
}
try {
const resp = await customFetch.get(url);
return resp.data;
}
}
)
JobsContainer.js
const {
jobs,
isLoading,
page,
totalJobs,
numOfPages,
search,
searchStatus,
searchType,
sort,
} = useSelector((store) => store.allJobs);
useEffect(() => {
dispatch(getAllJobs());
// eslint-disable-next-line
}, [page, search, searchStatus, searchType, sort]);
allJobsSlice.js
reducers:{
handleChange: (state, { payload: { name, value } }) => {
state.page = 1;
state[name] = value;
},
SearchContainer.js
const handleSearch = (e) => {
if (isLoading) return;
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
};
- create
- features/allJobs/allJobsThunk.js
import customFetch from '../../utils/axios';
export const getAllJobsThunk = async (thunkAPI) => {
const { page, search, searchStatus, searchType, sort } =
thunkAPI.getState().allJobs;
let url = `/jobs?page=${page}&status=${searchStatus}&jobType=${searchType}&sort=${sort}`;
if (search) {
url = url + `&search=${search}`;
}
try {
const resp = await customFetch.get(url);
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
export const showStatsThunk = async (_, thunkAPI) => {
try {
const resp = await customFetch.get('/jobs/stats');
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data.msg);
}
};
allJobsSlice.js
import { showStatsThunk, getAllJobsThunk } from './allJobsThunk';
export const getAllJobs = createAsyncThunk('allJobs/getJobs', getAllJobsThunk);
export const showStats = createAsyncThunk('allJobs/showStats', showStatsThunk);
allJobsSlice.js
reducers:{
clearAllJobsState: () => initialState,
}
userThunk.js
import { logoutUser } from './userSlice';
import { clearAllJobsState } from '../allJobs/allJobsSlice';
import { clearValues } from '../job/jobSlice';
export const clearStoreThunk = async (message, thunkAPI) => {
try {
// logout user
thunkAPI.dispatch(logoutUser(message));
// clear jobs value
thunkAPI.dispatch(clearAllJobsState());
// clear job input values
thunkAPI.dispatch(clearValues());
return Promise.resolve();
} catch (error) {
// console.log(error);
return Promise.reject();
}
};
userSlice.js
import { clearStoreThunk } from './userThunk';
export const clearStore = createAsyncThunk('user/clearStore', clearStoreThunk);
extraReducers:{
[clearStore.rejected]: () => {
toast.error('There was an error');
},
}
Navbar.js
import { clearStore } from '../features/user/userSlice';
return (
<button
type="button"
className="dropdown-btn"
onClick={() => {
dispatch(clearStore('Logout Successful...'));
}}
>
logout
</button>
);
axios.js
import { clearStore } from '../features/user/userSlice';
export const checkForUnauthorizedResponse = (error, thunkAPI) => {
if (error.response.status === 401) {
thunkAPI.dispatch(clearStore());
return thunkAPI.rejectWithValue('Unauthorized! Logging Out...');
}
return thunkAPI.rejectWithValue(error.response.data.msg);
};
allJobsThunk.js
import customFetch, { checkForUnauthorizedResponse } from '../../utils/axios';
export const showStatsThunk = async (_, thunkAPI) => {
try {
const resp = await customFetch.get('/jobs/stats');
return resp.data;
} catch (error) {
return checkForUnauthorizedResponse(error, thunkAPI);
}
};
- refactor in all authenticated requests
allJobsSlice.js
extraReducers: (builder) => {
builder
.addCase(getAllJobs.pending, (state) => {
state.isLoading = true;
})
.addCase(getAllJobs.fulfilled, (state, { payload }) => {
state.isLoading = false;
state.jobs = payload.jobs;
state.numOfPages = payload.numOfPages;
state.totalJobs = payload.totalJobs;
})
.addCase(getAllJobs.rejected, (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
})
.addCase(showStats.pending, (state) => {
state.isLoading = true;
})
.addCase(showStats.fulfilled, (state, { payload }) => {
state.isLoading = false;
state.stats = payload.defaultStats;
state.monthlyApplications = payload.monthlyApplications;
})
.addCase(showStats.rejected, (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
});
},
jobSlice.js
extraReducers: (builder) => {
builder
.addCase(createJob.pending, (state) => {
state.isLoading = true;
})
.addCase(createJob.fulfilled, (state) => {
state.isLoading = false;
toast.success('Job Created');
})
.addCase(createJob.rejected, (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
})
.addCase(deleteJob.fulfilled, (state, { payload }) => {
toast.success(payload);
})
.addCase(deleteJob.rejected, (state, { payload }) => {
toast.error(payload);
})
.addCase(editJob.pending, (state) => {
state.isLoading = true;
})
.addCase(editJob.fulfilled, (state) => {
state.isLoading = false;
toast.success('Job Modified...');
})
.addCase(editJob.rejected, (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
});
},
userSlice.js
extraReducers: (builder) => {
builder
.addCase(registerUser.pending, (state) => {
state.isLoading = true;
})
.addCase(registerUser.fulfilled, (state, { payload }) => {
const { user } = payload;
state.isLoading = false;
state.user = user;
addUserToLocalStorage(user);
toast.success(`Hello There ${user.name}`);
})
.addCase(registerUser.rejected, (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
})
.addCase(loginUser.pending, (state) => {
state.isLoading = true;
})
.addCase(loginUser.fulfilled, (state, { payload }) => {
const { user } = payload;
state.isLoading = false;
state.user = user;
addUserToLocalStorage(user);
toast.success(`Welcome Back ${user.name}`);
})
.addCase(loginUser.rejected, (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
})
.addCase(updateUser.pending, (state) => {
state.isLoading = true;
})
.addCase(updateUser.fulfilled, (state, { payload }) => {
const { user } = payload;
state.isLoading = false;
state.user = user;
addUserToLocalStorage(user);
toast.success(`User Updated!`);
})
.addCase(updateUser.rejected, (state, { payload }) => {
state.isLoading = false;
toast.error(payload);
})
.addCase(clearStore.rejected, () => {
toast.error('There was an error..');
});
},
- remove isLoading from handleSearch
- import useState and useMemo from react
- setup localSearch state value
- replace search input functionality so it updates localSearch
import { useState, useMemo } from 'react';
const SearchContainer = () => {
const [localSearch, setLocalSearch] = useState('');
const handleSearch = (e) => {
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
};
return (
<Wrapper>
<form className="form">
<h4>search form</h4>
<div className="form-center">
{/* search position */}
<FormRow
type="text"
name="search"
value={localSearch}
handleChange={(e) => setLocalSearch(e.target.value)}
/>
// ...rest of the code
</div>
</form>
</Wrapper>
);
};
export default SearchContainer;
import { useState, useMemo } from 'react';
const SearchContainer = () => {
const [localSearch, setLocalSearch] = useState('');
const handleSearch = (e) => {
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
};
const debounce = () => {
let timeoutID;
return (e) => {
setLocalSearch(e.target.value);
clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
}, 1000);
};
};
const handleSubmit = (e) => {
e.preventDefault();
setLocalSearch('');
dispatch(clearFilters());
};
const optimizedDebounce = useMemo(() => debounce(), []);
return (
<Wrapper>
<form className="form">
<h4>search form</h4>
<div className="form-center">
{/* search position */}
<FormRow
type="text"
name="search"
value={localSearch}
handleChange={optimizedDebounce}
/>
// ...rest of the code
</div>
</form>
</Wrapper>
);
};
export default SearchContainer;