video conferencing online

parent 40aaf3c7
......@@ -26,6 +26,14 @@
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous"
></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>
</head>
<body>
......
......@@ -14,6 +14,8 @@ import "./components.scss";
import ProtectedRoutes from "./components/ProtectedRoute";
import ApplicationsLayout from "./Layouts/ApplicationLayout";
import Applicant from "./views/Application";
import MeetingRoom from "./views/MeetingRoom";
import Organization from "./views/Organization.view";
function App() {
return (
......@@ -23,9 +25,11 @@ function App() {
<Route element={<ProtectedRoutes />}>
<Route path="/home" element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route path="/organizations" element={<Organization />} />
<Route element={<ApplicationsLayout />}>
<Route path="/applications" element={<Applications />} />
<Route path="/applicant" element={<Applicant />} />
<Route path="/meeting" element={<MeetingRoom />} />
</Route>
</Route>
</Routes>
......
......@@ -5,14 +5,18 @@ import NavBar from "../components/NavBar";
type OwnProps = {
title: string;
children?: JSX.Element | JSX.Element[];
suffix?: JSX.Element;
};
const Layout = ({ title, children }: OwnProps) => {
const Layout = ({ title, children, suffix }: OwnProps) => {
return (
<>
<NavBar />
<div className="container pb-5">
<div className="layout-title-container">
<h4 className="mb-3">{title}</h4>
{suffix}
</div>
{children}
</div>
</>
......
......@@ -44,3 +44,14 @@ p {
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) => ({
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) => ({
type: ACTIONS.SET_DIALOG,
payload,
......
......@@ -11,7 +11,6 @@ export enum ACTIONS {
UPDATE_ORG = "UPDATE_ORG",
SET_APP_STATE = "SET_APP_STATE",
SET_JOBS = "SET_JOBS",
......@@ -19,5 +18,9 @@ export enum ACTIONS {
CREATE_JOB = "CREATE_JOB",
UPDATE_JOB = "UPDATE_JOB",
UPDATE_APPLICATION = "UPDATE_APPLICATION",
UPDATE_APPLICATION_ACTION = "UPDATE_APPLICATION_ACTION",
SET_DIALOG = "SET_DIALOG",
CREATE_MEETING = "CREATE_MEETING",
END_MEETING = "END_MEETING",
}
......@@ -18,12 +18,20 @@ export default class CommonAPI {
static updateApplication = (payload: {
update: any;
applicationId: string;
candidateId: string;
candidateId?: string;
}) => 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: {
startTime: number;
endTime: number;
applicationId: string;
}) => 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 { 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 = {
standard: {
sd: 1.5,
......@@ -55,4 +55,5 @@ export const emotionsData = [
},
];
export const OPEN_API_KEY = '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'
\ No newline at end of file
export const OPEN_API_KEY =
"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";
......@@ -21,7 +21,8 @@ export const request = (
method: AxiosRequestConfig["method"],
requestData?: AxiosRequestConfig["data"] | AxiosRequestConfig["params"],
isGuest?: boolean,
contentType?: string
contentType?: string,
header?: AxiosRequestConfig["headers"]
) =>
new Promise(async (resolve, reject) => {
const endpoint = url?.replace?.("<BASE_URL>", BASE_URL);
......@@ -33,6 +34,7 @@ export const request = (
const headers = {
auth_token,
"Content-Type": contentType || "application/json",
...(header||{}),
};
logger("REQUEST: ", method, endpoint, headers, requestData);
......
import {
AddressType,
ApplicationStatus,
CandidateType,
OrganizationType,
Reducers,
......@@ -52,7 +53,7 @@ export const getUserId = (state: Reducers): string => {
return profile._id as string;
};
export const getStatusColor = (status?: string) => {
export const getStatusColor = (status?: ApplicationStatus) => {
const color =
status === "Accepted"
? "text-bg-success"
......@@ -60,6 +61,8 @@ export const getStatusColor = (status?: string) => {
? "text-bg-warning"
: status === "Rejected"
? "text-bg-danger"
: status === "Analyse"
? "text-bg-info"
: "text-bg-primary";
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";
import CommonAPI from "../apis/common";
import { APP_STATE, JobType, Reducers } from "../types";
function* getJobs({
export function* getJobs({
success,
}: {
type: typeof ACTIONS.GET_JOBS;
......@@ -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() {
yield takeLeading(ACTIONS.GET_JOBS, getJobs);
yield takeLeading(ACTIONS.CREATE_JOB, createJob);
yield takeLeading(ACTIONS.UPDATE_JOB, updateJob);
yield takeLeading(ACTIONS.UPDATE_APPLICATION, updateApplication);
}
......@@ -3,7 +3,8 @@ import { all } from "redux-saga/effects";
import Auth from "./auth";
import User from "./user";
import Common from "./common";
import Application from "./application";
export default function* rootSaga() {
yield all([Auth(), User(), Common()]);
yield all([Auth(), User(), Common(), Application()]);
}
......@@ -3,7 +3,14 @@ import { ACTIONS } from "../actions";
import CommonAPI from "../apis/common";
import UserAPI from "../apis/user";
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({
payload,
......@@ -125,11 +132,12 @@ function* applyForJob({
}
}
yield call(getJobs, { type: ACTIONS.GET_JOBS });
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
window.location.reload();
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
......@@ -138,8 +146,6 @@ function* applyForJob({
}
}
export default function* authSaga() {
yield takeLeading(ACTIONS.UPDATE_CANDIDATE, updateCandidate);
yield takeLeading(ACTIONS.UPDATE_ORG, updateOrganization);
......
......@@ -144,10 +144,17 @@ export type JobType = {
organization: string;
};
export type ApplicationStatus =
| "Pending"
| "In progress"
| "Analyse"
| "Accepted"
| "Rejected";
export type ApplicationType = {
candidate: string;
job: string;
status: "Pending" | "Accepted" | "In progress" | "Rejected";
status: ApplicationStatus;
interview?: {
date: string;
time: string;
......@@ -222,7 +229,7 @@ export type ApplicationPayloadType = {
_id: string;
candidate: CandidateType;
job: string;
status: string;
status: ApplicationStatus;
interview?: {
date: string;
time: string;
......
......@@ -149,6 +149,7 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 10px;
}
.skill {
background-color: #058700;
......@@ -183,6 +184,7 @@
h5 {
margin: 0;
}
margin-bottom: 10px;
label {
a {
......@@ -218,19 +220,74 @@
}
}
.upload-interview-video {
.interview-room {
background-color: #f5f5f5;
height: 400px;
height: 450px;
display: flex;
align-items: center;
justify-content: center;
input {
display: none;
}
div {
align-items: center;
display: flex;
flex-direction: column;
}
}
.questions-container{
.questions-container {
background-color: #ffefab80;
min-height: 400px;
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";
import CommonAPI from "../../common/apis/common";
import {
ApplicationPayloadType,
ApplicationStatus,
EmotionsPayloadType,
EmotionsType,
} from "../../common/types";
import Progress from "../Progress";
import { fileStorage } from "../../common/config";
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 ApplicationActions from "./ApplicationActions";
import MeetingsAPI from "../../common/apis/meetings";
type OwnProps = {
application: ApplicationPayloadType;
status?: ApplicationStatus;
onChangeStatus: (e: ChangeEvent<HTMLSelectElement>) => void;
onUpdateStatus: () => void;
};
type Analysis = {
......@@ -32,10 +39,15 @@ type Analysis = {
eyeBlinks?: number;
};
const Analyse = ({ application }: OwnProps) => {
const Analyse = ({
application,
status,
onChangeStatus,
onUpdateStatus,
}: OwnProps) => {
const dispatch = useDispatch();
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 [progress, setProgress] = useState<number>(0);
const [endTime, setEndTime] = useState<number>(120);
......@@ -48,7 +60,22 @@ const Analyse = ({ application }: OwnProps) => {
useEffect(() => {
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 = () => {
setIsFacialAnalyzing(true);
......@@ -147,13 +174,21 @@ const Analyse = ({ application }: OwnProps) => {
},
(err) => {},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
getDownloadURL(uploadTask.snapshot.ref).then(updateUserApplication);
}
);
}
};
const updateUserApplication = (url: string) => {
dispatch(
updateApplication(
{
applicationId: application._id,
candidateId: application.candidate._id || "",
update: { interview: { videoRef: url } },
update: {
interview: { ...application.interview, videoRef: url },
},
},
() => {
setProgress(0);
......@@ -161,11 +196,8 @@ const Analyse = ({ application }: OwnProps) => {
}
)
);
});
}
);
}
};
const onClickUpload = () => {
_hiddenFileInput?.current?.click();
};
......@@ -176,30 +208,6 @@ const Analyse = ({ application }: OwnProps) => {
</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) {
if (application.interview.voiceVerification === "Pending") {
......@@ -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 (
<div>
<ReactPlayer
......@@ -329,7 +361,7 @@ const Analyse = ({ application }: OwnProps) => {
<strong>Facial expressions</strong>
</label>
{emotions.length === 0 ? (
<div className="upload-interview-video">
<div className="interview-room">
<button
type="button"
className="btn btn-secondary"
......@@ -415,6 +447,13 @@ const Analyse = ({ application }: OwnProps) => {
</AreaChart>
</ResponsiveContainer>
)}
<hr />
<ApplicationActions
status={status}
onChangeStatus={onChangeStatus}
onUpdateStatus={onUpdateStatus}
visible={status === "Analyse"}
/>
</div>
);
};
......
import React from "react";
import React, { ChangeEvent, useState } from "react";
import ReactPlayer from "react-player";
import { getAddress } from "../../common/lib/util";
import { ApplicationPayloadType } from "../../common/types";
import { ApplicationPayloadType, ApplicationStatus } from "../../common/types";
import Avatar from "../Avatar";
import ApplicationActions from "./ApplicationActions";
type OwnProps = {
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 renderMatches = (match: string, index: number) => (
<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 (
<>
<div className="mb-3 row">
......@@ -111,6 +154,40 @@ const Applicant = ({ application }: OwnProps) => {
<strong>Secondary match</strong>
</label>
<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 {CreateCompletionResponseChoicesInner } from "openai";
import { ApplicationPayloadType } from '../../common/types';
import OpenAPI from '../../common/lib/openApi';
import React, { useState, useRef, ChangeEvent } from "react";
import { CreateCompletionResponseChoicesInner } from "openai";
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 = {
status?: ApplicationStatus;
onChangeStatus: (e: ChangeEvent<HTMLSelectElement>) => void;
onUpdateStatus: () => void;
application: ApplicationPayloadType;
}
};
const Interview = ({application}:OwnProps)=>{
const [isLoading, setIsLoading] = useState<boolean>(false)
const [questions, setQuestions] = useState<CreateCompletionResponseChoicesInner[]>([])
const subject = useRef<string>('')
const level = useRef<string>('easy')
const Interview = ({
status,
onChangeStatus,
onUpdateStatus,
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=()=>{
if(subject.current.length>0){
setQuestions([])
setIsLoading(true)
OpenAPI.getInterViewQuestions(subject.current, level.current).then(res=>{
setQuestions(res.data.choices)
setIsLoading(false)
})
const onPressGenerate = () => {
if (subject.current.length > 0) {
setQuestions([]);
setIsLoading(true);
OpenAPI.getInterViewQuestions(subject.current, level.current).then(
(res) => {
setQuestions(res.data.choices);
setIsLoading(false);
}
);
}
};
const onChange=(e:ChangeEvent<HTMLInputElement>)=>{
subject.current = e.target.value
}
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
subject.current = e.target.value;
};
const onSelectLevel = (e: ChangeEvent<HTMLSelectElement>) => {
level.current = e.target.value
}
level.current = e.target.value;
};
const renderQuestions=(question:CreateCompletionResponseChoicesInner, index:number)=>{
return (<p style={{whiteSpace:'pre-line'}}>{question.text}</p>)
}
const renderQuestions = (
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 (
<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>
<div className="row g-3" >
<label className="col-3 col-form-label" >Subject</label>
<div className="row g-3">
<label className="col-3 col-form-label">Subject</label>
<div className="col-6">
<div className="row g-3" >
<div className="col-8"> <input type="text" className="form-control" onChange={onChange} /></div>
<div className="row g-3">
<div className="col-8">
<input type="text" className="form-control" onChange={onChange} />
</div>
<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="intermediate">Medium</option>
<option value="hard">Hard</option>
......@@ -57,13 +128,27 @@ const Interview = ({application}:OwnProps)=>{
</div>
</div>
<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 className='questions-container' >
<div className="questions-container">
{questions.map(renderQuestions)}
</div>
</div>)
}
<hr />
<ApplicationActions
status={status}
onChangeStatus={onChangeStatus}
onUpdateStatus={onUpdateStatus}
visible={status === "In progress"}
/>
</div>
);
};
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 = {
profilePicPrev: string | null;
userId?: string;
onUpdateCandidate: (update: any) => void;
userType:USER_TYPE
userType: USER_TYPE;
};
const ProfilePicCrop = ({
......@@ -18,7 +18,7 @@ const ProfilePicCrop = ({
profilePicPrev,
userId,
onUpdateCandidate,
userType
userType,
}: OwnProps) => {
const [progress, setprogress] = useState(0);
const [crop, setCrop] = useState({ x: 0, y: 0 });
......@@ -36,7 +36,7 @@ const ProfilePicCrop = ({
const onPressSelect = async () => {
if (_croppedAreaPixels.current) {
const { file, _ }: any = await getCroppedImg(
const { file }: any = await getCroppedImg(
profilePicPrev,
_croppedAreaPixels.current
);
......
......@@ -44,6 +44,11 @@ const NavBar = () => {
/>
</a>
<ul className="dropdown-menu">
<li>
<a className="dropdown-item" href="/home">
Home
</a>
</li>
<li>
<a className="dropdown-item" href="/settings">
Settings
......
......@@ -8,9 +8,16 @@ type OwnProps = {
profilePic?: string;
email: string;
address?: AddressType;
editProfile?: boolean;
};
const Profile = ({ name, profilePic, email, address }: OwnProps) => {
const Profile = ({
name,
profilePic,
email,
address,
editProfile = true,
}: OwnProps) => {
const addressString = getAddress(address);
return (
<div className="card p-4 profile-card">
......@@ -18,6 +25,7 @@ const Profile = ({ name, profilePic, email, address }: OwnProps) => {
<h5>{name}</h5>
<p>{email}</p>
<p>{addressString}</p>
{editProfile && (
<a
className="btn btn-primary mt-3"
href="/settings"
......@@ -25,6 +33,7 @@ const Profile = ({ name, profilePic, email, address }: OwnProps) => {
>
Edit profile
</a>
)}
</div>
);
};
......
import React from "react";
import { ApplicationStatus } from "../common/types";
type OwnProps = {
tabs: string[];
selected?: string;
onSelect: (tab: string) => void;
selectedStatus?: ApplicationStatus;
};
const TabNavBar = ({ tabs, selected, onSelect }: OwnProps) => {
const renderTabs = (tab: string, index: number) => {
type TabType = { tab: string; status: ApplicationStatus; disabled: boolean };
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 onClick = () => onSelect(tab);
return (
<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}
</button>
</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;
import React, { useState, useEffect, ChangeEvent, useMemo } from "react";
import { useOutletContext, useSearchParams } from "react-router-dom";
import { ApplicationPayloadType, JobPayloadType } from "../common/types";
import React, { useState, useEffect, ChangeEvent } from "react";
import {
useNavigate,
useOutletContext,
useSearchParams,
} from "react-router-dom";
import {
ApplicationPayloadType,
ApplicationStatus,
JobPayloadType,
} from "../common/types";
import TabNavBar from "../components/TabNavBar";
import { useDispatch } from "react-redux";
import { updateApplication } from "../common/actions/common";
import { updateApplicationAction } from "../common/actions/application";
import Applicant from "../components/Application/Applicant";
import Analyse from "../components/Application/Analyse";
import Interview from "../components/Application/Interview";
const tabs = ["Candidate", "Interview", "Analyse"];
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 [];
}
}
//Status map - Pending [Candidate tab] -> In progress [Interview tab] -> Analyse [Analyse tab] -> Accepted | Rejected
const Application = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [selectedTab, setSelectedTab] = useState("Candidate");
const job = useOutletContext<JobPayloadType>();
......@@ -32,87 +29,77 @@ const Application = () => {
const application = (job.applications as ApplicationPayloadType[])?.find(
(_application) => _application._id === applicationId
);
const [status, setStatus] = useState(application?.status);
const statusValues = getStatusValues(application?.status)
const [status, setStatus] = useState<ApplicationStatus>("Pending");
useEffect(() => {
if (application) {
if (application?.status === "Pending") {
setSelectedTab("Candidate");
setStatus(application.status);
} else if (application?.status === "In progress") {
setSelectedTab("Interview");
setStatus("Schedule");
} else if (application?.status === "Analyse") {
setSelectedTab("Analyse");
}
setStatus(application.status);
}
}, [application]);
if (!application) return null;
const onUpdateStatus = () => {
let applicationStatus = status;
if (status === "Schedule") {
applicationStatus = "In progress";
} else if (status === "Reject") {
applicationStatus = "Rejected";
}
const onUpdateStatus = (update?: any) => {
dispatch(
updateApplication({
updateApplicationAction(
{
applicationId: application._id,
update: { status: applicationStatus },
candidateId: application.candidate._id || "",
})
update: { status, ...update },
},
() => {
if (status === "Accepted" || status === "Rejected") {
navigate(`/applications?jobId=${application.job}`);
}
}
)
);
};
const onChangeStatus = (e: ChangeEvent<HTMLSelectElement>) => {
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 (
<div className="card p-4">
<TabNavBar tabs={tabs} selected={selectedTab} onSelect={setSelectedTab} />
{selectedTab === "Candidate" && <Applicant application={application} />}
{selectedTab === "Interview" && <Interview application={application} />}
{selectedTab === "Analyse" && <Analyse application={application} />}
<hr />
<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}
>
<option value="select">Select</option>
{statusValues.map(renderOptions)}
</select>
<button
className="btn btn-primary "
type="button"
onClick={onUpdateStatus}
>
Update & Next
</button>
</div>
</div>
</div>
<TabNavBar
selected={selectedTab}
onSelect={setSelectedTab}
selectedStatus={application.status}
/>
{selectedTab === "Candidate" && (
<Applicant
application={application}
status={application.status}
onChangeStatus={onChangeStatus}
onUpdateStatus={onUpdateStatus}
selectedStatus={status}
/>
)}
{selectedTab === "Interview" && (
<Interview
status={application.status}
onChangeStatus={onChangeStatus}
onUpdateStatus={onUpdateStatus}
application={application}
/>
)}
{selectedTab === "Analyse" && (
<Analyse
application={application}
status={application.status}
onChangeStatus={onChangeStatus}
onUpdateStatus={onUpdateStatus}
/>
)}
</div>
);
};
......
import React, { useState, useMemo } from "react";
import React, {
useState,
useMemo,
useEffect,
useRef,
ChangeEvent,
} from "react";
import { useDispatch, useSelector } from "react-redux";
import { applyForJob } from "../common/actions/user";
import { getProfile, getStatusColor, getUserId } from "../common/lib/util";
......@@ -15,15 +21,16 @@ import Layout from "../Layouts/Layout";
import CreateUpdateJob from "../components/Modals/CreateUpdateJob";
import Profile from "../components/Profile";
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 navigate = useNavigate();
const candidate = useSelector(getProfile) as CandidateType;
const [selectedJob, setSelectedJob] = useState<number>(0);
const userId = useSelector(getUserId);
const jobs = useSelector(
(state: Reducers) => state.common.jobs
) as JobPayloadType[];
const job = jobs.length > 0 ? jobs[selectedJob] : null;
const renderSkills = (skill: string, index: number) => (
......@@ -36,15 +43,19 @@ const CandidateHome = () => {
const foundCandidate = jobs[selectedJob]?.applications?.findIndex(
(_item) => _item.candidate === userId
);
if (foundCandidate !== undefined && foundCandidate >= 0) {
const status = jobs[selectedJob]?.applications?.[foundCandidate].status;
const interview =
jobs[selectedJob]?.applications?.[foundCandidate].interview;
return {
applied: true,
status,
color: getStatusColor(status),
interview,
};
} else {
return { applied: false, status: "" };
return { applied: false, status: "", interview: null };
}
}, [jobs, selectedJob, userId]);
......@@ -58,6 +69,12 @@ const CandidateHome = () => {
}
};
const onClickJoin = () => {
navigate(
`/meeting?roomId=${alreadyApplied.interview?.link}&jobId=${jobs[selectedJob]._id}`
);
};
return (
<div className="row">
<div className="col-3 ">
......@@ -95,12 +112,34 @@ const CandidateHome = () => {
{job.secondarySkills.map(renderSkills)}
</div>
{alreadyApplied.applied ? (
<>
<p className="h5 mt-2">
Status :{" "}
<label className={alreadyApplied.color}>
{alreadyApplied.status}
</label>
</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}>
Apply
......@@ -174,13 +213,54 @@ const OrganizationHome = () => {
};
const Home = () => {
const [selectedJobs, setSelectedJobs] = useState<JobPayloadType[]>([]);
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 (
<Layout title="Vacancies and Jobs">
<Layout title="Vacancies and Jobs" suffix={renderSuffix}>
{userType === USER_TYPE.ORGANIZATION ? (
<OrganizationHome />
) : (
<CandidateHome />
<CandidateHome jobs={selectedJobs} />
)}
</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 @@
"@typescript-eslint/types" "5.46.1"
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":
version "1.11.1"
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:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.2.0:
events@^3.2.0, events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
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