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

Merge branch 'it19243122' into 'master'

video conferencing online

See merge request !8
parents 40aaf3c7 83390d6c
...@@ -26,6 +26,14 @@ ...@@ -26,6 +26,14 @@
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous" crossorigin="anonymous"
></script> ></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<title>Smart Recruiter</title> <title>Smart Recruiter</title>
</head> </head>
<body> <body>
......
...@@ -14,6 +14,8 @@ import "./components.scss"; ...@@ -14,6 +14,8 @@ import "./components.scss";
import ProtectedRoutes from "./components/ProtectedRoute"; import ProtectedRoutes from "./components/ProtectedRoute";
import ApplicationsLayout from "./Layouts/ApplicationLayout"; import ApplicationsLayout from "./Layouts/ApplicationLayout";
import Applicant from "./views/Application"; import Applicant from "./views/Application";
import MeetingRoom from "./views/MeetingRoom";
import Organization from "./views/Organization.view";
function App() { function App() {
return ( return (
...@@ -23,9 +25,11 @@ function App() { ...@@ -23,9 +25,11 @@ function App() {
<Route element={<ProtectedRoutes />}> <Route element={<ProtectedRoutes />}>
<Route path="/home" element={<Home />} /> <Route path="/home" element={<Home />} />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/organizations" element={<Organization />} />
<Route element={<ApplicationsLayout />}> <Route element={<ApplicationsLayout />}>
<Route path="/applications" element={<Applications />} /> <Route path="/applications" element={<Applications />} />
<Route path="/applicant" element={<Applicant />} /> <Route path="/applicant" element={<Applicant />} />
<Route path="/meeting" element={<MeetingRoom />} />
</Route> </Route>
</Route> </Route>
</Routes> </Routes>
......
...@@ -5,14 +5,18 @@ import NavBar from "../components/NavBar"; ...@@ -5,14 +5,18 @@ import NavBar from "../components/NavBar";
type OwnProps = { type OwnProps = {
title: string; title: string;
children?: JSX.Element | JSX.Element[]; children?: JSX.Element | JSX.Element[];
suffix?: JSX.Element;
}; };
const Layout = ({ title, children }: OwnProps) => { const Layout = ({ title, children, suffix }: OwnProps) => {
return ( return (
<> <>
<NavBar /> <NavBar />
<div className="container pb-5"> <div className="container pb-5">
<div className="layout-title-container">
<h4 className="mb-3">{title}</h4> <h4 className="mb-3">{title}</h4>
{suffix}
</div>
{children} {children}
</div> </div>
</> </>
......
...@@ -44,3 +44,14 @@ p { ...@@ -44,3 +44,14 @@ p {
margin-top: 10px; margin-top: 10px;
} }
} }
.layout-title-container {
display: flex;
flex-direction: row;
justify-content: space-between;
h4,
div {
flex: 1;
}
margin-bottom: 10px;
}
import { ACTIONS } from ".";
import { ApplicationPayloadType } from "../types";
export const updateApplication = (
payload: {
candidateId: string;
applicationId: string;
update: any;
},
success?: () => void
) => ({
type: ACTIONS.UPDATE_APPLICATION,
payload,
success,
});
export const updateApplicationAction = (
payload: {
applicationId: string;
update: any;
},
success?: () => void
) => ({
type: ACTIONS.UPDATE_APPLICATION_ACTION,
payload,
success,
});
export const createMeeting = (
payload: ApplicationPayloadType,
success?: (roomId: string | null) => void
) => ({
type: ACTIONS.CREATE_MEETING,
payload,
success,
});
export const endMeeting = (
payload: { application: ApplicationPayloadType; roomId: string },
success?: () => void
) => ({
type: ACTIONS.END_MEETING,
payload,
success,
});
...@@ -18,19 +18,6 @@ export const updateJob = (payload: JobType, success?: () => void) => ({ ...@@ -18,19 +18,6 @@ export const updateJob = (payload: JobType, success?: () => void) => ({
success, success,
}); });
export const updateApplication = (
payload: {
candidateId: string;
applicationId: string;
update: any;
},
success?: () => void
) => ({
type: ACTIONS.UPDATE_APPLICATION,
payload,
success,
});
export const setDialogBox = (payload: string | null) => ({ export const setDialogBox = (payload: string | null) => ({
type: ACTIONS.SET_DIALOG, type: ACTIONS.SET_DIALOG,
payload, payload,
......
...@@ -11,7 +11,6 @@ export enum ACTIONS { ...@@ -11,7 +11,6 @@ export enum ACTIONS {
UPDATE_ORG = "UPDATE_ORG", UPDATE_ORG = "UPDATE_ORG",
SET_APP_STATE = "SET_APP_STATE", SET_APP_STATE = "SET_APP_STATE",
SET_JOBS = "SET_JOBS", SET_JOBS = "SET_JOBS",
...@@ -19,5 +18,9 @@ export enum ACTIONS { ...@@ -19,5 +18,9 @@ export enum ACTIONS {
CREATE_JOB = "CREATE_JOB", CREATE_JOB = "CREATE_JOB",
UPDATE_JOB = "UPDATE_JOB", UPDATE_JOB = "UPDATE_JOB",
UPDATE_APPLICATION = "UPDATE_APPLICATION", UPDATE_APPLICATION = "UPDATE_APPLICATION",
UPDATE_APPLICATION_ACTION = "UPDATE_APPLICATION_ACTION",
SET_DIALOG = "SET_DIALOG", SET_DIALOG = "SET_DIALOG",
CREATE_MEETING = "CREATE_MEETING",
END_MEETING = "END_MEETING",
} }
...@@ -18,12 +18,20 @@ export default class CommonAPI { ...@@ -18,12 +18,20 @@ export default class CommonAPI {
static updateApplication = (payload: { static updateApplication = (payload: {
update: any; update: any;
applicationId: string; applicationId: string;
candidateId: string; candidateId?: string;
}) => request("<BASE_URL>/applications/update", "PUT", payload); }) => request("<BASE_URL>/applications/update", "PUT", payload);
static updateApplicationAction = (payload: {
update: any;
applicationId: string;
}) => request("<BASE_URL>/applications/update/action", "PUT", payload);
static analyseInterview = (payload: { static analyseInterview = (payload: {
startTime: number; startTime: number;
endTime: number; endTime: number;
applicationId: string; applicationId: string;
}) => request("<BASE_URL>/applications/analyse", "POST", payload); }) => request("<BASE_URL>/applications/analyse", "POST", payload);
static searchJobs = (key: string) =>
request("<BASE_URL>/jobs/search", "GET", { key });
} }
import { request } from "../lib/api";
const MEETINGS_URL = "https://api.videosdk.live/v2";
export let meetingToken: string;
export const getToken = async () => {
if (meetingToken) {
return meetingToken;
} else {
try {
const data: { token: string } = await MeetingsAPI.getToken();
meetingToken = data.token;
return data.token;
} catch (error) {}
}
};
export default class MeetingsAPI {
static getToken = (): Promise<any> =>
request("<BASE_URL>/auth/conference/key", "GET", {}, false, undefined);
static createRoom = async (customRoomId: string) =>
request(
`${MEETINGS_URL}/rooms`,
"POST",
{ customRoomId },
false,
undefined,
{ Authorization: await getToken() }
);
static endSession = async (roomId: string) =>
request(
`${MEETINGS_URL}/sessions/end`,
"POST",
{ roomId },
false,
undefined,
{ Authorization: await getToken() }
);
static deactivateRoom = async (roomId: string) =>
request(
`${MEETINGS_URL}/rooms/deactivate`,
"POST",
{ roomId },
false,
undefined,
{ Authorization: await getToken() }
);
static startRecord = async (roomId: string) =>
request(
`${MEETINGS_URL}/recordings/start`,
"POST",
{ roomId },
false,
undefined,
{ Authorization: await getToken() }
);
static getRecordings = async (roomId: string) =>
request(`${MEETINGS_URL}/recordings`, "GET", { roomId }, false, undefined, {
Authorization: await getToken(),
});
}
import { initializeApp } from "firebase/app"; import { initializeApp } from "firebase/app";
import { getStorage } from "firebase/storage"; import { getStorage } from "firebase/storage";
export const BASE_URL = "http://localhost:5000"; export const BASE_URL = "http://192.168.8.120:5000";
export const DEFAULT_CONTROLS = { export const DEFAULT_CONTROLS = {
standard: { standard: {
sd: 1.5, sd: 1.5,
...@@ -55,4 +55,5 @@ export const emotionsData = [ ...@@ -55,4 +55,5 @@ export const emotionsData = [
}, },
]; ];
export const OPEN_API_KEY = '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' export const OPEN_API_KEY =
\ No newline at end of file "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";
...@@ -21,7 +21,8 @@ export const request = ( ...@@ -21,7 +21,8 @@ export const request = (
method: AxiosRequestConfig["method"], method: AxiosRequestConfig["method"],
requestData?: AxiosRequestConfig["data"] | AxiosRequestConfig["params"], requestData?: AxiosRequestConfig["data"] | AxiosRequestConfig["params"],
isGuest?: boolean, isGuest?: boolean,
contentType?: string contentType?: string,
header?: AxiosRequestConfig["headers"]
) => ) =>
new Promise(async (resolve, reject) => { new Promise(async (resolve, reject) => {
const endpoint = url?.replace?.("<BASE_URL>", BASE_URL); const endpoint = url?.replace?.("<BASE_URL>", BASE_URL);
...@@ -33,6 +34,7 @@ export const request = ( ...@@ -33,6 +34,7 @@ export const request = (
const headers = { const headers = {
auth_token, auth_token,
"Content-Type": contentType || "application/json", "Content-Type": contentType || "application/json",
...(header||{}),
}; };
logger("REQUEST: ", method, endpoint, headers, requestData); logger("REQUEST: ", method, endpoint, headers, requestData);
......
import { import {
AddressType, AddressType,
ApplicationStatus,
CandidateType, CandidateType,
OrganizationType, OrganizationType,
Reducers, Reducers,
...@@ -52,7 +53,7 @@ export const getUserId = (state: Reducers): string => { ...@@ -52,7 +53,7 @@ export const getUserId = (state: Reducers): string => {
return profile._id as string; return profile._id as string;
}; };
export const getStatusColor = (status?: string) => { export const getStatusColor = (status?: ApplicationStatus) => {
const color = const color =
status === "Accepted" status === "Accepted"
? "text-bg-success" ? "text-bg-success"
...@@ -60,6 +61,8 @@ export const getStatusColor = (status?: string) => { ...@@ -60,6 +61,8 @@ export const getStatusColor = (status?: string) => {
? "text-bg-warning" ? "text-bg-warning"
: status === "Rejected" : status === "Rejected"
? "text-bg-danger" ? "text-bg-danger"
: status === "Analyse"
? "text-bg-info"
: "text-bg-primary"; : "text-bg-primary";
return color; return color;
}; };
......
import { put, takeLeading, call } from "redux-saga/effects";
import { ACTIONS } from "../actions";
import { APP_STATE, ApplicationPayloadType } from "../types";
import MeetingsAPI from "../apis/meetings";
import { getJobs } from "./common";
import CommonAPI from "../apis/common";
function* createMeeting({
payload,
success,
}: {
type: typeof ACTIONS.CREATE_MEETING;
payload: ApplicationPayloadType;
success?: (roomId: string | null) => void;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
const data: { roomId: string } = yield call(
MeetingsAPI.createRoom,
payload._id
);
yield call(updateApplicationAction, {
type: ACTIONS.UPDATE_APPLICATION_ACTION,
payload: {
applicationId: payload._id,
update: { interview: { ...payload.interview, link: data.roomId } },
},
});
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
success?.(data.roomId);
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
success?.(null);
}
}
export function* endMeeting({
payload,
success,
}: {
type: typeof ACTIONS.END_MEETING;
payload: { application: ApplicationPayloadType; roomId: string };
success?: () => void;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
// yield call(MeetingsAPI.endSession, payload.roomId);
// yield call(MeetingsAPI.deactivateRoom, payload.roomId);
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
success?.();
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
success?.();
}
}
export 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 function* updateApplicationAction({
payload,
success,
}: {
type: typeof ACTIONS.UPDATE_APPLICATION_ACTION;
payload: { applicationId: string; update: any };
success?: () => void;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
const data: { success: boolean } = yield call(
CommonAPI.updateApplicationAction,
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* applicationSaga() {
yield takeLeading(ACTIONS.CREATE_MEETING, createMeeting);
yield takeLeading(ACTIONS.END_MEETING, endMeeting);
yield takeLeading(ACTIONS.UPDATE_APPLICATION, updateApplication);
yield takeLeading(ACTIONS.UPDATE_APPLICATION_ACTION, updateApplicationAction);
}
...@@ -3,7 +3,7 @@ import { ACTIONS } from "../actions"; ...@@ -3,7 +3,7 @@ import { ACTIONS } from "../actions";
import CommonAPI from "../apis/common"; import CommonAPI from "../apis/common";
import { APP_STATE, JobType, Reducers } from "../types"; import { APP_STATE, JobType, Reducers } from "../types";
function* getJobs({ export function* getJobs({
success, success,
}: { }: {
type: typeof ACTIONS.GET_JOBS; type: typeof ACTIONS.GET_JOBS;
...@@ -103,46 +103,8 @@ function* updateJob({ ...@@ -103,46 +103,8 @@ 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() { export default function* authSaga() {
yield takeLeading(ACTIONS.GET_JOBS, getJobs); yield takeLeading(ACTIONS.GET_JOBS, getJobs);
yield takeLeading(ACTIONS.CREATE_JOB, createJob); yield takeLeading(ACTIONS.CREATE_JOB, createJob);
yield takeLeading(ACTIONS.UPDATE_JOB, updateJob); yield takeLeading(ACTIONS.UPDATE_JOB, updateJob);
yield takeLeading(ACTIONS.UPDATE_APPLICATION, updateApplication);
} }
...@@ -3,7 +3,8 @@ import { all } from "redux-saga/effects"; ...@@ -3,7 +3,8 @@ import { all } from "redux-saga/effects";
import Auth from "./auth"; import Auth from "./auth";
import User from "./user"; import User from "./user";
import Common from "./common"; import Common from "./common";
import Application from "./application";
export default function* rootSaga() { export default function* rootSaga() {
yield all([Auth(), User(), Common()]); yield all([Auth(), User(), Common(), Application()]);
} }
...@@ -3,7 +3,14 @@ import { ACTIONS } from "../actions"; ...@@ -3,7 +3,14 @@ import { ACTIONS } from "../actions";
import CommonAPI from "../apis/common"; import CommonAPI from "../apis/common";
import UserAPI from "../apis/user"; import UserAPI from "../apis/user";
import { getProfile } from "../lib/util"; import { getProfile } from "../lib/util";
import { APP_STATE, CandidateType, JobType, OrganizationType, Reducers } from "../types"; import {
APP_STATE,
CandidateType,
JobType,
OrganizationType,
Reducers,
} from "../types";
import { getJobs } from "./common";
function* updateCandidate({ function* updateCandidate({
payload, payload,
...@@ -125,11 +132,12 @@ function* applyForJob({ ...@@ -125,11 +132,12 @@ function* applyForJob({
} }
} }
yield call(getJobs, { type: ACTIONS.GET_JOBS });
yield put({ yield put({
type: ACTIONS.SET_APP_STATE, type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS }, payload: { state: APP_STATE.SUCCESS },
}); });
window.location.reload();
} catch (error) { } catch (error) {
yield put({ yield put({
type: ACTIONS.SET_APP_STATE, type: ACTIONS.SET_APP_STATE,
...@@ -138,8 +146,6 @@ function* applyForJob({ ...@@ -138,8 +146,6 @@ function* applyForJob({
} }
} }
export default function* authSaga() { export default function* authSaga() {
yield takeLeading(ACTIONS.UPDATE_CANDIDATE, updateCandidate); yield takeLeading(ACTIONS.UPDATE_CANDIDATE, updateCandidate);
yield takeLeading(ACTIONS.UPDATE_ORG, updateOrganization); yield takeLeading(ACTIONS.UPDATE_ORG, updateOrganization);
......
...@@ -144,10 +144,17 @@ export type JobType = { ...@@ -144,10 +144,17 @@ export type JobType = {
organization: string; organization: string;
}; };
export type ApplicationStatus =
| "Pending"
| "In progress"
| "Analyse"
| "Accepted"
| "Rejected";
export type ApplicationType = { export type ApplicationType = {
candidate: string; candidate: string;
job: string; job: string;
status: "Pending" | "Accepted" | "In progress" | "Rejected"; status: ApplicationStatus;
interview?: { interview?: {
date: string; date: string;
time: string; time: string;
...@@ -222,7 +229,7 @@ export type ApplicationPayloadType = { ...@@ -222,7 +229,7 @@ export type ApplicationPayloadType = {
_id: string; _id: string;
candidate: CandidateType; candidate: CandidateType;
job: string; job: string;
status: string; status: ApplicationStatus;
interview?: { interview?: {
date: string; date: string;
time: string; time: string;
......
...@@ -149,6 +149,7 @@ ...@@ -149,6 +149,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 10px;
} }
.skill { .skill {
background-color: #058700; background-color: #058700;
...@@ -183,6 +184,7 @@ ...@@ -183,6 +184,7 @@
h5 { h5 {
margin: 0; margin: 0;
} }
margin-bottom: 10px;
label { label {
a { a {
...@@ -218,19 +220,74 @@ ...@@ -218,19 +220,74 @@
} }
} }
.upload-interview-video { .interview-room {
background-color: #f5f5f5; background-color: #f5f5f5;
height: 400px; height: 450px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
input { input {
display: none; display: none;
} }
div {
align-items: center;
display: flex;
flex-direction: column;
}
} }
.questions-container{ .questions-container {
background-color: #ffefab80; background-color: #ffefab80;
min-height: 400px; min-height: 400px;
padding: 0 20px; padding: 0 20px;
} }
button.nav-link.active {
color: #0e60e4 !important;
font-weight: 600;
}
.meeting-container {
position: relative;
width: 100%;
height: 100%;
.action-containers {
position: absolute;
display: flex;
flex-direction: row;
bottom: 10px;
z-index: 10;
button {
margin: 0 10px;
}
.meeting-action {
background-color: #dc3545;
height: 40px;
width: 40px;
border-radius: 40px;
border: none;
color: white;
&.record {
background-color: white;
color: #dc3545;
}
}
}
.lobby-actions {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
}
.player-wrapper {
width: auto;
height: auto;
}
.react-player {
position: absolute;
}
...@@ -14,17 +14,24 @@ import { OnProgressProps } from "react-player/base"; ...@@ -14,17 +14,24 @@ import { OnProgressProps } from "react-player/base";
import CommonAPI from "../../common/apis/common"; import CommonAPI from "../../common/apis/common";
import { import {
ApplicationPayloadType, ApplicationPayloadType,
ApplicationStatus,
EmotionsPayloadType, EmotionsPayloadType,
EmotionsType, EmotionsType,
} from "../../common/types"; } from "../../common/types";
import Progress from "../Progress"; import Progress from "../Progress";
import { fileStorage } from "../../common/config"; import { fileStorage } from "../../common/config";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { getJobs, updateApplication } from "../../common/actions/common"; import { getJobs } from "../../common/actions/common";
import { updateApplication } from "../../common/actions/application";
import { getVerificationColor } from "../../common/lib/util"; import { getVerificationColor } from "../../common/lib/util";
import ApplicationActions from "./ApplicationActions";
import MeetingsAPI from "../../common/apis/meetings";
type OwnProps = { type OwnProps = {
application: ApplicationPayloadType; application: ApplicationPayloadType;
status?: ApplicationStatus;
onChangeStatus: (e: ChangeEvent<HTMLSelectElement>) => void;
onUpdateStatus: () => void;
}; };
type Analysis = { type Analysis = {
...@@ -32,10 +39,15 @@ type Analysis = { ...@@ -32,10 +39,15 @@ type Analysis = {
eyeBlinks?: number; eyeBlinks?: number;
}; };
const Analyse = ({ application }: OwnProps) => { const Analyse = ({
application,
status,
onChangeStatus,
onUpdateStatus,
}: OwnProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [analysis, setAnalysis] = useState<Analysis | null>(null); const [analysis, setAnalysis] = useState<Analysis | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(true);
const [startTime, setStartTime] = useState<number>(0); const [startTime, setStartTime] = useState<number>(0);
const [progress, setProgress] = useState<number>(0); const [progress, setProgress] = useState<number>(0);
const [endTime, setEndTime] = useState<number>(120); const [endTime, setEndTime] = useState<number>(120);
...@@ -48,7 +60,22 @@ const Analyse = ({ application }: OwnProps) => { ...@@ -48,7 +60,22 @@ const Analyse = ({ application }: OwnProps) => {
useEffect(() => { useEffect(() => {
dispatch(getJobs()); dispatch(getJobs());
}, []); getRecording();
}, [dispatch]);
const getRecording = () => {
if (application.interview?.link) {
MeetingsAPI.getRecordings(application.interview.link).then((res: any) => {
if (res.data?.[0]?.file?.fileUrl) {
updateUserApplication(res.data?.[0]?.file?.fileUrl);
} else {
setIsLoading(false);
}
});
} else {
setIsLoading(false);
}
};
const onPressStartFacialExpression = () => { const onPressStartFacialExpression = () => {
setIsFacialAnalyzing(true); setIsFacialAnalyzing(true);
...@@ -147,13 +174,21 @@ const Analyse = ({ application }: OwnProps) => { ...@@ -147,13 +174,21 @@ const Analyse = ({ application }: OwnProps) => {
}, },
(err) => {}, (err) => {},
() => { () => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => { getDownloadURL(uploadTask.snapshot.ref).then(updateUserApplication);
}
);
}
};
const updateUserApplication = (url: string) => {
dispatch( dispatch(
updateApplication( updateApplication(
{ {
applicationId: application._id, applicationId: application._id,
candidateId: application.candidate._id || "", candidateId: application.candidate._id || "",
update: { interview: { videoRef: url } }, update: {
interview: { ...application.interview, videoRef: url },
},
}, },
() => { () => {
setProgress(0); setProgress(0);
...@@ -161,11 +196,8 @@ const Analyse = ({ application }: OwnProps) => { ...@@ -161,11 +196,8 @@ const Analyse = ({ application }: OwnProps) => {
} }
) )
); );
});
}
);
}
}; };
const onClickUpload = () => { const onClickUpload = () => {
_hiddenFileInput?.current?.click(); _hiddenFileInput?.current?.click();
}; };
...@@ -176,30 +208,6 @@ const Analyse = ({ application }: OwnProps) => { ...@@ -176,30 +208,6 @@ const Analyse = ({ application }: OwnProps) => {
</div> </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 = () => { const renderVoiceVerification = () => {
if (application.interview?.voiceVerification) { if (application.interview?.voiceVerification) {
if (application.interview.voiceVerification === "Pending") { if (application.interview.voiceVerification === "Pending") {
...@@ -223,6 +231,30 @@ const Analyse = ({ application }: OwnProps) => { ...@@ -223,6 +231,30 @@ const Analyse = ({ application }: OwnProps) => {
); );
}; };
if (!application.interview?.videoRef) {
return (
<>
<div className="interview-room">
<button
type="button"
className="btn btn-secondary"
onClick={onClickUpload}
disabled={isLoading}
>
{isLoading ? "Loading..." : "Upload video"}
</button>
<input
type="file"
accept="video/mp4"
ref={_hiddenFileInput}
onChange={onSelectVideo}
/>
</div>
<Progress progress={progress} />
</>
);
}
return ( return (
<div> <div>
<ReactPlayer <ReactPlayer
...@@ -329,7 +361,7 @@ const Analyse = ({ application }: OwnProps) => { ...@@ -329,7 +361,7 @@ const Analyse = ({ application }: OwnProps) => {
<strong>Facial expressions</strong> <strong>Facial expressions</strong>
</label> </label>
{emotions.length === 0 ? ( {emotions.length === 0 ? (
<div className="upload-interview-video"> <div className="interview-room">
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
...@@ -415,6 +447,13 @@ const Analyse = ({ application }: OwnProps) => { ...@@ -415,6 +447,13 @@ const Analyse = ({ application }: OwnProps) => {
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
)} )}
<hr />
<ApplicationActions
status={status}
onChangeStatus={onChangeStatus}
onUpdateStatus={onUpdateStatus}
visible={status === "Analyse"}
/>
</div> </div>
); );
}; };
......
import React from "react"; import React, { ChangeEvent, useState } from "react";
import ReactPlayer from "react-player"; import ReactPlayer from "react-player";
import { getAddress } from "../../common/lib/util"; import { getAddress } from "../../common/lib/util";
import { ApplicationPayloadType } from "../../common/types"; import { ApplicationPayloadType, ApplicationStatus } from "../../common/types";
import Avatar from "../Avatar"; import Avatar from "../Avatar";
import ApplicationActions from "./ApplicationActions";
type OwnProps = { type OwnProps = {
application: ApplicationPayloadType; application: ApplicationPayloadType;
selectedStatus: ApplicationStatus;
status?: ApplicationStatus;
onChangeStatus: (e: ChangeEvent<HTMLSelectElement>) => void;
onUpdateStatus: (update?: any) => void;
}; };
const Applicant = ({ application }: OwnProps) => { const Applicant = ({
application,
status,
onChangeStatus,
onUpdateStatus,
selectedStatus,
}: OwnProps) => {
const [showDateTime, setShowDateTime] = useState<boolean>(false);
const [dateTime, setDateTime] = useState<
{ date: string; time: string } | any
>(null);
const { candidate, score } = application; const { candidate, score } = application;
const renderMatches = (match: string, index: number) => ( const renderMatches = (match: string, index: number) => (
<p key={index}>{match}</p> <p key={index}>{match}</p>
); );
const onChangeDateTime = (e: ChangeEvent<HTMLInputElement>) => {
setDateTime({ ...dateTime, [e.target.name]: e.target.value });
};
const onChangeAppicantStatus = (e: ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === "In progress") {
setShowDateTime(true);
} else {
setShowDateTime(false);
}
onChangeStatus(e);
};
const onUpdateApplication = () => {
if (selectedStatus === "In progress") {
if (dateTime) {
onUpdateStatus({
interview: { ...application.interview, ...dateTime },
});
}
} else {
onUpdateStatus();
}
};
console.log(application._id);
return ( return (
<> <>
<div className="mb-3 row"> <div className="mb-3 row">
...@@ -111,6 +154,40 @@ const Applicant = ({ application }: OwnProps) => { ...@@ -111,6 +154,40 @@ const Applicant = ({ application }: OwnProps) => {
<strong>Secondary match</strong> <strong>Secondary match</strong>
</label> </label>
<div className="matches">{score.secondaryMatch?.map(renderMatches)}</div> <div className="matches">{score.secondaryMatch?.map(renderMatches)}</div>
<hr />
{showDateTime && (
<div className="row mb-3">
<div className="col-md-6">
<div className="row">
<div className="col-md-6">
<label className="form-label">Date</label>
<input
type="date"
className="form-control"
name="date"
onChange={onChangeDateTime}
/>
</div>
<div className="col-md-6">
<label className="form-label">Time</label>
<input
type="time"
className="form-control"
name="time"
onChange={onChangeDateTime}
/>
</div>
</div>
</div>
</div>
)}
<ApplicationActions
status={status}
onChangeStatus={onChangeAppicantStatus}
onUpdateStatus={onUpdateApplication}
visible={status === "Pending" || status === "Rejected"}
/>
</> </>
); );
}; };
......
import React, { ChangeEvent } from "react";
import { ApplicationStatus } from "../../common/types";
type OwnProps = {
status?: string;
onChangeStatus: (e: ChangeEvent<HTMLSelectElement>) => void;
onUpdateStatus: () => void;
visible?: boolean;
};
const getStatusValues = (
status?: string
): { label: string; value: ApplicationStatus }[] => {
if (status === "Pending" || status === "Rejected") {
return [
{ label: "Schedule", value: "In progress" },
{ label: "Reject", value: "Rejected" },
];
} else if (status === "In progress") {
return [
{ label: "Analyse", value: "Analyse" },
{ label: "Reject", value: "Rejected" },
];
} else if (status === "Analyse") {
return [
{ label: "Accept", value: "Accepted" },
{ label: "Reject", value: "Rejected" },
];
} else {
return [];
}
};
const ApplicationActions = ({
status,
onChangeStatus,
onUpdateStatus,
visible = false,
}: OwnProps) => {
const statusValues = getStatusValues(status);
if (!visible) return null;
const renderOptions = (
{ label, value }: { label: string; value: ApplicationStatus },
index: number
) => (
<option key={index} value={value}>
{label}
</option>
);
const onClickUpdate = () => onUpdateStatus();
return (
<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"
onChange={onChangeStatus}
>
<option value="select">Select</option>
{statusValues.map(renderOptions)}
</select>
<button
className="btn btn-primary "
type="button"
onClick={onClickUpdate}
>
Update & Next
</button>
</div>
</div>
</div>
);
};
export default ApplicationActions;
import React, {useState, useRef, ChangeEvent} from 'react' import React, { useState, useRef, ChangeEvent } from "react";
import {CreateCompletionResponseChoicesInner } from "openai"; import { CreateCompletionResponseChoicesInner } from "openai";
import { ApplicationPayloadType } from '../../common/types'; import OpenAPI from "../../common/lib/openApi";
import OpenAPI from '../../common/lib/openApi'; import ApplicationActions from "./ApplicationActions";
import { ApplicationPayloadType, ApplicationStatus } from "../../common/types";
import { useDispatch } from "react-redux";
import { createMeeting, endMeeting } from "../../common/actions/application";
import Meeting from "../Meeting";
type OwnProps = { type OwnProps = {
status?: ApplicationStatus;
onChangeStatus: (e: ChangeEvent<HTMLSelectElement>) => void;
onUpdateStatus: () => void;
application: ApplicationPayloadType; application: ApplicationPayloadType;
} };
const Interview = ({application}:OwnProps)=>{ const Interview = ({
const [isLoading, setIsLoading] = useState<boolean>(false) status,
const [questions, setQuestions] = useState<CreateCompletionResponseChoicesInner[]>([]) onChangeStatus,
const subject = useRef<string>('') onUpdateStatus,
const level = useRef<string>('easy') application,
}: OwnProps) => {
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [roomId, setRoomId] = useState<string | null>(null);
const [questions, setQuestions] = useState<
CreateCompletionResponseChoicesInner[]
>([]);
const subject = useRef<string>("");
const level = useRef<string>("easy");
const onPressGenerate=()=>{ const onPressGenerate = () => {
if(subject.current.length>0){ if (subject.current.length > 0) {
setQuestions([]) setQuestions([]);
setIsLoading(true) setIsLoading(true);
OpenAPI.getInterViewQuestions(subject.current, level.current).then(res=>{ OpenAPI.getInterViewQuestions(subject.current, level.current).then(
setQuestions(res.data.choices) (res) => {
setIsLoading(false) setQuestions(res.data.choices);
}) setIsLoading(false);
} }
);
} }
};
const onChange=(e:ChangeEvent<HTMLInputElement>)=>{ const onChange = (e: ChangeEvent<HTMLInputElement>) => {
subject.current = e.target.value subject.current = e.target.value;
} };
const onSelectLevel = (e: ChangeEvent<HTMLSelectElement>) => { const onSelectLevel = (e: ChangeEvent<HTMLSelectElement>) => {
level.current = e.target.value level.current = e.target.value;
} };
const renderQuestions=(question:CreateCompletionResponseChoicesInner, index:number)=>{ const renderQuestions = (
return (<p style={{whiteSpace:'pre-line'}}>{question.text}</p>) question: CreateCompletionResponseChoicesInner,
} index: number
) => {
return (
<p key={index} style={{ whiteSpace: "pre-line" }}>
{question.text}
</p>
);
};
const btnText = isLoading ? "Generating.." : "Generate";
const btnText = isLoading?'Generating..':'Generate' const onClickStart = () => {
dispatch(
createMeeting(application, (_roomId) => {
setRoomId(_roomId);
})
);
};
const onMeetingLeave = () => {
if (roomId) {
dispatch(
endMeeting({ application, roomId }, () => {
setRoomId(null);
})
);
}
};
return ( return (
<div> <div>
<h4>Interview Room</h4>
{roomId ? (
<div className="interview-room">
<Meeting roomId={roomId} onMeetingLeave={onMeetingLeave} />
</div>
) : (
<div className="interview-room">
<div>
<p className="mb-2">{`Interview is scheduled on ${application.interview?.date} @ ${application.interview?.time}`}</p>
<button
type="button"
className="btn btn-secondary"
disabled={isLoading}
onClick={onClickStart}
>
Start now
</button>
</div>
</div>
)}
<hr />
<h4>Questions Generator</h4> <h4>Questions Generator</h4>
<div className="row g-3" > <div className="row g-3">
<label className="col-3 col-form-label" >Subject</label> <label className="col-3 col-form-label">Subject</label>
<div className="col-6"> <div className="col-6">
<div className="row g-3" > <div className="row g-3">
<div className="col-8"> <input type="text" className="form-control" onChange={onChange} /></div> <div className="col-8">
<input type="text" className="form-control" onChange={onChange} />
</div>
<div className="col-4"> <div className="col-4">
<select className="form-select" aria-label="Default select example" onChange={onSelectLevel}> <select
className="form-select"
aria-label="Default select example"
onChange={onSelectLevel}
>
<option value="easy">Easy</option> <option value="easy">Easy</option>
<option value="intermediate">Medium</option> <option value="intermediate">Medium</option>
<option value="hard">Hard</option> <option value="hard">Hard</option>
...@@ -57,13 +128,27 @@ const Interview = ({application}:OwnProps)=>{ ...@@ -57,13 +128,27 @@ const Interview = ({application}:OwnProps)=>{
</div> </div>
</div> </div>
<div className="col-3"> <div className="col-3">
<button className="btn btn-warning mb-3" onClick={onPressGenerate} disabled={isLoading} >{btnText}</button> <button
className="btn btn-warning mb-3"
onClick={onPressGenerate}
disabled={isLoading}
>
{btnText}
</button>
</div> </div>
</div> </div>
<div className='questions-container' > <div className="questions-container">
{questions.map(renderQuestions)} {questions.map(renderQuestions)}
</div> </div>
</div>) <hr />
} <ApplicationActions
status={status}
onChangeStatus={onChangeStatus}
onUpdateStatus={onUpdateStatus}
visible={status === "In progress"}
/>
</div>
);
};
export default Interview; export default Interview;
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
MeetingProvider,
useMeeting,
useParticipant,
} from "@videosdk.live/react-sdk";
import ReactPlayer from "react-player";
import { meetingToken } from "../common/apis/meetings";
function ParticipantView({ participantId }: { participantId: string }) {
const micRef = useRef<any>();
const { webcamStream, micStream, webcamOn, micOn, isLocal } =
useParticipant(participantId);
const videoStream = useMemo(() => {
if (webcamOn && webcamStream) {
const mediaStream = new MediaStream();
mediaStream.addTrack(webcamStream.track);
return mediaStream;
}
}, [webcamStream, webcamOn]);
useEffect(() => {
if (micRef.current) {
if (micOn && micStream) {
const mediaStream = new MediaStream();
mediaStream.addTrack(micStream.track);
micRef.current.srcObject = mediaStream;
micRef.current
.play()
.catch((error: any) =>
console.error("videoElem.current.play() failed", error)
);
} else {
micRef.current.srcObject = null;
}
}
}, [micStream, micOn]);
return (
<div>
<audio ref={micRef} autoPlay playsInline muted={isLocal} />
{webcamOn && (
<div className="player-wrapper">
<ReactPlayer
playsinline // very very imp prop
pip={false}
light={false}
controls={false}
muted={true}
playing={true}
url={videoStream}
width="100%"
height="100%"
onError={(err) => {
console.log(err, "participant video error");
}}
className="react-player"
/>
</div>
)}
</div>
);
}
function MeetingView({
onMeetingLeave,
record,
}: {
onMeetingLeave: () => void;
record?: boolean;
}) {
const [joined, setJoined] = useState<string | null>(null);
//Get the method which will be used to join the meeting.
//We will also get the participants list to display all participants
const {
join,
participants,
leave,
toggleMic,
toggleWebcam,
startRecording,
localMicOn,
localWebcamOn,
isRecording,
stopRecording,
} = useMeeting({
//callback for when meeting is joined successfully
onMeetingJoined: () => {
setJoined("JOINED");
},
onMeetingLeft: () => {
if (record) {
stopRecording();
}
onMeetingLeave();
},
});
const joinMeeting = () => {
setJoined("JOINING");
join();
};
const onToggleMic = () => {
toggleMic();
};
const onToggleCamera = () => {
toggleWebcam();
};
const onToggleRecord = () => {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
};
return (
<div className="meeting-container">
{joined && joined === "JOINED" ? (
<>
<div className="action-containers">
<button className="btn btn-danger" onClick={leave}>
Leave
</button>
<button className="meeting-action" onClick={onToggleMic}>
{!localMicOn ? (
<i className="fa-solid fa-microphone-slash"></i>
) : (
<i className="fa-solid fa-microphone"></i>
)}
</button>
<button className="meeting-action" onClick={onToggleCamera}>
{!localWebcamOn ? (
<i className="fa-solid fa-video-slash"></i>
) : (
<i className="fa-solid fa-video"></i>
)}
</button>
{record && (
<button
className="meeting-action record"
onClick={onToggleRecord}
>
{isRecording ? (
<i className="fa-solid fa-stop"></i>
) : (
<i className="fa-solid fa-record-vinyl"></i>
)}
</button>
)}
</div>
{[...participants.keys()].map((participantId) => (
<ParticipantView
participantId={participantId}
key={participantId}
/>
))}
</>
) : joined && joined === "JOINING" ? (
<div className="lobby-actions">
<p>Joining the meeting...</p>
</div>
) : (
<div className="lobby-actions">
<button
type="button"
className="btn btn-primary"
onClick={joinMeeting}
>
Join the meeting
</button>
</div>
)}
</div>
);
}
const Meeting = ({
roomId,
onMeetingLeave,
record = true,
}: {
roomId: string;
onMeetingLeave: () => void;
record?: boolean;
}) => {
if (!meetingToken) {
return <div>Session ended</div>;
}
return (
<MeetingProvider
config={{
meetingId: roomId,
micEnabled: true,
webcamEnabled: true,
name: "Kavi's Org",
}}
token={meetingToken}
>
<MeetingView onMeetingLeave={onMeetingLeave} record={record} />
</MeetingProvider>
);
};
export default Meeting;
...@@ -10,7 +10,7 @@ type OwnProps = { ...@@ -10,7 +10,7 @@ type OwnProps = {
profilePicPrev: string | null; profilePicPrev: string | null;
userId?: string; userId?: string;
onUpdateCandidate: (update: any) => void; onUpdateCandidate: (update: any) => void;
userType:USER_TYPE userType: USER_TYPE;
}; };
const ProfilePicCrop = ({ const ProfilePicCrop = ({
...@@ -18,7 +18,7 @@ const ProfilePicCrop = ({ ...@@ -18,7 +18,7 @@ const ProfilePicCrop = ({
profilePicPrev, profilePicPrev,
userId, userId,
onUpdateCandidate, onUpdateCandidate,
userType userType,
}: OwnProps) => { }: OwnProps) => {
const [progress, setprogress] = useState(0); const [progress, setprogress] = useState(0);
const [crop, setCrop] = useState({ x: 0, y: 0 }); const [crop, setCrop] = useState({ x: 0, y: 0 });
...@@ -36,7 +36,7 @@ const ProfilePicCrop = ({ ...@@ -36,7 +36,7 @@ const ProfilePicCrop = ({
const onPressSelect = async () => { const onPressSelect = async () => {
if (_croppedAreaPixels.current) { if (_croppedAreaPixels.current) {
const { file, _ }: any = await getCroppedImg( const { file }: any = await getCroppedImg(
profilePicPrev, profilePicPrev,
_croppedAreaPixels.current _croppedAreaPixels.current
); );
......
...@@ -44,6 +44,11 @@ const NavBar = () => { ...@@ -44,6 +44,11 @@ const NavBar = () => {
/> />
</a> </a>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li>
<a className="dropdown-item" href="/home">
Home
</a>
</li>
<li> <li>
<a className="dropdown-item" href="/settings"> <a className="dropdown-item" href="/settings">
Settings Settings
......
...@@ -8,9 +8,16 @@ type OwnProps = { ...@@ -8,9 +8,16 @@ type OwnProps = {
profilePic?: string; profilePic?: string;
email: string; email: string;
address?: AddressType; address?: AddressType;
editProfile?: boolean;
}; };
const Profile = ({ name, profilePic, email, address }: OwnProps) => { const Profile = ({
name,
profilePic,
email,
address,
editProfile = true,
}: OwnProps) => {
const addressString = getAddress(address); const addressString = getAddress(address);
return ( return (
<div className="card p-4 profile-card"> <div className="card p-4 profile-card">
...@@ -18,6 +25,7 @@ const Profile = ({ name, profilePic, email, address }: OwnProps) => { ...@@ -18,6 +25,7 @@ const Profile = ({ name, profilePic, email, address }: OwnProps) => {
<h5>{name}</h5> <h5>{name}</h5>
<p>{email}</p> <p>{email}</p>
<p>{addressString}</p> <p>{addressString}</p>
{editProfile && (
<a <a
className="btn btn-primary mt-3" className="btn btn-primary mt-3"
href="/settings" href="/settings"
...@@ -25,6 +33,7 @@ const Profile = ({ name, profilePic, email, address }: OwnProps) => { ...@@ -25,6 +33,7 @@ const Profile = ({ name, profilePic, email, address }: OwnProps) => {
> >
Edit profile Edit profile
</a> </a>
)}
</div> </div>
); );
}; };
......
import React from "react"; import React from "react";
import { ApplicationStatus } from "../common/types";
type OwnProps = { type OwnProps = {
tabs: string[];
selected?: string; selected?: string;
onSelect: (tab: string) => void; onSelect: (tab: string) => void;
selectedStatus?: ApplicationStatus;
}; };
const TabNavBar = ({ tabs, selected, onSelect }: OwnProps) => { type TabType = { tab: string; status: ApplicationStatus; disabled: boolean };
const renderTabs = (tab: string, index: number) => {
const getTabs = (status?: ApplicationStatus): TabType[] => {
if (status === "Pending") {
return [
{ tab: "Candidate", status: "Pending", disabled: false },
{ tab: "Interview", status: "In progress", disabled: true },
{ tab: "Analyse", status: "Analyse", disabled: true },
];
} else if (status === "In progress") {
return [
{ tab: "Candidate", status: "Pending", disabled: false },
{ tab: "Interview", status: "In progress", disabled: false },
{ tab: "Analyse", status: "Analyse", disabled: true },
];
} else {
return [
{ tab: "Candidate", status: "Pending", disabled: false },
{ tab: "Interview", status: "In progress", disabled: false },
{ tab: "Analyse", status: "Analyse", disabled: false },
];
}
};
const TabNavBar = ({ selected, onSelect, selectedStatus }: OwnProps) => {
const renderTabs = ({ tab, disabled }: TabType, index: number) => {
const className = tab === selected ? "nav-link active" : "nav-link"; const className = tab === selected ? "nav-link active" : "nav-link";
const onClick = () => onSelect(tab); const onClick = () => onSelect(tab);
return ( return (
<li className="nav-item" key={index}> <li className="nav-item" key={index}>
<button className={className} aria-current="page" onClick={onClick}> <button
className={className}
aria-current="page"
onClick={onClick}
disabled={disabled}
>
{tab} {tab}
</button> </button>
</li> </li>
); );
}; };
return <ul className="nav nav-tabs mb-4">{tabs.map(renderTabs)}</ul>; return (
<ul className="nav nav-tabs mb-4">
{getTabs(selectedStatus).map(renderTabs)}
</ul>
);
}; };
export default TabNavBar; export default TabNavBar;
import React, { useState, useEffect, ChangeEvent, useMemo } from "react"; import React, { useState, useEffect, ChangeEvent } from "react";
import { useOutletContext, useSearchParams } from "react-router-dom"; import {
useNavigate,
import { ApplicationPayloadType, JobPayloadType } from "../common/types"; useOutletContext,
useSearchParams,
} from "react-router-dom";
import {
ApplicationPayloadType,
ApplicationStatus,
JobPayloadType,
} from "../common/types";
import TabNavBar from "../components/TabNavBar"; import TabNavBar from "../components/TabNavBar";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { updateApplication } from "../common/actions/common"; import { updateApplicationAction } from "../common/actions/application";
import Applicant from "../components/Application/Applicant"; import Applicant from "../components/Application/Applicant";
import Analyse from "../components/Application/Analyse"; import Analyse from "../components/Application/Analyse";
import Interview from "../components/Application/Interview"; import Interview from "../components/Application/Interview";
const tabs = ["Candidate", "Interview", "Analyse"]; //Status map - Pending [Candidate tab] -> In progress [Interview tab] -> Analyse [Analyse tab] -> Accepted | Rejected
const getStatusValues = (status?:string) => {
if (status === "Pending") {
return ["Schedule", "Reject"];
} else if (status === "In progress") {
return ["Accept", "Reject"];
} else if (status === "Rejected") {
return ["Pending", "Schedule"];
} else {
return [];
}
}
const Application = () => { const Application = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [selectedTab, setSelectedTab] = useState("Candidate"); const [selectedTab, setSelectedTab] = useState("Candidate");
const job = useOutletContext<JobPayloadType>(); const job = useOutletContext<JobPayloadType>();
...@@ -32,87 +29,77 @@ const Application = () => { ...@@ -32,87 +29,77 @@ const Application = () => {
const application = (job.applications as ApplicationPayloadType[])?.find( const application = (job.applications as ApplicationPayloadType[])?.find(
(_application) => _application._id === applicationId (_application) => _application._id === applicationId
); );
const [status, setStatus] = useState(application?.status); const [status, setStatus] = useState<ApplicationStatus>("Pending");
const statusValues = getStatusValues(application?.status)
useEffect(() => { useEffect(() => {
if (application) { if (application) {
if (application?.status === "Pending") { if (application?.status === "Pending") {
setSelectedTab("Candidate"); setSelectedTab("Candidate");
setStatus(application.status);
} else if (application?.status === "In progress") { } else if (application?.status === "In progress") {
setSelectedTab("Interview"); setSelectedTab("Interview");
setStatus("Schedule"); } else if (application?.status === "Analyse") {
setSelectedTab("Analyse");
} }
setStatus(application.status);
} }
}, [application]); }, [application]);
if (!application) return null; if (!application) return null;
const onUpdateStatus = () => { const onUpdateStatus = (update?: any) => {
let applicationStatus = status;
if (status === "Schedule") {
applicationStatus = "In progress";
} else if (status === "Reject") {
applicationStatus = "Rejected";
}
dispatch( dispatch(
updateApplication({ updateApplicationAction(
{
applicationId: application._id, applicationId: application._id,
update: { status: applicationStatus }, update: { status, ...update },
candidateId: application.candidate._id || "", },
}) () => {
if (status === "Accepted" || status === "Rejected") {
navigate(`/applications?jobId=${application.job}`);
}
}
)
); );
}; };
const onChangeStatus = (e: ChangeEvent<HTMLSelectElement>) => { const onChangeStatus = (e: ChangeEvent<HTMLSelectElement>) => {
if (e.target.value !== "select") { if (e.target.value !== "select") {
setStatus(e.target.value); setStatus(e.target.value as ApplicationStatus);
} }
}; };
const renderOptions = (option: string, index: number) => (
<option key={index} value={option}>
{option}
</option>
);
return ( return (
<div className="card p-4"> <div className="card p-4">
<TabNavBar tabs={tabs} selected={selectedTab} onSelect={setSelectedTab} /> <TabNavBar
{selectedTab === "Candidate" && <Applicant application={application} />} selected={selectedTab}
{selectedTab === "Interview" && <Interview application={application} />} onSelect={setSelectedTab}
{selectedTab === "Analyse" && <Analyse application={application} />} selectedStatus={application.status}
/>
<hr /> {selectedTab === "Candidate" && (
<Applicant
<div className="row "> application={application}
<label className="col-sm-2 col-form-label">Status</label> status={application.status}
<div className="col-sm-6 "> onChangeStatus={onChangeStatus}
<div className="input-group "> onUpdateStatus={onUpdateStatus}
<select selectedStatus={status}
className="form-select" />
id="inputGroupSelect04" )}
aria-label="Example select with button addon" {selectedTab === "Interview" && (
value={status} <Interview
onChange={onChangeStatus} status={application.status}
> onChangeStatus={onChangeStatus}
<option value="select">Select</option> onUpdateStatus={onUpdateStatus}
{statusValues.map(renderOptions)} application={application}
</select> />
<button )}
className="btn btn-primary " {selectedTab === "Analyse" && (
type="button" <Analyse
onClick={onUpdateStatus} application={application}
> status={application.status}
Update & Next onChangeStatus={onChangeStatus}
</button> onUpdateStatus={onUpdateStatus}
</div> />
</div> )}
</div>
</div> </div>
); );
}; };
......
import React, { useState, useMemo } from "react"; import React, {
useState,
useMemo,
useEffect,
useRef,
ChangeEvent,
} from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { applyForJob } from "../common/actions/user"; import { applyForJob } from "../common/actions/user";
import { getProfile, getStatusColor, getUserId } from "../common/lib/util"; import { getProfile, getStatusColor, getUserId } from "../common/lib/util";
...@@ -15,15 +21,16 @@ import Layout from "../Layouts/Layout"; ...@@ -15,15 +21,16 @@ import Layout from "../Layouts/Layout";
import CreateUpdateJob from "../components/Modals/CreateUpdateJob"; import CreateUpdateJob from "../components/Modals/CreateUpdateJob";
import Profile from "../components/Profile"; import Profile from "../components/Profile";
import { setDialogBox } from "../common/actions/common"; import { setDialogBox } from "../common/actions/common";
import CommonAPI from "../common/apis/common";
import { useNavigate } from "react-router-dom";
const CandidateHome = () => { const CandidateHome = ({ jobs }: { jobs: JobPayloadType[] }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate();
const candidate = useSelector(getProfile) as CandidateType; const candidate = useSelector(getProfile) as CandidateType;
const [selectedJob, setSelectedJob] = useState<number>(0); const [selectedJob, setSelectedJob] = useState<number>(0);
const userId = useSelector(getUserId); const userId = useSelector(getUserId);
const jobs = useSelector(
(state: Reducers) => state.common.jobs
) as JobPayloadType[];
const job = jobs.length > 0 ? jobs[selectedJob] : null; const job = jobs.length > 0 ? jobs[selectedJob] : null;
const renderSkills = (skill: string, index: number) => ( const renderSkills = (skill: string, index: number) => (
...@@ -36,15 +43,19 @@ const CandidateHome = () => { ...@@ -36,15 +43,19 @@ const CandidateHome = () => {
const foundCandidate = jobs[selectedJob]?.applications?.findIndex( const foundCandidate = jobs[selectedJob]?.applications?.findIndex(
(_item) => _item.candidate === userId (_item) => _item.candidate === userId
); );
if (foundCandidate !== undefined && foundCandidate >= 0) { if (foundCandidate !== undefined && foundCandidate >= 0) {
const status = jobs[selectedJob]?.applications?.[foundCandidate].status; const status = jobs[selectedJob]?.applications?.[foundCandidate].status;
const interview =
jobs[selectedJob]?.applications?.[foundCandidate].interview;
return { return {
applied: true, applied: true,
status, status,
color: getStatusColor(status), color: getStatusColor(status),
interview,
}; };
} else { } else {
return { applied: false, status: "" }; return { applied: false, status: "", interview: null };
} }
}, [jobs, selectedJob, userId]); }, [jobs, selectedJob, userId]);
...@@ -58,6 +69,12 @@ const CandidateHome = () => { ...@@ -58,6 +69,12 @@ const CandidateHome = () => {
} }
}; };
const onClickJoin = () => {
navigate(
`/meeting?roomId=${alreadyApplied.interview?.link}&jobId=${jobs[selectedJob]._id}`
);
};
return ( return (
<div className="row"> <div className="row">
<div className="col-3 "> <div className="col-3 ">
...@@ -95,12 +112,34 @@ const CandidateHome = () => { ...@@ -95,12 +112,34 @@ const CandidateHome = () => {
{job.secondarySkills.map(renderSkills)} {job.secondarySkills.map(renderSkills)}
</div> </div>
{alreadyApplied.applied ? ( {alreadyApplied.applied ? (
<>
<p className="h5 mt-2"> <p className="h5 mt-2">
Status :{" "} Status :{" "}
<label className={alreadyApplied.color}> <label className={alreadyApplied.color}>
{alreadyApplied.status} {alreadyApplied.status}
</label> </label>
</p> </p>
{alreadyApplied?.interview?.date && (
<>
<p className="h6">
Interview is scheduled on :{" "}
<label>
{`${alreadyApplied.interview.date} at ${alreadyApplied.interview.time}`}
</label>
</p>
{alreadyApplied.interview?.link &&
alreadyApplied.interview?.link !== "ended" ? (
<button
className="btn btn-success mt-3"
onClick={onClickJoin}
>
Join now
</button>
) : null}
</>
)}
</>
) : ( ) : (
<button className="btn btn-success mt-3" onClick={onApply}> <button className="btn btn-success mt-3" onClick={onApply}>
Apply Apply
...@@ -174,13 +213,54 @@ const OrganizationHome = () => { ...@@ -174,13 +213,54 @@ const OrganizationHome = () => {
}; };
const Home = () => { const Home = () => {
const [selectedJobs, setSelectedJobs] = useState<JobPayloadType[]>([]);
const userType = useSelector((state: Reducers) => state.auth.userType); const userType = useSelector((state: Reducers) => state.auth.userType);
const _searchKey = useRef<string>("");
const jobs = useSelector(
(state: Reducers) => state.common.jobs
) as JobPayloadType[];
useEffect(() => {
setSelectedJobs(jobs);
}, [jobs]);
const onChangeSearchKey = (e: ChangeEvent<HTMLInputElement>) => {
_searchKey.current = e.target.value;
};
const onClickSearch = () => {
if (_searchKey.current !== "") {
CommonAPI.searchJobs(_searchKey.current).then(({ data }: any) => {
setSelectedJobs(data);
});
}
};
const renderSuffix =
userType === USER_TYPE.CANDIDATE ? (
<div className="input-group mb-3">
<input
type="text"
className="form-control"
placeholder="Search job"
onChange={onChangeSearchKey}
/>
<button
className="btn btn-secondary"
type="button"
onClick={onClickSearch}
>
Search
</button>
</div>
) : undefined;
return ( return (
<Layout title="Vacancies and Jobs"> <Layout title="Vacancies and Jobs" suffix={renderSuffix}>
{userType === USER_TYPE.ORGANIZATION ? ( {userType === USER_TYPE.ORGANIZATION ? (
<OrganizationHome /> <OrganizationHome />
) : ( ) : (
<CandidateHome /> <CandidateHome jobs={selectedJobs} />
)} )}
</Layout> </Layout>
); );
......
import React, { useState, useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import Meeting from "../components/Meeting";
import { getToken } from "../common/apis/meetings";
const MeetingRoom = () => {
const navigate = useNavigate();
const [isReady, setIsReady] = useState<boolean>(false);
const [searchParams] = useSearchParams();
const roomId = searchParams.get("roomId");
useEffect(() => {
if (roomId && roomId !== "ended") {
getReady();
}
}, [roomId]);
const getReady = async () => {
try {
await getToken();
setIsReady(true);
} catch (error) {}
};
const onMeetingLeave = () => {
navigate("/home");
};
if (!roomId) {
return null;
} else if (roomId === "ended") {
return (
<div className="card p-4">
<div className="interview-room">
<p>This session has ended!</p>
</div>
</div>
);
} else {
if (isReady) {
return (
<div className="card p-4">
<div className="interview-room">
<Meeting
roomId={roomId}
onMeetingLeave={onMeetingLeave}
record={false}
/>
</div>
</div>
);
} else {
return (
<div className="card p-4">
<div className="interview-room">
<p>Loading...</p>
</div>
</div>
);
}
}
};
export default MeetingRoom;
import React, { useState, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
CandidateType,
JobPayloadType,
OrganizationType,
Reducers,
} from "../common/types";
import Jobs from "../components/Jobs";
import Layout from "../Layouts/Layout";
import Profile from "../components/Profile";
import { useSearchParams } from "react-router-dom";
import { getProfile, getStatusColor, getUserId } from "../common/lib/util";
import { setDialogBox } from "../common/actions/common";
import { applyForJob } from "../common/actions/user";
const OrganizationHome = ({
organization,
jobs,
}: {
organization: OrganizationType;
jobs: JobPayloadType[];
}) => {
const dispatch = useDispatch();
const candidate = useSelector(getProfile) as CandidateType;
const [selectedJob, setSelectedJob] = useState<number>(0);
const job = jobs.length > 0 ? jobs[selectedJob] : null;
const userId = useSelector(getUserId);
const renderSkills = (skill: string, index: number) => (
<span className="skill" 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 = () => {
if (!candidate?.resume || !candidate?.selfIntro) {
dispatch(
setDialogBox("Your resume and self introduction video is required!")
);
} else {
dispatch(applyForJob(jobs[selectedJob]._id || ""));
}
};
return (
<div className="row">
<div className="col-3 ">
<Profile
name={organization.name}
profilePic={organization.profilePicture}
email={organization.contacts.email}
address={organization.contacts.address}
editProfile={false}
/>
</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>
{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 Organization = () => {
const [selectedOrg, setSelectedOrg] = useState<OrganizationType>();
const [selectedJobs, setSelectedJobs] = useState<JobPayloadType[]>([]);
const [searchParams] = useSearchParams();
const jobs = useSelector((state: Reducers) => state.common.jobs) as any[];
useEffect(() => {
const orgId = searchParams.get("id");
const foundjobs = jobs.filter((_job) => _job.organization[0]._id === orgId);
setSelectedOrg(foundjobs[0].organization[0]);
setSelectedJobs(foundjobs);
}, [jobs, searchParams]);
return (
<Layout title={selectedOrg?.name || "Searching.."}>
{selectedOrg && (
<OrganizationHome organization={selectedOrg} jobs={selectedJobs} />
)}
</Layout>
);
};
export default Organization;
...@@ -2798,6 +2798,19 @@ ...@@ -2798,6 +2798,19 @@
"@typescript-eslint/types" "5.46.1" "@typescript-eslint/types" "5.46.1"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@videosdk.live/js-sdk@0.0.67":
version "0.0.67"
resolved "https://registry.yarnpkg.com/@videosdk.live/js-sdk/-/js-sdk-0.0.67.tgz#7e6f19986429cc83f0267d97413698a4456ab0c5"
integrity sha512-figRT72vgF9A+Q+CpTy8gkecKmHWamAuQkdCS+pBCsWV2yZbXM289iUGr/SP2YaTVQWMSHm76o8k9AC78nAilw==
"@videosdk.live/react-sdk@^0.1.73":
version "0.1.73"
resolved "https://registry.yarnpkg.com/@videosdk.live/react-sdk/-/react-sdk-0.1.73.tgz#7b8c69193371bbb1994e422af5618cd6bccf0d8f"
integrity sha512-GWVj0vC9J3XffeJHZb2Kfkovv3fPT7zKrxd2J6o8UV1/Bc+371b09MTRDvp6eXfiJ3qSlZ04wCIj6yRmvlrVNw==
dependencies:
"@videosdk.live/js-sdk" "0.0.67"
events "^3.3.0"
"@webassemblyjs/ast@1.11.1": "@webassemblyjs/ast@1.11.1":
version "1.11.1" version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
...@@ -4907,7 +4920,7 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.1: ...@@ -4907,7 +4920,7 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.1:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.2.0: events@^3.2.0, events@^3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
......
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