Commit 0458675d authored by H.M.C. Nadunithara Wijerathne's avatar H.M.C. Nadunithara Wijerathne

Merge branch 'IT19980096' into 'master'

resume analysis

See merge request !2
parents 9b30774e 55f24436
......@@ -11,8 +11,10 @@
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"axios": "^1.3.4",
"firebase": "^9.17.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-modal": "^3.16.1",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.5",
"react-scripts": "5.0.1",
......@@ -48,6 +50,7 @@
]
},
"devDependencies": {
"@types/react-modal": "^3.13.1",
"sass": "^1.56.2"
},
"proxy": "http://localhost:5000"
......
......@@ -26,7 +26,7 @@
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous"
></script>
<title>React App</title>
<title>Smart Recruiter</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
......
......@@ -4,6 +4,7 @@ import { Routes, BrowserRouter as Router, Route } from "react-router-dom";
import Landing from "./views/Landing.view";
import Home from "./views/Home.view";
import Settings from "./views/Settings.view";
import Applications from "./views/Applications.view";
import AppState from "./components/AppState";
......@@ -19,6 +20,7 @@ function App() {
<Route element={<ProtectedRoutes />}>
<Route path="/home" element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route path="/applications" element={<Applications />} />
</Route>
</Routes>
<AppState />
......
import { ACTIONS } from ".";
import { ControlsType, Reducers, UpdatePasswordPayload } from "../types";
import {
ControlsType,
Reducers,
SignInPayload,
UpdatePasswordPayload,
} from "../types";
export const setAppState = (payload: Reducers["auth"]["appState"]) => ({
type: ACTIONS.SET_APP_STATE,
......@@ -15,3 +20,12 @@ export const changePassword = (payload: UpdatePasswordPayload) => ({
type: ACTIONS.CHANGE_PASSWORD,
payload,
});
export const signIn = (
payload: SignInPayload,
success: (state: string) => void
) => ({
type: ACTIONS.SIGN_IN,
payload,
success,
});
import { ACTIONS } from ".";
import { JobType } from "../types";
export const getJobs = (success?: () => void) => ({
type: ACTIONS.GET_JOBS,
success,
});
export const createJob = (payload: JobType, success?: () => void) => ({
type: ACTIONS.CREATE_JOB,
payload,
success,
});
export const updateJob = (payload: JobType, success?: () => void) => ({
type: ACTIONS.UPDATE_JOB,
payload,
success,
});
export enum ACTIONS {
SET_AUTH = "SET_AUTH",
SIGN_IN = "SIGN_IN",
SIGN_OUT_USER = "SIGN_OUT_USER",
UPDATE_KEYSTROKE_SETTINGS = "UPDATE_KEYSTROKE_SETTINGS",
CHANGE_PASSWORD = "CHANGE_PASSWORD",
SET_USER_TYPE = "SET_USER_TYPE",
UPDATE_CANDIDATE = "UPDATE_CANDIDATE",
APPLY_FOR_JOB = "APPLY_FOR_JOB",
SET_APP_STATE = "SET_APP_STATE",
SET_JOBS = "SET_JOBS",
GET_JOBS = "GET_JOBS",
CREATE_JOB = "CREATE_JOB",
UPDATE_JOB = "UPDATE_JOB",
}
import { ACTIONS } from ".";
import { CandidateType } from "../types";
export const updateCandidate = (payload: Partial<CandidateType>) => ({
type: ACTIONS.UPDATE_CANDIDATE,
payload,
});
export const applyForJob = (payload: string) => ({
type: ACTIONS.APPLY_FOR_JOB,
payload,
});
import { request } from "../lib/api";
import { ApplicationType, JobType } from "../types";
export default class CommonAPI {
static getJobs = () => request("<BASE_URL>/jobs", "GET");
static createJob = (payload: JobType) =>
request("<BASE_URL>/jobs", "POST", payload);
static updateJob = (payload: JobType) =>
request("<BASE_URL>/jobs", "PUT", payload);
static applyForJob = (payload: {
application: ApplicationType;
resumeUrl: string;
}) => request("<BASE_URL>/jobs/apply", "PUT", payload);
}
import { request } from "../lib/api";
import { CandidateType } from "../types";
export default class UserAPI {
static updateCandidate = (payload: Partial<CandidateType>) =>
request("<BASE_URL>/user/candidate", "POST", payload);
}
import { initializeApp } from "firebase/app";
import { getStorage, ref } from "firebase/storage";
export const BASE_URL = "http://localhost:5000";
export const DEFAULT_CONTROLS = {
standard: {
......@@ -10,3 +13,14 @@ export const DEFAULT_CONTROLS = {
use: true,
},
};
const FIREBASE_CONFIG = {
apiKey: "AIzaSyAVE9S4CZfUpUcib8DczFuzTrlWOQcqk80",
authDomain: "smart-recruiter-909f6.firebaseapp.com",
projectId: "smart-recruiter-909f6",
storageBucket: "smart-recruiter-909f6.appspot.com",
messagingSenderId: "512045119582",
appId: "1:512045119582:web:8751af70c3d75da0295ccf",
};
const app = initializeApp(FIREBASE_CONFIG);
export const fileStorage = getStorage(app);
import {
ApplicationType,
CandidateType,
OrganizationType,
Reducers,
USER_TYPE,
} from "../types";
export const logger = (log: any, ...optionalparams: any[]) => {
console.log(log, ...optionalparams);
};
......@@ -28,3 +36,30 @@ export function keyEvent(e: any) {
time: Date.now(),
};
}
export const getProfile = (
state: Reducers
): CandidateType | OrganizationType | {} => {
if (state.auth.userType === USER_TYPE.CANDIDATE) {
return state.auth.candidate;
}
return state.auth.organization;
};
export const getUserId = (state: Reducers): string => {
const profile: any = getProfile(state);
return profile._id as string;
};
export const getStatusColor = (status?: ApplicationType["status"]) => {
const color =
status === "Accepted"
? "text-bg-success"
: status === "Pending"
? "text-bg-warning"
: status === "Rejected"
? "text-bg-danger"
: "text-bg-secondary";
return color;
};
......@@ -21,6 +21,9 @@ const authReducer = (
case ACTIONS.SET_APP_STATE:
return { ...state, appState: payload };
case ACTIONS.SET_USER_TYPE:
return { ...state, userType: payload };
case ACTIONS.SIGN_OUT_USER:
return { ...state, token: null };
......
import { ACTIONS } from "../actions/index";
import { CommonReducer } from "../types";
const INITIAL_STATE: CommonReducer = {
jobs: [],
};
const commonReducer = (
state = INITIAL_STATE,
{ type, payload }: { type: ACTIONS; payload: any }
): CommonReducer => {
switch (type) {
case ACTIONS.SET_JOBS:
return { ...state, jobs: payload };
case ACTIONS.SIGN_OUT_USER:
return INITIAL_STATE;
default:
return state;
}
};
export default commonReducer;
......@@ -3,12 +3,19 @@ import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import authReducer from "../reducers/auth";
import commonReducer from "../reducers/common";
const AuthPersistConfig = {
storage,
key: "auth",
};
const CommonPersistConfig = {
storage,
key: "common",
};
export default combineReducers({
auth: persistReducer(AuthPersistConfig, authReducer),
common: persistReducer(CommonPersistConfig, commonReducer),
});
......@@ -6,9 +6,35 @@ import {
ControlsType,
KeystrokeResultType,
Reducers,
SignInPayload,
UpdatePasswordPayload,
USER_TYPE,
} from "../types";
function* signIn({
payload,
success,
}: {
type: typeof ACTIONS.SIGN_IN;
payload: SignInPayload;
success: (state: string) => void;
}) {
try {
const data: { user: any } = yield call(AuthAPI.signin, payload);
yield put({ type: ACTIONS.SET_USER_TYPE, payload: payload.userType });
const auth = {
...data,
organization:
payload.userType === USER_TYPE.ORGANIZATION ? data.user : {},
candidate: payload.userType === USER_TYPE.CANDIDATE ? data.user : {},
};
yield put({ type: ACTIONS.SET_AUTH, payload: auth });
success(data.user.state);
} catch (error) {
success("failed");
}
}
function* updateKeystrokeSettings({
payload,
}: {
......@@ -35,7 +61,7 @@ function* updateKeystrokeSettings({
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED, msg: error },
payload: { state: APP_STATE.FAILED },
});
}
}
......@@ -59,12 +85,13 @@ function* changePassword({
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED, msg: error },
payload: { state: APP_STATE.FAILED },
});
}
}
export default function* authSaga() {
yield takeLeading(ACTIONS.SIGN_IN, signIn);
yield takeLeading(ACTIONS.UPDATE_KEYSTROKE_SETTINGS, updateKeystrokeSettings);
yield takeLeading(ACTIONS.CHANGE_PASSWORD, changePassword);
}
import { takeLeading, call, put, select } from "redux-saga/effects";
import { ACTIONS } from "../actions";
import CommonAPI from "../apis/common";
import { APP_STATE, JobType, Reducers } from "../types";
function* getJobs({
success,
}: {
type: typeof ACTIONS.GET_JOBS;
success: () => void;
}) {
try {
const data: { jobs: JobType; success: boolean } = yield call(
CommonAPI.getJobs
);
if (data.success) {
yield put({ type: ACTIONS.SET_JOBS, payload: data.jobs });
}
success();
} catch (error) {
success();
}
}
function* createJob({
payload,
success,
}: {
type: typeof ACTIONS.CREATE_JOB;
payload: JobType;
success: () => void;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
const data: { success: boolean; job: JobType } = yield call(
CommonAPI.createJob,
payload
);
if (data.success) {
const jobs: JobType[] = yield select(
(state: Reducers) => state.common.jobs
);
yield put({ type: ACTIONS.SET_JOBS, payload: [data.job, ...jobs] });
}
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
success();
} catch (error) {
success();
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
}
}
function* updateJob({
payload,
success,
}: {
type: typeof ACTIONS.UPDATE_JOB;
payload: JobType;
success: () => void;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
const data: { success: boolean } = yield call(CommonAPI.updateJob, payload);
if (data.success) {
let jobs: JobType[] = yield select(
(state: Reducers) => state.common.jobs
);
const found = jobs.findIndex((_job) => _job._id === payload._id);
jobs[found] = { ...jobs[found], ...payload };
yield put({ type: ACTIONS.SET_JOBS, payload: [...jobs] });
}
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
success();
} catch (error) {
success();
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
}
}
export default function* authSaga() {
yield takeLeading(ACTIONS.GET_JOBS, getJobs);
yield takeLeading(ACTIONS.CREATE_JOB, createJob);
yield takeLeading(ACTIONS.UPDATE_JOB, updateJob);
}
import { all } from "redux-saga/effects";
import Auth from "./auth";
import User from "./user";
import Common from "./common";
export default function* rootSaga() {
yield all([Auth()]);
yield all([Auth(), User(), Common()]);
}
import { put, takeLeading, call, select } from "redux-saga/effects";
import { ACTIONS } from "../actions";
import CommonAPI from "../apis/common";
import UserAPI from "../apis/user";
import { getProfile } from "../lib/util";
import { APP_STATE, CandidateType, JobType, Reducers } from "../types";
function* updateCandidate({
payload,
}: {
type: typeof ACTIONS.UPDATE_CANDIDATE;
payload: Partial<CandidateType>;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
yield call(UserAPI.updateCandidate, payload);
const candidate: CandidateType = yield select(getProfile);
yield put({
type: ACTIONS.SET_AUTH,
payload: { candidate: { ...candidate, ...payload } },
});
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
}
}
function* applyForJob({
payload,
}: {
type: typeof ACTIONS.APPLY_FOR_JOB;
payload: string;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
const profile: CandidateType = yield select(getProfile);
if (profile._id && profile.resume) {
const data: { applicationId: string; success: boolean } = yield call(
CommonAPI.applyForJob,
{
application: {
candidate: profile._id,
job: payload,
status: "Pending",
},
resumeUrl: profile.resume,
}
);
if (data.success) {
let jobs: JobType[] = yield select(
(state: Reducers) => state.common.jobs
);
const found = jobs.findIndex((_job) => _job._id === payload);
jobs[found] = {
...jobs[found],
applications: [
...(jobs[found]?.applications || []),
data.applicationId,
],
};
yield put({ type: ACTIONS.SET_JOBS, payload: [...jobs] });
}
}
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
}
}
export default function* authSaga() {
yield takeLeading(ACTIONS.UPDATE_CANDIDATE, updateCandidate);
yield takeLeading(ACTIONS.APPLY_FOR_JOB, applyForJob);
}
......@@ -93,6 +93,8 @@ export type CandidateType = {
dateOfBirth: string;
jobIds: string[];
profilePicture: string;
state: "INTIAL" | "READY";
resume?: string;
};
export type OrganizationType = {
......@@ -120,6 +122,33 @@ export type ControlsType = {
};
};
export type JobType = {
_id?: string;
title: string;
description: string;
primarySkills: string[];
secondarySkills: string[];
salary: {
min: number;
max: number;
currency: string;
};
applications?: string[];
organization: string;
};
export type ApplicationType = {
candidate: string;
job: string;
status: "Pending" | "Accepted" | "In progress" | "Rejected";
interview?: {
date: string;
time: string;
link: string;
videoRef?: string;
};
};
//PAYLOADS
export type SignUpPayload = {
passwords: string[];
......@@ -146,6 +175,41 @@ export type UpdatePasswordPayload = {
oldPassword: string;
};
export type JobPayloadType = {
_id?: string;
title: string;
description: string;
primarySkills: string[];
secondarySkills: string[];
salary: {
min: number;
max: number;
currency: string;
};
applications?: ApplicationType[] | ApplicationPayloadType[];
organization: OrganizationType[];
};
export type ApplicationPayloadType = {
candidate: CandidateType;
job: string;
status: "Pending" | "Accepted" | "In progress" | "Rejected";
interview?: {
date: string;
time: string;
link: string;
videoRef?: string;
};
score: {
primary: number;
primatyMatch?: string[];
secondary: number;
secondaryMatch?: string[];
similarity: number;
total: number;
};
};
//REDUCERS
export type AuthReducer = {
token: string | null;
......@@ -160,6 +224,11 @@ export type AuthReducer = {
} | null;
};
export type CommonReducer = {
jobs: JobType[] | JobPayloadType[];
};
export type Reducers = {
auth: AuthReducer;
common: CommonReducer;
};
......@@ -44,7 +44,6 @@
align-items: center;
justify-content: center;
color: white;
margin: auto;
&.lg {
height: 100px;
......@@ -132,3 +131,79 @@
);
}
}
.ReactModal__Overlay {
opacity: 0;
transition: all 300ms ease-in-out;
}
.ReactModal__Overlay--after-open {
opacity: 1;
}
.ReactModal__Overlay--before-close {
opacity: 0;
}
.skills {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.skill {
background-color: #058700;
padding: 2px 10px 3px 10px;
border-radius: 10px;
color: white;
margin-right: 5px;
margin-top: 1px;
&.secondary {
background-color: #f3d13b;
color: black;
}
&.display {
background-color: #e0dfdf;
color: black;
}
.span-btn {
font-size: 12px;
padding: 1px 0px 1px 6px;
cursor: pointer;
font-weight: bold;
}
}
.job-card,
.job-preview {
cursor: pointer;
h6,
h5 {
margin: 0;
}
label {
a {
color: gray;
}
}
.desc {
font-size: 12px;
line-height: 15px;
color: gray;
}
&.active {
background-color: #ffefab80;
}
}
.job-preview {
label {
font-weight: bold;
margin-top: 5px;
}
}
......@@ -10,7 +10,7 @@ const Avatar = ({
size?: "sm" | "md" | "lg";
}) => {
if (url) return <img src={url} alt={name} className={`avatar img ${size}`} />;
return <div className={`avatar ${size}`}>{name[0]}</div>;
return <div className={`avatar ${size}`}>{name[0].toUpperCase()}</div>;
};
export default Avatar;
import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage";
import React, { useState, useEffect, ChangeEvent } from "react";
import { useDispatch, useSelector } from "react-redux";
import { ACTIONS } from "../common/actions";
import { updateCandidate } from "../common/actions/user";
import { fileStorage } from "../common/config";
import { getProfile } from "../common/lib/util";
import { APP_STATE, CandidateType } from "../common/types";
import Avatar from "./Avatar";
const CandidateProfile = () => {
const dispath = useDispatch();
const candidate = useSelector(getProfile) as CandidateType;
const [profile, setProfile] = useState<CandidateType>(candidate);
const [resume, setResume] = useState<File | null>(null);
useEffect(() => {
if (candidate) {
setProfile(candidate);
}
}, [candidate]);
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.name === "phone") {
setProfile({
...profile,
contacts: { ...profile.contacts, phone: e.target.value },
});
} else {
setProfile({
...profile,
[e.target.name]: e.target.value,
});
}
};
const onChangeAddress = (e: ChangeEvent<HTMLInputElement>) => {
setProfile({
...profile,
contacts: {
...profile.contacts,
address: {
...profile.contacts.address,
[e.target.name]: e.target.value,
},
},
});
};
const onUpdateCandidate = (payload: Partial<CandidateType>) => {
dispath(updateCandidate(payload));
};
const onSubmit = () => {
const { jobIds, state, _id, ...rest } = profile;
dispath({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
if (resume) {
const resumeRef = ref(fileStorage, `/resumes/${profile._id}.pdf`);
const uploadTask = uploadBytesResumable(resumeRef, resume);
uploadTask.on(
"state_changed",
(snapshot) => {
const percent = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
console.log(percent);
},
(err) => {
console.log("ERROR ", err);
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
console.log(url);
onUpdateCandidate({ ...rest, resume: url });
});
}
);
} else {
onUpdateCandidate(rest);
}
};
const onSelectResume = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setResume(e.target.files[0]);
}
};
return (
<div className="card p-4 mb-3">
<h5>Edit/Update profile</h5>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">Name</label>
<div className="col-sm-9">
<input
type="text"
className="form-control"
name="name"
value={profile.name}
onChange={onChange}
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">Address</label>
<div className="col-sm-9">
<input
type="text"
name="addressLine"
className="form-control mb-2"
placeholder="Address line"
value={profile.contacts?.address?.addressLine}
onChange={onChangeAddress}
/>
<input
type="text"
className="form-control mb-2"
placeholder="City"
name="city"
value={profile.contacts?.address?.city}
onChange={onChangeAddress}
/>
<input
type="text"
className="form-control mb-2"
placeholder="Country"
name="country"
value={profile.contacts?.address?.country}
onChange={onChangeAddress}
/>
</div>
</div>
<div className="mb-3 row">
<div className="col-sm-6">
<div className="row">
<label className="col-sm-4 col-form-label">Date of birth</label>
<div className="col-sm-8">
<input
type="date"
className="form-control"
name="dateOfBirth"
value={profile?.dateOfBirth}
onChange={onChange}
/>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="row">
<label className="col-sm-3 col-form-label">Phone</label>
<div className="col-sm-9">
<input
type="text"
className="form-control"
name="phone"
value={profile.contacts?.phone}
onChange={onChange}
/>
</div>
</div>
</div>
</div>
<div className="mb-3 row">
<div className="col-sm-6">
<div className="row">
<label className="col-sm-4 col-form-label">Resume</label>
<div className="col-sm-8">
{profile?.resume && (
<a
className="btn btn-light mb-2"
href={profile.resume}
target="_blank"
rel="noreferrer"
>
Preview
</a>
)}
<input
type="file"
className="form-control"
onChange={onSelectResume}
accept="application/pdf"
/>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="row">
<label className="col-sm-3 col-form-label">Self Intro</label>
<div className="col-sm-9">
<input type="file" className="form-control" name="oldPassword" />
</div>
</div>
</div>
</div>
<div className="mb-3 row">
<div className="col-sm-6">
<div className="row">
<div className="col-sm-4 ">
<label className="col-form-label">Profile picuture</label>
</div>
<div className="col-sm-8">
<Avatar
name={profile?.name}
url={profile?.profilePicture}
size="lg"
/>
<button
type="button"
className="btn btn-link"
style={{ padding: "0" }}
>
Upload picture
</button>
</div>
</div>
</div>
</div>
<div className="d-md-block">
<button
type="button"
className="btn btn-primary"
style={{ width: "300px", float: "right" }}
onClick={onSubmit}
>
Update Profile
</button>
</div>
</div>
);
};
export default CandidateProfile;
......@@ -189,14 +189,16 @@ const ChangePassword = () => {
</div>
</div>
<button
type="button"
className="btn btn-primary"
style={{ margin: "auto", width: "300px" }}
onClick={onSumit}
>
Save Password
</button>
<div className="d-md-block">
<button
type="button"
className="btn btn-primary"
style={{ width: "300px", float: "right" }}
onClick={onSumit}
>
Save Password
</button>
</div>
<Alert alert={alert} setAlert={setAlert} />
</div>
);
......
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getJobs } from "../common/actions/common";
import { JobPayloadType, JobType, Reducers, USER_TYPE } from "../common/types";
type OwnProps = {
jobs: JobType[] | JobPayloadType[];
onSelectJob?: (item: number) => void;
selectedIndex?: number;
};
const Jobs = ({ jobs, onSelectJob, selectedIndex }: OwnProps) => {
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState<boolean>(true);
const userType = useSelector((state: Reducers) => state.auth.userType);
useEffect(() => {
dispatch(
getJobs(() => {
setIsLoading(false);
})
);
}, [dispatch]);
const renderStatus = () => {
if (isLoading) {
return <p style={{ textAlign: "center" }}>Loading jobs...</p>;
} else if (jobs.length === 0) {
if (userType === USER_TYPE.CANDIDATE) {
return <p style={{ textAlign: "center" }}>No jobs found yet!</p>;
} else {
return (
<p style={{ textAlign: "center" }}>
You don't have posted any jobs yet!
</p>
);
}
}
return null;
};
const renderJobs = (job: JobType | JobPayloadType, index: number) => {
const onClick = () => onSelectJob?.(index);
const className = `card p-3 job-card ${
index === selectedIndex && "active"
}`;
return (
<div className={className} key={job._id} onClick={onClick}>
<h6>{job.title}</h6>
<p>
Salary : {job.salary.min} - {job.salary.max} {job.salary.currency}{" "}
</p>
<p className="desc">{job.description.slice(0, 200)}...</p>
<p className="desc mt-2">
<label>Applicattions : </label>{" "}
<a href={`/applications?jobId=${job._id}`}>
({job.applications?.length}) Candidates
</a>
</p>
</div>
);
};
return (
<div>
{renderStatus()}
{jobs.map(renderJobs)}
</div>
);
};
export default Jobs;
import React, { useState, useRef } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { ACTIONS } from "../common/actions";
import AuthAPI from "../common/apis/auth";
import { signIn } from "../common/actions/auth";
import { keyEvent } from "../common/lib/util";
import { KeyDetails, SignInPayload, USER_TYPE } from "../common/types";
......@@ -43,22 +42,17 @@ const Login = ({ userType, setAlert }: OwnProps) => {
userType,
};
clearData();
AuthAPI.signin(data)
.then((res: any) => {
if (res.success) {
const payload = {
...res,
organization: userType === USER_TYPE.ORGANIZATION ? res.user : {},
candidate: userType === USER_TYPE.CANDIDATE ? res.user : {},
};
dispatch({ type: ACTIONS.SET_AUTH, payload });
dispatch(
signIn(data, (state) => {
if (state === "failed") {
setAlert("Attempt failed! Try again");
} else if (state === "INTIAL") {
navigate("/settings");
} else {
navigate("/home");
}
})
.catch((_) => {
setAlert("Attempt failed! Try again");
});
);
};
const onKeyDown = (e: any) => {
......
import React, { useState, ChangeEvent } from "react";
import { useDispatch, useSelector } from "react-redux";
import { createJob, updateJob } from "../../common/actions/common";
import { getUserId } from "../../common/lib/util";
import { JobType } from "../../common/types";
import CustomModal from "./Modal";
const initialJob: JobType = {
title: "",
description: "",
primarySkills: [],
secondarySkills: [],
salary: {
min: 0,
max: 0,
currency: "LKR",
},
organization: "",
};
type OwnProps = {
mode: "create" | "update";
defaultJob?: JobType;
};
const CreateUpdateJob = ({
mode = "create",
defaultJob = initialJob,
}: OwnProps) => {
const dispatch = useDispatch();
const userId = useSelector(getUserId);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [job, setJob] = useState<JobType>(defaultJob);
const [primarySkill, setPrimarySkill] = useState<string>("");
const [secondarySkill, setSecondarySkill] = useState<string>("");
const toggleModal = () => setIsOpen(!isOpen);
const onChangeSkills = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.name === "primary") {
setPrimarySkill(e.target.value);
} else {
setSecondarySkill(e.target.value);
}
};
const onChangeJob = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setJob({ ...job, [e.target.name]: e.target.value });
};
const onChangeSalary = (e: ChangeEvent<HTMLInputElement>) => {
setJob({
...job,
salary: { ...job.salary, [e.target.name]: e.target.value },
});
};
const onAddSkill = (type: "primary" | "secondary") => {
if (type === "primary") {
if (primarySkill.length > 0) {
setJob({
...job,
primarySkills: [...job.primarySkills, primarySkill],
});
setPrimarySkill("");
}
} else {
if (secondarySkill.length > 0) {
setJob({
...job,
secondarySkills: [...job.secondarySkills, secondarySkill],
});
setSecondarySkill("");
}
}
};
const removeSkill = (type: string, index: number) => {
let key: "primarySkills" | "secondarySkills" = "secondarySkills";
if (type === "primary") {
key = "primarySkills";
}
const skills = job[key].filter((_, i) => i !== index);
setJob({ ...job, [key]: [...skills] });
};
const renderPrimarySkills = (skill: string, index: number) => {
const onClick = () => removeSkill("primary", index);
return (
<span className="skill" key={index}>
{skill}{" "}
<span className="span-btn" onClick={onClick}>
X
</span>
</span>
);
};
const renderSecondarykills = (skill: string, index: number) => {
const onClick = () => removeSkill("secondary", index);
return (
<span className="skill secondary" key={index}>
{skill}{" "}
<span className="span-btn" onClick={onClick}>
X
</span>
</span>
);
};
const onSubmit = () => {
const {
title,
description,
salary: { min, max },
primarySkills,
} = job;
if (
title.length > 0 &&
description.length > 0 &&
primarySkills.length > 0 &&
min > 0 &&
max > 0
) {
const payload: JobType = {
...job,
salary: {
currency: job.salary.currency,
max: Number(max),
min: Number(min),
},
organization: userId,
};
setIsLoading(true);
if (mode === "create") {
dispatch(createJob(payload, callback));
} else {
dispatch(updateJob(payload, callback));
}
}
};
const callback = () => {
setIsLoading(false);
toggleModal();
};
const btnText = mode === "create" ? "Create job" : "Update job";
return (
<div className="mt-3">
<button
className="btn btn-primary"
style={{ width: "100%" }}
onClick={toggleModal}
>
{btnText}
</button>
<CustomModal
isOpen={isOpen}
onRequestClose={toggleModal}
size="lg"
shouldCloseOnOverlayClick={false}
>
<h4>{mode === "create" ? " Create new job" : "Edit job"}</h4>
<div className="mb-3">
<label className="form-label">Job title</label>
<input
type="text"
className="form-control"
name="title"
onChange={onChangeJob}
value={job.title}
/>
</div>
<div className="mb-3">
<label className="form-label">Job description</label>
<textarea
className="form-control"
rows={5}
name="description"
onChange={onChangeJob}
value={job.description}
/>
</div>
<div className="mb-2">
<label className="form-label">Primary skills</label>
<div className="input-group mb-3">
<input
type="text"
className="form-control"
placeholder=""
name="primary"
onChange={onChangeSkills}
value={primarySkill}
/>
<button
className="btn btn-secondary"
type="button"
id="button-addon1"
onClick={() => onAddSkill("primary")}
>
Add
</button>
</div>
</div>
<div className="skills">
{job.primarySkills.map(renderPrimarySkills)}
</div>
<div className="mb-2">
<label className="form-label">Secondary skills (optional)</label>
<div className="input-group mb-3">
<input
type="text"
className="form-control"
placeholder=""
name="secondary"
onChange={onChangeSkills}
value={secondarySkill}
/>
<button
className="btn btn-secondary"
type="button"
id="button-addon1"
onClick={() => onAddSkill("secondary")}
>
Add
</button>
</div>
</div>
<div className="skills">
{job.secondarySkills.map(renderSecondarykills)}{" "}
</div>
<div className="mb-3">
<label className="form-label">Salary Range</label>
<div className="input-group mb-3">
<input
type="number"
className="form-control"
placeholder="min"
name="min"
onChange={onChangeSalary}
value={job.salary.min}
/>
<span className="input-group-text">-</span>
<input
type="number"
className="form-control"
placeholder="max"
name="max"
onChange={onChangeSalary}
value={job.salary.max}
/>
<span className="input-group-text">-</span>
<input
type="text"
className="form-control"
placeholder="currency"
name="currency"
onChange={onChangeSalary}
value={job.salary.currency}
/>
</div>
</div>
<div className="d-md-block" style={{ float: "right" }}>
<button
className="btn btn-primary m-2"
onClick={onSubmit}
disabled={isLoading}
>
{btnText}
</button>
<button className="btn btn-danger" onClick={toggleModal}>
Cancel
</button>
</div>
</CustomModal>
</div>
);
};
export default CreateUpdateJob;
import React from "react";
import Modal from "react-modal";
type OwnProps = {
isOpen: boolean;
onRequestClose: () => void;
children: JSX.Element | JSX.Element[];
size?: "md" | "lg";
shouldCloseOnOverlayClick?: boolean;
};
const CustomModal = ({
isOpen,
onRequestClose,
children,
size = "md",
shouldCloseOnOverlayClick = true,
}: OwnProps) => {
const width = size === "md" ? "400px" : "500px";
return (
<Modal
isOpen={isOpen}
ariaHideApp={false}
onRequestClose={onRequestClose}
closeTimeoutMS={300}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
style={{
overlay: {
backgroundColor: "rgba(0, 0, 0, 0.5)",
},
content: {
width,
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
maxHeight: "95%",
},
}}
>
{children}
</Modal>
);
};
export default CustomModal;
/* eslint-disable jsx-a11y/anchor-is-valid */
import React from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { ACTIONS } from "../common/actions";
import { getProfile } from "../common/lib/util";
import Avatar from "./Avatar";
const NavBar = () => {
const dispatch = useDispatch();
const profile: any = useSelector(getProfile);
const onClickLogout = () => dispatch({ type: ACTIONS.SIGN_OUT_USER });
return (
......@@ -42,7 +43,11 @@ const NavBar = () => {
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Avatar name="Hashan" size="sm" />
<Avatar
name={profile?.name}
url={profile?.profilePicture}
size="sm"
/>
</a>
<ul className="dropdown-menu">
<li>
......
import React from "react";
const Profile = () => {
return <div className="card p-4">Profile</div>;
};
export default Profile;
import React, { useEffect, useState, ChangeEvent } from "react";
import { useSelector } from "react-redux";
import { useSearchParams } from "react-router-dom";
import { getStatusColor } from "../common/lib/util";
import {
JobPayloadType,
Reducers,
ApplicationPayloadType,
} from "../common/types";
import Layout from "../components/Layout";
const Applications = () => {
const jobs = useSelector(
(state: Reducers) => state.common.jobs
) as JobPayloadType[];
const [searchParams] = useSearchParams();
const [job, setJob] = useState<JobPayloadType | null>();
const jobId = searchParams.get("jobId");
useEffect(() => {
if (jobId) {
const found = jobs.find((_job) => _job._id === jobId);
sortApplications("skills", found);
}
}, [jobId, jobs]);
const onSelectSort = (e: ChangeEvent<HTMLSelectElement>) => {
sortApplications(e.target.value, job);
};
const sortApplications = (sortBy: string, _job?: JobPayloadType | null) => {
if (_job?.applications) {
const _applications = _job.applications as ApplicationPayloadType[];
if (sortBy === "skills") {
_applications.sort((current, prev) => {
const skill = current.score.primary + current.score.secondary;
const prevSkill = prev.score.primary + prev.score.secondary;
if (skill < prevSkill) {
return 1;
} else if (skill > prevSkill) {
return -1;
}
return 0;
});
} else if (sortBy === "similarity") {
_applications.sort((current, prev) => {
if (current.score.similarity < prev.score.similarity) {
return 1;
} else if (current.score.similarity > prev.score.similarity) {
return -1;
}
return 0;
});
} else {
_applications.sort((current, prev) => {
if (current.score.total < prev.score.total) {
return 1;
} else if (current.score.total > prev.score.total) {
return -1;
}
return 0;
});
}
setJob({ ..._job, applications: _applications });
}
};
const renderSkills = (skill: string, index: number) => (
<span className="skill" key={index}>
{skill}
</span>
);
const renderApplications = (_application: any, index: number) => {
const application = _application as ApplicationPayloadType;
const skillsScore = `PS ${application.score.primary} + SS ${application.score?.secondary} `;
const status = `badge ${getStatusColor(application.status)}`;
return (
<tr key={index}>
<th scope="row">{application.candidate.name}</th>
<td>{skillsScore}</td>
<td>{application.score.similarity}</td>
<td>{application.score.total}</td>
<td>
<span className={status}>{application.status}</span>
</td>
<td>{application.candidate.contacts?.phone || ""}</td>
<td>
<a
className="nav-link"
href={application.candidate.resume}
target="_blank"
rel="noreferrer"
>
View
</a>
</td>
</tr>
);
};
if (!job) return <Layout title="Applications"></Layout>;
return (
<Layout title={job.title}>
<div className="card p-4 job-preview">
<p>
Salary : {job.salary.min} - {job.salary.max} {job.salary.currency}{" "}
</p>
<p className="desc">{job.description}</p>
<label>Primary skills</label>
<div className="skills">{job.primarySkills.map(renderSkills)}</div>
<label>Secondary skills</label>
<div className="skills">{job.secondarySkills.map(renderSkills)}</div>
<div className="mt-2">
<label>Applicattions : </label>{" "}
<a href={`/applications?jobId=${job._id}`}>
({job.applications?.length}) Candidates
</a>
</div>
</div>
<div className="container">
<div className="row mt-5">
<h4 className="mb-3">Applications</h4>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label">Sort by</label>
<div className="col-sm-2">
<select
className="form-select"
aria-label="Default select example"
onChange={onSelectSort}
>
<option value="skills">Skills score</option>
<option value="similarity">Similarity</option>
<option value="total">Total score</option>
</select>
</div>
</div>
<div className="card p-4 table-responsive">
<table className="table table-hover table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Skills Score</th>
<th scope="col">Similarity</th>
<th scope="col">Total score</th>
<th scope="col">Status</th>
<th scope="col">Phone</th>
<th scope="col">Resume</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>{job.applications?.map(renderApplications)}</tbody>
</table>
</div>
</div>
</div>
</Layout>
);
};
export default Applications;
import React from "react";
import NavBar from "../components/NavBar";
import React, { useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { applyForJob } from "../common/actions/user";
import { getStatusColor, getUserId } from "../common/lib/util";
import { JobPayloadType, JobType, Reducers, USER_TYPE } from "../common/types";
import Jobs from "../components/Jobs";
import Layout from "../components/Layout";
import CreateUpdateJob from "../components/Modals/CreateUpdateJob";
import Profile from "../components/Profile";
const CandidateHome = () => {
const dispatch = useDispatch();
const [selectedJob, setSelectedJob] = useState<number>(0);
const userId = useSelector(getUserId);
const jobs = useSelector(
(state: Reducers) => state.common.jobs
) as JobPayloadType[];
const job = jobs.length > 0 ? jobs[selectedJob] : null;
const renderSkills = (skill: string, index: number) => (
<span className="skill display" key={index}>
{skill}
</span>
);
const alreadyApplied = useMemo(() => {
const foundCandidate = jobs[selectedJob]?.applications?.findIndex(
(_item) => _item.candidate === userId
);
if (foundCandidate !== undefined && foundCandidate >= 0) {
const status = jobs[selectedJob]?.applications?.[foundCandidate].status;
return {
applied: true,
status,
color: getStatusColor(status),
};
} else {
return { applied: false, status: "" };
}
}, [jobs, selectedJob, userId]);
const onApply = () => {
dispatch(applyForJob(jobs[selectedJob]._id || ""));
};
const Home = () => {
return (
<div>
<NavBar />
<div className="row">
<div className="col-3 ">
<Profile />
</div>
<div className="col">
<Jobs
jobs={jobs}
onSelectJob={setSelectedJob}
selectedIndex={selectedJob}
/>
</div>
<div className="col">
{job && (
<div className="card p-4 job-preview">
<h5>{job.title}</h5>
<label>
<a href={`/organizations?id=${job.organization[0]._id}`}>
By {job.organization[0].name}
</a>
</label>
<p>
Salary : {job.salary.min} - {job.salary.max} {job.salary.currency}{" "}
</p>
<p className="desc">{job.description}</p>
<label>Primary skills</label>
<div className="skills">{job.primarySkills.map(renderSkills)}</div>
<label>Secondary skills</label>
<div className="skills">
{job.secondarySkills.map(renderSkills)}
</div>
{alreadyApplied.applied ? (
<p className="h5 mt-2">
Status :{" "}
<label className={alreadyApplied.color}>
{alreadyApplied.status}
</label>
</p>
) : (
<button className="btn btn-success mt-3" onClick={onApply}>
Apply
</button>
)}
</div>
)}
</div>
</div>
);
};
const OrganizationHome = () => {
const [selectedJob, setSelectedJob] = useState<number>(0);
const jobs = useSelector((state: Reducers) => state.common.jobs) as JobType[];
const job = jobs.length > 0 ? jobs[selectedJob] : null;
const renderSkills = (skill: string, index: number) => (
<span className="skill" key={index}>
{skill}
</span>
);
return (
<div className="row">
<div className="col-3 ">
<Profile />
<CreateUpdateJob mode="create" />
</div>
<div className="col">
<Jobs
jobs={jobs}
onSelectJob={setSelectedJob}
selectedIndex={selectedJob}
/>
</div>
<div className="col">
{job && (
<div className="card p-4 job-preview">
<h5>{job.title}</h5>
<p>
Salary : {job.salary.min} - {job.salary.max} {job.salary.currency}{" "}
</p>
<p className="desc">{job.description}</p>
<label>Primary skills</label>
<div className="skills">{job.primarySkills.map(renderSkills)}</div>
<label>Secondary skills</label>
<div className="skills">
{job.secondarySkills.map(renderSkills)}
</div>
<div className="mt-2">
<label>Applicattions : </label>{" "}
<a href={`/applications?jobId=${job._id}`}>
({job.applications?.length}) Candidates
</a>
</div>
<CreateUpdateJob defaultJob={job} mode="update" />
</div>
)}
</div>
</div>
);
};
const Home = () => {
const userType = useSelector((state: Reducers) => state.auth.userType);
return (
<Layout title="Vacancies and Jobs">
{userType === USER_TYPE.ORGANIZATION ? (
<OrganizationHome />
) : (
<CandidateHome />
)}
</Layout>
);
};
export default Home;
......@@ -3,10 +3,10 @@ import { useDispatch, useSelector } from "react-redux";
import { updateKeystrokeSettings } from "../common/actions/auth";
import { DEFAULT_CONTROLS } from "../common/config";
import { ControlsType, Reducers } from "../common/types";
import Avatar from "../components/Avatar";
import ChangePassword from "../components/ChangePassword";
import Charts from "../components/Charts";
import Layout from "../components/Layout";
import CandidateProfile from "../components/CandidateProfile";
import ResultTable from "../components/ResultTable";
const Settings = () => {
......@@ -54,21 +54,7 @@ const Settings = () => {
<Layout title="Profile and Settings">
<div className="row">
<div className="col-8">
<div className="card p-4 mb-3">
<div className="mb-3 row">
<div className="col-sm-3">
<Avatar name="Hashan" size="lg" />
</div>
<div className="col-sm-9 ">
<input
className="form-control"
type="text"
placeholder="Default input"
aria-label="default input example"
/>
</div>
</div>
</div>
<CandidateProfile />
<ChangePassword />
</div>
<div className="col-4">
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment