Преглед изворни кода

Merge branch 'feature/1582_applicant_details_page' of Neca/HRCenter into FE_dev

pull/40/head
safet.purkovic пре 3 година
родитељ
комит
19df915cbe

+ 66
- 0
package-lock.json Прегледај датотеку

@@ -33,6 +33,7 @@
"react-dom": "^17.0.2",
"react-helmet-async": "^1.0.9",
"react-i18next": "^11.10.0",
"react-mentions": "^4.4.7",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
@@ -18562,6 +18563,29 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-mentions": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.4.7.tgz",
"integrity": "sha512-VNriu2h/uOB+RS0mwZgPG2Vf+UtdDvRh5zbXa2TNc1WqacKuNDgTdhlbo9LEOZRBxRzIeTUYQmYJ7p9M9rDHqQ==",
"dependencies": {
"@babel/runtime": "7.4.5",
"invariant": "^2.2.4",
"prop-types": "^15.5.8",
"substyle": "^9.1.0"
},
"peerDependencies": {
"react": ">=16.8.3",
"react-dom": ">=16.8.3"
}
},
"node_modules/react-mentions/node_modules/@babel/runtime": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
"integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
"dependencies": {
"regenerator-runtime": "^0.13.2"
}
},
"node_modules/react-redux": {
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
@@ -21268,6 +21292,18 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
"integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag=="
},
"node_modules/substyle": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz",
"integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==",
"dependencies": {
"@babel/runtime": "^7.3.4",
"invariant": "^2.2.4"
},
"peerDependencies": {
"react": ">=16.8.3"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -38790,6 +38826,27 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"react-mentions": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.4.7.tgz",
"integrity": "sha512-VNriu2h/uOB+RS0mwZgPG2Vf+UtdDvRh5zbXa2TNc1WqacKuNDgTdhlbo9LEOZRBxRzIeTUYQmYJ7p9M9rDHqQ==",
"requires": {
"@babel/runtime": "7.4.5",
"invariant": "^2.2.4",
"prop-types": "^15.5.8",
"substyle": "^9.1.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
"integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
}
}
},
"react-redux": {
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
@@ -40961,6 +41018,15 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
"integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag=="
},
"substyle": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz",
"integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==",
"requires": {
"@babel/runtime": "^7.3.4",
"invariant": "^2.2.4"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

+ 1
- 0
package.json Прегледај датотеку

@@ -28,6 +28,7 @@
"react-dom": "^17.0.2",
"react-helmet-async": "^1.0.9",
"react-i18next": "^11.10.0",
"react-mentions": "^4.4.7",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",

+ 6
- 5
src/App.js Прегледај датотеку

@@ -6,16 +6,17 @@ import history from "./store/utils/history";
import MainContainer from "./components/Section/MainContainer";
import AppRoutes from "./AppRoutes";
import { useDispatch } from "react-redux";
import {refreshUserToken} from "./store/actions/login/loginActions";
import { refreshUserToken } from "./store/actions/login/loginActions";
import { BASE_PAGE } from "./constants/pages";
const App = () => {
const dispatch = useDispatch()
const dispatch = useDispatch();

useEffect(() => {
if(history.location.pathname === BASE_PAGE)
if (history.location.pathname === BASE_PAGE) {
return;
dispatch(refreshUserToken())
},[])
}
dispatch(refreshUserToken());
}, []);

return (
<>

+ 4
- 0
src/AppRoutes.js Прегледај датотеку

@@ -14,6 +14,8 @@ import {
USERS_PAGE,
CANDIDATES_PAGE,
USER_DETAILS_PAGE
CANDIDATES_PAGE,
CANDIDATES_DETAILS_PAGE
} from "./constants/pages";

// import LoginPage from './pages/LoginPage/LoginPage';
@@ -32,6 +34,7 @@ import UsersPage from "./pages/UsersPage/UsersPage";
import CandidatesPage from './pages/CandidatesPage/CandidatesPage'
import AdDetailsPage from "./pages/AdsPage/AdDetailsPage";
import UserDetails from "./pages/UsersPage/UserDetails";
import CandidateDetailsPage from "./pages/CandidateDetailsPage/CandidateDetailsPage";

const AppRoutes = () => (
<Switch>
@@ -51,6 +54,7 @@ const AppRoutes = () => (
<PrivateRoute exact path={USER_DETAILS_PAGE} component={UserDetails} />
<PrivateRoute exact path={USERS_PAGE} component={UsersPage} />
<PrivateRoute exact path={CANDIDATES_PAGE} component={CandidatesPage} />
<PrivateRoute exact path={CANDIDATES_DETAILS_PAGE} component={CandidateDetailsPage} />
<Redirect from="*" to={NOT_FOUND_PAGE} />
</Switch>
);

BIN
src/assets/images/delete.png Прегледај датотеку


BIN
src/assets/images/planeVectorBlue.png Прегледај датотеку


+ 486
- 0
src/assets/styles/components/_candidatePage.scss Прегледај датотеку

@@ -0,0 +1,486 @@
.main-candidate-container {
display: flex;
flex-direction: column;
}

.top-candidate-container {
display: flex;
width: 100%;
justify-content: space-between;
}

.candidate-header {
height: 36px;
font-family: Source Sans Pro;
font-size: 36px;
font-weight: 600;
line-height: 36px;
letter-spacing: 0.02em;
text-align: left;
color: #272727;
}

.separation-line {
margin-left: 5px;
margin-right: 5px;
font-size: 20px;
align-self: flex-end;
}

.candidate-lower-header {
font-family: Source Sans Pro;
font-size: 24px;
font-weight: 600;
line-height: 32px;
letter-spacing: 0.02em;
text-align: left;
color: #226cb0;
align-self: flex-end;
}
.candidate-option-container {
display: flex;
height: 38px;
}

.content-candidate-container {
display: flex;
justify-content: space-between;
margin-top: 14px;
}

.technologies-candidate-container {
display: flex;
margin-top: 18px;
}

.technology-candidate-card {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 9px;
gap: 10px;
background: #ffffff;
border: 1px solid #e4e4e4;
border-radius: 9px;
}

.technology-candidate-card:not(:last-child) {
margin-right: 18px;
}

.comment-container {
width: 612px;
height: 404px;
background: #ffffff;
border: 1px solid #e4e4e4;
border-radius: 12px;
margin-top: 16px;
}

.candidate-informations-container {
display: flex;
flex-direction: column;
}

.candidate-informations-sub-container {
margin-top: 36px;
}

.informations-candidate-header {
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: #272727;
}

.candidate-property-container {
display: flex;
flex-direction: column;
}

.candidate-property {
margin-top: 18px;
font-family: Source Sans Pro;
font-size: 16px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0em;
}

.candidate-property-value {
@extend .candidate-property;
color: #1e92d0;
}

.candidate-informations-sub-container {
display: flex;
}

.comment-container {
display: flex;
flex-direction: column;
padding-left: 36px;
padding-right: 36px;
padding-top: 36px;
}

.comment-sub-container {
display: flex;
align-items: center;
}

.comment-sub-container:not(:first-child) {
margin-top: 36px;
}

.comment-sender {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
box-sizing: border-box;
border: 1px solid;
border-color: #226cb0;
}

.comment-sender p {
color: #226cb0;
}

.comment-message {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
padding: 9px;
gap: 9px;
height: 62px;
background: #f4f4f4;
border-radius: 12px;
margin-left: 18px;
}

.comment-message-content {
align-self: flex-start;
height: 20px;
font-family: "Source Sans Pro";
font-style: normal;
font-size: 16px;
line-height: 20px;
}

.comment-message-date {
align-self: flex-end;
height: 15px;
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 15px;
color: #272727;
}

.comment-separation-line {
width: 100%;
height: 0px;
border: 1px solid #e4e4e4;
background: #e4e4e4;
}

.send-comment-container {
margin-top: 18px;
}

.send-comment-container p {
font-family: "Source Sans Pro";
font-weight: 400;
font-size: 16px;
line-height: 20px;
color: #9d9d9d;
}

.send-comment-sub-container {
display: flex;
margin-top: 9px;
height: 56px;
margin-bottom: 36px;
}

.comment-send-btn {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 18px 36px;
gap: 10px;
background: #ffffff;
border: 1px solid #226cb0;
border-radius: 9px;
border: 1px solid #226cb0;
width: 156px;
margin-left: 18px;
}

.comment-send-btn:hover {
cursor: pointer;
}

.comment-send-btn p {
width: 62px;
height: 15px;
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 600;
font-size: 12px;
line-height: 15px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #226cb0;
flex: none;
order: 1;
flex-grow: 0;
}

.comment-send-btn img {
width: 12px;
height: 12px;
}

.candidate-users {
background-color: #f4f4f4;
}

.candidate-user {
color: #226cb0;
}

.applicant-ads-container {
margin-top: 36px;
}

.applicant-ads-container > p {
font-family: "Source Sans Pro";
font-weight: 600;
font-size: 24px;
line-height: 32px;
letter-spacing: 0.02em;
color: #272727;
}

.applicant-ads-sub-container {
margin-top: 18px;
}

.applicant-add {
display: flex;
flex-direction: column;
align-items: center;
padding: 36px;
gap: 18px;
background: #ffffff;
border: 1px solid #e4e4e4;
border-radius: 12px;
width: 247px;
height: 238px;
}

.applicant-add-date {
font-family: Source Sans Pro;
font-size: 12px;
font-weight: 400;
line-height: 15px;
letter-spacing: 0em;
}

.applicant-add-title {
font-family: "Source Sans Pro";
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: #226cb0;
}

.applicant-add-site {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #ffffff;
padding: 5px;
border: 1px solid #e4e4e4;
border-radius: 8px;
font-size: 16px;
font-family: "Source Sans Pro";
font-weight: 400;
color: #272727;
}

.applicant-ads-buttons-container {
display: flex;
align-self: flex-end;
align-items: center;
margin-bottom: 54px;
margin-top: 18px;
}

.applicant-cv-button {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 18px 72px;
gap: 10px;
background: #226cb0;
border-radius: 9px;
width: 212px;
height: 51px;
font-family: "Source Sans Pro";
font-weight: 600;
font-size: 12px;
line-height: 15px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #ffffff;
}

.applicant-ads-back-button {
margin-right: 36px;
font-family: "Source Sans Pro";
font-weight: 400;
font-size: 16px;
line-height: 20px;
text-decoration-line: underline;
color: #226cb0;
}

.tagStyle {
color: #226cb0;
font-family: Source Sans Pro;
font-size: 16px;
font-weight: 600;
line-height: 20px;
letter-spacing: 0em;
}

.comment-input {
@extend .tagStyle;
width: 368px;
}


.comment-input ::placeholder {
width: 139px;
height: 20px;
font-family: "Source Sans Pro";
font-style: italic;
font-weight: 400;
font-size: 16px;
line-height: 20px;
color: #9d9d9d;
flex: none;
order: 0;
flex-grow: 0;
}

.comment-input-list{
@extend .tagStyle;
}

@media only screen and (max-width: 930px) {
.comment-container {
width:500px;
}
}

@media only screen and (max-width: 820px) {
.comment-container {
width:400px;
};
.comment-send-btn{
width: 130;
padding: 12px 28px;
};
.send-comment-sub-container{
height: 45px;
margin-bottom: 20px
};
.comment-input ::placeholder{
font-size: 14px;
line-height: 18px;
padding-top:2px;
}
}

@media only screen and (max-width: 700px) {
.comment-container {
width:380px;
}
.comment-send-btn{
width: 110;
padding: 10px 20px;
};
}

@media only screen and (max-width:680px) {
.content-candidate-container{
flex-direction: column;
}
.comment-container {
margin-top: 30px;
}
}

@media only screen and (max-width:540px) {
.candidate-header{
height: 30px;
font-family: Source Sans Pro;
font-size: 30px;
line-height: 30px;
}

.candidate-lower-header{
font-size: 18px;
line-height: 26px;
}

.applicant-cv-button {
padding: 10px 52px;
gap: 7px;
border-radius: 7px;
width: 180px;
height: 40px;
font-size: 10px;
line-height: 12px;
text-align: center;
}
}

@media only screen and (max-width:480px) {
.comment-container {
width:297px;
}
.comment-send-btn{
padding: 5px 5px;
};
.comment-container{
padding: 20px;
}

.candidate-header{
height: 20x;
font-size: 20px;
line-height: 20px;
}

.candidate-lower-header{
font-size: 17px;
line-height:17px;
}
}

+ 0
- 3
src/assets/styles/components/_candidatesPage.scss Прегледај датотеку

@@ -1,9 +1,6 @@
.main-candidates-container {
display: flex;
flex-direction: column;
margin-top: 30px;
margin-left: 100px;
margin-right: 100px;
}

.top-candidates-container {

+ 2
- 2
src/components/MUI/NavbarComponent.js Прегледај датотеку

@@ -31,7 +31,7 @@ import CloseIcon from "@mui/icons-material/Close";
import LogoutIcon from "@mui/icons-material/Logout";
import UserProfile from "../Profile/UserProfile";
import { useSelector } from "react-redux";
import { userSelector } from "../../store/selectors/userSelectors";
//import { userSelector } from "../../store/selectors/userSelectors";
import { Link } from "react-router-dom";

const NavbarComponent = () => {
@@ -55,7 +55,7 @@ const NavbarComponent = () => {
let btnRef = useRef();

// get authenticated user
const user = useSelector(userSelector);
const {user} = useSelector(s => s.user);

const { t } = useTranslation();


+ 5
- 6
src/components/Profile/UserProfile.js Прегледај датотеку

@@ -5,20 +5,19 @@ import React from "react";
import { useTranslation } from "react-i18next";
import avatarLogo from "../../assets/images/Avatar.png";
import PropTypes from "prop-types";
import { userSelector } from "../../store/selectors/userSelectors";
import { useDispatch, useSelector } from "react-redux";
import { logoutUser } from '../../store/actions/login/loginActions'
import { logoutUser } from "../../store/actions/login/loginActions";

const UserProfile = ({ show, innerRef }) => {
const { t } = useTranslation();
const dispatch = useDispatch()
const dispatch = useDispatch();
// const theme = useTheme();
// const matches = useMediaQuery(theme.breakpoints.down("sm"));
const user = useSelector(userSelector);
const { user } = useSelector((s) => s.user);

const logout = () => {
dispatch(logoutUser())
}
dispatch(logoutUser());
};

return (
<Box className={`user-view flex-center ${show && "active"}`}>

+ 1
- 0
src/constants/pages.js Прегледај датотеку

@@ -8,5 +8,6 @@ export const NOT_FOUND_PAGE = '/not-found';
export const USERS_PAGE = '/users';
export const USER_DETAILS_PAGE = '/users/:id';
export const CANDIDATES_PAGE = '/candidates';
export const CANDIDATES_DETAILS_PAGE = '/candidates/:id';
export const FORGOT_PASSWORD_CONFIRMATION_PAGE = '/forgot-password-confirmation';
export const RESET_PASSWORD_PAGE = '/reset-password';

+ 1
- 0
src/main.scss Прегледај датотеку

@@ -9,6 +9,7 @@
@import './assets/styles/components/app-button';
@import './assets/styles/components/usersPage';
@import './assets/styles/components/candidatesPage';
@import './assets/styles/components/candidatePage';
@import './assets/styles/components/loader';
@import './assets/styles/components/radio';
@import './assets/styles/components/modal';

+ 265
- 0
src/pages/CandidateDetailsPage/CandidateDetailsPage.js Прегледај датотеку

@@ -0,0 +1,265 @@
import React from "react";
import { useEffect, useState, useRef } from "react";
import { Link, useParams } from "react-router-dom";
import editImage from "../../../src/assets/images/edit.png";
import deleteImage from "../../../src/assets/images/delete.png";
import planeImage from "../../../src/assets/images/planeVectorBlue.png";
import dotnetImage from "../../../src/assets/images/.net_icon.png";
import IconButton from "../../components/IconButton/IconButton";
import { formatDateTime, formatDate } from "../../util/helpers/dateHelpers";
import { setUsersReq } from "../../store/actions/users/usersActions";
import { createCandidateComment } from "../../store/actions/candidate/candidateActions";
import { useDispatch, useSelector } from "react-redux";
import { MentionsInput, Mention } from "react-mentions";
import { fetchCandidate } from "../../store/actions/candidate/candidateActions";
import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/system";

const CandidateDetailsPage = () => {
const dispatch = useDispatch();
const { users } = useSelector((s) => s.users);
const { user } = useSelector((s) => s.user);
const { candidate } = useSelector((s) => s.candidate);
const [value, setValue] = useState("");
let { id } = useParams();
const messageContainer = useRef();
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("680"));

useEffect(() => {
dispatch(fetchCandidate({ id }));
dispatch(setUsersReq());
}, [dispatch]);

useEffect(() => {
messageContainer.current !== undefined
? (messageContainer.current.scrollTop =
messageContainer.current.scrollHeight)
: "";
}, [messageContainer.current, candidate.comments]);

const handleChange = (event) => {
setValue(event.target.value);
};

const getArray = () => {
let newArray = users.map(function (value) {
return { id: value.id, display: value.firstName + value.lastName };
});

return newArray;
};

const tranformDisplay = (id, display) => {
console.log(id);
return "@" + display + " ";
};

const sendComment = () => {
// can't send an empty message
if (value.trim().length === 0) {
return;
}
dispatch(
createCandidateComment({
content: value,
userId: user.id,
applicantId: parseInt(id),
user: {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
},
})
);
setValue("");
};

return (candidate && Object.keys(candidate).length === 0) ||
user === undefined ? (
<p>Loading...</p>
) : (
<div className="main-candidate-container pl-144 pt-36px">
<div className="l-t-rectangle"></div>
<div className="r-b-rectangle"></div>
<div className="top-candidate-container">
<div>
<p className="candidate-header">Kandidat</p>
<span className="separation-line">|</span>
<p className="candidate-lower-header">
{candidate.firstName} {candidate.lastName}
</p>
</div>
<div className="candidate-option-container">
<IconButton className="c-btn c-btn--primary-outlined candidate-btn">
Obrisi
<img src={deleteImage} alt="delete" className="candidates-image" />
</IconButton>
<IconButton className="c-btn c-btn--primary-outlined candidate-btn">
Uredi profil
<img src={editImage} alt="edit" className="candidates-image" />
</IconButton>
</div>
</div>
<div className="content-candidate-container">
<div className="details-candidate-container">
<p style={{ margin: 0 }}>
{candidate.experience === 0
? "No experience"
: "Experience:" + candidate.experience}
</p>
<div className="technologies-candidate-container">
{candidate.technologyApplicants.map((obj, index) => (
<div className="technology-candidate-card" key={index}>
{obj.technology.name}
</div>
))}
</div>
<div className="candidate-informations-container">
<div className="candidate-informations-sub-container">
<div className="candidate-property-container">
<p className="informations-candidate-header">Kontakt</p>
<p className="candidate-property">Email:</p>
<p className="candidate-property">Telefon:</p>
</div>
<div
style={{ alignSelf: "flex-end", marginLeft: 42 }}
className="candidate-property-container"
>
<p className="candidate-property-value">{candidate.email}</p>
<p className="candidate-property-value">
{candidate.phoneNumber}
</p>
</div>
</div>
<div className="candidate-informations-sub-container">
<div className="candidate-property-container">
<p className="informations-candidate-header">Drustvene mreze</p>
<p className="candidate-property">Linkedln</p>
<p className="candidate-property">GitHub</p>
<p className="candidate-property">BitBucket</p>
</div>
<div
style={{ alignSelf: "flex-end", marginLeft: 42 }}
className="candidate-property-container"
>
<p className="candidate-property-value">
{candidate.linkedlnLink ?? "/"}
</p>
<p className="candidate-property-value">
{candidate.gitHubLink ?? "/"}
</p>
<p className="candidate-property-value">
{candidate.bitBucketLink ?? "/"}
</p>
</div>
</div>
</div>
</div>
<div className="comment-container">
<div
style={{
minHeight: "219px",
overflowX: "auto",
paddingBottom: "10px",
paddingTop: "10px",
}}
ref={messageContainer}
>
{candidate.comments.map((comment, index) => (
<div className="comment-sub-container" key={index}>
<div className="comment-sender">
<p>
{comment.user.firstName.charAt(0)}
{comment.user.lastName.charAt(0)}
</p>
</div>
<div className="comment-message">
<p className="comment-message-content">{comment.content}</p>
<p className="comment-message-date">
{formatDateTime(comment.dateOfSending)}
</p>
</div>
</div>
))}
</div>
<div className="comment-separation-line"></div>
<div className="send-comment-container">
<p>Komentar</p>
<div className="send-comment-sub-container">
<MentionsInput
value={value}
onChange={handleChange}
className="comment-input"
placeholder={"ex. Odlican kandidat"}
style={{
input: !matches
? { padding: 18, borderRadius: 7 }
: { padding: 9, borderRadius: 5 },
control: !matches
? {
padding: 18,
color: "#fff",
suggestions: { list: { backgroundColor: "#F4F4F4" } },
}
: {
padding: 9,
color: "#fff",
suggestions: { list: { backgroundColor: "#F4F4F4" } },
},
}}
>
<Mention
trigger="@"
className="comment-input-list"
data={getArray}
displayTransform={(id, display) =>
tranformDisplay(id, display)
}
markup="@[__display__]"
style={{ highlighter: { padding: 18, borderRadius: 7 } }}
/>
</MentionsInput>
<div className="comment-send-btn" onClick={sendComment}>
<img
src={planeImage}
alt="plane"
className="candidates-image"
/>
<p>Komentar</p>
</div>
</div>
</div>
</div>
</div>
<div className="applicant-ads-container">
<p>Sve prijave</p>
<div className="applicant-ads-sub-container">
{candidate.applicantAds.map((obj, index) => (
<div key={index} className="applicant-add">
<p className="applicant-add-date">
{formatDate(obj.add.createdAt)}
</p>
<p className="applicant-add-title">{obj.add.title}</p>
<img
src={dotnetImage}
alt="technology-image"
className="applicant-add-technology-image"
/>
<div className="applicant-add-site">dilig.net</div>
</div>
))}
</div>
</div>
<div className="applicant-ads-buttons-container">
<Link to="/candidates" className="applicant-ads-back-button">
Nazad na sve kandidate
</Link>
<div className="applicant-cv-button">Preuzmi cv</div>
</div>
</div>
);
};

export default CandidateDetailsPage;

+ 27
- 5
src/pages/CandidatesPage/CandidatesPage.js Прегледај датотеку

@@ -2,7 +2,6 @@ import IconButton from "../../components/IconButton/IconButton";
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchCandidates } from "../../store/actions/candidates/candidatesActions";
import { refreshUserToken } from "../../store/actions/login/loginActions";
import tableImage from "../../assets/images/table.png";
import modeImage from "../../assets/images/mode.png";
import filterImage from "../../assets/images/filters.png";
@@ -10,22 +9,34 @@ import { formatDate } from "../../util/helpers/dateHelpers";
import planeImage from "../../assets/images/planeVector.png";
import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/system";
import { CANDIDATES_PAGE } from "../../constants/pages";
import PropTypes from 'prop-types';

const CandidatesPage = () => {
const CandidatesPage = ({history}) => {
const dispatch = useDispatch();
const {candidates} = useSelector((s) => s.candidates);
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("sm"));

useEffect(() => {
dispatch(refreshUserToken());
dispatch(fetchCandidates());
}, [dispatch]);

const navigate = (applicantId) => {
history.push({
pathname: CANDIDATES_PAGE + "/" + applicantId,
state: {
from: history.location.pathname,
},
})
}

return candidates[0] === undefined ? (
<p>Loading...</p>
) : (
<div className="main-candidates-container">
<div className="main-candidates-container pl-144 pt-36px">
<div className="l-t-rectangle"></div>
<div className="r-b-rectangle"></div>
<div className="top-candidates-container">
{!matches ? (
<p className="candidates-header">Kandidati</p>
@@ -106,7 +117,8 @@ const CandidatesPage = () => {
<tr
key={index}
className="secondaryRow cadidate-row"
style={{ width: "800px", height: "40px", borderRadius: "12px" }}
style={{ width: "800px", height: "40px", borderRadius: "12px",cursor:"pointer" }}
onClick={() => navigate(candidate.applicantId)}
>
<td>
{candidate.firstName} {candidate.lastName}
@@ -133,4 +145,14 @@ const CandidatesPage = () => {
);
};

CandidatesPage.propTypes = {
history: PropTypes.shape({
replace: PropTypes.func,
push: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string,
}),
}),
}

export default CandidatesPage;

+ 9
- 0
src/request/apiEndpoints.js Прегледај датотеку

@@ -22,4 +22,13 @@ export default {
allArchiveAds: base + "/ads/archive",
adDetails: base + "/ads/details",
},
<<<<<<< HEAD
candidates:{
allCandidates:base + "/applicants",
},
comments:{
addComment:base + '/comments'
}
=======
>>>>>>> FE_dev
};

+ 4
- 2
src/request/candidatesRequest.js Прегледај датотеку

@@ -1,4 +1,6 @@
import { getRequest } from ".";
import { getRequest, postRequest } from ".";
import apiEndpoints from "./apiEndpoints";

export const getAllCandidates = () => getRequest(apiEndpoints.candidates.allCandidates)
export const getAllCandidates = () => getRequest(apiEndpoints.candidates.allCandidates)
export const getCandidate = (id) => getRequest(apiEndpoints.candidates.allCandidates + "/" + id)
export const createComment = (data) => postRequest(apiEndpoints.comments.addComment,data)

+ 20
- 0
src/store/actions/candidate/candidateActionConstants.js Прегледај датотеку

@@ -0,0 +1,20 @@
import {
createErrorType,
createFetchType,
createLoadingType,
createSuccessType,
} from "../actionHelpers";
const CANDIDATE_SCOPE = 'CANDIDATE'
const CANDIDATE_SCOPE_COMMENTS = 'CANDIDATE_COMMENTS'
export const CANDIDATE_FETCH = createFetchType(CANDIDATE_SCOPE);
export const CANDIDATE_ERROR = createErrorType(CANDIDATE_SCOPE)
export const CANDIDATE_SUCCESS = createSuccessType(CANDIDATE_SCOPE)
export const CANDIDATE_LOADING = createLoadingType(CANDIDATE_SCOPE)
export const CANDIDATE_COMMENTS_FETCH = createFetchType(CANDIDATE_SCOPE_COMMENTS);
export const CANDIDATE_COMMENTS_ERROR = createErrorType(CANDIDATE_SCOPE_COMMENTS)
export const CANDIDATE_COMMENTS_SUCCESS = createSuccessType(CANDIDATE_SCOPE_COMMENTS)
export const CANDIDATE_COMMENTS_LOADING = createLoadingType(CANDIDATE_SCOPE_COMMENTS)

+ 38
- 0
src/store/actions/candidate/candidateActions.js Прегледај датотеку

@@ -0,0 +1,38 @@
import {
CANDIDATE_FETCH,
CANDIDATE_ERROR,
CANDIDATE_SUCCESS,
CANDIDATE_COMMENTS_FETCH,
CANDIDATE_COMMENTS_ERROR,
CANDIDATE_COMMENTS_SUCCESS,
} from "./candidateActionConstants";

export const fetchCandidate = (payload) => ({
type: CANDIDATE_FETCH,
payload,
});

export const fetchCandidateError = (payload) => ({
type: CANDIDATE_ERROR,
payload,
});

export const fetchCandidateSuccess = (payload) => ({
type: CANDIDATE_SUCCESS,
payload,
});

export const createCandidateComment = (payload) => ({
type: CANDIDATE_COMMENTS_FETCH,
payload,
});

export const createCandidateCommentError = (payload) => ({
type: CANDIDATE_COMMENTS_ERROR,
payload,
});

export const createCandidateCommentSuccess = (payload) => ({
type: CANDIDATE_COMMENTS_SUCCESS,
payload,
});

+ 2
- 1
src/store/actions/candidates/candidatesActions.js Прегледај датотеку

@@ -16,4 +16,5 @@ import {
export const fetchCandidatesSuccess = (payload) => ({
type: CANDIDATES_SUCCESS,
payload,
});
});


+ 1
- 1
src/store/middleware/requestStatusMiddleware.js Прегледај датотеку

@@ -10,7 +10,7 @@ export default ({ dispatch }) => (next) => (action) => {
if (
error.response.config.url !== apiEndpoints.authentications.login &&
error.response.config.url !==
apiEndpoints.authentications.confirmSecurityQuestion &&
apiEndpoints.authentications.confirmSecurityQuestion &&
error.response.status === 401
) {
return dispatch(logoutUser());

+ 75
- 0
src/store/reducers/candidate/candidateReducer.js Прегледај датотеку

@@ -0,0 +1,75 @@
import createReducer from "../../utils/createReducer";
import {
CANDIDATE_ERROR,
CANDIDATE_SUCCESS,
CANDIDATE_COMMENTS_SUCCESS,
CANDIDATE_COMMENTS_ERROR,
} from "../../actions/candidate/candidateActionConstants";

const initialState = {
candidate: {},
errorMessage: "",
};

export default createReducer(
{
[CANDIDATE_SUCCESS]: setCandidate,
[CANDIDATE_ERROR]: setError,
[CANDIDATE_COMMENTS_SUCCESS]: setComments,
[CANDIDATE_COMMENTS_ERROR]: setCommentsError,
},
initialState
);

function setCandidate(state, action) {
return {
...state,
candidate: action.payload,
};
}

function setError(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

function setComments(state, action) {
const currentDate = new Date();
var datetime =
currentDate.getFullYear() +
"-" +
(currentDate.getMonth() + 1) +
"-" +
currentDate.getDate() +
"T" +
currentDate.getHours() +
":" +
(currentDate.getMinutes() <= 9
? "0" + currentDate.getMinutes()
: currentDate.getMinutes()) +
":" +
(currentDate.getSeconds() <= 9
? "0" + currentDate.getSeconds()
: currentDate.getSeconds());
const obj = {
content: action.payload.myObj.content,
dateOfSending: datetime,
user: action.payload.user,
};
return {
...state,
candidate: {
...state.candidate,
comments: [...state.candidate.comments, obj],
},
};
}

function setCommentsError(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

+ 2
- 2
src/store/reducers/candidates/candidatesReducer.js Прегледај датотеку

@@ -12,7 +12,7 @@ const initialState = {
export default createReducer(
{
[CANDIDATES_SUCCESS]: setCandidates,
[CANDIDATES_ERROR]: setError,
[CANDIDATES_ERROR]: setError
},
initialState
);
@@ -29,4 +29,4 @@ function setError(state, action) {
...state,
errorMessage: action.payload,
};
}
}

+ 16
- 0
src/store/reducers/index.js Прегледај датотеку

@@ -1,3 +1,13 @@
<<<<<<< HEAD
import { combineReducers } from 'redux';
import loginReducer from './login/loginReducer';
import loadingReducer from './loading/loadingReducer';
import userReducer from './user/userReducer';
import randomDataReducer from './randomData/randomDataReducer';
import usersReducer from './user/usersReducer';
import candidatesReducer from './candidates/candidatesReducer';
import candidateReducer from './candidate/candidateReducer';
=======
import { combineReducers } from "redux";
import loginReducer from "./login/loginReducer";
import loadingReducer from "./loading/loadingReducer";
@@ -8,6 +18,7 @@ import adsReducer from "./ad/adsReducer";
import adReducer from "./ad/adReducer";
import archiveAdsReducer from "./ad/archiveAdsReducer";
import candidatesReducer from "./candidates/candidatesReducer";
>>>>>>> FE_dev

export default combineReducers({
login: loginReducer,
@@ -15,8 +26,13 @@ export default combineReducers({
loading: loadingReducer,
randomData: randomDataReducer,
users: usersReducer,
<<<<<<< HEAD
candidates:candidatesReducer,
candidate:candidateReducer
=======
ads: adsReducer,
ad: adReducer,
archiveAds: archiveAdsReducer,
candidates: candidatesReducer,
>>>>>>> FE_dev
});

+ 16
- 17
src/store/reducers/user/userReducer.js Прегледај датотеку

@@ -1,36 +1,35 @@
import createReducer from '../../utils/createReducer';
import createReducer from "../../utils/createReducer";
import {
SET_USER,
SET_USER_ERROR,
RESET_USER_STATE
} from '../../actions/user/userActionConstants';
RESET_USER_STATE,
} from "../../actions/user/userActionConstants";

const initialState = {
id:"",
firstName:"",
lastName:"",
username:"",
token:"",
refreshToken:""
user: {
id: "",
firstName: "",
lastName: "",
username: "",
token: "",
refreshToken: "",
},
errorMessage: "",
};

export default createReducer(
{
[SET_USER]: setUser,
[SET_USER_ERROR]: setUserError,
[RESET_USER_STATE]:resetUserState
[RESET_USER_STATE]: resetUserState,
},
initialState,
initialState
);

function setUser(state, action) {
return {
id:action.payload.id,
firstName:action.payload.firstName,
lastName:action.payload.lastName,
username:action.payload.username,
token:action.payload.token,
refreshToken:action.payload.refreshToken,
...state,
user: action.payload,
};
}


+ 31
- 3
src/store/saga/candidatesSaga.js Прегледај датотеку

@@ -1,15 +1,16 @@
import { all, call, put, takeEvery } from "redux-saga/effects";
import { JWT_TOKEN } from "../../constants/localStorage";
import { addHeaderToken } from "../../request";
import { getAllCandidates } from "../../request/candidatesRequest";
import { getAllCandidates,createComment,getCandidate } from "../../request/candidatesRequest";
import { authScopeStringGetHelper } from "../../util/helpers/authScopeHelpers";
import { CANDIDATES_FETCH } from "../actions/candidates/candidatesActionConstants";
import { CANDIDATES_FETCH} from "../actions/candidates/candidatesActionConstants";
import { fetchCandidatesError,fetchCandidatesSuccess } from "../actions/candidates/candidatesActions";
import { rejectErrorCodeHelper } from '../../util/helpers/rejectErrorCodeHelper';
import { fetchCandidateError, fetchCandidateSuccess,createCandidateCommentSuccess,createCandidateCommentError } from "../actions/candidate/candidateActions";
import { CANDIDATE_FETCH,CANDIDATE_COMMENTS_FETCH } from "../actions/candidate/candidateActionConstants";

export function* getCandidates() {
try {
console.log('oVde smo')
const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN);
yield call(addHeaderToken, JwtToken);
const {data} = yield call(getAllCandidates);
@@ -20,6 +21,33 @@ export function* getCandidates() {
}
}

export function* getSingleCandidate({payload}){
try{
const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN);
yield call(addHeaderToken, JwtToken);
const {data} = yield call(getCandidate,payload.id)
yield put(fetchCandidateSuccess(data))
} catch(error){
const errorMessage = yield call(rejectErrorCodeHelper, error);
yield put(fetchCandidateError(errorMessage))
}
}

export function* addComment(data) {
const {user,...myObj} = data.payload
try{
const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN);
yield call(addHeaderToken, JwtToken);
yield call(createComment,myObj);
yield put(createCandidateCommentSuccess({user,myObj}))
} catch (error){
const errorMessage = yield call(rejectErrorCodeHelper, error);
yield put(createCandidateCommentError(errorMessage))
}
}

export default function* candidatesSaga() {
yield all([takeEvery(CANDIDATES_FETCH, getCandidates)]);
yield all([takeEvery(CANDIDATE_FETCH, getSingleCandidate)]);
yield all([takeEvery(CANDIDATE_COMMENTS_FETCH, addComment)]);
}

+ 8
- 0
src/store/selectors/candidateSelectors.js Прегледај датотеку

@@ -0,0 +1,8 @@
import { createSelector } from "@reduxjs/toolkit";

export const candidateSelector = (state) => state.candidate;

export const selectCandidate = createSelector(
candidateSelector,
(state) => state.candidate,
);

+ 1
- 1
src/util/helpers/dateHelpers.js Прегледај датотеку

@@ -9,7 +9,7 @@ export function formatDate(date, fmt = 'dd.MM.y', locale = enUS) {

export function formatDateTime(date) {
const dt = new Date(date);
return format(dt, 'MM/dd/y hh:mm aa');
return format(dt, 'hh:mm dd.MM.y');
}

export function getDateDay(date) {

+ 30
- 5
yarn.lock Прегледај датотеку

@@ -1156,7 +1156,7 @@
"core-js-pure" "^3.0.0"
"regenerator-runtime" "^0.13.4"

"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@7.12.1":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@7.12.1":
"integrity" "sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA=="
"resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz"
"version" "7.12.1"
@@ -1205,6 +1205,13 @@
dependencies:
"regenerator-runtime" "^0.13.4"

"@babel/runtime@7.4.5":
"integrity" "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ=="
"resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz"
"version" "7.4.5"
dependencies:
"regenerator-runtime" "^0.13.2"

"@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3":
"integrity" "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA=="
"resolved" "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz"
@@ -10355,7 +10362,7 @@
"strip-ansi" "6.0.0"
"text-table" "0.2.0"

"react-dom@^17.0.2":
"react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.0 || ^17.0.0", "react-dom@^17.0.0 || ^18.0.0", "react-dom@^17.0.2", "react-dom@^17.0.2 || ^18.0.0", "react-dom@<18.0.0", "react-dom@>=16.6.0", "react-dom@>=16.8.3":
"integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
"version" "17.0.2"
@@ -10430,7 +10437,17 @@
"resolved" "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz"
"version" "18.2.0"

"react-redux@^7.2.4":
"react-mentions@^4.4.7":
"integrity" "sha512-VNriu2h/uOB+RS0mwZgPG2Vf+UtdDvRh5zbXa2TNc1WqacKuNDgTdhlbo9LEOZRBxRzIeTUYQmYJ7p9M9rDHqQ=="
"resolved" "https://registry.npmjs.org/react-mentions/-/react-mentions-4.4.7.tgz"
"version" "4.4.7"
dependencies:
"@babel/runtime" "7.4.5"
"invariant" "^2.2.4"
"prop-types" "^15.5.8"
"substyle" "^9.1.0"

"react-redux@^7.2.1 || ^8.0.2", "react-redux@^7.2.4":
"integrity" "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ=="
"resolved" "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz"
"version" "7.2.9"
@@ -10592,7 +10609,7 @@
"loose-envify" "^1.4.0"
"prop-types" "^15.6.2"

"react@^17.0.2":
"react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.3.0 || ^17.0.0", "react@^16.6.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0", "react@^16.8.3 || ^17 || ^18", "react@^16.9.0 || ^17.0.0 || ^18", "react@^17.0.0 || ^18.0.0", "react@^17.0.2", "react@^17.0.2 || ^18.0.0", "react@^18.2.0", "react@<18.0.0", "react@>= 16", "react@>= 16.8.0", "react@>=15", "react@>=16.6.0", "react@>=16.8.0", "react@>=16.8.3", "react@17.0.2":
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
"version" "17.0.2"
@@ -10829,7 +10846,7 @@
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz"
"version" "0.11.1"

"regenerator-runtime@^0.13.4", "regenerator-runtime@^0.13.7":
"regenerator-runtime@^0.13.2", "regenerator-runtime@^0.13.4", "regenerator-runtime@^0.13.7":
"integrity" "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz"
"version" "0.13.7"
@@ -12085,6 +12102,14 @@
"resolved" "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz"
"version" "4.0.13"

"substyle@^9.1.0":
"integrity" "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA=="
"resolved" "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz"
"version" "9.4.1"
dependencies:
"@babel/runtime" "^7.3.4"
"invariant" "^2.2.4"

"supports-color@^5.3.0":
"integrity" "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="
"resolved" "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"

Loading…
Откажи
Сачувај