| @@ -4,6 +4,7 @@ import { Redirect, Route, Switch } from "react-router-dom"; | |||
| import { | |||
| HOME_PAGE, | |||
| ADS_PAGE, | |||
| AD_DETAILS_PAGE, | |||
| FORGOT_PASSWORD_PAGE, | |||
| FORGOT_PASSWORD_CONFIRMATION_PAGE, | |||
| NOT_FOUND_PAGE, | |||
| @@ -28,6 +29,7 @@ import ForgotPasswordConfirmationPage from "./pages/ForgotPasswordPage/ForgotPas | |||
| import ResetPasswordPage from "./pages/ForgotPasswordPage/ResetPasswordPageMUI"; | |||
| import UsersPage from "./pages/UsersPage/UsersPage"; | |||
| import CandidatesPage from './pages/CandidatesPage/CandidatesPage' | |||
| import AdDetailsPage from "./pages/AdsPage/AdDetailsPage"; | |||
| const AppRoutes = () => ( | |||
| <Switch> | |||
| @@ -43,6 +45,7 @@ const AppRoutes = () => ( | |||
| <Route path={RESET_PASSWORD_PAGE} component={ResetPasswordPage} /> | |||
| <PrivateRoute exact path={HOME_PAGE} component={HomePage} /> | |||
| <PrivateRoute exact path={ADS_PAGE} component={AdsPage} /> | |||
| <PrivateRoute exact path={AD_DETAILS_PAGE} component={AdDetailsPage} /> | |||
| <PrivateRoute exact path={USERS_PAGE} component={UsersPage} /> | |||
| <PrivateRoute exact path={CANDIDATES_PAGE} component={CandidatesPage} /> | |||
| <Redirect from="*" to={NOT_FOUND_PAGE} /> | |||
| @@ -174,6 +174,7 @@ h3 { | |||
| border-radius: 18px; | |||
| gap: 18px; | |||
| margin-right: 36px; | |||
| cursor: pointer; | |||
| } | |||
| .ad-card-date p { | |||
| @@ -339,4 +340,101 @@ h3 { | |||
| .ad-filters-search > * { | |||
| width: 100%; | |||
| } | |||
| .ad-details { | |||
| padding: 45px 72px 18px 223px; | |||
| } | |||
| .ad-details-tech-logo { | |||
| position: relative; | |||
| left: -80px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| } | |||
| .ad-details-tech-logo-title { | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| .ad-details-tech-logo-title-img { | |||
| margin-right: 18px; | |||
| } | |||
| .ad-details-tech-logo-title-sub sub { | |||
| margin-left: 9px; | |||
| font-size: 1.25rem; | |||
| color: $mainBlue !important; | |||
| font-weight: 600; | |||
| } | |||
| .ad-details-tech-logo-date p span { | |||
| color: #9d9d9d; | |||
| } | |||
| .ad-details-content-experience { | |||
| margin-top: 18px; | |||
| } | |||
| .ad-details-content-experience p { | |||
| color: #272727; | |||
| font-family: "Source Sans Pro"; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| } | |||
| .ad-details-content-work-time { | |||
| margin-top: 18px; | |||
| } | |||
| .ad-details-content-work-time button { | |||
| box-sizing: border-box; | |||
| flex-direction: row; | |||
| padding: 9px; | |||
| gap: 10px; | |||
| width: 76px; | |||
| height: 38px; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 9px; | |||
| flex: none; | |||
| order: 0; | |||
| flex-grow: 0; | |||
| margin-right: 18px; | |||
| cursor: pointer; | |||
| } | |||
| .ad-details-content-content { | |||
| margin-top: 18px; | |||
| } | |||
| .ad-details-content-conten-description { | |||
| margin-top: 18px; | |||
| } | |||
| .ad-details-content-conten-description h3 { | |||
| margin-bottom: 9px; | |||
| } | |||
| .ad-details-content-conten-description ul { | |||
| list-style: circle; | |||
| padding-left: 36px; | |||
| } | |||
| .ad-details-buttons { | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| align-items: center; | |||
| margin-top: 36px; | |||
| } | |||
| .ad-details-buttons > button { | |||
| margin-left: 36px; | |||
| } | |||
| .ad-details-buttons-link { | |||
| color: $mainBlue; | |||
| } | |||
| @@ -1,15 +1,25 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import logoReact from "../../assets/images/logo_react.png"; | |||
| const Ad = () => { | |||
| const Ad = ({ | |||
| title, | |||
| minimumExperience, | |||
| createdAt, | |||
| expiredAt, | |||
| onShowAdDetails, | |||
| }) => { | |||
| return ( | |||
| <div className="ad-card"> | |||
| <div className="ad-card" onClick={onShowAdDetails}> | |||
| <div className="ad-card-date"> | |||
| <p>30.09.22 - 30.10.22</p> | |||
| <p> | |||
| {new Date(createdAt).toLocaleDateString()} -{" "} | |||
| {new Date(expiredAt).toLocaleDateString()} | |||
| </p> | |||
| </div> | |||
| <div className="ad-card-title"> | |||
| <h3>React Developer</h3> | |||
| <h3>{title}</h3> | |||
| </div> | |||
| <div className="ad-card-logo"> | |||
| @@ -17,7 +27,7 @@ const Ad = () => { | |||
| </div> | |||
| <div className="ad-card-experience"> | |||
| <p>3+ years of experience</p> | |||
| <p>{minimumExperience}+ years of experience</p> | |||
| </div> | |||
| <div className="ad-card-buttons"> | |||
| @@ -29,4 +39,13 @@ const Ad = () => { | |||
| ); | |||
| }; | |||
| Ad.propTypes = { | |||
| id: PropTypes.number, | |||
| title: PropTypes.string, | |||
| minimumExperience: PropTypes.number, | |||
| createdAt: PropTypes.any, | |||
| expiredAt: PropTypes.any, | |||
| onShowAdDetails: PropTypes.func, | |||
| }; | |||
| export default Ad; | |||
| @@ -0,0 +1,29 @@ | |||
| import { IconButton } from "@mui/material"; | |||
| import PropTypes from "prop-types"; | |||
| import React from "react"; | |||
| import filters from "../../assets/images/filters.png"; | |||
| const FilterButton = ({ onShowFilters }) => { | |||
| return ( | |||
| <IconButton | |||
| className={"c-btn--primary-outlined c-btn userPageBtn ml-20px no-padding"} | |||
| onClick={onShowFilters} | |||
| > | |||
| Filteri | |||
| <img | |||
| style={{ | |||
| position: "relative", | |||
| top: -0.25, | |||
| marginLeft: "9px", | |||
| }} | |||
| src={filters} | |||
| /> | |||
| </IconButton> | |||
| ); | |||
| }; | |||
| FilterButton.propTypes = { | |||
| onShowFilters: PropTypes.func, | |||
| }; | |||
| export default FilterButton; | |||
| @@ -215,11 +215,11 @@ const NavbarComponent = () => { | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| width: matches ? '100%' : 'auto' | |||
| width: matches ? "100%" : "auto", | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Box | |||
| <Box | |||
| className="responsive-nav-cont" | |||
| style={{ | |||
| display: "flex", | |||
| @@ -230,14 +230,14 @@ const NavbarComponent = () => { | |||
| <img | |||
| style={{ height: "37px", width: "37px", marginLeft: "0px" }} | |||
| src={HrLogo} | |||
| className='responsive-logo' | |||
| className="responsive-logo" | |||
| /> | |||
| <div | |||
| style={{ | |||
| display: "flex", | |||
| alignItems: "center", | |||
| }} | |||
| className='icons-cont' | |||
| className="icons-cont" | |||
| > | |||
| <img src={searchIcon} /> | |||
| <IconButton | |||
| @@ -297,7 +297,7 @@ const NavbarComponent = () => { | |||
| }} | |||
| className="text-black" | |||
| as={Link} | |||
| to={n} | |||
| to={`/${n}`} | |||
| > | |||
| {t("nav." + n)} | |||
| </Typography> | |||
| @@ -2,6 +2,7 @@ export const BASE_PAGE = '/'; | |||
| export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | |||
| export const HOME_PAGE = '/home'; | |||
| export const ADS_PAGE = '/ads'; | |||
| export const AD_DETAILS_PAGE = '/ads/:id'; | |||
| export const ERROR_PAGE = '/error-page'; | |||
| export const NOT_FOUND_PAGE = '/not-found'; | |||
| export const USERS_PAGE = '/users'; | |||
| @@ -0,0 +1,107 @@ | |||
| import { IconButton } from "@mui/material"; | |||
| import React from "react"; | |||
| import aspNetIcon from "../../assets/images/.net_icon.png"; | |||
| import { Link } from "react-router-dom"; | |||
| const AdDetailsPage = () => { | |||
| 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" /> | |||
| </div> | |||
| <div className="ad-details-tech-logo-title-title"> | |||
| <h1>.NET Developer</h1> | |||
| </div> | |||
| <div className="ad-details-tech-logo-title-sub"> | |||
| <sub>| 4 prijavljenih</sub> | |||
| </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> | |||
| ); | |||
| }; | |||
| export default AdDetailsPage; | |||
| @@ -1,18 +1,33 @@ | |||
| import React, { useState } from "react"; | |||
| import React, { useState, useEffect } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import Ad from "../../components/Ads/Ad"; | |||
| import ArchiveAd from "../../components/Ads/ArchiveAd"; | |||
| import IconButton from "../../components/IconButton/IconButton"; | |||
| import filterVector from "../../assets/images/filter_vector.png"; | |||
| import arrow_left from "../../assets/images/arrow_left.png"; | |||
| import arrow_right from "../../assets/images/arrow_right.png"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import AddAdModal from "../../components/Ads/AddAdModal"; | |||
| 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 { AD_DETAILS_PAGE } from "../../constants/pages"; | |||
| import FilterButton from "../../components/Button/FilterButton"; | |||
| const AdsPage = () => { | |||
| const AdsPage = ({ history }) => { | |||
| const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false); | |||
| const [toggleModal, setToggleModal] = useState(false); | |||
| const ads = useSelector(selectAds); | |||
| const errorMessage = useSelector(selectAdsError); | |||
| const { t } = useTranslation(); | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| dispatch(setAdsReq()); | |||
| }, []); | |||
| console.log(errorMessage); | |||
| const handleToggleFiltersDrawer = () => { | |||
| setToggleFiltersDrawer((oldState) => !oldState); | |||
| @@ -26,42 +41,47 @@ const AdsPage = () => { | |||
| <> | |||
| <div className="l-t-rectangle"></div> | |||
| <div className="r-b-rectangle"></div> | |||
| <AdFilters /> | |||
| {/* <AdFilters /> */} | |||
| <AdFilters | |||
| open={toggleFiltersDrawer} | |||
| handleClose={handleToggleFiltersDrawer} | |||
| /> | |||
| <AddAdModal open={toggleModal} handleClose={handleToggleModal} /> | |||
| <div className="ads"> | |||
| <div className="active-ads"> | |||
| <div className="active-ads-header"> | |||
| <h1>{t("ads.activeAds")}</h1> | |||
| <IconButton | |||
| sx={{ marginLeft: "15px" }} | |||
| className="c-btn c-btn--primary-outlined" | |||
| onClick={handleToggleFiltersDrawer} | |||
| > | |||
| Filteri{" "} | |||
| <img src={filterVector} alt="filter" className="filter-vector" /> | |||
| </IconButton> | |||
| </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> | |||
| {ads && ads.length > 0 && ( | |||
| <div className="active-ads"> | |||
| <div className="active-ads-header"> | |||
| <h1>{t("ads.activeAds")}</h1> | |||
| <FilterButton onShowFilters={handleToggleFiltersDrawer} /> | |||
| </div> | |||
| <div className="active-ads-ads-ad"> | |||
| <Ad /> | |||
| <Ad /> | |||
| <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> | |||
| </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} | |||
| /> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| )} | |||
| <div className="archived-ads"> | |||
| <div className="archived-ads-header"> | |||
| @@ -98,4 +118,8 @@ const AdsPage = () => { | |||
| ); | |||
| }; | |||
| AdsPage.propTypes = { | |||
| history: PropTypes.any, | |||
| }; | |||
| export default AdsPage; | |||
| @@ -0,0 +1,4 @@ | |||
| import { getRequest } from "."; | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const getAllAds = () => getRequest(apiEndpoints.ads.allAds); | |||
| @@ -14,5 +14,8 @@ export default { | |||
| }, | |||
| candidates:{ | |||
| allCandidates:base + "/applicants" | |||
| } | |||
| }, | |||
| ads: { | |||
| allAds: base + "/ads", | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,19 @@ | |||
| import { | |||
| FETCH_ADS_REQ, | |||
| FETCH_ADS_ERR, | |||
| FETCH_ADS_SUCCESS, | |||
| } from "./adsActionConstants"; | |||
| export const setAdsReq = () => ({ | |||
| type: FETCH_ADS_REQ, | |||
| }); | |||
| export const setAdsError = (payload) => ({ | |||
| type: FETCH_ADS_ERR, | |||
| payload, | |||
| }); | |||
| export const setAds = (payload) => ({ | |||
| type: FETCH_ADS_SUCCESS, | |||
| payload, | |||
| }); | |||
| @@ -0,0 +1,3 @@ | |||
| export const FETCH_ADS_REQ = 'FETCH_ADS_REQ'; | |||
| export const FETCH_ADS_ERR = 'FETCH_ADS_ERR'; | |||
| export const FETCH_ADS_SUCCESS = 'FETCH_ADS_SUCCESS'; | |||
| @@ -0,0 +1,32 @@ | |||
| import { | |||
| FETCH_ADS_ERR, | |||
| FETCH_ADS_SUCCESS, | |||
| } from "../../actions/ads/adsActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| const initialState = { | |||
| ads: [], | |||
| errorMessage: "", | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [FETCH_ADS_SUCCESS]: setStateAds, | |||
| [FETCH_ADS_ERR]: setAdsErrorMessage, | |||
| }, | |||
| initialState | |||
| ); | |||
| function setStateAds(state, action) { | |||
| return { | |||
| ...state, | |||
| ads: action.payload, | |||
| }; | |||
| } | |||
| function setAdsErrorMessage(state, action) { | |||
| return { | |||
| ...state, | |||
| errorMessage: action.payload, | |||
| }; | |||
| } | |||
| @@ -1,16 +1,18 @@ | |||
| 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 { 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 adsReducer from "./ad/adsReducer"; | |||
| import candidatesReducer from './candidates/candidatesReducer'; | |||
| export default combineReducers({ | |||
| login: loginReducer, | |||
| user: userReducer, | |||
| loading:loadingReducer, | |||
| loading: loadingReducer, | |||
| randomData: randomDataReducer, | |||
| users: usersReducer, | |||
| ads: adsReducer, | |||
| candidates:candidatesReducer | |||
| }); | |||
| @@ -19,7 +19,6 @@ export default createReducer( | |||
| ); | |||
| function setStateUsers(state, action) { | |||
| console.log("POZIV"); | |||
| return { | |||
| ...state, | |||
| users: action.payload, | |||
| @@ -0,0 +1,17 @@ | |||
| import { all, call, put, takeLatest } from "redux-saga/effects"; | |||
| import { getAllAds } from "../../request/adsRequest"; | |||
| import { setAds, setAdsError } from "../actions/ads/adsAction"; | |||
| import { FETCH_ADS_REQ } from "../actions/ads/adsActionConstants"; | |||
| export function* getAds() { | |||
| try { | |||
| const result = yield call(getAllAds); | |||
| yield put(setAds(result.data)); | |||
| } catch (error) { | |||
| yield put(setAdsError(error)); | |||
| } | |||
| } | |||
| export default function* adsSaga() { | |||
| yield all([takeLatest(FETCH_ADS_REQ, getAds)]); | |||
| } | |||
| @@ -1,12 +1,14 @@ | |||
| import { all } from 'redux-saga/effects'; | |||
| import { all } from "redux-saga/effects"; | |||
| import adsSaga from "./adsSaga"; | |||
| import candidatesSaga from './candidatesSaga'; | |||
| import loginSaga from './loginSaga'; | |||
| import usersSaga from './usersSaga'; | |||
| import loginSaga from "./loginSaga"; | |||
| import usersSaga from "./usersSaga"; | |||
| export default function* rootSaga() { | |||
| yield all([ | |||
| loginSaga(), | |||
| usersSaga(), | |||
| adsSaga(), | |||
| candidatesSaga() | |||
| ]); | |||
| } | |||
| @@ -94,9 +94,9 @@ function* forgetPassword({ payload }) { | |||
| function* resetPassword({ payload }) { | |||
| try { | |||
| console.log(payload) | |||
| // console.log(payload) | |||
| const { data } = yield call(sendResetPassword, payload); | |||
| console.log(data); | |||
| // console.log(data); | |||
| yield put(forgetPasswordSuccess(data)); | |||
| if (payload.handleApiResponseSuccess) { | |||
| yield call(payload.handleApiResponseSuccess); | |||
| @@ -11,7 +11,7 @@ export function* getUsers() { | |||
| const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||
| yield call(addHeaderToken, JwtToken); | |||
| const result = yield call(getAllUsers); | |||
| console.log(result.data) | |||
| // console.log(result.data) | |||
| yield put(setUsers(result.data)); | |||
| } catch (error) { | |||
| yield put(setUsersError(error)) | |||
| @@ -0,0 +1,10 @@ | |||
| import { createSelector } from "@reduxjs/toolkit"; | |||
| export const adsSelector = (state) => state.ads; | |||
| export const selectAds = createSelector(adsSelector, (state) => state.ads); | |||
| export const selectAdsError = createSelector( | |||
| adsSelector, | |||
| (state) => state.errorMessage | |||
| ); | |||