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

Merge branch 'IT19953298' into 'master'

voice recognition

See merge request !3
parents 0458675d 16289d9f
......@@ -11,6 +11,8 @@ import AppState from "./components/AppState";
import "./app.scss";
import "./components.scss";
import ProtectedRoutes from "./components/ProtectedRoute";
import ApplicationsLayout from "./Layouts/ApplicationLayout";
import Applicant from "./views/Application";
function App() {
return (
......@@ -20,7 +22,10 @@ function App() {
<Route element={<ProtectedRoutes />}>
<Route path="/home" element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route element={<ApplicationsLayout />}>
<Route path="/applications" element={<Applications />} />
<Route path="/applicant" element={<Applicant />} />
</Route>
</Route>
</Routes>
<AppState />
......
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Outlet, useSearchParams } from "react-router-dom";
import { JobPayloadType, Reducers } from "../common/types";
import Layout from "../Layouts/Layout";
const ApplicationsLayout = () => {
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);
setJob(found);
}
}, [jobId, jobs]);
const renderSkills = (skill: string, index: number) => (
<span className="skill" key={index}>
{skill}
</span>
);
if (!job) return <Layout title="Applications"></Layout>;
return (
<Layout title={job.title}>
<div className="row">
<div className="col-4">
<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>
<div className="col-8">
<Outlet context={job} />
</div>
</div>
</Layout>
);
};
export default ApplicationsLayout;
import React from "react";
import NavBar from "./NavBar";
import NavBar from "../components/NavBar";
type OwnProps = {
title: string;
......
......@@ -17,3 +17,16 @@ export const updateJob = (payload: JobType, success?: () => void) => ({
payload,
success,
});
export const updateApplication = (
payload: {
candidateId: string;
applicationId: string;
update: any;
},
success?: () => void
) => ({
type: ACTIONS.UPDATE_APPLICATION,
payload,
success,
});
......@@ -15,4 +15,5 @@ export enum ACTIONS {
GET_JOBS = "GET_JOBS",
CREATE_JOB = "CREATE_JOB",
UPDATE_JOB = "UPDATE_JOB",
UPDATE_APPLICATION = "UPDATE_APPLICATION",
}
......@@ -13,5 +13,17 @@ export default class CommonAPI {
static applyForJob = (payload: {
application: ApplicationType;
resumeUrl: string;
}) => request("<BASE_URL>/jobs/apply", "PUT", payload);
}) => request("<BASE_URL>/applications/apply", "POST", payload);
static updateApplication = (payload: {
update: any;
applicationId: string;
candidateId: string;
}) => request("<BASE_URL>/applications/update", "PUT", payload);
static analyseInterview = (payload: {
startTime: number;
endTime: number;
applicationId: string;
}) => request("<BASE_URL>/applications/analyse", "POST", payload);
}
import { initializeApp } from "firebase/app";
import { getStorage, ref } from "firebase/storage";
import { getStorage } from "firebase/storage";
export const BASE_URL = "http://localhost:5000";
export const DEFAULT_CONTROLS = {
......
import {
AddressType,
ApplicationType,
CandidateType,
OrganizationType,
......@@ -60,6 +61,20 @@ export const getStatusColor = (status?: ApplicationType["status"]) => {
? "text-bg-warning"
: status === "Rejected"
? "text-bg-danger"
: "text-bg-secondary";
: "text-bg-primary";
return color;
};
export const getAddress = (address: AddressType) => {
return `${address.addressLine}, ${address.city}, ${address.country}`;
};
export const getVerificationColor = (score: number) => {
const color =
score >= 80
? "text-success"
: score >= 60 && score < 80
? "text-warning"
: "text-danger";
return color;
};
......@@ -7,7 +7,7 @@ function* getJobs({
success,
}: {
type: typeof ACTIONS.GET_JOBS;
success: () => void;
success?: () => void;
}) {
try {
const data: { jobs: JobType; success: boolean } = yield call(
......@@ -17,9 +17,9 @@ function* getJobs({
yield put({ type: ACTIONS.SET_JOBS, payload: data.jobs });
}
success();
success?.();
} catch (error) {
success();
success?.();
}
}
......@@ -103,8 +103,46 @@ function* updateJob({
}
}
function* updateApplication({
payload,
success,
}: {
type: typeof ACTIONS.UPDATE_APPLICATION;
payload: { applicationId: string; update: any; candidateId: string };
success?: () => void;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
const data: { success: boolean } = yield call(
CommonAPI.updateApplication,
payload
);
if (data.success) {
yield call(getJobs, { type: ACTIONS.GET_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);
yield takeLeading(ACTIONS.UPDATE_APPLICATION, updateApplication);
}
......@@ -95,6 +95,7 @@ export type CandidateType = {
profilePicture: string;
state: "INTIAL" | "READY";
resume?: string;
selfIntro?: string;
};
export type OrganizationType = {
......@@ -191,6 +192,7 @@ export type JobPayloadType = {
};
export type ApplicationPayloadType = {
_id: string;
candidate: CandidateType;
job: string;
status: "Pending" | "Accepted" | "In progress" | "Rejected";
......@@ -199,6 +201,7 @@ export type ApplicationPayloadType = {
time: string;
link: string;
videoRef?: string;
voiceVerification?: number;
};
score: {
primary: number;
......
......@@ -207,3 +207,24 @@
margin-top: 5px;
}
}
.matches {
display: flex;
p {
padding: 2px 10px;
background: lightcoral;
margin-right: 10px;
border-radius: 8px;
}
}
.upload-interview-video {
background-color: #f5f5f5;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
input {
display: none;
}
}
import React from "react";
import ReactPlayer from "react-player";
import { getAddress } from "../../common/lib/util";
import { ApplicationPayloadType } from "../../common/types";
import Avatar from "../Avatar";
type OwnProps = {
application: ApplicationPayloadType;
};
const Applicant = ({ application }: OwnProps) => {
const { candidate, score } = application;
const renderMatches = (match: string, index: number) => (
<p key={index}>{match}</p>
);
return (
<>
<div className="mb-3 row">
<div className="col-sm-3 ">
<Avatar
name={candidate?.name}
url={candidate?.profilePicture}
size="lg"
/>
</div>
<div className="col-sm-9">
<div className="mb-3 row">
<label className="col-sm-3">Name</label>
<div className="col-sm-9">
<p>
<strong>{candidate.name}</strong>
</p>
</div>
</div>
<div className="row">
<label className="col-sm-3 col-form-label">Resume</label>
<div className="col-sm-9">
<a
className="btn btn-secondary btn-sm"
href={candidate.resume}
target="_blank"
rel="noreferrer"
>
Preview
</a>
</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">Address</label>
<div className="col-sm-8">
<textarea
className="form-control"
rows={3}
value={getAddress(candidate.contacts.address)}
disabled
/>
</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={candidate.contacts?.phone}
disabled
/>
</div>
</div>
</div>
</div>
<ReactPlayer
url={candidate.selfIntro}
controls
width="100%"
height="100%"
/>
<hr />
<table className="table table-hover table-sm">
<thead>
<tr>
<th scope="col">Primary Score</th>
<th scope="col">Secondary Score</th>
<th scope="col">Similarity</th>
<th scope="col">Total score</th>
</tr>
</thead>
<tbody>
<tr>
<td>{score.primary}</td>
<td>{score.secondary}</td>
<td>{score.similarity}</td>
<td>{score.total}</td>
</tr>
</tbody>
</table>
<label className="mb-2">
<strong>Primary match</strong>
</label>
<div className="matches">{score.primatyMatch?.map(renderMatches)}</div>
<label className="mt-3">
<strong>Secondary match</strong>
</label>
<div className="matches">{score.secondaryMatch?.map(renderMatches)}</div>
</>
);
};
export default Applicant;
import React, { useState, useRef, ChangeEvent } from "react";
import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage";
import moment from "moment";
import ReactPlayer from "react-player";
import { OnProgressProps } from "react-player/base";
import CommonAPI from "../../common/apis/common";
import { ApplicationPayloadType } from "../../common/types";
import Progress from "../Progress";
import { fileStorage } from "../../common/config";
import { useDispatch } from "react-redux";
import { updateApplication } from "../../common/actions/common";
import { getVerificationColor } from "../../common/lib/util";
type OwnProps = {
application: ApplicationPayloadType;
};
type Analysis = {
voice: string;
};
const Interview = ({ application }: OwnProps) => {
const dispatch = useDispatch();
const [analysis, setAnalysis] = useState<Analysis | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [startTime, setStartTime] = useState<number>(0);
const [progress, setProgress] = useState<number>(0);
const [endTime, setEndTime] = useState<number>(120);
const _selectedTime = useRef<number>(0);
const _video = useRef<ReactPlayer>(null);
const _hiddenFileInput = useRef<HTMLInputElement>(null);
const onSeek = (e: number) => (_selectedTime.current = e);
const onProgress = (e: OnProgressProps) =>
(_selectedTime.current = e.playedSeconds);
const onSetStartTime = () => setStartTime(_selectedTime.current);
const onSetEndTime = () => setEndTime(_selectedTime.current);
const displayStartTime = `${moment.utc(startTime * 1000).format("HH:mm:ss")}`;
const displayEndTime = `${moment.utc(endTime * 1000).format("HH:mm:ss")}`;
const onClickAnalyse = () => {
if (startTime !== endTime) {
setIsLoading(true);
CommonAPI.analyseInterview({
startTime,
endTime,
applicationId: application._id,
})
.then((res: any) => {
setAnalysis(res);
setIsLoading(false);
})
.catch((err) => {
setIsLoading(false);
});
}
};
const onSelectVideo = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setIsLoading(true);
const path = `/interviews/${application._id}.mp4`;
const uploadRef = ref(fileStorage, path);
const uploadTask = uploadBytesResumable(uploadRef, e.target.files[0]);
uploadTask.on(
"state_changed",
(snapshot) => {
const percent = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setProgress(percent);
},
(err) => {},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
dispatch(
updateApplication(
{
applicationId: application._id,
candidateId: application.candidate._id || "",
update: { interview: { videoRef: url } },
},
() => {
setProgress(0);
setIsLoading(false);
}
)
);
});
}
);
}
};
const onClickUpload = () => {
_hiddenFileInput?.current?.click();
};
const renderLoading = isLoading && (
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
);
if (!application.interview?.videoRef) {
return (
<>
<div className="upload-interview-video">
<button
type="button"
className="btn btn-secondary"
onClick={onClickUpload}
disabled={isLoading}
>
Upload video
</button>
<input
type="file"
accept="video/mp4"
ref={_hiddenFileInput}
onChange={onSelectVideo}
/>
</div>
<Progress progress={progress} />
</>
);
}
const renderVoiceVerification = () => {
if (application.interview?.voiceVerification) {
const score = application?.interview?.voiceVerification * 100;
const percentage = `${score?.toFixed(2)}%`;
return (
<strong
className={getVerificationColor(
application?.interview?.voiceVerification * 100
)}
>
Voice verification: {percentage}
</strong>
);
}
return null;
};
return (
<div>
<ReactPlayer
ref={_video}
url={application.interview.videoRef}
controls
height="100%"
width="100%"
onSeek={onSeek}
onProgress={onProgress}
/>
{renderVoiceVerification()}
<div className="row" style={{ padding: "12px" }}>
<div className="col-sm-4">
<div className="row">
<button
type="button"
className="col-sm-4 btn btn-warning btn-sm"
onClick={onSetStartTime}
>
Start time
</button>
<div className="col-sm-8">
<input
type="text"
className="form-control"
name="startTime"
disabled
value={displayStartTime}
/>
</div>
</div>
</div>
<div className="col-sm-4">
<div className="row">
<button
type="button"
className="col-sm-4 btn btn-warning btn-sm"
onClick={onSetEndTime}
>
End time
</button>
<div className="col-sm-8">
<input
type="text"
className="form-control"
name="endTime"
disabled
value={displayEndTime}
/>
</div>
</div>
</div>
<div className="col-sm-4">
<div className="row">
<div className="col-sm-8">
<button
type="button"
className="btn btn-primary w-100"
onClick={onClickAnalyse}
disabled={isLoading}
>
Analyse
</button>
</div>
{renderLoading}
</div>
</div>
</div>
<div className="row mt-2">
<div className="mb-3 row">
<label className="col-sm-2 col-form-label">Voice</label>
<label className="col-sm-5 col-form-label">
<strong>{analysis?.voice}</strong>
</label>
</div>
</div>
</div>
);
};
export default Interview;
......@@ -7,12 +7,15 @@ import { fileStorage } from "../common/config";
import { getProfile } from "../common/lib/util";
import { APP_STATE, CandidateType } from "../common/types";
import Avatar from "./Avatar";
import Progress from "./Progress";
const CandidateProfile = () => {
const dispath = useDispatch();
const candidate = useSelector(getProfile) as CandidateType;
const [profile, setProfile] = useState<CandidateType>(candidate);
const [resume, setResume] = useState<File | null>(null);
const [selfIntro, setSelfIntro] = useState<File | null>(null);
const [progress, setProgress] = useState<number>(0);
useEffect(() => {
if (candidate) {
......@@ -51,33 +54,66 @@ const CandidateProfile = () => {
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);
const uploadSingleFile = (path: string, file: File) => {
return new Promise((resolve, reject) => {
const uploadRef = ref(fileStorage, path);
const uploadTask = uploadBytesResumable(uploadRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const percent = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
console.log(percent);
setProgress(percent);
},
(err) => {
console.log("ERROR ", err);
reject(err);
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
console.log(url);
onUpdateCandidate({ ...rest, resume: url });
setProgress(0);
resolve(url);
});
}
);
});
};
const onSubmit = async () => {
const { jobIds, state, _id, ...rest } = profile;
dispath({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
if (resume || selfIntro) {
const files = [];
let update: any = {};
if (resume) {
files.push({
path: `/resumes/${profile._id}.pdf`,
file: resume,
key: "resume",
});
}
if (selfIntro) {
files.push({
path: `/selfIntros/${profile._id}.mp4`,
file: selfIntro,
key: "selfIntro",
});
}
await Promise.all(
files.map(async (_file) => {
try {
const url = await uploadSingleFile(_file.path, _file.file);
update[_file.key] = url;
} catch (error) {}
})
);
onUpdateCandidate({ ...rest, ...update });
} else {
onUpdateCandidate(rest);
}
......@@ -89,6 +125,12 @@ const CandidateProfile = () => {
}
};
const onSelectSelfIntro = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setSelfIntro(e.target.files[0]);
}
};
return (
<div className="card p-4 mb-3">
<h5>Edit/Update profile</h5>
......@@ -171,7 +213,7 @@ const CandidateProfile = () => {
<div className="col-sm-8">
{profile?.resume && (
<a
className="btn btn-light mb-2"
className="btn btn-secondary mb-2"
href={profile.resume}
target="_blank"
rel="noreferrer"
......@@ -192,7 +234,12 @@ const CandidateProfile = () => {
<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" />
<input
type="file"
className="form-control"
accept="video/mp4"
onChange={onSelectSelfIntro}
/>
</div>
</div>
</div>
......@@ -231,6 +278,7 @@ const CandidateProfile = () => {
Update Profile
</button>
</div>
<Progress progress={progress} />
</div>
);
};
......
import React from "react";
type OwnProps = {
progress?: number;
};
const Progress = ({ progress }: OwnProps) => {
if (!progress || progress === 0) return null;
return (
<div className="progress mt-3">
<div
className="progress-bar progress-bar-striped"
style={{ width: `${progress}%` }}
></div>
</div>
);
};
export default Progress;
import React from "react";
type OwnProps = {
tabs: string[];
selected?: string;
onSelect: (tab: string) => void;
};
const TabNavBar = ({ tabs, selected, onSelect }: OwnProps) => {
const renderTabs = (tab: string, index: number) => {
const className = tab === selected ? "nav-link active" : "nav-link";
const onClick = () => onSelect(tab);
return (
<li className="nav-item" key={index}>
<button className={className} aria-current="page" onClick={onClick}>
{tab}
</button>
</li>
);
};
return <ul className="nav nav-tabs mb-4">{tabs.map(renderTabs)}</ul>;
};
export default TabNavBar;
import React, { useState, useEffect, ChangeEvent, useMemo } from "react";
import { useOutletContext, useSearchParams } from "react-router-dom";
import { ApplicationPayloadType, JobPayloadType } from "../common/types";
import TabNavBar from "../components/TabNavBar";
import { useDispatch } from "react-redux";
import { updateApplication } from "../common/actions/common";
import Applicant from "../components/Application/Applicant";
import Interview from "../components/Application/Interview";
const tabs = ["Candidate", "Interview"];
const Application = () => {
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const [selectedTab, setSelectedTab] = useState("Candidate");
const [status, setStatus] = useState("Pending");
const job = useOutletContext<JobPayloadType>();
const applicationId = searchParams.get("applicationId");
const application = (job.applications as ApplicationPayloadType[])?.find(
(_application) => _application._id === applicationId
);
const statusValues = useMemo(() => {
if (status === "Pending") {
return ["Schedule", "Reject"];
} else if (status === "Schedule") {
return ["Accept", "Reject"];
} else if (status === "Rejected") {
return ["Pending", "Schedule"];
} else {
return [];
}
}, [status]);
useEffect(() => {
if (application) {
if (application?.status === "Pending") {
setSelectedTab("Candidate");
setStatus(application.status);
} else if (application?.status === "In progress") {
setSelectedTab("Interview");
setStatus("Schedule");
}
}
}, [application]);
if (!application) return null;
const onUpdateStatus = () => {
let applicationStatus = status;
if (status === "Schedule") {
applicationStatus = "In progress";
} else if (status === "Reject") {
applicationStatus = "Rejected";
}
dispatch(
updateApplication({
applicationId: application._id,
update: { status: applicationStatus },
candidateId: application.candidate._id || "",
})
);
};
const onChangeStatus = (e: ChangeEvent<HTMLSelectElement>) => {
setStatus(e.target.value);
};
const renderOptions = (option: string, index: number) => (
<option key={index} value={option}>
{option}
</option>
);
return (
<div className="card p-4">
<TabNavBar tabs={tabs} selected={selectedTab} onSelect={setSelectedTab} />
{selectedTab === "Candidate" && <Applicant application={application} />}
{selectedTab === "Interview" && <Interview application={application} />}
<hr />
{statusValues.length > 0 && (
<div className="row ">
<label className="col-sm-2 col-form-label">Status</label>
<div className="col-sm-6 ">
<div className="input-group ">
<select
className="form-select"
id="inputGroupSelect04"
aria-label="Example select with button addon"
value={status}
onChange={onChangeStatus}
>
{statusValues.map(renderOptions)}
</select>
<button
className="btn btn-primary "
type="button"
onClick={onUpdateStatus}
>
Update & Next
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Application;
import React, { useEffect, useState, ChangeEvent } from "react";
import { useSelector } from "react-redux";
import { useSearchParams } from "react-router-dom";
import { useNavigate, useOutletContext } from "react-router-dom";
import { getStatusColor } from "../common/lib/util";
import {
JobPayloadType,
Reducers,
ApplicationPayloadType,
} from "../common/types";
import Layout from "../components/Layout";
import { JobPayloadType, ApplicationPayloadType } from "../common/types";
import Layout from "../Layouts/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");
const navigate = useNavigate();
const job = useOutletContext<JobPayloadType>();
const [applications, setApplications] = useState<ApplicationPayloadType[]>(
[]
);
useEffect(() => {
if (jobId) {
const found = jobs.find((_job) => _job._id === jobId);
sortApplications("skills", found);
if (job) {
sortApplications("skills", job);
}
}, [jobId, jobs]);
}, [job]);
const onSelectSort = (e: ChangeEvent<HTMLSelectElement>) => {
sortApplications(e.target.value, job);
......@@ -61,23 +54,19 @@ const Applications = () => {
});
}
setJob({ ..._job, applications: _applications });
setApplications([..._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)}`;
const onClick = () =>
navigate(`/applicant?jobId=${job._id}&applicationId=${_application._id}`);
return (
<tr key={index}>
<tr key={index} onClick={onClick} style={{ cursor: "pointer" }}>
<th scope="row">{application.candidate.name}</th>
<td>{skillsScore}</td>
<td>{application.score.similarity}</td>
......@@ -86,45 +75,17 @@ const Applications = () => {
<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>
<>
<h4>Applications</h4>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label">Sort by</label>
<div className="col-sm-2">
<div className="col-sm-4">
<select
className="form-select"
aria-label="Default select example"
......@@ -147,16 +108,12 @@ const Applications = () => {
<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>
<tbody>{applications?.map(renderApplications)}</tbody>
</table>
</div>
</div>
</div>
</Layout>
</>
);
};
......
......@@ -4,7 +4,7 @@ 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 Layout from "../Layouts/Layout";
import CreateUpdateJob from "../components/Modals/CreateUpdateJob";
import Profile from "../components/Profile";
......
......@@ -5,7 +5,7 @@ import { DEFAULT_CONTROLS } from "../common/config";
import { ControlsType, Reducers } from "../common/types";
import ChangePassword from "../components/ChangePassword";
import Charts from "../components/Charts";
import Layout from "../components/Layout";
import Layout from "../Layouts/Layout";
import CandidateProfile from "../components/CandidateProfile";
import ResultTable from "../components/ResultTable";
......
......@@ -4215,6 +4215,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
deepmerge@^4.0.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
......@@ -6746,6 +6751,11 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
load-script@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4"
integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==
loader-runner@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
......@@ -6901,6 +6911,11 @@ memfs@^3.1.2, memfs@^3.4.3:
dependencies:
fs-monkey "^1.0.3"
memoize-one@^5.1.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
......@@ -6994,6 +7009,11 @@ mkdirp@~0.5.1:
dependencies:
minimist "^1.2.6"
moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
......@@ -8238,6 +8258,11 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
react-fast-compare@^3.0.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f"
integrity sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==
react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
......@@ -8268,6 +8293,17 @@ react-modal@^3.16.1:
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-player@^2.12.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.12.0.tgz#2fc05dbfec234c829292fbca563b544064bd14f0"
integrity sha512-rymLRz/2GJJD+Wc01S7S+i9pGMFYnNmQibR2gVE3KmHJCBNN8BhPAlOPTGZtn1uKpJ6p4RPLlzPQ1OLreXd8gw==
dependencies:
deepmerge "^4.0.0"
load-script "^1.0.0"
memoize-one "^5.1.1"
prop-types "^15.7.2"
react-fast-compare "^3.0.1"
react-redux@^8.0.5:
version "8.0.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
......
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