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

Merge branch 'it19243122' into 'master'

Organization profile update

See merge request !6
parents 5eda1d85 ab94d701
...@@ -5,6 +5,7 @@ import Landing from "./views/Landing.view"; ...@@ -5,6 +5,7 @@ import Landing from "./views/Landing.view";
import Home from "./views/Home.view"; import Home from "./views/Home.view";
import Settings from "./views/Settings.view"; import Settings from "./views/Settings.view";
import Applications from "./views/Applications.view"; import Applications from "./views/Applications.view";
import DialogBox from "./components/Modals/DialogBox";
import AppState from "./components/AppState"; import AppState from "./components/AppState";
...@@ -29,6 +30,7 @@ function App() { ...@@ -29,6 +30,7 @@ function App() {
</Route> </Route>
</Routes> </Routes>
<AppState /> <AppState />
<DialogBox />
</Router> </Router>
); );
} }
......
...@@ -30,3 +30,17 @@ p { ...@@ -30,3 +30,17 @@ p {
} }
} }
} }
.hidden-input {
display: none;
}
.profile-card {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
h5 {
margin-top: 10px;
}
}
...@@ -30,3 +30,8 @@ export const updateApplication = ( ...@@ -30,3 +30,8 @@ export const updateApplication = (
payload, payload,
success, success,
}); });
export const setDialogBox = (payload: string | null) => ({
type: ACTIONS.SET_DIALOG,
payload,
});
...@@ -9,6 +9,9 @@ export enum ACTIONS { ...@@ -9,6 +9,9 @@ export enum ACTIONS {
UPDATE_CANDIDATE = "UPDATE_CANDIDATE", UPDATE_CANDIDATE = "UPDATE_CANDIDATE",
APPLY_FOR_JOB = "APPLY_FOR_JOB", APPLY_FOR_JOB = "APPLY_FOR_JOB",
UPDATE_ORG = "UPDATE_ORG",
SET_APP_STATE = "SET_APP_STATE", SET_APP_STATE = "SET_APP_STATE",
SET_JOBS = "SET_JOBS", SET_JOBS = "SET_JOBS",
...@@ -16,4 +19,5 @@ export enum ACTIONS { ...@@ -16,4 +19,5 @@ 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",
SET_DIALOG = "SET_DIALOG",
} }
import { ACTIONS } from "."; import { ACTIONS } from ".";
import { CandidateType } from "../types"; import { CandidateType, OrganizationType } from "../types";
export const updateCandidate = (payload: Partial<CandidateType>) => ({ export const updateCandidate = (
payload: Partial<CandidateType>,
success?: (candidate: CandidateType) => void
) => ({
type: ACTIONS.UPDATE_CANDIDATE, type: ACTIONS.UPDATE_CANDIDATE,
payload, payload,
success,
});
export const updateOrganization = (
payload: Partial<OrganizationType>,
success?: (organization: OrganizationType) => void
) => ({
type: ACTIONS.UPDATE_ORG,
payload,
success,
}); });
export const applyForJob = (payload: string) => ({ export const applyForJob = (payload: string) => ({
type: ACTIONS.APPLY_FOR_JOB, type: ACTIONS.APPLY_FOR_JOB,
payload, payload,
}); });
\ No newline at end of file
import { request } from "../lib/api"; import { request } from "../lib/api";
import { CandidateType } from "../types"; import { CandidateType, OrganizationType } from "../types";
export default class UserAPI { export default class UserAPI {
static updateCandidate = (payload: Partial<CandidateType>) => static updateCandidate = (payload: Partial<CandidateType>) =>
request("<BASE_URL>/user/candidate", "POST", payload); request("<BASE_URL>/user/candidate", "POST", payload);
static updateOrganization = (payload: Partial<OrganizationType>) =>
request("<BASE_URL>/user/organization", "POST", payload);
} }
import { Area } from "react-easy-crop";
export const createImage = (url: string) =>
new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox
image.src = url;
});
export function getRadianAngle(degreeValue: number) {
return (degreeValue * Math.PI) / 180;
}
/**
* Returns the new bounding area of a rotated rectangle.
*/
export function rotateSize(width: number, height: number, rotation: number) {
const rotRad = getRadianAngle(rotation);
return {
width:
Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
height:
Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
};
}
/**
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
*/
export default async function getCroppedImg(
imageSrc: string,
pixelCrop: Area,
rotation = 0,
flip = { horizontal: false, vertical: false }
) {
const image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
return null;
}
const rotRad = getRadianAngle(rotation);
// calculate bounding box of the rotated image
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
image.width,
image.height,
rotation
);
// set canvas size to match the bounding box
canvas.width = bBoxWidth;
canvas.height = bBoxHeight;
// translate canvas context to a central location to allow rotating and flipping around the center
ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
ctx.rotate(rotRad);
ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
ctx.translate(-image.width / 2, -image.height / 2);
// draw rotated image
ctx.drawImage(image, 0, 0);
// croppedAreaPixels values are bounding box relative
// extract the cropped image using these values
const data = ctx.getImageData(
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height
);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image at the top left corner
ctx.putImageData(data, 0, 0);
// As Base64 string
// return canvas.toDataURL('image/jpeg');
// As a blob
return new Promise((resolve, reject) => {
canvas.toBlob((file: any) => {
file.name = "cropped.jpeg";
resolve({ file: file, url: URL.createObjectURL(file) });
}, "image/jpeg");
});
}
...@@ -64,7 +64,7 @@ export const getStatusColor = (status?: string) => { ...@@ -64,7 +64,7 @@ export const getStatusColor = (status?: string) => {
return color; return color;
}; };
export const getAddress = (address: AddressType) => { export const getAddress = (address?: AddressType) => {
if (!address) return ""; if (!address) return "";
return `${address.addressLine}, ${address.city}, ${address.country}`; return `${address.addressLine}, ${address.city}, ${address.country}`;
}; };
......
...@@ -3,6 +3,7 @@ import { CommonReducer } from "../types"; ...@@ -3,6 +3,7 @@ import { CommonReducer } from "../types";
const INITIAL_STATE: CommonReducer = { const INITIAL_STATE: CommonReducer = {
jobs: [], jobs: [],
dialogAlert: null,
}; };
const commonReducer = ( const commonReducer = (
...@@ -13,6 +14,9 @@ const commonReducer = ( ...@@ -13,6 +14,9 @@ const commonReducer = (
case ACTIONS.SET_JOBS: case ACTIONS.SET_JOBS:
return { ...state, jobs: payload }; return { ...state, jobs: payload };
case ACTIONS.SET_DIALOG:
return { ...state, dialogAlert: payload };
case ACTIONS.SIGN_OUT_USER: case ACTIONS.SIGN_OUT_USER:
return INITIAL_STATE; return INITIAL_STATE;
......
...@@ -3,13 +3,15 @@ import { ACTIONS } from "../actions"; ...@@ -3,13 +3,15 @@ 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, Reducers } from "../types"; import { APP_STATE, CandidateType, JobType, OrganizationType, Reducers } from "../types";
function* updateCandidate({ function* updateCandidate({
payload, payload,
success,
}: { }: {
type: typeof ACTIONS.UPDATE_CANDIDATE; type: typeof ACTIONS.UPDATE_CANDIDATE;
payload: Partial<CandidateType>; payload: Partial<CandidateType>;
success?: (candidate?: CandidateType | null) => void;
}) { }) {
try { try {
yield put({ yield put({
...@@ -18,20 +20,58 @@ function* updateCandidate({ ...@@ -18,20 +20,58 @@ function* updateCandidate({
}); });
yield call(UserAPI.updateCandidate, payload); yield call(UserAPI.updateCandidate, payload);
const candidate: CandidateType = yield select(getProfile); const candidate: CandidateType = yield select(getProfile);
const newCandidate = { ...candidate, ...payload };
yield put({ yield put({
type: ACTIONS.SET_AUTH, type: ACTIONS.SET_AUTH,
payload: { candidate: { ...candidate, ...payload } }, payload: { candidate: newCandidate },
}); });
yield put({ yield put({
type: ACTIONS.SET_APP_STATE, type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS }, payload: { state: APP_STATE.SUCCESS },
}); });
success?.(newCandidate);
} catch (error) { } catch (error) {
yield put({ yield put({
type: ACTIONS.SET_APP_STATE, type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED }, payload: { state: APP_STATE.FAILED },
}); });
success?.(null);
}
}
function* updateOrganization({
payload,
success,
}: {
type: typeof ACTIONS.UPDATE_ORG;
payload: Partial<OrganizationType>;
success?: (organization?: OrganizationType | null) => void;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
yield call(UserAPI.updateOrganization, payload);
const org: OrganizationType = yield select(getProfile);
const newOrg = { ...org, ...payload };
yield put({
type: ACTIONS.SET_AUTH,
payload: { organization: newOrg },
});
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
success?.(newOrg);
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
success?.(null);
} }
} }
...@@ -89,6 +129,7 @@ function* applyForJob({ ...@@ -89,6 +129,7 @@ function* applyForJob({
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,
...@@ -97,7 +138,10 @@ function* applyForJob({ ...@@ -97,7 +138,10 @@ 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.APPLY_FOR_JOB, applyForJob); yield takeLeading(ACTIONS.APPLY_FOR_JOB, applyForJob);
} }
...@@ -9,6 +9,12 @@ export enum APP_STATE { ...@@ -9,6 +9,12 @@ export enum APP_STATE {
SUCCESS = "success", SUCCESS = "success",
} }
export enum OnboardSteps {
RESUME = "RESUME",
SELF_INTRO = "SELF_INTRO",
COMPLETE = "COMPLETE",
}
export type KeyDetails = { export type KeyDetails = {
key: any; key: any;
code: any; code: any;
...@@ -104,7 +110,7 @@ export type OrganizationType = { ...@@ -104,7 +110,7 @@ export type OrganizationType = {
description: string; description: string;
contacts: { contacts: {
email: string; email: string;
phone: string[]; phone: string;
address: AddressType; address: AddressType;
website: string; website: string;
}; };
...@@ -250,6 +256,7 @@ export type AuthReducer = { ...@@ -250,6 +256,7 @@ export type AuthReducer = {
export type CommonReducer = { export type CommonReducer = {
jobs: JobType[] | JobPayloadType[]; jobs: JobType[] | JobPayloadType[];
dialogAlert: string | null;
}; };
export type Reducers = { export type Reducers = {
......
import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage"; import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage";
import React, { useState, useEffect, ChangeEvent } from "react"; import React, { useState, useEffect, ChangeEvent, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { ACTIONS } from "../common/actions"; import { ACTIONS } from "../common/actions";
import { updateCandidate } from "../common/actions/user"; import { updateCandidate } from "../common/actions/user";
import { fileStorage } from "../common/config"; import { fileStorage } from "../common/config";
import { getProfile } from "../common/lib/util"; import { getProfile } from "../common/lib/util";
import { APP_STATE, CandidateType } from "../common/types"; import { APP_STATE, CandidateType, OnboardSteps, USER_TYPE } from "../common/types";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import Progress from "./Progress"; import Progress from "./Progress";
import Onboarder from "./Onboarder";
import ProfilePicCrop from "./Modals/ProfilePicCrop";
const CandidateProfile = () => { const CandidateProfile = () => {
const dispath = useDispatch(); const dispath = useDispatch();
...@@ -16,10 +18,18 @@ const CandidateProfile = () => { ...@@ -16,10 +18,18 @@ const CandidateProfile = () => {
const [resume, setResume] = useState<File | null>(null); const [resume, setResume] = useState<File | null>(null);
const [selfIntro, setSelfIntro] = useState<File | null>(null); const [selfIntro, setSelfIntro] = useState<File | null>(null);
const [progress, setProgress] = useState<number>(0); const [progress, setProgress] = useState<number>(0);
const [onboardStep, setOnboardStep] = useState<OnboardSteps>(
OnboardSteps.COMPLETE
);
const [profilePicPrev, setprofilePicPrev] = useState<string | null>(null);
const _resumeInput = useRef<HTMLInputElement>(null);
const _introInput = useRef<HTMLInputElement>(null);
const _profilePicInput = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
if (candidate) { if (candidate) {
setProfile(candidate); setProfile(candidate);
checkOnboardingStatus(candidate);
} }
}, [candidate]); }, [candidate]);
...@@ -50,8 +60,11 @@ const CandidateProfile = () => { ...@@ -50,8 +60,11 @@ const CandidateProfile = () => {
}); });
}; };
const onUpdateCandidate = (payload: Partial<CandidateType>) => { const onUpdateCandidate = (
dispath(updateCandidate(payload)); payload: Partial<CandidateType>,
callBack?: (candidate: CandidateType) => void
) => {
dispath(updateCandidate(payload, callBack));
}; };
const uploadSingleFile = (path: string, file: File) => { const uploadSingleFile = (path: string, file: File) => {
...@@ -131,6 +144,60 @@ const CandidateProfile = () => { ...@@ -131,6 +144,60 @@ const CandidateProfile = () => {
} }
}; };
const onSelectProfilePic = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setprofilePicPrev(URL.createObjectURL(e.target.files[0]));
}
};
const onPressOnboard = async () => {
const update: any = {};
if (onboardStep === OnboardSteps.RESUME && resume) {
const url = await uploadSingleFile(`/resumes/${profile._id}.pdf`, resume);
update.resume = url;
}
if (onboardStep === OnboardSteps.SELF_INTRO && selfIntro) {
const url = await uploadSingleFile(
`/selfIntros/${profile._id}.mp4`,
selfIntro
);
update.selfIntro = url;
}
onUpdateCandidate(update, checkOnboardingStatus);
};
const checkOnboardingStatus = (_candidate: CandidateType) => {
if (_candidate) {
if (!_candidate?.resume) {
setOnboardStep(OnboardSteps.RESUME);
} else if (!_candidate?.selfIntro) {
setOnboardStep(OnboardSteps.SELF_INTRO);
} else {
setOnboardStep(OnboardSteps.COMPLETE);
}
}
};
const onPressUploadProfilePic = () => {
_profilePicInput?.current?.click();
};
const onPressUploadResume = () => {
_resumeInput?.current?.click();
};
const onPressUploadSelfIntro = () => {
_introInput?.current?.click();
};
const onRequestCloseProfilePic = () => {
setprofilePicPrev(null);
};
const isOnboarded = onboardStep === OnboardSteps.COMPLETE;
return ( return (
<div className="card p-4 mb-3"> <div className="card p-4 mb-3">
<h5>Edit/Update profile</h5> <h5>Edit/Update profile</h5>
...@@ -218,28 +285,47 @@ const CandidateProfile = () => { ...@@ -218,28 +285,47 @@ const CandidateProfile = () => {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
Preview Preview resume
</a> </a>
)} )}
<input <div className="d-md-block">
type="file" <button
className="form-control" type="button"
onChange={onSelectResume} className="btn btn-warning btn-sm"
accept="application/pdf" style={{ width: "100%" }}
/> onClick={onPressUploadResume}
>
Select Resume
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
<div className="row"> <div className="row">
<label className="col-sm-3 col-form-label">Self Intro</label> <label className="col-sm-4 col-form-label">Self Intro</label>
<div className="col-sm-9">
<input <div className="col-sm-8">
type="file" {profile?.selfIntro && (
className="form-control" <a
accept="video/mp4" className="btn btn-secondary mb-2"
onChange={onSelectSelfIntro} href={profile.selfIntro}
/> target="_blank"
rel="noreferrer"
>
Preview video
</a>
)}
<div className="d-md-block">
<button
type="button"
className="btn btn-warning btn-sm"
style={{ width: "100%" }}
onClick={onPressUploadSelfIntro}
>
Select Video
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -260,6 +346,7 @@ const CandidateProfile = () => { ...@@ -260,6 +346,7 @@ const CandidateProfile = () => {
type="button" type="button"
className="btn btn-link" className="btn btn-link"
style={{ padding: "0" }} style={{ padding: "0" }}
onClick={onPressUploadProfilePic}
> >
Upload picture Upload picture
</button> </button>
...@@ -279,6 +366,44 @@ const CandidateProfile = () => { ...@@ -279,6 +366,44 @@ const CandidateProfile = () => {
</button> </button>
</div> </div>
<Progress progress={progress} /> <Progress progress={progress} />
<input
type="file"
className="form-control hidden-input"
onChange={onSelectResume}
accept="application/pdf"
ref={_resumeInput}
/>
<input
type="file"
className="form-control hidden-input"
accept="video/mp4"
onChange={onSelectSelfIntro}
ref={_introInput}
/>
<input
type="file"
className="form-control hidden-input"
accept="image/*"
onChange={onSelectProfilePic}
ref={_profilePicInput}
/>
<Onboarder
isOpen={!isOnboarded}
onboardStep={onboardStep}
onPressUploadResume={onPressUploadResume}
onPressUploadSelfIntro={onPressUploadSelfIntro}
progress={progress}
onPressOnboard={onPressOnboard}
/>
<ProfilePicCrop
onRequestClose={onRequestCloseProfilePic}
profilePicPrev={profilePicPrev}
userId={profile._id}
onUpdateCandidate={onUpdateCandidate}
userType={USER_TYPE.CANDIDATE}
/>
</div> </div>
); );
}; };
......
...@@ -46,8 +46,6 @@ const Login = ({ userType, setAlert }: OwnProps) => { ...@@ -46,8 +46,6 @@ const Login = ({ userType, setAlert }: OwnProps) => {
signIn(data, (state) => { signIn(data, (state) => {
if (state === "failed") { if (state === "failed") {
setAlert("Attempt failed! Try again"); setAlert("Attempt failed! Try again");
} else if (state === "INTIAL") {
navigate("/settings");
} else { } else {
navigate("/home"); navigate("/home");
} }
......
import React from "react";
import Modal from "./Modal";
import { useDispatch, useSelector } from "react-redux";
import { Reducers } from "../../common/types";
import { setDialogBox } from "../../common/actions/common";
const DialogBox = () => {
const dispatch = useDispatch();
const dialogString = useSelector(
(state: Reducers) => state.common.dialogAlert
);
if (!dialogString) return null;
const onPressCancel = () => {
dispatch(setDialogBox(null));
};
const isOpen = dialogString !== null;
return (
<Modal isOpen={isOpen} onRequestClose={onPressCancel}>
<p>{dialogString}</p>
<div style={{ display: "block" }}>
<button
type="button"
className="btn btn-primary btn-sm mt-3"
style={{ float: "right" }}
onClick={onPressCancel}
>
Got it
</button>
</div>
</Modal>
);
};
export default DialogBox;
...@@ -16,7 +16,7 @@ const CustomModal = ({ ...@@ -16,7 +16,7 @@ const CustomModal = ({
size = "md", size = "md",
shouldCloseOnOverlayClick = true, shouldCloseOnOverlayClick = true,
}: OwnProps) => { }: OwnProps) => {
const width = size === "md" ? "400px" : "500px"; const width = size === "md" ? "400px" : "600px";
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
......
import React, { useRef, useState } from "react";
import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage";
import Cropper, { Area } from "react-easy-crop";
import getCroppedImg from "../../common/lib/crop";
import { fileStorage } from "../../common/config";
import { USER_TYPE } from "../../common/types";
type OwnProps = {
onRequestClose: () => void;
profilePicPrev: string | null;
userId?: string;
onUpdateCandidate: (update: any) => void;
userType:USER_TYPE
};
const ProfilePicCrop = ({
onRequestClose,
profilePicPrev,
userId,
onUpdateCandidate,
userType
}: OwnProps) => {
const [progress, setprogress] = useState(0);
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const _croppedAreaPixels = useRef<Area>();
if (!profilePicPrev) return null;
const onCropComplete = (croppedArea: Area, croppedAreaPixels: Area) => {
_croppedAreaPixels.current = croppedAreaPixels;
};
const onChangeZoom = (e: React.ChangeEvent<HTMLInputElement>) => {
setZoom(Number(e.target.value));
};
const onPressSelect = async () => {
if (_croppedAreaPixels.current) {
const { file, _ }: any = await getCroppedImg(
profilePicPrev,
_croppedAreaPixels.current
);
const extension = file.name.split(".")[1];
const uploadRef = ref(fileStorage, `/${userType}/${userId}.${extension}`);
const uploadTask = uploadBytesResumable(uploadRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const percent = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setprogress(percent);
},
(err) => {},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
setprogress(0);
onUpdateCandidate({ profilePicture: url });
onRequestClose();
});
}
);
}
};
return (
<div
style={{
position: "absolute",
top: "0",
left: "0",
right: "0",
bottom: "0",
}}
>
<Cropper
image={profilePicPrev}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
style={{
containerStyle: {
backgroundColor: "white",
borderRadius: "15px",
},
}}
/>
<div
style={{
position: "absolute",
bottom: "0",
height: "80px",
background: "white",
width: "100%",
borderBottomLeftRadius: "15px",
borderBottomRightRadius: "15px",
padding: "0 20px",
}}
>
{progress > 0 && (
<div className="progress ">
<div
className="progress-bar progress-bar-striped"
style={{ width: `${progress}%` }}
></div>
</div>
)}
<div className="row mt-4">
<div className="col-6">
<input
type="range"
min={1}
max={10}
onChange={onChangeZoom}
className="form-range"
/>
</div>
<div className="col-auto">
<button
type="button"
className="btn btn-warning btn-sm"
onClick={onPressSelect}
>
Crop & select picture
</button>
</div>
<div className="col-auto">
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={onRequestClose}
>
Cancel
</button>
</div>
</div>
</div>
</div>
);
};
export default ProfilePicCrop;
...@@ -28,13 +28,7 @@ const NavBar = () => { ...@@ -28,13 +28,7 @@ const NavBar = () => {
<span className="navbar-toggler-icon"></span> <span className="navbar-toggler-icon"></span>
</button> </button>
<div className="collapse navbar-collapse" id="navbarSupportedContent"> <div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0"> <ul className="navbar-nav me-auto mb-2 mb-lg-0"></ul>
<li className="nav-item">
<a className="nav-link" href="/">
Link
</a>
</li>
</ul>
<nav className="nav-item dropdown"> <nav className="nav-item dropdown">
<a <a
className="nav-link dropdown-toggle" className="nav-link dropdown-toggle"
...@@ -50,11 +44,6 @@ const NavBar = () => { ...@@ -50,11 +44,6 @@ const NavBar = () => {
/> />
</a> </a>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li>
<a className="dropdown-item" href="/">
Action
</a>
</li>
<li> <li>
<a className="dropdown-item" href="/settings"> <a className="dropdown-item" href="/settings">
Settings Settings
......
import React from "react";
import CustomModal from "./Modals/Modal";
import { OnboardSteps } from "../common/types";
import Progress from "./Progress";
type OwnProps = {
isOpen: boolean;
onboardStep: OnboardSteps;
onPressUploadResume: () => void;
onPressUploadSelfIntro: () => void;
progress: number;
onPressOnboard: () => void;
};
const Onboarder = ({
isOpen,
onboardStep,
onPressUploadResume,
onPressUploadSelfIntro,
progress,
onPressOnboard,
}: OwnProps) => {
return (
<CustomModal
isOpen={isOpen}
onRequestClose={() => {}}
shouldCloseOnOverlayClick={false}
size="lg"
>
<h5 className="mb-3">Complete your profile</h5>
{onboardStep === OnboardSteps.RESUME ? (
<div className="row">
<label className="col-sm-6 col-form-label">
<h6>1. Upload your resume</h6>
</label>
<div className="col-sm-6">
<button
type="button"
className="btn btn-primary"
style={{ width: "200px" }}
onClick={onPressUploadResume}
>
Choose resume
</button>
</div>
</div>
) : (
<div className="row">
<label className="col-sm-6 col-form-label">
<h6>2. Upload a self introduction video</h6>
</label>
<div className="col-sm-6">
<button
type="button"
className="btn btn-primary"
style={{ width: "200px" }}
onClick={onPressUploadSelfIntro}
>
Choose video
</button>
</div>
</div>
)}
<Progress progress={progress} />
<hr />
<div className="d-md-block">
<button
type="button"
className="btn btn-warning btn-sm"
style={{ width: "100px", float: "right" }}
onClick={onPressOnboard}
>
Next
</button>
</div>
</CustomModal>
);
};
export default Onboarder;
import React, { useState, useEffect, ChangeEvent, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { ACTIONS } from "../common/actions";
import { updateOrganization } from "../common/actions/user";
import { getProfile } from "../common/lib/util";
import { APP_STATE, OrganizationType, USER_TYPE } from "../common/types";
import Avatar from "./Avatar";
import ProfilePicCrop from "./Modals/ProfilePicCrop";
const OrganizationProfile = () => {
const dispath = useDispatch();
const org = useSelector(getProfile) as OrganizationType;
const [profile, setProfile] = useState<OrganizationType>(org);
const [profilePicPrev, setprofilePicPrev] = useState<string | null>(null);
const _profilePicInput = useRef<HTMLInputElement>(null);
useEffect(() => {
if (org) {
setProfile(org);
}
}, [org]);
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.name === "phone") {
setProfile({
...profile,
contacts: { ...profile.contacts, phone: e.target.value },
});
} else {
setProfile({
...profile,
[e.target.name]: e.target.value,
});
}
};
const onChangeAddress = (e: ChangeEvent<HTMLInputElement>) => {
setProfile({
...profile,
contacts: {
...profile.contacts,
address: {
...profile.contacts.address,
[e.target.name]: e.target.value,
},
},
});
};
const onUpdateOrg = (
payload: Partial<OrganizationType>,
) => {
dispath(updateOrganization(payload));
};
const onSubmit = async () => {
const { _id, ...rest } = profile;
dispath({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
onUpdateOrg(rest);
};
const onSelectProfilePic = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setprofilePicPrev(URL.createObjectURL(e.target.files[0]));
}
};
const onPressUploadProfilePic = () => {
_profilePicInput?.current?.click();
};
const onRequestCloseProfilePic = () => {
setprofilePicPrev(null);
};
return (
<div className="card p-4 mb-3">
<h5>Edit/Update profile</h5>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">Org Name</label>
<div className="col-sm-9">
<input
type="text"
className="form-control"
name="name"
value={profile.name}
onChange={onChange}
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">Address</label>
<div className="col-sm-9">
<input
type="text"
name="addressLine"
className="form-control mb-2"
placeholder="Address line"
value={profile.contacts?.address?.addressLine}
onChange={onChangeAddress}
/>
<input
type="text"
className="form-control mb-2"
placeholder="City"
name="city"
value={profile.contacts?.address?.city}
onChange={onChangeAddress}
/>
<input
type="text"
className="form-control mb-2"
placeholder="Country"
name="country"
value={profile.contacts?.address?.country}
onChange={onChangeAddress}
/>
</div>
</div>
<div className="mb-3 row">
<div className="col-sm-6">
<div className="row">
<label className="col-sm-3 col-form-label">Phone</label>
<div className="col-sm-9">
<input
type="text"
className="form-control"
name="phone"
value={profile.contacts?.phone}
onChange={onChange}
/>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="row">
<label className="col-sm-4 col-form-label">Profile picuture</label>
<div className="col-sm-8">
<Avatar
name={profile?.name}
url={profile?.profilePicture}
size="lg"
/>
<button
type="button"
className="btn btn-link"
style={{ padding: "0" }}
onClick={onPressUploadProfilePic}
>
Upload picture
</button>
</div>
</div>
</div>
</div>
<button
type="button"
className="btn btn-primary"
style={{ width: "300px" }}
onClick={onSubmit}
>
Update Profile
</button>
<input
type="file"
className="form-control hidden-input"
accept="image/*"
onChange={onSelectProfilePic}
ref={_profilePicInput}
/>
<ProfilePicCrop
onRequestClose={onRequestCloseProfilePic}
profilePicPrev={profilePicPrev}
userId={profile._id}
onUpdateCandidate={onUpdateOrg}
userType={USER_TYPE.ORGANIZATION}
/>
</div>
);
};
export default OrganizationProfile;
import React from "react"; import React from "react";
import Avatar from "./Avatar";
import { getAddress } from "../common/lib/util";
import { AddressType } from "../common/types";
const Profile = () => { type OwnProps = {
return <div className="card p-4">Profile</div>; name: string;
profilePic?: string;
email: string;
address?: AddressType;
};
const Profile = ({ name, profilePic, email, address }: OwnProps) => {
const addressString = getAddress(address);
return (
<div className="card p-4 profile-card">
<Avatar size="lg" name={name} url={profilePic} />
<h5>{name}</h5>
<p>{email}</p>
<p>{addressString}</p>
<a
className="btn btn-primary mt-3"
href="/settings"
style={{ width: "100%" }}
>
Edit profile
</a>
</div>
);
}; };
export default Profile; export default Profile;
import React from "react"; import React, { CSSProperties } from "react";
type OwnProps = { type OwnProps = {
progress?: number; progress?: number;
style?: CSSProperties;
}; };
const Progress = ({ progress }: OwnProps) => { const Progress = ({ progress, style }: OwnProps) => {
if (!progress || progress === 0) return null; if (!progress || progress === 0) return null;
return ( return (
<div className="progress mt-3"> <div className="progress mt-3" style={style}>
<div <div
className="progress-bar progress-bar-striped" className="progress-bar progress-bar-striped"
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
......
...@@ -25,9 +25,9 @@ const ResultTable = ({ data }: { data?: Stat | null }) => { ...@@ -25,9 +25,9 @@ const ResultTable = ({ data }: { data?: Stat | null }) => {
</tr> </tr>
<tr> <tr>
<th className="text-right">Score/Distance</th> <th className="text-right">Score/Distance</th>
<td>{(data.standard.inRangePercent.full as number).toFixed(2)}</td> <td>{(data.standard.inRangePercent.full as number)?.toFixed(2)}</td>
<td> <td>
{(data.fullStandard.normedDistance.full as number).toFixed(2)} {(data.fullStandard.normedDistance.full as number)?.toFixed(2)}
</td> </td>
</tr> </tr>
<tr> <tr>
......
import React, { useState, useMemo } from "react"; import React, { useState, useMemo } 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 { getStatusColor, getUserId } from "../common/lib/util"; import { getProfile, getStatusColor, getUserId } from "../common/lib/util";
import { JobPayloadType, JobType, Reducers, USER_TYPE } from "../common/types"; import {
CandidateType,
JobPayloadType,
JobType,
OrganizationType,
Reducers,
USER_TYPE,
} from "../common/types";
import Jobs from "../components/Jobs"; import Jobs from "../components/Jobs";
import Layout from "../Layouts/Layout"; 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";
const CandidateHome = () => { const CandidateHome = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
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( const jobs = useSelector(
...@@ -40,13 +49,24 @@ const CandidateHome = () => { ...@@ -40,13 +49,24 @@ const CandidateHome = () => {
}, [jobs, selectedJob, userId]); }, [jobs, selectedJob, userId]);
const onApply = () => { const onApply = () => {
dispatch(applyForJob(jobs[selectedJob]._id || "")); if (!candidate?.resume || !candidate?.selfIntro) {
dispatch(
setDialogBox("Your resume and self introduction video is required!")
);
} else {
dispatch(applyForJob(jobs[selectedJob]._id || ""));
}
}; };
return ( return (
<div className="row"> <div className="row">
<div className="col-3 "> <div className="col-3 ">
<Profile /> <Profile
name={candidate.name}
email={candidate.contacts.email}
profilePic={candidate.profilePicture}
address={candidate.contacts.address}
/>
</div> </div>
<div className="col"> <div className="col">
<Jobs <Jobs
...@@ -95,6 +115,9 @@ const CandidateHome = () => { ...@@ -95,6 +115,9 @@ const CandidateHome = () => {
const OrganizationHome = () => { const OrganizationHome = () => {
const [selectedJob, setSelectedJob] = useState<number>(0); const [selectedJob, setSelectedJob] = useState<number>(0);
const organization: OrganizationType = useSelector(
(state: Reducers) => state.auth.organization
) as OrganizationType;
const jobs = useSelector((state: Reducers) => state.common.jobs) as JobType[]; const jobs = useSelector((state: Reducers) => state.common.jobs) as JobType[];
const job = jobs.length > 0 ? jobs[selectedJob] : null; const job = jobs.length > 0 ? jobs[selectedJob] : null;
...@@ -107,7 +130,12 @@ const OrganizationHome = () => { ...@@ -107,7 +130,12 @@ const OrganizationHome = () => {
return ( return (
<div className="row"> <div className="row">
<div className="col-3 "> <div className="col-3 ">
<Profile /> <Profile
name={organization.name}
profilePic={organization.profilePicture}
email={organization.contacts.email}
address={organization.contacts.address}
/>
<CreateUpdateJob mode="create" /> <CreateUpdateJob mode="create" />
</div> </div>
<div className="col"> <div className="col">
......
...@@ -32,7 +32,7 @@ const Landing = () => { ...@@ -32,7 +32,7 @@ const Landing = () => {
className="btn btn-link" className="btn btn-link"
onClick={() => setForm("sign-in")} onClick={() => setForm("sign-in")}
> >
Sing in Sign in
</button> </button>
</p> </p>
</section> </section>
...@@ -48,7 +48,7 @@ const Landing = () => { ...@@ -48,7 +48,7 @@ const Landing = () => {
className="btn btn-link" className="btn btn-link"
onClick={() => setForm("sign-up")} onClick={() => setForm("sign-up")}
> >
Sing up Sign up
</button> </button>
</p> </p>
</section> </section>
......
...@@ -2,15 +2,17 @@ import React, { ChangeEvent, useState } from "react"; ...@@ -2,15 +2,17 @@ import React, { ChangeEvent, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateKeystrokeSettings } from "../common/actions/auth"; import { updateKeystrokeSettings } from "../common/actions/auth";
import { DEFAULT_CONTROLS } from "../common/config"; import { DEFAULT_CONTROLS } from "../common/config";
import { ControlsType, Reducers } from "../common/types"; import { ControlsType, Reducers, USER_TYPE } from "../common/types";
import ChangePassword from "../components/ChangePassword"; import ChangePassword from "../components/ChangePassword";
import Charts from "../components/Charts"; import Charts from "../components/Charts";
import Layout from "../Layouts/Layout"; import Layout from "../Layouts/Layout";
import CandidateProfile from "../components/CandidateProfile"; import CandidateProfile from "../components/CandidateProfile";
import OrganizationProfile from "../components/OrganizationProfile";
import ResultTable from "../components/ResultTable"; import ResultTable from "../components/ResultTable";
const Settings = () => { const Settings = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const userType = useSelector((state:Reducers)=>state.auth.userType)
const keystrokeResult = useSelector( const keystrokeResult = useSelector(
(state: Reducers) => state.auth.keystrokeResult (state: Reducers) => state.auth.keystrokeResult
); );
...@@ -54,7 +56,7 @@ const Settings = () => { ...@@ -54,7 +56,7 @@ const Settings = () => {
<Layout title="Profile and Settings"> <Layout title="Profile and Settings">
<div className="row"> <div className="row">
<div className="col-8"> <div className="col-8">
<CandidateProfile /> { userType === USER_TYPE.CANDIDATE ? <CandidateProfile /> : <OrganizationProfile/> }
<ChangePassword /> <ChangePassword />
</div> </div>
<div className="col-4"> <div className="col-4">
......
...@@ -7107,6 +7107,11 @@ normalize-url@^6.0.1: ...@@ -7107,6 +7107,11 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
normalize-wheel@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
integrity sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==
npm-run-path@^4.0.1: npm-run-path@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
...@@ -8253,6 +8258,14 @@ react-dom@^18.2.0: ...@@ -8253,6 +8258,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-easy-crop@^4.7.4:
version "4.7.4"
resolved "https://registry.yarnpkg.com/react-easy-crop/-/react-easy-crop-4.7.4.tgz#3106bfb3de371d9393aedc0b9ae1fb97c2b9f722"
integrity sha512-oDi1375Jo/zuPUvo3oauxnNbfy8L4wsbmHD1KB2vT55fdgu+q8/K0w/rDWzy9jz4jfQ94Q9+3Yu366sDDFVmiA==
dependencies:
normalize-wheel "^1.0.1"
tslib "2.0.1"
react-error-overlay@^6.0.11: react-error-overlay@^6.0.11:
version "6.0.11" version "6.0.11"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
...@@ -9462,6 +9475,11 @@ tsconfig-paths@^3.14.1: ...@@ -9462,6 +9475,11 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6" minimist "^1.2.6"
strip-bom "^3.0.0" strip-bom "^3.0.0"
tslib@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
tslib@^1.8.1: tslib@^1.8.1:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
......
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