Преглед на файлове

Merge branch 'feature/1518_ad_details_fe_slider' of Neca/HRCenter into FE_dev

pull/40/head
safet.purkovic преди 3 години
родител
ревизия
2bbb2bcce1

+ 99
- 0
package-lock.json Целия файл

@@ -37,9 +37,11 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-select": "^4.3.1",
"react-slick": "^0.29.0",
"redux": "^4.1.0",
"redux-saga": "^1.1.3",
"sass": "^1.34.1",
"slick-carousel": "^1.8.1",
"web-vitals": "^1.1.2",
"yup": "^0.32.9"
},
@@ -6442,6 +6444,11 @@
"node": ">=0.10.0"
}
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/clean-css": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
@@ -8232,6 +8239,11 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/enquire.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
"integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw=="
},
"node_modules/enquirer": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
@@ -14524,6 +14536,14 @@
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
},
"node_modules/json2mq": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
"integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
"dependencies": {
"string-convert": "^0.2.0"
}
},
"node_modules/json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
@@ -18813,6 +18833,22 @@
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-slick": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz",
"integrity": "sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA==",
"dependencies": {
"classnames": "^2.2.5",
"enquire.js": "^2.1.6",
"json2mq": "^0.2.0",
"lodash.debounce": "^4.0.8",
"resize-observer-polyfill": "^1.5.0"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-test-renderer": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz",
@@ -19259,6 +19295,11 @@
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz",
"integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ=="
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/resolve": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz",
@@ -20376,6 +20417,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/slick-carousel": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
"integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
"peerDependencies": {
"jquery": ">=1.8.0"
}
},
"node_modules/snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -20992,6 +21041,11 @@
}
]
},
"node_modules/string-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="
},
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -29337,6 +29391,11 @@
}
}
},
"classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"clean-css": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
@@ -30779,6 +30838,11 @@
}
}
},
"enquire.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
"integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw=="
},
"enquirer": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
@@ -35491,6 +35555,14 @@
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
},
"json2mq": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
"integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
"requires": {
"string-convert": "^0.2.0"
}
},
"json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
@@ -38936,6 +39008,18 @@
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
}
},
"react-slick": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz",
"integrity": "sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA==",
"requires": {
"classnames": "^2.2.5",
"enquire.js": "^2.1.6",
"json2mq": "^0.2.0",
"lodash.debounce": "^4.0.8",
"resize-observer-polyfill": "^1.5.0"
}
},
"react-test-renderer": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz",
@@ -39286,6 +39370,11 @@
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz",
"integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ=="
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz",
@@ -40171,6 +40260,11 @@
}
}
},
"slick-carousel": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
"integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA=="
},
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -40697,6 +40791,11 @@
}
}
},
"string-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="
},
"string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",

+ 2
- 0
package.json Целия файл

@@ -32,9 +32,11 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-select": "^4.3.1",
"react-slick": "^0.29.0",
"redux": "^4.1.0",
"redux-saga": "^1.1.3",
"sass": "^1.34.1",
"slick-carousel": "^1.8.1",
"web-vitals": "^1.1.2",
"yup": "^0.32.9"
},

Двоични данни
src/assets/images/facebook.png Целия файл


Двоични данни
src/assets/images/instagram.png Целия файл


Двоични данни
src/assets/images/linkedin.png Целия файл


+ 210
- 16
src/assets/styles/components/_ads.scss Целия файл

@@ -7,6 +7,9 @@ h3 {
.ads {
margin-top: 36px;
padding-left: 72px;
@include media-below($bp-xl) {
padding-left: 36px !important;
}
}

.active-ads-header {
@@ -15,6 +18,9 @@ h3 {
justify-content: space-between;
align-items: center;
padding-right: 5rem;
@include media-below($bp-xl) {
padding: 0 0.75rem !important;
}
}

.filter-vector {
@@ -25,6 +31,7 @@ h3 {
display: flex;
margin-top: 39px;
position: relative;
width: 100%;
}

.active-ads-ads-a {
@@ -38,6 +45,12 @@ h3 {
flex-direction: column;
align-items: center;
justify-content: center;
@include media-below($bp-xl) {
flex-direction: row;
gap: 10px;
justify-content: flex-start;
padding-left: 0.75rem !important;
}
}

.active-ads-ads-arrows button {
@@ -54,36 +67,50 @@ h3 {
.active-ads-ads-ad {
padding-left: 81px;
display: flex;
flex-direction: row;
width: 100%;
@include media-below($bp-xl) {
padding-left: 0;
}
}

.archived-ads {
.archived-ads,
.ad-details-applicants {
margin-top: 56px;
}

.archived-ads-header {
.archived-ads-header,
.ad-details-applicants-header {
padding-left: 81px;
@include media-below($bp-xl) {
padding: 0 0.75rem;
}
}

.archived-ads-ads {
.archived-ads-ads,
.ad-details-applicants-applicants {
display: flex;
margin-top: 27px;
position: relative;
}

.archived-ads-ads-a {
.archived-ads-ads-a,
.ad-details-applicants-applicants-a {
position: absolute;
top: 50%;
transform: translate(0, -50%);
}

.archived-ads-ads-arrows {
.archived-ads-ads-arrows,
.ad-details-applicants-applicants-arrows {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.archived-ads-ads-arrows button {
.archived-ads-ads-arrows button,
.ad-details-applicants-applicants-arrows button {
margin: 9px 0;
box-sizing: border-box;
width: 45px;
@@ -94,9 +121,15 @@ h3 {
cursor: pointer;
}

.archived-ads-ads-ad {
.archived-ads-ads-ad,
.ad-details-applicants-applicants-applicant {
padding-left: 81px;
display: flex;
flex-direction: row;
width: 100%;
@include media-below($bp-xl) {
padding: 0;
}
}

.archive-ad {
@@ -107,14 +140,42 @@ h3 {
align-items: center;
padding: 36px;
gap: 18px;
width: 247px;
height: 215px;
left: 1px;
left: 0px;
top: 0px;
background: #ffffff;
border: 1px solid #e4e4e4;
border-radius: 12px;
margin-right: 27px;
transition: 0.3s;
cursor: pointer;
}

.ad-details-candidate {
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 36px;
left: 0px;
top: 0px;
background: #ffffff;
border: 1px solid #e4e4e4;
border-radius: 12px;
margin-right: 27px;
transition: 0.3s;
cursor: pointer;
}

.ad-details-candidate {
padding: 54px 72px !important;
}

.archive-ad:hover,
.ad-details-candidate:hover {
scale: 1.05;
border-color: $mainBlue !important;
background-color: $mainBlueLight !important;
}

.archive-ad-date p {
@@ -129,7 +190,21 @@ h3 {
flex-grow: 0;
}

.archive-ad-title h3 {
.ad-details-candidate-date p {
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 15px;
color: #272727;
margin-bottom: 18px !important;
flex: none;
order: 0;
flex-grow: 0;
}

.archive-ad-title h3,
.ad-details-candidate-title h3 {
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 600;
@@ -149,7 +224,12 @@ h3 {
flex-grow: 0;
}

.archive-ad-experience p {
.ad-details-candidate-experience {
margin-bottom: 9px;
}

.archive-ad-experience p,
.ad-details-candidate-experience p {
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 400;
@@ -161,13 +241,26 @@ h3 {
flex-grow: 0;
}

.slick-list {
padding: 0.75rem !important;
overflow-y: visible !important;
@include media-below($bp-xl) {
}
}

.slick-dots {
display: none !important;
}

.slick-arrow {
display: none !important;
}

.ad-card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 425px;
height: 370px;
padding: 72px;
background: #ffffff;
border: 1px solid #e4e4e4;
@@ -175,6 +268,34 @@ h3 {
gap: 18px;
margin-right: 36px;
cursor: pointer;
transition: 0.3s;
@include media-below($bp-xl) {
margin-right: 20px !important;
padding: 36px !important;
}
}

.ad-card:hover {
scale: 1.05;
border-color: $mainBlue !important;
background-color: $mainBlueLight !important;
}

.ad-card-buttons-button{
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 9px !important;
gap: 10px !important;
width: 36px !important;
height: 36px !important;
border: 1px solid #e4e4e4 !important;
background-color: white;
border-radius: 9px !important;
flex: none;
order: 0;
flex-grow: 0;
}

.ad-card-date p {
@@ -223,17 +344,46 @@ h3 {
}

.ad-card-buttons {
overflow: hidden;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
padding: 0px;
gap: 18px;
width: 281px;
height: 38px;
flex: none;
order: 4;
flex-grow: 0;
@include media-below($bp-xl) {
gap: 9px !important;
}
}

.ad-details-candidate-buttons {
display: flex;
align-items: center;
justify-content: space-between !important;
margin-bottom: 9px;
width: 205px !important;
flex-wrap: wrap;
}

.ad-details-candidate-cv a {
color: $mainBlue;
}

.ad-details-candidate-buttons button {
box-sizing: border-box;
margin: 0 4.5px;
padding: 9px;
gap: 10px;
border: 1px solid #e4e4e4;
border-radius: 9px;
flex: none;
order: 0;
flex-grow: 0;
background-color: white;
margin-bottom: 9px;
}

.ad-card-buttons button {
@@ -253,6 +403,10 @@ h3 {
flex-grow: 0;
}

.ad-details-candidate-technologies button {
background-color: white !important;
}

.add-ad {
margin-top: 49px;
display: flex;
@@ -260,6 +414,9 @@ h3 {
align-items: center;
padding-right: 5rem !important;
padding-bottom: 49px;
@include media-below($bp-xl) {
padding-right: 18px !important;
}
}

.add-ad-btn {
@@ -273,6 +430,9 @@ h3 {
height: 51px;
background: #226cb0;
border-radius: 9px;
@include media-below($bp-xl) {
width: 147px;
}
}

.ad-filters-header-container {
@@ -344,6 +504,9 @@ h3 {

.ad-details {
padding: 45px 72px 18px 223px;
@include media-below($bp-xl) {
padding: 18px 36px !important;
}
}

.ad-details-tech-logo {
@@ -352,6 +515,17 @@ h3 {
display: flex;
align-items: center;
justify-content: space-between;
@include media-below($bp-xl) {
left: 0;
}
}

.ad-details-applicants {
position: relative;
left: -80px !important;
@include media-below($bp-xl) {
left: 0 !important;
}
}

.ad-details-tech-logo-title {
@@ -370,6 +544,12 @@ h3 {
font-weight: 600;
}

.ad-details-tech-logo-date {
@include media-below($bp-xl) {
margin-bottom: 18px;
}
}

.ad-details-tech-logo-date p span {
color: #9d9d9d;
}
@@ -429,6 +609,9 @@ h3 {
justify-content: flex-end;
align-items: center;
margin-top: 36px;
@include media-below($bp-xl) {
justify-content: center;
}
}

.ad-details-buttons > button {
@@ -437,4 +620,15 @@ h3 {

.ad-details-buttons-link {
color: $mainBlue;
}

.ad-details-candidate-technologies {
display: flex;
justify-content: space-between;
max-width: 208px;
flex-wrap: wrap;
}

.hiddenAd {
visibility: hidden !important;
}

+ 31
- 6
src/components/Ads/Ad.js Целия файл

@@ -1,6 +1,11 @@
import React from "react";
import PropTypes from "prop-types";
import logoReact from "../../assets/images/logo_react.png";
import { useTheme } from "@emotion/react";
import { useMediaQuery } from "@mui/material";
import linkedin from "../../assets/images/linkedin.png";
import facebook from "../../assets/images/facebook.png";
import instagram from "../../assets/images/instagram.png";

const Ad = ({
title,
@@ -8,9 +13,13 @@ const Ad = ({
createdAt,
expiredAt,
onShowAdDetails,
className,
}) => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("sm"));

return (
<div className="ad-card" onClick={onShowAdDetails}>
<div className={`ad-card ${className}`} onClick={onShowAdDetails}>
<div className="ad-card-date">
<p>
{new Date(createdAt).toLocaleDateString()} -{" "}
@@ -30,11 +39,26 @@ const Ad = ({
<p>{minimumExperience}+ years of experience</p>
</div>

<div className="ad-card-buttons">
<button>LinkedIn</button>
<button>Facebook</button>
<button disabled>Instagram</button>
</div>
{!matches && (
<div className="ad-card-buttons">
<button>LinkedIn</button>
<button>Facebook</button>
<button disabled>Instagram</button>
</div>
)}
{matches && (
<div className="ad-card-buttons">
<button className="ad-card-buttons-button">
<img src={linkedin} />
</button>
<button className="ad-card-buttons-button">
<img src={facebook} />
</button>
<button disabled className="ad-card-buttons-button">
<img src={instagram} />
</button>
</div>
)}
</div>
);
};
@@ -46,6 +70,7 @@ Ad.propTypes = {
createdAt: PropTypes.any,
expiredAt: PropTypes.any,
onShowAdDetails: PropTypes.func,
className: PropTypes.any,
};

export default Ad;

+ 48
- 0
src/components/Ads/AdDetailsCandidateCard.js Целия файл

@@ -0,0 +1,48 @@
import React from "react";
import PropTypes from "prop-types";

const AdDetailsCandidateCard = ({
className,
firstName,
lastName,
experience,
cv,
}) => {
return (
<div className={`ad-details-candidate ${className}`}>
<div className="ad-details-candidate-date">
<p>{new Date().toLocaleDateString()}</p>
</div>
<div className="ad-details-candidate-title">
<h3>
{firstName} {lastName}
</h3>
</div>
<div className="ad-details-candidate-experience">
{experience > 0 ? (
<p>{experience}+ years of experience</p>
) : (
<p>No experience</p>
)}
</div>
<div className="ad-details-candidate-buttons">
<button>React</button>
<button>.NET</button>
<button>Angular</button>
</div>
<div className="ad-details-candidate-cv">
<a href="#">{cv}</a>
</div>
</div>
);
};

AdDetailsCandidateCard.propTypes = {
className: PropTypes.any,
firstName: PropTypes.string,
lastName: PropTypes.string,
experience: PropTypes.number,
cv: PropTypes.string,
};

export default AdDetailsCandidateCard;

+ 30
- 5
src/components/Ads/ArchiveAd.js Целия файл

@@ -1,23 +1,48 @@
import React from "react";
import PropTypes from "prop-types";
import net_icon from "../../assets/images/.net_icon.png";

const ArchiveAd = () => {
const ArchiveAd = ({
className,
title,
minimumExperience,
createdAt,
expiredAt,
onShowAdDetails,
}) => {
return (
<div className="archive-ad">
<div className={`archive-ad ${className}`} onClick={onShowAdDetails}>
<div className="archive-ad-date">
<p>05.07.22 - 20.08.22</p>
<p>
{new Date(createdAt).toLocaleDateString()} -{" "}
{new Date(expiredAt).toLocaleDateString()}
</p>
</div>
<div className="archive-ad-title">
<h3>.NET Intern</h3>
<h3>{title}</h3>
</div>
<div className="archive-ad-image">
<img src={net_icon} alt=".net icon" />
</div>
<div className="archive-ad-experience">
<p>No experience needed</p>
{minimumExperience > 0 ? (
<p>{minimumExperience}+ years of experience</p>
) : (
<p>No experience needed</p>
)}
</div>
</div>
);
};

ArchiveAd.propTypes = {
id: PropTypes.number,
title: PropTypes.string,
minimumExperience: PropTypes.number,
createdAt: PropTypes.any,
expiredAt: PropTypes.any,
onShowAdDetails: PropTypes.func,
className: PropTypes.any,
};

export default ArchiveAd;

+ 7
- 2
src/components/Button/FilterButton.js Целия файл

@@ -2,19 +2,24 @@ import { IconButton } from "@mui/material";
import PropTypes from "prop-types";
import React from "react";
import filters from "../../assets/images/filters.png";
import { useTheme } from "@mui/system";
import { useMediaQuery } from "@mui/material";

const FilterButton = ({ onShowFilters }) => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("sm"));

return (
<IconButton
className={"c-btn--primary-outlined c-btn userPageBtn ml-20px no-padding"}
onClick={onShowFilters}
>
Filteri
{!matches && "Filteri"}
<img
style={{
position: "relative",
top: -0.25,
marginLeft: "9px",
paddingLeft: matches ? "0px" : "10px",
}}
src={filters}
/>

+ 89
- 79
src/i18n/resources/en.js Целия файл

@@ -1,105 +1,115 @@
export default {
app: {
title: 'HR Center'
title: "HR Center",
},
refresh: {
title: 'Are you active?',
cta:
"You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.",
title: "Are you active?",
cta: "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.",
},
common: {
close: 'Close',
trademark: 'TM',
search: 'Search',
error: 'Error',
continue: 'Continue',
labelUsername: 'Username',
labelPassword: 'Password',
or: 'or',
next: 'Next',
nextPage: 'Next page',
previousPage: 'Previous page',
back: 'Back',
goBack: 'Go Back',
ok: 'Ok',
done: 'Done',
confirm: 'Confirm',
printDownload: 'Print/Download',
cancel: 'Cancel',
remove: 'Remove',
invite: 'Invite',
save: 'Save',
complete: 'Complete',
download: 'Download',
yes: 'Yes',
no: 'No',
to: 'to',
select: 'Select...',
none: 'None',
close: "Close",
trademark: "TM",
search: "Search",
error: "Error",
continue: "Continue",
labelUsername: "Username",
labelPassword: "Password",
or: "or",
next: "Next",
nextPage: "Next page",
previousPage: "Previous page",
back: "Back",
goBack: "Go Back",
ok: "Ok",
done: "Done",
confirm: "Confirm",
printDownload: "Print/Download",
cancel: "Cancel",
remove: "Remove",
invite: "Invite",
save: "Save",
complete: "Complete",
download: "Download",
yes: "Yes",
no: "No",
to: "to",
select: "Select...",
none: "None",
date: {
range: '{{start}} to {{end}}',
range: "{{start}} to {{end}}",
},
},
login: {
welcome: 'Welcome!',
welcome: "Welcome!",
dontHaveAccount: "Don't have an account? ",
emailFormat: 'Invalid email address format.',
emailRequired: 'An email or username is required.',
noUsers: 'There are no users with that email.',
passwordStrength: 'Your password is {{strength}}.',
passwordLength: 'Your password contain between 8 and 50 characters.',
signUpRecommendation: 'Sign up',
email: 'Please enter your email address or username to log in:',
logInTitle: 'Log In',
logIn: 'Log In',
signUp: 'Sign Up',
usernameRequired: 'Username is required.',
passwordRequired: 'A Password is required.',
forgotYourPassword: 'Forgot your password?',
forgotPasswordEmail:'Email',
useDifferentEmail: 'Use different email address or username',
signInWithGoogle: 'Continue with google'
emailFormat: "Invalid email address format.",
emailRequired: "An email or username is required.",
noUsers: "There are no users with that email.",
passwordStrength: "Your password is {{strength}}.",
passwordLength: "Your password contain between 8 and 50 characters.",
signUpRecommendation: "Sign up",
email: "Please enter your email address or username to log in:",
logInTitle: "Log In",
logIn: "Log In",
signUp: "Sign Up",
usernameRequired: "Username is required.",
passwordRequired: "A Password is required.",
forgotYourPassword: "Forgot your password?",
forgotPasswordEmail: "Email",
useDifferentEmail: "Use different email address or username",
signInWithGoogle: "Continue with google",
},
password: {
weak: 'weak',
average: 'average',
good: 'good',
strong: 'strong',
weak: "weak",
average: "average",
good: "good",
strong: "strong",
},
forgotPassword: {
title: 'Forgot Password',
label: 'Send email',
emailRequired: 'An email is required.',
emailFormat: 'Invalid email address format.',
title: "Forgot Password",
label: "Send email",
emailRequired: "An email is required.",
emailFormat: "Invalid email address format.",
forgotPassword: {
title: 'Forgot Password',
title: "Forgot Password",
subtitle:
'Please answer the security question to gain access to your account:',
label: 'Reset Password',
"Please answer the security question to gain access to your account:",
label: "Reset Password",
},
},
notFound: {
text: "We're sorry but we couldn't find the page you were looking for.",
goBack: 'Go back to homepage',
goBack: "Go back to homepage",
},
errorPage: {
text:
"We're sorry, an internal server error came up. Please be patient or try again later.",
goBack: 'Go back to homepage',
logout: 'Logout',
text: "We're sorry, an internal server error came up. Please be patient or try again later.",
goBack: "Go back to homepage",
logout: "Logout",
},
apiErrors:{
ClientIpAddressIsNullOrEmpty:"Client Ip address is null or empty",
UsernameDoesNotExist: "Username does not exist"
apiErrors: {
ClientIpAddressIsNullOrEmpty: "Client Ip address is null or empty",
UsernameDoesNotExist: "Username does not exist",
},
nav: {
ads: "Ads",
selectionFlow: "Selection flow",
candidates: "Candidates",
planer: "Planer",
patterns: "Patterns",
stats: "Stats",
users: "Users",
signOut: "Sign Out",
},
ads: {
activeAds: "Active Ads",
archiveAds: "Archive",
adDetailsDescription:
"Team Diligent is constantly growing! We are looking for a team player that will work with experienced engineers. If technology is your passion and you are ready to move the boundaries of your knowledge every day, then, Diligent is the right place for you. If you are not from Niš, we are offering a full remote position.",
adDetailsExperience: "years of experience",
adDetailsExpiredAt: "Expired at",
adDetailsKeyResponsibilities: "Key Responsibilities",
adDetailsRequirements: "Requirements",
adDetailsOffer: "What We Offer",
archiveAdsCandidates: "Registered candidates",
},
nav:{
ads: 'Ads',
selectionFlow: 'Selection flow',
candidates: 'Candidates',
planer: 'Planer',
patterns: 'Patterns',
stats: 'Stats',
users: 'Users',
signOut: 'Sign Out'
}
};

+ 90
- 80
src/i18n/resources/rs.js Целия файл

@@ -1,77 +1,79 @@
export default {
app: {
title: 'HR Centar'
title: "HR Centar",
},
refresh: {
// title: 'Are you active?',
// cta:
// "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.",
// title: 'Are you active?',
// cta:
// "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.",
},
common: {
// close: 'Close',
// trademark: 'TM',
// search: 'Search',
// error: 'Error',
// continue: 'Continue',
labelUsername: 'Korisničko ime',
labelPassword: 'Šifra',
labelConfirmPassword: 'Ponovljena šifra',
or: 'ili',
// next: 'Next',
// nextPage: 'Next page',
// previousPage: 'Previous page',
// back: 'Back',
// goBack: 'Go Back',
// ok: 'Ok',
// done: 'Done',
// confirm: 'Confirm',
// printDownload: 'Print/Download',
// cancel: 'Cancel',
// remove: 'Remove',
// invite: 'Invite',
// save: 'Save',
// complete: 'Complete',
// download: 'Download',
// yes: 'Yes',
// no: 'No',
// to: 'to',
// select: 'Select...',
// none: 'None',
// date: {
// range: '{{start}} to {{end}}',
// },
// close: 'Close',
// trademark: 'TM',
// search: 'Search',
// error: 'Error',
// continue: 'Continue',
labelUsername: "Korisničko ime",
labelPassword: "Šifra",
labelConfirmPassword: "Ponovljena šifra",
or: "ili",
// next: 'Next',
// nextPage: 'Next page',
// previousPage: 'Previous page',
// back: 'Back',
// goBack: 'Go Back',
// ok: 'Ok',
// done: 'Done',
// confirm: 'Confirm',
// printDownload: 'Print/Download',
// cancel: 'Cancel',
// remove: 'Remove',
// invite: 'Invite',
// save: 'Save',
// complete: 'Complete',
// download: 'Download',
// yes: 'Yes',
// no: 'No',
// to: 'to',
// select: 'Select...',
// none: 'None',
// date: {
// range: '{{start}} to {{end}}',
// },
},
login: {
welcome: 'Dobrodošli!',
// dontHaveAccount: "Don't have an account? ",
// emailFormat: 'Invalid email address format.',
// emailRequired: 'An email or username is required.',
// noUsers: 'There are no users with that email.',
// passwordStrength: 'Your password is {{strength}}.',
// passwordLength: 'Your password contain between 8 and 50 characters.',
// signUpRecommendation: 'Sign up',
// email: 'Please enter your email address or username to log in:',
logInTitle: 'Prijavi se',
logIn: 'Prijavi se',
// signUp: 'Sign Up',
usernameRequired: 'Potrebno je uneti korisničko ime.',
passwordRequired: 'Potrebno je uneti šifru.',
forgotYourPassword: 'Zaboravio/la si šifru?',
resetYourPassword: 'Nova šifra',
resetYourPasswordHelpText: 'Unesi novu šifru.',
forgotYourPasswordHelpText: 'Samo unesi e-mail adresu svog HR Center profila.',
forgotYourPasswordButton: 'POŠALJI',
forgotYourPasswordBackLink: 'Nazad na Login',
forgotYourPasswordConfimation: 'Proveri email adresu da bi resetovao šifru.',
passwordDontMatch: 'Šifre se ne poklapaju.',
// _useDifferentEmail: 'Use different email address or username',
// get useDifferentEmail() {
// return this._useDifferentEmail;
// },
// set useDifferentEmail(value) {
// this._useDifferentEmail = value;
// },
signInWithGoogle: 'Prijava putem Google-a'
welcome: "Dobrodošli!",
// dontHaveAccount: "Don't have an account? ",
// emailFormat: 'Invalid email address format.',
// emailRequired: 'An email or username is required.',
// noUsers: 'There are no users with that email.',
// passwordStrength: 'Your password is {{strength}}.',
// passwordLength: 'Your password contain between 8 and 50 characters.',
// signUpRecommendation: 'Sign up',
// email: 'Please enter your email address or username to log in:',
logInTitle: "Prijavi se",
logIn: "Prijavi se",
// signUp: 'Sign Up',
usernameRequired: "Potrebno je uneti korisničko ime.",
passwordRequired: "Potrebno je uneti šifru.",
forgotYourPassword: "Zaboravio/la si šifru?",
resetYourPassword: "Nova šifra",
resetYourPasswordHelpText: "Unesi novu šifru.",
forgotYourPasswordHelpText:
"Samo unesi e-mail adresu svog HR Center profila.",
forgotYourPasswordButton: "POŠALJI",
forgotYourPasswordBackLink: "Nazad na Login",
forgotYourPasswordConfimation:
"Proveri email adresu da bi resetovao šifru.",
passwordDontMatch: "Šifre se ne poklapaju.",
// _useDifferentEmail: 'Use different email address or username',
// get useDifferentEmail() {
// return this._useDifferentEmail;
// },
// set useDifferentEmail(value) {
// this._useDifferentEmail = value;
// },
signInWithGoogle: "Prijava putem Google-a",
},
// password: {
// weak: 'weak',
@@ -105,18 +107,26 @@ export default {
// ClientIpAddressIsNullOrEmpty:"Client Ip address is null or empty",
// UsernameDoesNotExist: "Username does not exist"
// },
nav:{
ads: 'Oglasi',
selectionFlow: 'Tok Selekcije',
candidates: 'Kandidati',
planer: 'Planer',
patterns: 'Šabloni',
stats: 'Statistika',
users: 'Korisnici',
signOut: 'Izloguj se'
},
ads: {
activeAds: "Aktivni Oglasi",
archiveAds: "Arhiva"
}
nav: {
ads: "Oglasi",
selectionFlow: "Tok Selekcije",
candidates: "Kandidati",
planer: "Planer",
patterns: "Šabloni",
stats: "Statistika",
users: "Korisnici",
signOut: "Izloguj se",
},
ads: {
activeAds: "Aktivni Oglasi",
archiveAds: "Arhiva",
adDetailsDescription:
"Tim Diligent neprestano raste! Tražimo timskog igrača koji će raditi s iskusnim inženjerima. Ako je tehnologija vaša strast i spremni ste svaki dan pomicati granice svog znanja, onda je Diligent pravo mesto za vas. Ukoliko niste iz Niša, nudimo full-time remote poziciju.",
adDetailsExperience: "godina iskustva",
adDetailsExpiredAt: "Rok prijave do",
adDetailsKeyResponsibilities: "Ključne Odgovornosti",
adDetailsRequirements: "Zahtevi",
adDetailsOffer: "Šta Nudimo",
archiveAdsCandidates: "Prijavljeni kandidati",
},
};

+ 209
- 94
src/pages/AdsPage/AdDetailsPage.js Целия файл

@@ -1,106 +1,221 @@
import { IconButton } from "@mui/material";
import React from "react";
import React, { useEffect, useRef } from "react";
import { IconButton, useMediaQuery } from "@mui/material";
import aspNetIcon from "../../assets/images/.net_icon.png";
import { Link } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { setAdReq } from "../../store/actions/ad/adActions";
import { selectAd } from "../../store/selectors/adSelectors";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import arrow_left from "../../assets/images/arrow_left.png";
import arrow_right from "../../assets/images/arrow_right.png";
import AdDetailsCandidateCard from "../../components/Ads/AdDetailsCandidateCard";

const AdDetailsPage = () => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("sm"));
const { id } = useParams();
const ad = useSelector(selectAd);
const dispatch = useDispatch();
const { t } = useTranslation();
const archiveAdsSliderRef = useRef();

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

var settings = {
dots: false,
infinite: false,
speed: 500,
initialSlide: 0,
responsive: [
{
breakpoint: 1024,
settings: {
slidesToShow: 3,
slidesToScroll: 3,
infinite: true,
dots: false,
},
},
{
breakpoint: 900,
settings: {
slidesToShow: 2,
slidesToScroll: 2,
initialSlide: 0,
},
},
{
breakpoint: 480,
settings: {
slidesToShow: 1,
slidesToScroll: 1,
},
},
],
};

const archiveAdsArrowLeftHandler = () => {
archiveAdsSliderRef.current.slickPrev();
};

const archiveAdsArrowRightHandler = () => {
archiveAdsSliderRef.current.slickNext();
};

const getDummyArchiveAds = (len) => {
const ads = [];

for (let i = 0; i < 5 - len + 1; i++) {
ads.push(<AdDetailsCandidateCard key={i} className="hiddenAd" />);
}

return ads;
};

return (
<div className="ad-details">
<div className="ad-details-tech-logo">
<div className="ad-details-tech-logo-title">
<div className="ad-details-tech-logo-title-img">
<img src={aspNetIcon} alt="asp-net-icon" />
<>
{!ad && <p>Radi</p>}
{ad && (
<div className="ad-details">
{matches && (
<div className="ad-details-tech-logo-date">
<p>
<span>{t("ads.adDetailsExpiredAt")}: </span>
{new Date(ad.expiredAt).toLocaleDateString()}
</p>
</div>
)}
<div className="ad-details-tech-logo">
<div className="ad-details-tech-logo-title">
<div className="ad-details-tech-logo-title-img">
<img src={aspNetIcon} alt="asp-net-icon" />
</div>
<div className="ad-details-tech-logo-title-title">
<h1>{ad.title}</h1>
</div>
<div className="ad-details-tech-logo-title-sub">
<sub>| {ad.totalApplicants} prijavljenih</sub>
</div>
</div>
{!matches && (
<div className="ad-details-tech-logo-date">
<p>
<span>{t("ads.adDetailsExpiredAt")}: </span>
{new Date(ad.expiredAt).toLocaleDateString()}
</p>
</div>
)}
</div>
<div className="ad-details-tech-logo-title-title">
<h1>.NET Developer</h1>
<div className="ad-details-content">
<div className="ad-details-content-experience">
<p>
{ad.minimumExperience}+ {t("ads.adDetailsExperience")}
</p>
</div>
<div className="ad-details-content-work-time">
<button>Posao</button>
<button>Full-time</button>
</div>
<div className="ad-details-content-content">
<div className="ad-details-content-conten-description">
<p>{t("ads.adDetailsDescription")}</p>
</div>
<div className="ad-details-content-conten-description">
<h3>{t("ads.adDetailsKeyResponsibilities")}</h3>
<ul>
{ad.keyResponsibilities.split("|").map((r, index) => (
<li key={index}>{r}</li>
))}
</ul>
</div>
<div className="ad-details-content-conten-description">
<h3>{t("ads.adDetailsRequirements")}</h3>
<ul>
{ad.requirements.split("|").map((r, index) => (
<li key={index}>{r}</li>
))}
</ul>
</div>
<div className="ad-details-content-conten-description">
<h3>{t("ads.adDetailsOffer")}</h3>
<ul>
{ad.offer.split("|").map((o, index) => (
<li key={index}>{o}</li>
))}
</ul>
</div>
</div>
</div>
<div className="ad-details-tech-logo-title-sub">
<sub>| 4 prijavljenih</sub>
{ad.applicants && ad.applicants.length > 0 && (
<div className="ad-details-applicants">
<div className="ad-details-applicants-header">
<h2>{t("ads.archiveAdsCandidates")}</h2>
</div>
<div className="ad-details-applicants-applicants">
{!matches && (
<div className="ad-details-applicants-applicants-a">
<div className="ad-details-applicants-applicants-arrows">
<button onClick={archiveAdsArrowLeftHandler}>
<img src={arrow_left} alt="arrow-left" />
</button>
<button onClick={archiveAdsArrowRightHandler}>
<img src={arrow_right} alt="arrow-right" />
</button>
</div>
</div>
)}
<div className="ad-details-applicants-applicants-applicant">
<Slider
ref={archiveAdsSliderRef}
{...settings}
slidesToShow={3}
slidesToScroll={3}
style={{ width: "100%" }}
>
{ad.applicants.map((applicant, index) => (
<AdDetailsCandidateCard
key={index}
firstName={applicant.firstName}
lastName={applicant.lastName}
experience={applicant.experience}
cv={applicant.cv}
/>
))}
{ad.applicants.length <= 3 &&
getDummyArchiveAds(ad.applicants.length)}
</Slider>
</div>
</div>
{matches && (
<div className="active-ads-ads-arrows">
<button onClick={archiveAdsArrowLeftHandler}>
<img src={arrow_left} alt="arrow-left" />
</button>
<button onClick={archiveAdsArrowRightHandler}>
<img src={arrow_right} alt="arrow-right" />
</button>
</div>
)}
</div>
)}
<div className="ad-details-buttons">
<Link className="ad-details-buttons-link" to="/ads">
Nazad na sve oglase
</Link>
<IconButton className="c-btn c-btn--primary add-ad-btn">
PRIJAVI SE
</IconButton>
</div>
</div>
<div className="ad-details-tech-logo-date">
<p>
<span>Rok prijave do: </span>14.12.2022.
</p>
</div>
</div>
<div className="ad-details-content">
<div className="ad-details-content-experience">
<p>3+ years of experience</p>
</div>
<div className="ad-details-content-work-time">
<button>Posao</button>
<button>Full-time</button>
</div>
<div className="ad-details-content-content">
<div className="ad-details-content-conten-description">
<p>
Team Diligent is constantly growing! We are looking for a team
player that will work with experienced engineers. If technology is
your passion and you are ready to move the boundaries of your
knowledge every day, then, Diligent is the right place for you. If
you are not from Niš, we are offering a full remote position.
</p>
</div>
<div className="ad-details-content-conten-description">
<h3>Key Responsibilities</h3>
<ul>
<li>
Working as a full-stack developer on various project and
products
</li>
<li>Working with 3rd-party APIs</li>
<li>Working on different integration scenarios</li>
<li>Setting up project structure and architecture</li>
<li>
Being involved in full project development, from writing a
specification to deploying a finished product
</li>
</ul>
</div>
<div className="ad-details-content-conten-description">
<h3>Requirements</h3>
<ul>
<li>
Good software development fundamentals and knowledge of .NET
architecture concepts & patterns
</li>
<li>Good knowledge of software design patterns</li>
<li>Good knowledge of databases and database design</li>
<li>Experience in working with microservices is a big plus</li>
<li>
The ability to work in a big team but also to work independently
</li>
<li>Excellent communication skills</li>
</ul>
</div>
<div className="ad-details-content-conten-description">
<h3>What we offer</h3>
<ul>
<li>Full Remote position</li>
<li>
A fast-growth company with stable projects and strong
international clients
</li>
<li>Opportunity to work in teams with experienced engineers</li>
<li>Competitive employment conditions</li>
<li>
An environment that will make you feel good about your job
</li>
<li>Challenging and diverse projects</li>
<li>Support in your personal and professional growth</li>
<li>Flexible working hours Private health insurance</li>
</ul>
</div>
</div>
<div className="ad-details-buttons">
<Link className="ad-details-buttons-link" to='/ads'>Nazad na sve oglase</Link>
<IconButton className="c-btn c-btn--primary add-ad-btn">
PRIJAVI SE
</IconButton>
</div>
</div>
</div>
)}
</>
);
};


+ 173
- 40
src/pages/AdsPage/AdsPage.js Целия файл

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import Ad from "../../components/Ads/Ad";
import ArchiveAd from "../../components/Ads/ArchiveAd";
@@ -11,24 +11,34 @@ import AdFilters from "../../components/Ads/AdFilters";
import { useDispatch } from "react-redux";
import { setAdsReq } from "../../store/actions/ads/adsAction";
import { useSelector } from "react-redux";
import { selectAds, selectAdsError } from "../../store/selectors/adsSelectors";
import { selectAds } from "../../store/selectors/adsSelectors";
import { AD_DETAILS_PAGE } from "../../constants/pages";
import FilterButton from "../../components/Button/FilterButton";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { useTheme } from "@emotion/react";
import { useMediaQuery } from "@mui/material";
import { selectArchiveAds } from "../../store/selectors/archiveAdsSelectors";
import { setArchiveAdsReq } from "../../store/actions/archiveAds/archiveAdsActions";

const AdsPage = ({ history }) => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("sm"));
const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false);
const [toggleModal, setToggleModal] = useState(false);
const ads = useSelector(selectAds);
const errorMessage = useSelector(selectAdsError);
const archiveAds = useSelector(selectArchiveAds);
const activeAdsSliderRef = useRef();
const archiveAdsSliderRef = useRef();
const { t } = useTranslation();
const dispatch = useDispatch();

useEffect(() => {
dispatch(setAdsReq());
dispatch(setArchiveAdsReq());
}, []);

console.log(errorMessage);

const handleToggleFiltersDrawer = () => {
setToggleFiltersDrawer((oldState) => !oldState);
};
@@ -37,6 +47,75 @@ const AdsPage = ({ history }) => {
setToggleModal((oldState) => !oldState);
};

var settings = {
dots: false,
infinite: false,
speed: 500,
initialSlide: 0,
responsive: [
{
breakpoint: 1024,
settings: {
slidesToShow: 3,
slidesToScroll: 3,
infinite: true,
dots: false,
},
},
{
breakpoint: 900,
settings: {
slidesToShow: 2,
slidesToScroll: 2,
initialSlide: 0,
},
},
{
breakpoint: 480,
settings: {
slidesToShow: 1,
slidesToScroll: 1,
},
},
],
};

const activeAdsArrowLeftHandler = () => {
activeAdsSliderRef.current.slickPrev();
};

const activeAdsArrowRightHandler = () => {
activeAdsSliderRef.current.slickNext();
};

const archiveAdsArrowLeftHandler = () => {
archiveAdsSliderRef.current.slickPrev();
};

const archiveAdsArrowRightHandler = () => {
archiveAdsSliderRef.current.slickNext();
};

const getDummyAds = (len) => {
const ads = [];

for (let i = 0; i < 5 - len + 1; i++) {
ads.push(<Ad key={i} className="hiddenAd" />);
}

return ads;
};

const getDummyArchiveAds = (len) => {
const ads = [];

for (let i = 0; i < 5 - len + 1; i++) {
ads.push(<ArchiveAd key={i} className="hiddenAd" />);
}

return ads;
};

return (
<>
<div className="l-t-rectangle"></div>
@@ -56,54 +135,108 @@ const AdsPage = ({ history }) => {
</div>
<div className="active-ads-ads">
<div className="active-ads-ads-a">
<div className="active-ads-ads-arrows">
<button>
<img src={arrow_left} alt="arrow-left" />
</button>
<button>
<img src={arrow_right} alt="arrow-right" />
</button>
</div>
{!matches && (
<div className="active-ads-ads-arrows">
<button onClick={activeAdsArrowLeftHandler}>
<img src={arrow_left} alt="arrow-left" />
</button>
<button onClick={activeAdsArrowRightHandler}>
<img src={arrow_right} alt="arrow-right" />
</button>
</div>
)}
</div>
<div className="active-ads-ads-ad">
{ads.map((ad, index) => (
<Ad
onShowAdDetails={() =>
history.push(AD_DETAILS_PAGE.replace(":id", ad.id))
}
key={index}
title={ad.title}
minimumExperience={ad.minimumExperience}
createdAt={ad.createdAt}
expiredAt={ad.expiredAt}
/>
))}
<Slider
{...settings}
slidesToShow={3}
slidesToScroll={3}
style={{ width: "100%" }}
ref={activeAdsSliderRef}
>
{ads.map((ad, index) => (
<Ad
onShowAdDetails={() =>
history.push(AD_DETAILS_PAGE.replace(":id", ad.id))
}
key={index}
title={ad.title}
minimumExperience={ad.minimumExperience}
createdAt={ad.createdAt}
expiredAt={ad.expiredAt}
/>
))}
{ads.length <= 5 && getDummyAds(ads.length)}
</Slider>
</div>
</div>
</div>
)}

<div className="archived-ads">
<div className="archived-ads-header">
<h2>{t("ads.archiveAds")}</h2>
{matches && (
<div className="active-ads-ads-arrows">
<button onClick={activeAdsArrowLeftHandler}>
<img src={arrow_left} alt="arrow-left" />
</button>
<button onClick={activeAdsArrowRightHandler}>
<img src={arrow_right} alt="arrow-right" />
</button>
</div>
<div className="archived-ads-ads">
<div className="archived-ads-ads-a">
<div className="archived-ads-ads-arrows">
<button>
)}
{archiveAds && archiveAds.length > 0 && (
<div className="archived-ads">
<div className="archived-ads-header">
<h2>{t("ads.archiveAds")}</h2>
</div>
<div className="archived-ads-ads">
{!matches && (
<div className="archived-ads-ads-a">
<div className="archived-ads-ads-arrows">
<button onClick={archiveAdsArrowLeftHandler}>
<img src={arrow_left} alt="arrow-left" />
</button>
<button onClick={archiveAdsArrowRightHandler}>
<img src={arrow_right} alt="arrow-right" />
</button>
</div>
</div>
)}
<div className="archived-ads-ads-ad">
<Slider
ref={archiveAdsSliderRef}
{...settings}
slidesToShow={5}
slidesToScroll={5}
style={{ width: "100%" }}
>
{archiveAds.map((ad, index) => (
<ArchiveAd
key={index}
title={ad.title}
minimumExperience={ad.minimumExperience}
createdAt={ad.createdAt}
expiredAt={ad.expiredAt}
onShowAdDetails={() =>
history.push(AD_DETAILS_PAGE.replace(":id", ad.id))
}
/>
))}
{archiveAds.length <= 5 &&
getDummyArchiveAds(archiveAds.length)}
</Slider>
</div>
</div>
{matches && (
<div className="active-ads-ads-arrows">
<button onClick={archiveAdsArrowLeftHandler}>
<img src={arrow_left} alt="arrow-left" />
</button>
<button>
<button onClick={archiveAdsArrowRightHandler}>
<img src={arrow_right} alt="arrow-right" />
</button>
</div>
</div>
<div className="archived-ads-ads-ad">
<ArchiveAd />
<ArchiveAd />
</div>
)}
</div>
</div>
)}
</div>

<div className="add-ad">

+ 3
- 0
src/request/adsRequest.js Целия файл

@@ -2,3 +2,6 @@ import { getRequest } from ".";
import apiEndpoints from "./apiEndpoints";

export const getAllAds = () => getRequest(apiEndpoints.ads.allAds);
export const getAllArchiveAds = () =>
getRequest(apiEndpoints.ads.allArchiveAds);
export const getAdDetailsById = (id) => getRequest(`${apiEndpoints.ads.adDetails}/${id}`);

+ 4
- 2
src/request/apiEndpoints.js Целия файл

@@ -14,10 +14,12 @@ export default {
user:'http://localhost:26081/v1/users/{id}',
toggleEnabled:'http://localhost:26081/v1/users/toggleEnable/{id}',
},
candidates:{
allCandidates:base + "/applicants"
candidates: {
allCandidates: base + "/applicants",
},
ads: {
allAds: base + "/ads",
allArchiveAds: base + "/ads/archive",
adDetails: base + "/ads/details",
},
};

+ 3
- 0
src/store/actions/ad/adActionConstants.js Целия файл

@@ -0,0 +1,3 @@
export const FETCH_AD_REQ = 'FETCH_AD_REQ';
export const FETCH_AD_ERR = 'FETCH_AD_ERR';
export const FETCH_AD_SUCCESS = 'FETCH_AD_SUCCESS';

+ 20
- 0
src/store/actions/ad/adActions.js Целия файл

@@ -0,0 +1,20 @@
import {
FETCH_AD_REQ,
FETCH_AD_ERR,
FETCH_AD_SUCCESS,
} from "./adActionConstants";

export const setAdReq = (payload) => ({
type: FETCH_AD_REQ,
payload,
});

export const setAdError = (payload) => ({
type: FETCH_AD_ERR,
payload,
});

export const setAd = (payload) => ({
type: FETCH_AD_SUCCESS,
payload,
});

+ 3
- 0
src/store/actions/archiveAds/archiveAdsActionConstants.js Целия файл

@@ -0,0 +1,3 @@
export const FETCH_ARCHIVE_ADS_REQ = 'FETCH_ARCHIVE_ADS_REQ';
export const FETCH_ARCHIVE_ADS_ERR = 'FETCH_ARCHIVE_ADS_ERR';
export const FETCH_ARCHIVE_ADS_SUCCESS = 'FETCH_ARCHIVE_ADS_SUCCESS';

+ 19
- 0
src/store/actions/archiveAds/archiveAdsActions.js Целия файл

@@ -0,0 +1,19 @@
import {
FETCH_ARCHIVE_ADS_REQ,
FETCH_ARCHIVE_ADS_SUCCESS,
FETCH_ARCHIVE_ADS_ERR,
} from "./archiveAdsActionConstants";

export const setArchiveAdsReq = () => ({
type: FETCH_ARCHIVE_ADS_REQ,
});

export const setArchiveAdsError = (payload) => ({
type: FETCH_ARCHIVE_ADS_ERR,
payload,
});

export const setArchiveAds = (payload) => ({
type: FETCH_ARCHIVE_ADS_SUCCESS,
payload,
});

+ 26
- 0
src/store/reducers/ad/adReducer.js Целия файл

@@ -0,0 +1,26 @@
import {
FETCH_AD_ERR,
FETCH_AD_SUCCESS,
} from "../../actions/ad/adActionConstants";
import createReducer from "../../utils/createReducer";

const initialState = {
ad: null,
errorMessage: "",
};

export default createReducer(
{
[FETCH_AD_SUCCESS]: setStateAd,
[FETCH_AD_ERR]: setStateErrorMessage,
},
initialState
);

function setStateAd(state, action) {
return { ...state, ad: action.payload };
}

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

+ 32
- 0
src/store/reducers/ad/archiveAdsReducer.js Целия файл

@@ -0,0 +1,32 @@
import {
FETCH_ARCHIVE_ADS_SUCCESS,
FETCH_ARCHIVE_ADS_ERR,
} from "../../actions/archiveAds/archiveAdsActionConstants";
import createReducer from "../../utils/createReducer";

const initialState = {
archiveAds: [],
errorMessage: "",
};

export default createReducer(
{
[FETCH_ARCHIVE_ADS_SUCCESS]: setStateArchiveAds,
[FETCH_ARCHIVE_ADS_ERR]: setArchiveAdsError,
},
initialState
);

function setStateArchiveAds(state, action) {
return {
...state,
archiveAds: action.payload,
};
}

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

+ 6
- 2
src/store/reducers/index.js Целия файл

@@ -5,7 +5,9 @@ import userReducer from "./user/userReducer";
import randomDataReducer from "./randomData/randomDataReducer";
import usersReducer from "./user/usersReducer";
import adsReducer from "./ad/adsReducer";
import candidatesReducer from './candidates/candidatesReducer';
import adReducer from "./ad/adReducer";
import archiveAdsReducer from "./ad/archiveAdsReducer";
import candidatesReducer from "./candidates/candidatesReducer";

export default combineReducers({
login: loginReducer,
@@ -14,5 +16,7 @@ export default combineReducers({
randomData: randomDataReducer,
users: usersReducer,
ads: adsReducer,
candidates:candidatesReducer
ad: adReducer,
archiveAds: archiveAdsReducer,
candidates: candidatesReducer,
});

+ 35
- 2
src/store/saga/adsSaga.js Целия файл

@@ -1,7 +1,18 @@
import { all, call, put, takeLatest } from "redux-saga/effects";
import { getAllAds } from "../../request/adsRequest";
import {
getAllAds,
getAdDetailsById,
getAllArchiveAds,
} from "../../request/adsRequest";
import { setAds, setAdsError } from "../actions/ads/adsAction";
import { setAd, setAdError } from "../actions/ad/adActions";
import {
setArchiveAds,
setArchiveAdsError,
} from "../actions/archiveAds/archiveAdsActions";
import { FETCH_ADS_REQ } from "../actions/ads/adsActionConstants";
import { FETCH_AD_REQ } from "../actions/ad/adActionConstants";
import { FETCH_ARCHIVE_ADS_REQ } from "../actions/archiveAds/archiveAdsActionConstants";

export function* getAds() {
try {
@@ -12,6 +23,28 @@ export function* getAds() {
}
}

export function* getAd({ payload }) {
try {
const result = yield call(getAdDetailsById, payload.id);
yield put(setAd(result.data));
} catch (error) {
yield put(setAdError(error));
}
}

export function* getArchiveAds() {
try {
const result = yield call(getAllArchiveAds);
yield put(setArchiveAds(result.data));
} catch (error) {
yield put(setArchiveAdsError(error));
}
}

export default function* adsSaga() {
yield all([takeLatest(FETCH_ADS_REQ, getAds)]);
yield all([
takeLatest(FETCH_ADS_REQ, getAds),
takeLatest(FETCH_AD_REQ, getAd),
takeLatest(FETCH_ARCHIVE_ADS_REQ, getArchiveAds),
]);
}

+ 10
- 0
src/store/selectors/adSelectors.js Целия файл

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

export const adSelector = (state) => state.ad;

export const selectAd = createSelector(adSelector, (state) => state.ad);

export const selectAdError = createSelector(
adSelector,
(state) => state.errorMessage
);

+ 13
- 0
src/store/selectors/archiveAdsSelectors.js Целия файл

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

export const archiveAdsSelector = (state) => state.archiveAds;

export const selectArchiveAds = createSelector(
archiveAdsSelector,
(state) => state.archiveAds
);

export const selectArchiveAdsError = createSelector(
archiveAdsSelector,
(state) => state.errorMessage
);

+ 43
- 0
yarn.lock Целия файл

@@ -3782,6 +3782,11 @@
"isobject" "^3.0.0"
"static-extend" "^0.1.1"

"classnames@^2.2.5":
"integrity" "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
"resolved" "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
"version" "2.3.2"

"clean-css@^4.2.3":
"integrity" "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA=="
"resolved" "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz"
@@ -4978,6 +4983,11 @@
"memory-fs" "^0.5.0"
"tapable" "^1.0.0"

"enquire.js@^2.1.6":
"integrity" "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw=="
"resolved" "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz"
"version" "2.1.6"

"enquirer@^2.3.5":
"integrity" "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg=="
"resolved" "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz"
@@ -7769,6 +7779,13 @@
"resolved" "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
"version" "1.0.1"

"json2mq@^0.2.0":
"integrity" "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="
"resolved" "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz"
"version" "0.2.0"
dependencies:
"string-convert" "^0.2.0"

"json3@^3.3.3":
"integrity" "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA=="
"resolved" "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz"
@@ -10545,6 +10562,17 @@
"object-assign" "^4.1.1"
"react-is" "^16.12.0 || ^17.0.0 || ^18.0.0"

"react-slick@^0.29.0":
"integrity" "sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA=="
"resolved" "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz"
"version" "0.29.0"
dependencies:
"classnames" "^2.2.5"
"enquire.js" "^2.1.6"
"json2mq" "^0.2.0"
"lodash.debounce" "^4.0.8"
"resize-observer-polyfill" "^1.5.0"

"react-test-renderer@^18.2.0":
"integrity" "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA=="
"resolved" "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz"
@@ -10934,6 +10962,11 @@
"resolved" "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz"
"version" "4.1.6"

"resize-observer-polyfill@^1.5.0":
"integrity" "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
"resolved" "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
"version" "1.5.1"

"resolve-cwd@^2.0.0":
"integrity" "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo="
"resolved" "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz"
@@ -11551,6 +11584,11 @@
"astral-regex" "^2.0.0"
"is-fullwidth-code-point" "^3.0.0"

"slick-carousel@^1.8.1":
"integrity" "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA=="
"resolved" "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz"
"version" "1.8.1"

"snapdragon-node@^2.0.1":
"integrity" "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw=="
"resolved" "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz"
@@ -11854,6 +11892,11 @@
dependencies:
"safe-buffer" "~5.2.0"

"string-convert@^0.2.0":
"integrity" "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="
"resolved" "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz"
"version" "0.2.1"

"string-length@^4.0.1":
"integrity" "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="
"resolved" "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz"

Loading…
Отказ
Запис