| @@ -21,7 +21,8 @@ import { | |||
| PATTERN_DETAILS_PAGE, | |||
| SCHEDULE_PAGE, | |||
| STATS_PAGE, | |||
| REGISTER_PAGE | |||
| REGISTER_PAGE, | |||
| CREATE_AD_PAGE | |||
| } from "./constants/pages"; | |||
| // import LoginPage from './pages/LoginPage/LoginPage'; | |||
| @@ -48,6 +49,7 @@ import PatternDetailsPage from "./pages/PatternsPage/PatternDetailsPage"; | |||
| import SchedulePage from "./pages/SchedulePage/SchedulePage"; | |||
| import StatsPage from "./pages/StatsPage/StatsPage"; | |||
| import RegisterPage from "./pages/RegisterPage/RegisterPage"; | |||
| import CreateAdPage from "./pages/AdsPage/CreateAdPage"; | |||
| const AppRoutes = () => ( | |||
| <Switch> | |||
| @@ -68,6 +70,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={CREATE_AD_PAGE} component={CreateAdPage} /> | |||
| <PrivateRoute | |||
| exact | |||
| path={CANDIDATES_DETAILS_PAGE} | |||
| @@ -1038,3 +1038,134 @@ h3 { | |||
| text-decoration: underline; | |||
| font-size: 16px; | |||
| } | |||
| .create-ad-page { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| height: 100% !important; | |||
| position: relative; | |||
| } | |||
| .create-ad-page-content { | |||
| max-width: 630px !important; | |||
| min-width: 630px !important; | |||
| @include media-below($bp-xl) { | |||
| padding: 20px; | |||
| min-width: 0 !important; | |||
| } | |||
| } | |||
| .create-ad-page-content-header { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 9px; | |||
| margin-bottom: 36px; | |||
| } | |||
| .create-ad-page-content-header sub { | |||
| color: $mainBlue; | |||
| } | |||
| .create-ad-form-control { | |||
| display: flex; | |||
| flex-direction: column; | |||
| margin-bottom: 18px; | |||
| } | |||
| .create-ad-form-control label { | |||
| font-family: "Source Sans Pro"; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| color: #272727; | |||
| margin-bottom: 4.5px; | |||
| } | |||
| .create-ad-form-control input { | |||
| box-sizing: border-box; | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| padding: 18px; | |||
| gap: 10px; | |||
| background: #ffffff; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 7px; | |||
| } | |||
| .create-ad-form-control-buttons { | |||
| display: flex; | |||
| } | |||
| .create-ad-form-control-buttons > button { | |||
| margin-right: 9px; | |||
| } | |||
| .create-ad-buttons { | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| flex-wrap: wrap; | |||
| } | |||
| .create-ad-buttons-back { | |||
| margin-left: 18px; | |||
| box-sizing: border-box; | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: center; | |||
| align-items: center; | |||
| padding: 18px 72px; | |||
| gap: 10px; | |||
| background: #f4f4f4; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 9px; | |||
| margin-left: 9px; | |||
| cursor: pointer; | |||
| @include media-below($bp-xl) { | |||
| margin-bottom: 9px !important; | |||
| } | |||
| } | |||
| .create-ad-buttons-forward { | |||
| box-sizing: border-box; | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: center; | |||
| align-items: center; | |||
| padding: 18px 72px; | |||
| gap: 10px; | |||
| background: #226cb0; | |||
| border-radius: 9px; | |||
| border: none; | |||
| color: white; | |||
| margin-left: 9px; | |||
| cursor: pointer; | |||
| @include media-below($bp-xl) { | |||
| margin-bottom: 9px !important; | |||
| } | |||
| } | |||
| .create-ad-go-back { | |||
| position: absolute; | |||
| right: 72px; | |||
| bottom: 36px; | |||
| } | |||
| .create-ad-go-back button { | |||
| border: none; | |||
| background-color: transparent; | |||
| padding: 0; | |||
| outline: none; | |||
| margin: 0; | |||
| font-family: "Source Sans Pro"; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| text-align: right; | |||
| text-decoration-line: underline; | |||
| color: #226cb0; | |||
| cursor: pointer; | |||
| } | |||
| @@ -18,3 +18,4 @@ export const PATTERN_DETAILS_PAGE = '/patterns/:id'; | |||
| export const SCHEDULE_PAGE = '/schedule' | |||
| export const STATS_PAGE = '/statistics'; | |||
| export const REGISTER_PAGE = '/register'; | |||
| export const CREATE_AD_PAGE = '/create-ad'; | |||
| @@ -7,7 +7,6 @@ import arrow_left from "../../assets/images/arrow_left.png"; | |||
| import arrow_right from "../../assets/images/arrow_right.png"; | |||
| import searchImage from "../../assets/images/search.png"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import AddAdModal from "../../components/Ads/AddAdModal"; | |||
| import AdFilters from "../../components/Ads/AdFilters"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { | |||
| @@ -16,7 +15,7 @@ import { | |||
| } from "../../store/actions/ads/adsAction"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectAds } from "../../store/selectors/adsSelectors"; | |||
| import { AD_DETAILS_PAGE } from "../../constants/pages"; | |||
| import { AD_DETAILS_PAGE, CREATE_AD_PAGE } from "../../constants/pages"; | |||
| import FilterButton from "../../components/Button/FilterButton"; | |||
| import Slider from "react-slick"; | |||
| import "slick-carousel/slick/slick.css"; | |||
| @@ -37,7 +36,6 @@ const AdsPage = ({ history }) => { | |||
| const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false); | |||
| const [isSearchFieldVisible, setIsSearchFieldVisible] = useState(false); | |||
| const [searchInput, setSearchInput] = useState(""); | |||
| const [toggleModal, setToggleModal] = useState(false); | |||
| const [tmp, setTmp] = useState(null); | |||
| const ads = useSelector(selectAds); | |||
| const technologies = useSelector(selectTechnologies); | |||
| @@ -104,8 +102,8 @@ const AdsPage = ({ history }) => { | |||
| setToggleFiltersDrawer((oldState) => !oldState); | |||
| }; | |||
| const handleToggleModal = () => { | |||
| setToggleModal((oldState) => !oldState); | |||
| const createAd = () => { | |||
| history.push(CREATE_AD_PAGE); | |||
| }; | |||
| var settings = { | |||
| @@ -229,7 +227,11 @@ const AdsPage = ({ history }) => { | |||
| <div style={{ postion: "absolute" }}> | |||
| {!matches && ( | |||
| <> | |||
| <Fade in={isSearchFieldVisible} timeout={500}className="ads-page-search-by-title"> | |||
| <Fade | |||
| in={isSearchFieldVisible} | |||
| timeout={500} | |||
| className="ads-page-search-by-title" | |||
| > | |||
| {inputNormal} | |||
| </Fade> | |||
| <Fade in={isSearchFieldVisible} timeout={500}> | |||
| @@ -266,7 +268,6 @@ const AdsPage = ({ history }) => { | |||
| </> | |||
| )} | |||
| </div> | |||
| <AddAdModal open={toggleModal} handleClose={handleToggleModal} /> | |||
| <div className="ads" onClick={() => handleChangeVisibility(false)}> | |||
| {ads && ads.length > 0 && ( | |||
| <div className="active-ads"> | |||
| @@ -360,7 +361,7 @@ const AdsPage = ({ history }) => { | |||
| <div className="add-ad add-ad-no-ads"> | |||
| <IconButton | |||
| className="c-btn ads-page-btn c-btn--primary add-ad-btn" | |||
| onClick={handleToggleModal} | |||
| onClick={() => history.push(CREATE_AD_PAGE)} | |||
| > | |||
| Dodaj Oglas | |||
| </IconButton> | |||
| @@ -438,7 +439,7 @@ const AdsPage = ({ history }) => { | |||
| <div className="add-ad"> | |||
| <IconButton | |||
| className="c-btn ads-page-btn c-btn--primary add-ad-btn" | |||
| onClick={handleToggleModal} | |||
| onClick={createAd} | |||
| > | |||
| + Oglas | |||
| </IconButton> | |||
| @@ -0,0 +1,106 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| const CreateAdFirstStep = ({ | |||
| title, | |||
| setTitle, | |||
| employmentType, | |||
| setEmploymentType, | |||
| workHour, | |||
| setWorkHour, | |||
| expiredAt, | |||
| setExpiredAt, | |||
| }) => { | |||
| const employmentTypeHandler = (type) => { | |||
| setEmploymentType(type); | |||
| }; | |||
| const workHourHandler = (type) => { | |||
| setWorkHour(type); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Naslov</label> | |||
| <input | |||
| type="text" | |||
| onChange={(e) => setTitle(e.target.value)} | |||
| value={title} | |||
| placeholder="ex. Medior React Developer" | |||
| /> | |||
| </div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Tip zaposlenja</label> | |||
| <div className="create-ad-form-control-buttons"> | |||
| <button | |||
| className={`c-btn ${ | |||
| employmentType === "Work" | |||
| ? "c-btn c-btn--primary" | |||
| : "c-btn--primary-outlined" | |||
| }`} | |||
| onClick={employmentTypeHandler.bind(this, "Work")} | |||
| > | |||
| Posao | |||
| </button> | |||
| <button | |||
| className={`c-btn ${ | |||
| employmentType === "Intership" | |||
| ? "c-btn c-btn--primary" | |||
| : "c-btn--primary-outlined" | |||
| }`} | |||
| onClick={employmentTypeHandler.bind(this, "Intership")} | |||
| > | |||
| Intership | |||
| </button> | |||
| </div> | |||
| </div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Radno vreme</label> | |||
| <div className="create-ad-form-control-buttons"> | |||
| <button | |||
| className={`c-btn ${ | |||
| workHour === "PartTime" | |||
| ? "c-btn c-btn--primary" | |||
| : "c-btn--primary-outlined" | |||
| }`} | |||
| onClick={workHourHandler.bind(this, "PartTime")} | |||
| > | |||
| Part-time | |||
| </button> | |||
| <button | |||
| className={`c-btn ${ | |||
| workHour === "FullTime" | |||
| ? "c-btn c-btn--primary" | |||
| : "c-btn--primary-outlined" | |||
| }`} | |||
| onClick={workHourHandler.bind(this, "FullTime")} | |||
| > | |||
| Full-time | |||
| </button> | |||
| </div> | |||
| </div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Datum isteka oglasa</label> | |||
| <input | |||
| type="date" | |||
| onChange={(e) => setExpiredAt(e.target.value)} | |||
| value={expiredAt} | |||
| /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| CreateAdFirstStep.propTypes = { | |||
| title: PropTypes.string, | |||
| setTitle: PropTypes.any, | |||
| employmentType: PropTypes.string, | |||
| setEmploymentType: PropTypes.any, | |||
| workHour: PropTypes.string, | |||
| setWorkHour: PropTypes.any, | |||
| expiredAt: PropTypes.any, | |||
| setExpiredAt: PropTypes.any, | |||
| }; | |||
| export default CreateAdFirstStep; | |||
| @@ -0,0 +1,170 @@ | |||
| import React, { useState } from "react"; | |||
| import { useRef } from "react"; | |||
| import { useEffect } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import plusIcon from "../../assets/images/plus.png"; | |||
| import { | |||
| resetIsCheckedAddAdValue, | |||
| setTechnologiesAddAdReq, | |||
| } from "../../store/actions/addAdTechnologies/addAdTechnologiesActions"; | |||
| import { setCreateAdReq } from "../../store/actions/createAd/createAdActions"; | |||
| import CreateAdFirstStep from "./CreateAdFirstStep"; | |||
| import CreateAdSecondStep from "./CreateAdSecondStep"; | |||
| import CreateAdThirdStep from "./CreateAdThirdStep"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { ADS_PAGE } from "../../constants/pages"; | |||
| const CreateAdPage = () => { | |||
| const [stage, setStage] = useState(1); | |||
| const [title, setTitle] = useState(""); | |||
| const [employmentType, setEmploymentType] = useState("Work"); | |||
| const [workHour, setWorkHour] = useState("FullTime"); | |||
| const [expiredAt, setExpiredAt] = useState(""); | |||
| const [experience, setExperience] = useState(0); | |||
| const childRef = useRef(); | |||
| const history = useHistory(); | |||
| const technologies = useSelector( | |||
| (state) => state.addAdTechnologies.technologies | |||
| ); | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| dispatch(setTechnologiesAddAdReq()); | |||
| }, []); | |||
| const createAd = (keyResponsibilities, requirements, offer) => { | |||
| const onSuccessAddAd = () => { | |||
| setStage(1); | |||
| setTitle(""); | |||
| setEmploymentType("Work"); | |||
| setWorkHour("FullTime"); | |||
| setExpiredAt(""); | |||
| setExperience(0); | |||
| dispatch(resetIsCheckedAddAdValue()); | |||
| history.push(ADS_PAGE); | |||
| }; | |||
| const technologiesIds = technologies | |||
| .filter((x) => x.isChecked === true) | |||
| .map((x) => x.technologyId); | |||
| dispatch( | |||
| setCreateAdReq({ | |||
| title, | |||
| minimumExperience: experience, | |||
| createdAt: new Date(), | |||
| expiredAt, | |||
| keyResponsibilities, | |||
| requirements, | |||
| offer, | |||
| workHour, | |||
| employmentType, | |||
| technologiesIds, | |||
| onSuccessAddAd, | |||
| }) | |||
| ); | |||
| }; | |||
| const backClickHandler = () => { | |||
| if (stage === 1) { | |||
| return; | |||
| } | |||
| setStage((oldState) => oldState - 1); | |||
| }; | |||
| const forwardClickHandler = () => { | |||
| if (stage === 1 && (title === "" || expiredAt === "")) { | |||
| return; | |||
| } | |||
| if (stage === 2) { | |||
| const checkedTechnologies = technologies.filter( | |||
| (x) => x.isChecked === true | |||
| ); | |||
| if (checkedTechnologies.length === 0) { | |||
| return; | |||
| } | |||
| } | |||
| if (stage === 3) { | |||
| const { | |||
| keyResponsibilities: k, | |||
| requirements: r, | |||
| offer: o, | |||
| } = childRef.current(); | |||
| if (k === "" || r === "" || o === "") return; | |||
| createAd(k, r, o); | |||
| } | |||
| if (stage < 3) { | |||
| setStage((oldState) => oldState + 1); | |||
| return; | |||
| } | |||
| }; | |||
| return ( | |||
| <div className="create-ad-page"> | |||
| <div className="create-ad-page-content"> | |||
| <div className="create-ad-page-content-header"> | |||
| <img | |||
| src={plusIcon} | |||
| alt="plus" | |||
| style={{ width: "18px", height: "18px" }} | |||
| /> | |||
| <h2>Dodavanje</h2> | |||
| <h2> | |||
| <sub> | Oglas</sub> | |||
| </h2> | |||
| </div> | |||
| <div className="create-ad-steps"> | |||
| {stage === 1 && ( | |||
| <CreateAdFirstStep | |||
| title={title} | |||
| setTitle={setTitle} | |||
| employmentType={employmentType} | |||
| setEmploymentType={setEmploymentType} | |||
| workHour={workHour} | |||
| setWorkHour={setWorkHour} | |||
| expiredAt={expiredAt} | |||
| setExpiredAt={setExpiredAt} | |||
| /> | |||
| )} | |||
| {stage === 2 && ( | |||
| <CreateAdSecondStep | |||
| technologies={technologies} | |||
| experience={experience} | |||
| setExperience={setExperience} | |||
| /> | |||
| )} | |||
| {stage === 3 && <CreateAdThirdStep childRef={childRef} />} | |||
| </div> | |||
| <div className="create-ad-buttons" style={{ marginBottom: "2rem" }}> | |||
| <button | |||
| className="create-ad-buttons-back" | |||
| disabled={stage === 1} | |||
| onClick={backClickHandler} | |||
| > | |||
| NAZAD | |||
| </button> | |||
| <button | |||
| className="create-ad-buttons-forward" | |||
| onClick={forwardClickHandler} | |||
| > | |||
| NASTAVI | |||
| </button> | |||
| </div> | |||
| </div> | |||
| <div className="create-ad-go-back"> | |||
| <button onClick={() => history.push(ADS_PAGE)}> | |||
| Nazad na sve oglase | |||
| </button> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default CreateAdPage; | |||
| @@ -0,0 +1,102 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { Checkbox, FormControlLabel } from "@mui/material"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { changeIsCheckedAddAdValue } from "../../store/actions/addAdTechnologies/addAdTechnologiesActions"; | |||
| const CreateAdSecondStep = ({ technologies, experience, setExperience }) => { | |||
| const dispatch = useDispatch(); | |||
| const handleCheckboxes = (technologyId) => { | |||
| dispatch(changeIsCheckedAddAdValue(technologyId)); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Napredne tehnologije</label> | |||
| </div> | |||
| <div className="add-ad-modal-stage-sub-card"> | |||
| <div className="add-ad-modal-stage-sub-card-checkboxes-group"> | |||
| <label>Front-End</label> | |||
| <div className="add-ad-modal-stage-sub-card-checkboxes"> | |||
| {technologies | |||
| .filter((x) => x.technologyType === "Frontend") | |||
| .map((x) => ( | |||
| <FormControlLabel | |||
| key={x.technologyId} | |||
| control={ | |||
| <Checkbox | |||
| value={x.name} | |||
| checked={x.isChecked} | |||
| onChange={handleCheckboxes.bind(this, x.technologyId)} | |||
| /> | |||
| } | |||
| label={x.name} | |||
| /> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| <div className="add-ad-modal-stage-sub-card-checkboxes-group"> | |||
| <label>Back-End</label> | |||
| <div className="add-ad-modal-stage-sub-card-checkboxes"> | |||
| {technologies | |||
| .filter((x) => x.technologyType === "Backend") | |||
| .map((x) => ( | |||
| <FormControlLabel | |||
| key={x.technologyId} | |||
| control={ | |||
| <Checkbox | |||
| value={x.name} | |||
| checked={x.isChecked} | |||
| onChange={handleCheckboxes.bind(this, x.technologyId)} | |||
| /> | |||
| } | |||
| label={x.name} | |||
| /> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| <div className="add-ad-modal-stage-sub-card-checkboxes-group"> | |||
| <label>Other</label> | |||
| <div className="add-ad-modal-stage-sub-card-checkboxes"> | |||
| {technologies | |||
| .filter((x) => x.technologyType === "Other") | |||
| .map((x) => ( | |||
| <FormControlLabel | |||
| key={x.technologyId} | |||
| control={ | |||
| <Checkbox | |||
| value={x.name} | |||
| checked={x.isChecked} | |||
| onChange={handleCheckboxes.bind(this, x.technologyId)} | |||
| /> | |||
| } | |||
| label={x.name} | |||
| /> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Godine iskustva</label> | |||
| <input | |||
| type="number" | |||
| min={0} | |||
| onChange={(e) => setExperience(e.target.value)} | |||
| value={experience} | |||
| /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| CreateAdSecondStep.propTypes = { | |||
| technologies: PropTypes.any, | |||
| experience: PropTypes.number, | |||
| setExperience: PropTypes.any, | |||
| }; | |||
| export default CreateAdSecondStep; | |||
| @@ -0,0 +1,57 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { Editor } from "@tinymce/tinymce-react"; | |||
| import { useRef } from "react"; | |||
| import { useEffect } from "react"; | |||
| const CreateAdThirdStep = ({ childRef }) => { | |||
| const editorKeyResponsibilitiesRef = useRef(); | |||
| const editorRequirementsRef = useRef(); | |||
| const editorOfferRef = useRef(); | |||
| useEffect(() => { | |||
| childRef.current = alertUser; | |||
| }, []); | |||
| function alertUser() { | |||
| return { | |||
| keyResponsibilities: editorKeyResponsibilitiesRef.current.getContent(), | |||
| requirements: editorRequirementsRef.current.getContent(), | |||
| offer: editorOfferRef.current.getContent(), | |||
| }; | |||
| } | |||
| return ( | |||
| <div style={{ padding: "50rem 0 0 0" }}> | |||
| <div className="create-ad-form-control"> | |||
| <label>Glavna zaduženja</label> | |||
| <Editor | |||
| onInit={(evt, editor) => | |||
| (editorKeyResponsibilitiesRef.current = editor) | |||
| } | |||
| style={{ height: "1rem !important" }} | |||
| /> | |||
| </div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Uslovi</label> | |||
| <Editor | |||
| onInit={(evt, editor) => (editorRequirementsRef.current = editor)} | |||
| style={{ height: "1rem !important" }} | |||
| /> | |||
| </div> | |||
| <div className="create-ad-form-control"> | |||
| <label>Šta nudimo</label> | |||
| <Editor | |||
| onInit={(evt, editor) => (editorOfferRef.current = editor)} | |||
| style={{ height: "1rem !important" }} | |||
| /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| CreateAdThirdStep.propTypes = { | |||
| childRef: PropTypes.any, | |||
| }; | |||
| export default CreateAdThirdStep; | |||