| @@ -14544,6 +14544,11 @@ | |||
| "react-transition-group": "^4.3.0" | |||
| } | |||
| }, | |||
| "react-singleton-hook": { | |||
| "version": "3.4.0", | |||
| "resolved": "https://registry.npmjs.org/react-singleton-hook/-/react-singleton-hook-3.4.0.tgz", | |||
| "integrity": "sha512-eQEpyacGAaRejmWUizUdNNQFn5AO0iaKRSl1jxgC0FQadVY/I1WFuPrYiutglPzO9s8yEbIh95UXVJQel4d7HQ==" | |||
| }, | |||
| "react-toastify": { | |||
| "version": "9.0.3", | |||
| "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.3.tgz", | |||
| @@ -34,6 +34,7 @@ | |||
| "react-router-dom": "^5.2.0", | |||
| "react-scripts": "4.0.3", | |||
| "react-select": "^4.3.1", | |||
| "react-singleton-hook": "^3.4.0", | |||
| "react-toastify": "^9.0.3", | |||
| "redux": "^4.1.0", | |||
| "redux-persist": "^6.0.0", | |||
| @@ -83,14 +83,14 @@ const App = () => { | |||
| <Header /> | |||
| <GlobalStyle /> | |||
| <ToastContainer /> | |||
| {/* <div> | |||
| {/* <div> | |||
| <p>Connected: {"" + isConnected}</p> | |||
| <br /> | |||
| <p>Last pong: {lastPong || "-"}</p> | |||
| <br /> | |||
| <button onClick={sendPing}>Send ping</button> | |||
| </div> */} | |||
| <AppRoutes /> | |||
| <AppRoutes /> | |||
| </StyledEngineProvider> | |||
| </Router> | |||
| ); | |||
| @@ -1,6 +1,6 @@ | |||
| /* eslint-disable */ | |||
| import React from 'react'; | |||
| import { Redirect, Route, Switch } from 'react-router-dom'; | |||
| import React from "react"; | |||
| import { Redirect, Route, Switch } from "react-router-dom"; | |||
| import { | |||
| LOGIN_PAGE, | |||
| @@ -18,51 +18,52 @@ import { | |||
| PROFILE_PAGE, | |||
| CHAT_MESSAGE_PAGE, | |||
| CHAT_PAGE, | |||
| MY_OFFERS_PAGE | |||
| } from './constants/pages'; | |||
| import LoginPage from './pages/LoginPage/LoginPage'; | |||
| import HomePage from './pages/HomePage/HomePageMUI'; | |||
| import NotFoundPage from './pages/ErrorPages/NotFoundPage'; | |||
| import ErrorPage from './pages/ErrorPages/ErrorPage'; | |||
| import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPage'; | |||
| import PrivateRoute from './components/Router/PrivateRoute'; | |||
| import MailSent from './pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent'; | |||
| import Register from './pages/RegisterPages/Register/Register'; | |||
| import RegisterSuccessful from './pages/RegisterPages/RegisterSuccessful.js/RegisterSuccessful'; | |||
| import ResetPasswordPage from './pages/ResetPasswordPage/ResetPasswordPage'; | |||
| import CreateOffer from './pages/CreateOffer/CreateOffer'; | |||
| import ItemDetailsPage from './pages/ItemDetailsPage/ItemDetailsPageMUI'; | |||
| import ProfilePage from './pages/ProfilePage/ProfilePage'; | |||
| import ChatMessagesPage from './pages/ChatMessages/ChatMessages'; | |||
| import ChatPage from './pages/Chat/Chat'; | |||
| import MyOffers from './pages/MyOffers/MyOffers'; | |||
| MY_OFFERS_PAGE, | |||
| } from "./constants/pages"; | |||
| import LoginPage from "./pages/LoginPage/LoginPage"; | |||
| import HomePage from "./pages/HomePage/HomePageMUI"; | |||
| import NotFoundPage from "./pages/ErrorPages/NotFoundPage"; | |||
| import ErrorPage from "./pages/ErrorPages/ErrorPage"; | |||
| import ForgotPasswordPage from "./pages/ForgotPasswordPage/ForgotPasswordPage"; | |||
| import PrivateRoute from "./components/Router/PrivateRoute"; | |||
| import MailSent from "./pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent"; | |||
| import Register from "./pages/RegisterPages/Register/Register"; | |||
| import RegisterSuccessful from "./pages/RegisterPages/RegisterSuccessful.js/RegisterSuccessful"; | |||
| import ResetPasswordPage from "./pages/ResetPasswordPage/ResetPasswordPage"; | |||
| import CreateOffer from "./pages/CreateOffer/CreateOffer"; | |||
| import ItemDetailsPage from "./pages/ItemDetailsPage/ItemDetailsPageMUI"; | |||
| import ProfilePage from "./pages/ProfilePage/ProfilePage"; | |||
| import ChatMessagesPage from "./pages/ChatMessages/ChatMessages"; | |||
| import ChatPage from "./pages/Chat/Chat"; | |||
| import MyOffers from "./pages/MyOffers/MyOffers"; | |||
| const AppRoutes = () => { | |||
| return ( | |||
| <Switch> | |||
| <Route exact path={BASE_PAGE} component={HomePage} /> | |||
| <Route exact path={LOGIN_PAGE} component={LoginPage} /> | |||
| <Route path={NOT_FOUND_PAGE} component={NotFoundPage} /> | |||
| <Route path={REGISTER_SUCCESSFUL_PAGE} component={RegisterSuccessful} /> | |||
| <Route path={REGISTER_PAGE} component={Register} /> | |||
| <Route path={ERROR_PAGE} component={ErrorPage} /> | |||
| <Route path={FORGOT_PASSWORD_MAIL_SENT} component={MailSent} /> | |||
| <Route path={FORGOT_PASSWORD_PAGE} component={ForgotPasswordPage}/> | |||
| <Route path={RESET_PASSWORD_PAGE} component={ResetPasswordPage}/> | |||
| <Route path={CREATE_OFFER_PAGE} component={CreateOffer}/> | |||
| <Route path={ITEM_DETAILS_PAGE} component={ItemDetailsPage} /> | |||
| <Route path={PROFILE_PAGE} component={ProfilePage} /> | |||
| <Route path={HOME_PAGE} component={(props) => { | |||
| return <HomePage key={props.match.params.id} />; | |||
| }}/> | |||
| <PrivateRoute path={CHAT_MESSAGE_PAGE} component={ChatMessagesPage} /> | |||
| <PrivateRoute path={CHAT_PAGE} component={ChatPage} /> | |||
| <PrivateRoute path={MY_OFFERS_PAGE} component={MyOffers} /> | |||
| <Redirect from="*" to={NOT_FOUND_PAGE} /> | |||
| </Switch> | |||
| )}; | |||
| <Switch> | |||
| <Route exact path={BASE_PAGE} component={HomePage} /> | |||
| <Route exact path={LOGIN_PAGE} component={LoginPage} /> | |||
| <Route path={NOT_FOUND_PAGE} component={NotFoundPage} /> | |||
| <Route path={REGISTER_SUCCESSFUL_PAGE} component={RegisterSuccessful} /> | |||
| <Route path={REGISTER_PAGE} component={Register} /> | |||
| <Route path={ERROR_PAGE} component={ErrorPage} /> | |||
| <Route path={FORGOT_PASSWORD_MAIL_SENT} component={MailSent} /> | |||
| <Route path={FORGOT_PASSWORD_PAGE} component={ForgotPasswordPage} /> | |||
| <Route path={RESET_PASSWORD_PAGE} component={ResetPasswordPage} /> | |||
| <Route path={CREATE_OFFER_PAGE} component={CreateOffer} /> | |||
| <Route path={ITEM_DETAILS_PAGE} component={ItemDetailsPage} /> | |||
| <Route path={PROFILE_PAGE} component={ProfilePage} /> | |||
| <Route | |||
| path={HOME_PAGE} | |||
| component={(props) => { | |||
| return <HomePage key={props.match.params.id} />; | |||
| }} | |||
| /> | |||
| <PrivateRoute path={CHAT_MESSAGE_PAGE} component={ChatMessagesPage} /> | |||
| <PrivateRoute path={CHAT_PAGE} component={ChatPage} /> | |||
| <PrivateRoute path={MY_OFFERS_PAGE} component={MyOffers} /> | |||
| <Redirect from="*" to={NOT_FOUND_PAGE} /> | |||
| </Switch> | |||
| ); | |||
| }; | |||
| export default AppRoutes; | |||
| @@ -3,6 +3,7 @@ body { | |||
| -webkit-font-smoothing: antialiased; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| overflow-anchor: none; | |||
| background-color: #F1F1F1; | |||
| } | |||
| * { | |||
| @@ -0,0 +1,23 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { BackIcon } from "./BackButton.styled"; | |||
| import { ReactComponent as ArrowBack } from "../../../../assets/images/svg/arrow-back.svg"; | |||
| const BackButton = (props) => { | |||
| const backButtonHandler = () => { | |||
| props.setCurrentStep((prevState) => prevState - 1); | |||
| }; | |||
| return ( | |||
| <BackIcon onClick={backButtonHandler}> | |||
| {props.currentStep !== 1 ? <ArrowBack /> : ""} | |||
| </BackIcon> | |||
| ); | |||
| }; | |||
| BackButton.propTypes = { | |||
| setCurrentStep: PropTypes.func, | |||
| currentStep: PropTypes.bool, | |||
| }; | |||
| export default BackButton; | |||
| @@ -0,0 +1,16 @@ | |||
| import styled from "styled-components"; | |||
| import { Box } from "@mui/system"; | |||
| export const BackIcon = styled(Box)` | |||
| cursor: pointer; | |||
| position: absolute; | |||
| left: 40px; | |||
| @media screen and (max-width: 600px) { | |||
| left: 20px; | |||
| & svg { | |||
| width: 20px; | |||
| } | |||
| } | |||
| `; | |||
| @@ -0,0 +1,22 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { CloseIcon } from "./CloseButton.styled"; | |||
| import { ReactComponent as CloseButtonIcon } from "../../../../assets/images/svg/close-modal.svg"; | |||
| const CloseButton = (props) => { | |||
| const closeModalHandler = () => { | |||
| props.closeCreateOfferModal(false); | |||
| }; | |||
| return ( | |||
| <CloseIcon onClick={closeModalHandler}> | |||
| <CloseButtonIcon /> | |||
| </CloseIcon> | |||
| ); | |||
| }; | |||
| CloseButton.propTypes = { | |||
| closeCreateOfferModal: PropTypes.func, | |||
| }; | |||
| export default CloseButton; | |||
| @@ -0,0 +1,16 @@ | |||
| import styled from "styled-components"; | |||
| import { Box } from "@mui/system"; | |||
| export const CloseIcon = styled(Box)` | |||
| cursor: pointer; | |||
| position: absolute; | |||
| right: 40px; | |||
| @media screen and (max-width: 600px) { | |||
| right: 20px; | |||
| & svg { | |||
| width: 20px; | |||
| } | |||
| } | |||
| `; | |||
| @@ -1,113 +1,52 @@ | |||
| /* eslint-disable */ | |||
| import React, { useEffect, useState } from "react"; | |||
| import React, { useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useFormik } from "formik"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { NavLink, useHistory } from "react-router-dom"; | |||
| import * as Yup from "yup"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { fetchLogin } from "../../../store/actions/login/loginActions"; | |||
| import { FORGOT_PASSWORD_PAGE, HOME_PAGE } from "../../../constants/pages"; | |||
| import { ReactComponent as VisibilityOn } from "../../../assets/images/svg/eye-striked.svg"; | |||
| import { ReactComponent as VisibilityOff } from "../../../assets/images/svg/eye.svg"; | |||
| import Backdrop from "../../MUI/BackdropComponent"; | |||
| import { selectIsLoadingByActionType } from "../../../store/selectors/loadingSelectors"; | |||
| import { LOGIN_USER_LOADING } from "../../../store/actions/login/loginActionConstants"; | |||
| import { TextField } from "../../TextFields/TextField/TextField"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import Link from "../../Link/Link"; | |||
| import { | |||
| CreateOfferContainer, | |||
| CreateOfferTitle, | |||
| CreateOfferFormContainer, | |||
| RegisterAltText, | |||
| RegisterTextContainer, | |||
| FieldLabel, | |||
| ModalCreateOfferContainer, | |||
| ModalBackDrop, | |||
| ModalHeader, | |||
| BackIcon, | |||
| CloseIcon, | |||
| } from "./CreateOffer.styled"; | |||
| import selectedTheme from "../../../themes"; | |||
| import StepProgress from "../../StepProgress/StepProgress"; | |||
| import { Label } from "../../CheckBox/Label"; | |||
| import FirstPartCreateOffer from "./FirstPart/FirstPartCreateOffer"; | |||
| import SecondPartCreateOffer from "./SecondPart/SecondPartCreateOffer"; | |||
| import ThirdPartCreateOffer from "./ThirdPart/ThirdPartCreateOffer"; | |||
| import { | |||
| addOffer, | |||
| fetchOffers, | |||
| fetchOneOffer, | |||
| fetchProfileOffers, | |||
| } from "../../../store/actions/offers/offersActions"; | |||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | |||
| import { editOneOffer } from "../../../store/actions/offers/offersActions"; | |||
| import { ReactComponent as ArrowBack } from "../../../assets/images/svg/arrow-back.svg"; | |||
| import { ReactComponent as CloseButton } from "../../../assets/images/svg/close-modal.svg"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import BackdropComponent from "../../MUI/BackdropComponent"; | |||
| import CloseButton from "./CloseButton/CloseButton"; | |||
| import BackButton from "./BackButton/BackButton"; | |||
| const CreateOffer = ({ history, closeCreateOfferModal, editOffer, offer }) => { | |||
| const CreateOffer = ({ closeCreateOfferModal, editOffer, offer }) => { | |||
| const dispatch = useDispatch(); | |||
| const { t } = useTranslation(); | |||
| const [informations, setInformations] = useState({}); | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const [currentStep, setCurrentStep] = useState(1); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| const categories = useSelector((state) => state.categories.categories); | |||
| const historyRouter = useHistory(); | |||
| // When user refreshes page | |||
| // useEffect(() => { | |||
| // function redirectClient() { | |||
| // if (!tokens.RefreshToken && !tokens.JwtToken) { | |||
| // return; | |||
| // } | |||
| // } | |||
| // redirectClient(); | |||
| // }, [history, tokens]); | |||
| const isLoading = useSelector( | |||
| selectIsLoadingByActionType(LOGIN_USER_LOADING) | |||
| ); | |||
| const { t } = useTranslation(); | |||
| const userId = useSelector(selectUserId); | |||
| const handleApiResponseSuccess = (status) => { | |||
| if (editOffer === undefined) { | |||
| const userId = historyRouter.location.pathname.slice( | |||
| 9, | |||
| historyRouter.location.pathname.length | |||
| ); | |||
| const handleApiResponseSuccess = () => { | |||
| if (editOffer) { | |||
| dispatch(fetchOneOffer(offer._id)); | |||
| dispatch(fetchProfileOffers(userId)); | |||
| historyRouter.push({ | |||
| pathname: HOME_PAGE, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| } else { | |||
| const userId = offer.userId; | |||
| dispatch(fetchOffers({ queryString: "" })); | |||
| dispatch(fetchProfileOffers(userId)); | |||
| dispatch(fetchOneOffer(offer._id)); | |||
| } | |||
| }; | |||
| const handleSubmit = (values) => { | |||
| const { username: email, password: password } = values; | |||
| dispatch( | |||
| fetchLogin({ | |||
| email, | |||
| password, | |||
| handleApiResponseSuccess, | |||
| }) | |||
| ); | |||
| }; | |||
| const handleNext = (values) => { | |||
| setInformations({ ...informations, ...values }); | |||
| setCurrentStep((prevState) => prevState + 1); | |||
| }; | |||
| console.log(informations); | |||
| const newImgs = | |||
| informations.images && | |||
| informations.images | |||
| @@ -119,15 +58,6 @@ const CreateOffer = ({ history, closeCreateOfferModal, editOffer, offer }) => { | |||
| .replace("data:image/png;base64,", "") | |||
| ); | |||
| let subcategories = []; | |||
| for (const element of categories) { | |||
| if (element.name === informations.category) { | |||
| subcategories = element.subcategories.map((item) => item.name); | |||
| } | |||
| } | |||
| console.log(informations); | |||
| const offerData = { | |||
| name: informations.nameOfProduct, | |||
| description: informations.description, | |||
| @@ -160,14 +90,6 @@ const CreateOffer = ({ history, closeCreateOfferModal, editOffer, offer }) => { | |||
| closeCreateOfferModal(false); | |||
| }; | |||
| const backButtonHandler = () => { | |||
| setCurrentStep((prevState) => prevState - 1); | |||
| }; | |||
| const closeModalHandler = () => { | |||
| closeCreateOfferModal(false); | |||
| }; | |||
| const goStepBack = (stepNumber) => { | |||
| setCurrentStep(stepNumber); | |||
| const { | |||
| @@ -180,13 +102,21 @@ const CreateOffer = ({ history, closeCreateOfferModal, editOffer, offer }) => { | |||
| subcategory, | |||
| } = informations; | |||
| if (stepNumber === 1) { | |||
| setInformations({}); | |||
| setInformations({ | |||
| category, | |||
| condition, | |||
| description, | |||
| location, | |||
| nameOfProduct, | |||
| subcategory, | |||
| }); | |||
| } | |||
| if (stepNumber === 2) { | |||
| setInformations({ | |||
| category, | |||
| condition, | |||
| description, | |||
| images, | |||
| location, | |||
| nameOfProduct, | |||
| subcategory, | |||
| @@ -198,25 +128,26 @@ const CreateOffer = ({ history, closeCreateOfferModal, editOffer, offer }) => { | |||
| <> | |||
| <BackdropComponent | |||
| isLoading | |||
| handleClose={closeModalHandler} | |||
| handleClose={closeCreateOfferModal} | |||
| position="fixed" | |||
| /> | |||
| <ModalCreateOfferContainer currentStep={currentStep}> | |||
| <CreateOfferContainer currentStep={currentStep}> | |||
| <ModalHeader> | |||
| <BackIcon onClick={backButtonHandler}> | |||
| {currentStep !== 1 ? <ArrowBack /> : ""} | |||
| </BackIcon> | |||
| <BackButton | |||
| currentStep={currentStep} | |||
| setCurrentStep={setCurrentStep} | |||
| /> | |||
| <CreateOfferTitle component="h1" variant="h5"> | |||
| {currentStep === 3 | |||
| ? "Pregled" | |||
| ? `${t("offer.review")}` | |||
| : `${ | |||
| editOffer !== undefined ? "Izmena Objave" : "Nova Objava" | |||
| editOffer !== undefined | |||
| ? `${t("offer.changeOffer")}` | |||
| : `${t("offer.newOffer")}` | |||
| }`} | |||
| </CreateOfferTitle> | |||
| <CloseIcon onClick={closeModalHandler}> | |||
| <CloseButton /> | |||
| </CloseIcon> | |||
| <CloseButton closeCreateOfferModal={closeCreateOfferModal} /> | |||
| </ModalHeader> | |||
| <StepProgress | |||
| @@ -225,10 +156,18 @@ const CreateOffer = ({ history, closeCreateOfferModal, editOffer, offer }) => { | |||
| functions={[() => goStepBack(1), () => goStepBack(2)]} | |||
| /> | |||
| {currentStep === 1 && ( | |||
| <FirstPartCreateOffer handleNext={handleNext} offer={offer} /> | |||
| <FirstPartCreateOffer | |||
| handleNext={handleNext} | |||
| offer={offer} | |||
| informations={informations} | |||
| /> | |||
| )} | |||
| {currentStep === 2 && ( | |||
| <SecondPartCreateOffer handleNext={handleNext} offer={offer} /> | |||
| <SecondPartCreateOffer | |||
| handleNext={handleNext} | |||
| offer={offer} | |||
| informations={informations} | |||
| /> | |||
| )} | |||
| {currentStep === 3 && ( | |||
| <ThirdPartCreateOffer | |||
| @@ -250,5 +189,8 @@ CreateOffer.propTypes = { | |||
| pathname: PropTypes.string, | |||
| }), | |||
| }), | |||
| closeCreateOfferModal: PropTypes.func, | |||
| editOffer: PropTypes.bool, | |||
| offer: PropTypes.object, | |||
| }; | |||
| export default CreateOffer; | |||
| @@ -7,7 +7,7 @@ import Select from "../../Select/Select"; | |||
| export const ModalCreateOfferContainer = styled(Box)` | |||
| background-color: #fff; | |||
| position: fixed; | |||
| ${props => props.currentStep === 3 && `overflow-y: auto;`} | |||
| ${(props) => props.currentStep === 3 && `overflow-y: auto;`} | |||
| max-height: 90vh; | |||
| top: ${(props) => | |||
| props.currentStep === 1 ? "calc(50% - 400px);" : "calc(50% - 350px);"}; | |||
| @@ -29,8 +29,8 @@ export const ModalCreateOfferContainer = styled(Box)` | |||
| scrollbar-color: #ddd; | |||
| @media (max-height: 820px) { | |||
| top: ${props => props.currentStep === 1 ? 'calc(50% - 340px)' : 'calc(50% - 340px)'}; | |||
| top: ${(props) => | |||
| props.currentStep === 1 ? "calc(50% - 340px)" : "calc(50% - 340px)"}; | |||
| } | |||
| @media screen and (max-width: 628px) { | |||
| @@ -42,7 +42,6 @@ export const ModalCreateOfferContainer = styled(Box)` | |||
| left: 0; | |||
| padding: 0 30px; | |||
| } | |||
| `; | |||
| export const ModalHeader = styled(Box)` | |||
| @@ -65,19 +64,19 @@ export const BackIcon = styled(Box)` | |||
| } | |||
| `; | |||
| export const CloseIcon = styled(Box)` | |||
| cursor: pointer; | |||
| position: absolute; | |||
| right: 40px; | |||
| // export const CloseIcon = styled(Box)` | |||
| // cursor: pointer; | |||
| // position: absolute; | |||
| // right: 40px; | |||
| @media screen and (max-width: 600px) { | |||
| right: 20px; | |||
| // @media screen and (max-width: 600px) { | |||
| // right: 20px; | |||
| & svg { | |||
| width: 20px; | |||
| } | |||
| } | |||
| `; | |||
| // & svg { | |||
| // width: 20px; | |||
| // } | |||
| // } | |||
| // `; | |||
| export const CreateOfferContainer = styled(Container)` | |||
| margin-top: 0px; | |||
| @@ -126,7 +125,7 @@ export const CreateOfferDescription = styled(Typography)` | |||
| export const CreateOfferFormContainer = styled(Box)` | |||
| width: 335px; | |||
| height: 700px; | |||
| ${props => props.currentStep === 3 && `width: 120%; height: 420px;`} | |||
| ${(props) => props.currentStep === 3 && `width: 120%; height: 420px;`} | |||
| `; | |||
| export const RegisterAltText = styled(Typography)` | |||
| font-family: "Poppins"; | |||
| @@ -24,14 +24,31 @@ const FirstPartCreateOffer = (props) => { | |||
| const { t } = useTranslation(); | |||
| useEffect(() => { | |||
| if (!props.offer) { | |||
| if (Object.keys(props.informations).length !== 0) { | |||
| formik.setFieldValue("nameOfProduct", props.informations.nameOfProduct); | |||
| formik.setFieldValue("description", props.informations.description); | |||
| formik.setFieldValue("location", props.informations.location); | |||
| formik.setFieldValue("category", props.informations.category); | |||
| formik.setFieldValue("subcategory", props.informations.subcategory); | |||
| let scat = categories.filter( | |||
| (cat) => cat.name === props.informations.category | |||
| ); | |||
| setSubcat(scat[0].subcategories.map((x) => x.name)); | |||
| } | |||
| } else { | |||
| formik.setFieldValue("location", props.offer.location.city); | |||
| formik.setFieldValue("category", props.offer.category.name); | |||
| formik.setFieldValue("subcategory", props.offer.subcategory); | |||
| } | |||
| }, [props.offer, props.informations]); | |||
| useEffect(() => { | |||
| if (props.offer !== undefined) { | |||
| let scat = categories.filter( | |||
| (cat) => cat.name === props.offer.category.name | |||
| ); | |||
| console.log(categories); | |||
| console.log(scat[0].subcategories.map((x) => x.name)); | |||
| setSubcat(scat[0].subcategories.map((x) => x.name)); | |||
| } | |||
| }, [props.offer]); | |||
| @@ -41,15 +58,11 @@ const FirstPartCreateOffer = (props) => { | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| nameOfProduct: `${props.offer === undefined ? "" : props.offer.name}`, | |||
| description: `${ | |||
| props.offer === undefined ? "" : props.offer.description | |||
| }`, | |||
| location: `${props.offer === undefined ? "" : props.offer.location.city}`, | |||
| category: `${props.offer === undefined ? "" : props.offer.category.name}`, | |||
| subcategory: `${ | |||
| props.offer === undefined ? "" : props.offer.subcategory | |||
| }`, | |||
| nameOfProduct: `${!props.offer ? "" : props.offer.name}`, | |||
| description: `${!props.offer ? "" : props.offer.description}`, | |||
| location: "default", | |||
| category: "default", | |||
| subcategory: "default", | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| nameOfProduct: Yup.string().required(t("login.nameOfProductRequired")), | |||
| @@ -67,15 +80,12 @@ const FirstPartCreateOffer = (props) => { | |||
| const handleSubcategories = (category) => { | |||
| const filtered = categories.filter((cat) => cat.name === category); | |||
| console.log(filtered[0].subcategories.map((c) => c.name)); | |||
| setSubcat(filtered[0].subcategories.map((c) => c.name)); | |||
| }; | |||
| return ( | |||
| <> | |||
| <CreateOfferFormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| {/* <Backdrop position="absolute" isLoading={isLoading} /> */} | |||
| <FieldLabel leftText={t("offer.title")} /> | |||
| <TitleField | |||
| name="nameOfProduct" | |||
| @@ -122,68 +132,70 @@ const FirstPartCreateOffer = (props) => { | |||
| /> | |||
| )} | |||
| <FieldLabel leftText={t("offer.location")} /> | |||
| <SelectField | |||
| defaultValue={ | |||
| props.offer === undefined ? "default" : props.offer.location.city | |||
| } | |||
| onChange={(value) => { | |||
| formik.setFieldValue("location", value.target.value); | |||
| }} | |||
| > | |||
| <SelectOption value="default">{t("offer.choseLocation")}</SelectOption> | |||
| {locations.map((loc) => { | |||
| return ( | |||
| <SelectOption key={loc._if} value={loc.city}> | |||
| {loc.city} | |||
| </SelectOption> | |||
| ); | |||
| })} | |||
| </SelectField> | |||
| <FieldLabel leftText={t("offer.category")} /> | |||
| <SelectField | |||
| defaultValue={ | |||
| props.offer === undefined ? "default" : props.offer.category.name | |||
| } | |||
| onChange={(value) => { | |||
| formik.setFieldValue("category", value.target.value); | |||
| }} | |||
| > | |||
| <SelectOption value="default">{t("offer.choseCategory")}</SelectOption> | |||
| {categories.map((cat, i) => { | |||
| return ( | |||
| <SelectOption | |||
| key={i} | |||
| value={cat.name} | |||
| onClick={() => handleSubcategories(cat.name)} | |||
| > | |||
| {cat.name} | |||
| </SelectOption> | |||
| ); | |||
| })} | |||
| </SelectField> | |||
| <FieldLabel leftText={t("offer.location")} /> | |||
| <SelectField | |||
| defaultValue={formik.values.location} | |||
| onChange={(value) => { | |||
| formik.setFieldValue("location", value.target.value); | |||
| }} | |||
| value={formik.values.location} | |||
| > | |||
| <SelectOption style={{ display: "none" }} value="default"> | |||
| {t("offer.choseLocation")} | |||
| </SelectOption> | |||
| {locations.map((loc) => { | |||
| return ( | |||
| <SelectOption key={loc._id} value={loc.city}> | |||
| {loc.city} | |||
| </SelectOption> | |||
| ); | |||
| })} | |||
| </SelectField> | |||
| <FieldLabel leftText={t("offer.subcategory")} /> | |||
| <SelectField | |||
| defaultValue={ | |||
| props.offer === undefined ? "default" : props.offer.subcategory | |||
| } | |||
| // defaultValue="default" | |||
| onChange={(value) => { | |||
| formik.setFieldValue("subcategory", value.target.value); | |||
| }} | |||
| > | |||
| <SelectOption value="default">{t("offer.choseSubcategory")}</SelectOption> | |||
| {subcat && | |||
| subcat.map((sub, i) => { | |||
| <FieldLabel leftText={t("offer.category")} /> | |||
| <SelectField | |||
| defaultValue={formik.values.category} | |||
| onChange={(value) => { | |||
| formik.setFieldValue("category", value.target.value); | |||
| }} | |||
| value={formik.values.category} | |||
| > | |||
| <SelectOption style={{ display: "none" }} value="default"> | |||
| {t("offer.choseCategory")} | |||
| </SelectOption> | |||
| {categories.map((cat, i) => { | |||
| return ( | |||
| <SelectOption key={i} value={sub}> | |||
| {sub} | |||
| <SelectOption | |||
| key={i} | |||
| value={cat.name} | |||
| onClick={() => handleSubcategories(cat.name)} | |||
| > | |||
| {cat.name} | |||
| </SelectOption> | |||
| ); | |||
| })} | |||
| </SelectField> | |||
| </SelectField> | |||
| <FieldLabel leftText={t("offer.subcategory")} /> | |||
| <SelectField | |||
| defaultValue={formik.values.subcategory} | |||
| onChange={(value) => { | |||
| formik.setFieldValue("subcategory", value.target.value); | |||
| }} | |||
| value={formik.values.subcategory} | |||
| > | |||
| <SelectOption style={{ display: "none" }} value="default"> | |||
| {t("offer.choseSubcategory")} | |||
| </SelectOption> | |||
| {subcat && | |||
| subcat.map((sub, i) => { | |||
| return ( | |||
| <SelectOption key={i} value={sub}> | |||
| {sub} | |||
| </SelectOption> | |||
| ); | |||
| })} | |||
| </SelectField> | |||
| </CreateOfferFormContainer> | |||
| <NextButton | |||
| @@ -201,10 +213,13 @@ const FirstPartCreateOffer = (props) => { | |||
| !formik.values?.description || | |||
| formik.values?.category?.length === 0 || | |||
| !formik.values?.category || | |||
| formik.values?.category === "default" || | |||
| formik.values?.subcategory?.length === 0 || | |||
| !formik.values?.subcategory || | |||
| formik.values?.subcategory === "default" || | |||
| formik.values?.location?.length === 0 || | |||
| !formik.values?.location | |||
| !formik.values?.location || | |||
| formik.values?.location === "default" | |||
| } | |||
| > | |||
| {t("offer.continue")} | |||
| @@ -217,6 +232,7 @@ FirstPartCreateOffer.propTypes = { | |||
| children: PropTypes.any, | |||
| handleNext: PropTypes.func, | |||
| offer: PropTypes.node, | |||
| informations: PropTypes.any, | |||
| }; | |||
| export default FirstPartCreateOffer; | |||
| @@ -25,19 +25,29 @@ const SecondPartCreateOffer = (props) => { | |||
| const [images, setImages] = useState( | |||
| Array.apply(null, Array(numberOfImages)).map(() => {}) | |||
| ); // 3 images | |||
| const { t } = useTranslation(); | |||
| useEffect(() => { | |||
| // let editedImages = []; | |||
| if (!props.offer) { | |||
| if (Object.keys(props.informations).length > 6) { | |||
| setImages( | |||
| props.informations?.images | |||
| ? [...props.informations.images] | |||
| : [...images] | |||
| ); | |||
| formik.setFieldValue( | |||
| "condition", | |||
| props.informations?.condition | |||
| ? props.informations.condition | |||
| : "default" | |||
| ); | |||
| } | |||
| } else { | |||
| formik.setFieldValue("condition", props.offer.condition); | |||
| } | |||
| }, [props.offer, props.informations]); | |||
| // if (props.offer !== undefined && props.offer.images.length === 1) { | |||
| // editedImages.push(props.offer.images[0]); | |||
| // editedImages.push(""); | |||
| // editedImages.push(""); | |||
| // } else if (props.offer !== undefined && props.offer.images.length === 2) { | |||
| // editedImages.push(props.offer.images[0]); | |||
| // editedImages.push(props.offer.images[1]); | |||
| // editedImages.push(""); | |||
| // } | |||
| useEffect(() => { | |||
| setImages((prevState) => { | |||
| let editedImages = [...prevState]; | |||
| if (props.offer !== undefined && props.offer.images.length === 1) { | |||
| @@ -46,7 +56,6 @@ const SecondPartCreateOffer = (props) => { | |||
| if (props.offer !== undefined && props.offer.images.length === 2) { | |||
| editedImages[0] = props.offer.images[0]; | |||
| editedImages[1] = props.offer.images[1]; | |||
| } | |||
| @@ -61,7 +70,6 @@ const SecondPartCreateOffer = (props) => { | |||
| return [...newState]; | |||
| }); | |||
| }; | |||
| const { t } = useTranslation(); | |||
| const imagesEmpty = useMemo(() => { | |||
| let numOfImagesEmpty = 0; | |||
| @@ -70,28 +78,32 @@ const SecondPartCreateOffer = (props) => { | |||
| }); | |||
| return numOfImagesEmpty; | |||
| }, [images]); | |||
| // for (let i = 0; i < numberOfImages; i++) { | |||
| // let item = images[i]; | |||
| // if (item === null || item === undefined) imagesEmpty++; | |||
| // } | |||
| const handleSubmit = (values) => { | |||
| props.handleNext(values); | |||
| }; | |||
| const conditionSelectEnumArray = Object.values(conditionSelectEnum); | |||
| const filteredconditionSelectEnumArray = conditionSelectEnumArray.map( | |||
| (item) => item.mainText | |||
| ); | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| images: images, | |||
| condition: `${props.offer === undefined ? "" : props.offer.condition}`, | |||
| condition: | |||
| props.informations?.condition || props.offer?.condition || "default", | |||
| }, | |||
| validationSchema: Yup.object().shape({}), | |||
| validationSchema: Yup.object().shape({ | |||
| condition: Yup.string() | |||
| .required() | |||
| .oneOf(filteredconditionSelectEnumArray), | |||
| }), | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| console.log("slike", images); | |||
| return ( | |||
| <> | |||
| <CreateOfferFormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| @@ -107,25 +119,6 @@ const SecondPartCreateOffer = (props) => { | |||
| /> | |||
| ); | |||
| })} | |||
| {/* {props.offer === undefined | |||
| ? images.map((item, index) => ( | |||
| <ImagePicker | |||
| key={index} | |||
| image={item} | |||
| setImage={(image) => setImage(index, image)} | |||
| deleteImage={() => setImage(index, null)} | |||
| showDeleteIcon | |||
| /> | |||
| )) | |||
| : editedImages.map((item, index) => ( | |||
| <ImagePicker | |||
| key={index} | |||
| image={item} | |||
| setImage={(image) => setImage(index, image)} | |||
| deleteImage={() => setImage(index, null)} | |||
| showDeleteIcon | |||
| /> | |||
| ))} */} | |||
| </Scroller> | |||
| <SupportedFormats> | |||
| <Trans i18nKey="offer.supportedImagesFormats" /> | |||
| @@ -133,14 +126,12 @@ const SecondPartCreateOffer = (props) => { | |||
| <InputButtonContainer> | |||
| <FieldLabel leftText={t("offer.condition")} /> | |||
| <SelectField | |||
| defaultValue={ | |||
| props.offer === undefined ? "default" : props.offer.condition | |||
| } | |||
| onChange={(value) => { | |||
| formik.setFieldValue("condition", value.target.value); | |||
| }} | |||
| value={formik.values.condition} | |||
| > | |||
| <SelectOption value="default"> | |||
| <SelectOption style={{ display: "none" }} value="default"> | |||
| {t("offer.choseCondition")} | |||
| </SelectOption> | |||
| {Object.keys(conditionSelectEnum).map((key) => { | |||
| @@ -163,9 +154,13 @@ const SecondPartCreateOffer = (props) => { | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| onClick={formik.handleSubmit} | |||
| // disabled={imagesEmpty === numberOfImages} | |||
| disabled={ | |||
| props.offer === undefined ? imagesEmpty === numberOfImages : false | |||
| (props.offer === undefined | |||
| ? imagesEmpty === numberOfImages | |||
| : false) || | |||
| formik.values?.condition?.length === 0 || | |||
| !formik.values?.condition || | |||
| formik.values?.condition === "default" | |||
| } | |||
| > | |||
| {t("offer.continue")} | |||
| @@ -178,6 +173,7 @@ SecondPartCreateOffer.propTypes = { | |||
| children: PropTypes.node, | |||
| handleNext: PropTypes.func, | |||
| offer: PropTypes.node, | |||
| informations: PropTypes.any, | |||
| }; | |||
| export default SecondPartCreateOffer; | |||
| @@ -1,16 +1,13 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| // CreateOfferFormContainer, | |||
| PreviewCard, | |||
| } from "./ThirdPartCreateOffer.styled"; | |||
| import { PreviewCard } from "./ThirdPartCreateOffer.styled"; | |||
| import { NextButton } from "../FirstPart/FirstPartCreateOffer.styled"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { CreateOfferFormContainer } from "../CreateOffer.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const ThirdPartCreateOffer = (props) => { | |||
| const {t} = useTranslation(); | |||
| const { t } = useTranslation(); | |||
| const offer = { | |||
| offer: { | |||
| category: { | |||
| @@ -31,7 +28,11 @@ const ThirdPartCreateOffer = (props) => { | |||
| return ( | |||
| <> | |||
| <CreateOfferFormContainer currentStep={3} component="form" onSubmit={handleSubmit}> | |||
| <CreateOfferFormContainer | |||
| currentStep={3} | |||
| component="form" | |||
| onSubmit={handleSubmit} | |||
| > | |||
| <PreviewCard | |||
| offer={offer} | |||
| showBarterButton={false} | |||
| @@ -48,10 +49,6 @@ const ThirdPartCreateOffer = (props) => { | |||
| textcolor="white" | |||
| fullWidth | |||
| onClick={handleSubmit} | |||
| // disabled={ | |||
| // formik.values.username.length === 0 || | |||
| // formik.values.password.length === 0 | |||
| // } | |||
| > | |||
| {t("offer.publish")} | |||
| </NextButton> | |||
| @@ -13,27 +13,27 @@ const CategoryChoser = (props) => { | |||
| const filters = props.filters; | |||
| const { t } = useTranslation(); | |||
| const handleSelectCategory = (category) => { | |||
| filters.setSelectedCategory(category); | |||
| filters.clearSelectedSubcategory(); | |||
| filters.category.setSelectedCategory(category); | |||
| filters.subcategory.setSelectedSubcategory({}); | |||
| }; | |||
| return ( | |||
| <FilterRadioDropdown | |||
| data={[...filters?.categories]} | |||
| data={[...filters?.category.allCategories]} | |||
| icon={ | |||
| filters.selectedCategory?.name ? ( | |||
| filters.category.selectedCategoryLocally?.name ? ( | |||
| <CategoryChosenIcon /> | |||
| ) : ( | |||
| <CategoryIcon /> | |||
| ) | |||
| } | |||
| title={ | |||
| filters.selectedCategory?.name | |||
| ? filters.selectedCategory?.name | |||
| filters.category.selectedCategoryLocally?.name | |||
| ? filters.category.selectedCategoryLocally?.name | |||
| : t("filters.categories.title") | |||
| } | |||
| searchPlaceholder={t("filters.categories.placeholder")} | |||
| setSelected={handleSelectCategory} | |||
| selected={filters.selectedCategory} | |||
| selected={filters.category.selectedCategoryLocally} | |||
| firstOption={firstCategoryOption} | |||
| /> | |||
| ); | |||
| @@ -10,15 +10,11 @@ const LocationChoser = (props) => { | |||
| return ( | |||
| <FilterCheckboxDropdown | |||
| searchPlaceholder={t("filters.location.placeholder")} | |||
| data={[...filters.locations]} | |||
| filters={ | |||
| filters?.selectedLocations?.length > 0 | |||
| ? [...filters.selectedLocations] | |||
| : [] | |||
| } | |||
| data={[...filters.locations.allLocations]} | |||
| filters={[...filters.locations.selectedLocationsLocally]} | |||
| icon={<LocationIcon />} | |||
| title={t("filters.location.title")} | |||
| setItemsSelected={filters.setSelectedLocations} | |||
| setItemsSelected={filters.locations.setSelectedLocations} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -2,56 +2,56 @@ import React, { useEffect, useMemo, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { SubcategoryIcon } from "./SubcategoryChoser.styled"; | |||
| import FilterRadioDropdown from "../../FilterDropdown/Radio/FilterRadioDropdown"; | |||
| import _ from "lodash"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const firstSubcategoryOption = { | |||
| label: "SVE PODKATEGORIJE", | |||
| value: { _id: 0 }, | |||
| }; | |||
| // const firstSubcategoryOption = { | |||
| // label: "SVE PODKATEGORIJE", | |||
| // value: { _id: 0 }, | |||
| // }; | |||
| const SubcategoryChoser = (props) => { | |||
| const filters = props.filters; | |||
| const { t } = useTranslation(); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const [isDisabled, setIsDisabled] = useState(true); | |||
| const setInitialOpen = useMemo(() => { | |||
| return _.once(() => { | |||
| setIsOpened(true); | |||
| }); | |||
| }, []); | |||
| const subcategories = useMemo(() => { | |||
| return filters.category.getSubcategories( | |||
| filters.category.selectedCategoryLocally?.name | |||
| ); | |||
| }, [filters.category.selectedCategoryLocally]); | |||
| useEffect(() => { | |||
| if (!filters.selectedCategory || filters.selectedCategory?._id === 0) { | |||
| if (!filters.category.selectedCategoryLocally || filters.category.selectedCategoryLocally?._id === 0) { | |||
| setIsOpened(false); | |||
| setIsDisabled(true); | |||
| } else { | |||
| setIsDisabled(false); | |||
| setInitialOpen(); | |||
| setIsOpened(true); | |||
| } | |||
| }, [filters.selectedCategory]); | |||
| }, [filters.category.selectedCategoryLocally]); | |||
| const handleOpen = () => { | |||
| setIsOpened((prevState) => !prevState); | |||
| }; | |||
| console.log(filters); | |||
| return ( | |||
| <FilterRadioDropdown | |||
| data={filters.subcategories ? [...filters.subcategories] : []} | |||
| data={subcategories} | |||
| icon={<SubcategoryIcon />} | |||
| title={ | |||
| filters.selectedSubcategory?.name | |||
| ? filters.selectedSubcategory?.name | |||
| filters.subcategory.selectedSubcategory?.name | |||
| ? filters.subcategory.selectedSubcategory?.name | |||
| : t("filters.subcategories.title") | |||
| } | |||
| searchPlaceholder={t("filters.subcategories.placeholder")} | |||
| setSelected={filters.setSelectedSubcategory} | |||
| selected={filters.selectedSubcategory} | |||
| setSelected={filters.subcategory.setSelectedSubcategory} | |||
| selected={filters.subcategory.selectedSubcategoryLocally} | |||
| open={isOpened} | |||
| disabled={isDisabled} | |||
| handleOpen={handleOpen} | |||
| firstOption={firstSubcategoryOption} | |||
| firstOption={filters.subcategory.initialOption} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -1,26 +1,31 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { ContentContainer, FilterCardContainer } from "./FilterCard.styled"; | |||
| import useFilters from "../../../hooks/useFilters"; | |||
| import HeaderBack from "../../ItemDetails/Header/Header"; | |||
| import FilterHeader from "./FilterHeader/FilterHeader"; | |||
| import FilterFooter from "./FilterFooter/FilterFooter"; | |||
| import CategoryChoser from "./Choser/CategoryChoser/CategoryChoser"; | |||
| import SubcategoryChoser from "./Choser/SubcategoryChoser/SubcategoryChoser"; | |||
| import LocationChoser from "./Choser/LocationChoser/LocationChoser"; | |||
| import SkeletonFilterCard from "./Skeleton/SkeletonFilterCard"; | |||
| const FilterCard = (props) => { | |||
| const filters = useFilters(props.myOffers); | |||
| const offers = props.offers; | |||
| const filters = offers.filters; | |||
| return ( | |||
| <FilterCardContainer | |||
| responsiveOpen={props.responsiveOpen} | |||
| responsive={props.responsive} | |||
| filtersOpened={props.filtersOpened} | |||
| myOffers={props.myOffers} | |||
| skeleton={props.skeleton} | |||
| > | |||
| <SkeletonFilterCard | |||
| animationStage={props.animationStage} | |||
| skeleton={props.skeleton} | |||
| /> | |||
| {/* Header title for my offers */} | |||
| {props.myOffers && <HeaderBack />} | |||
| <FilterHeader /> | |||
| <FilterHeader filters={filters} /> | |||
| <ContentContainer> | |||
| {/* Categories */} | |||
| @@ -34,8 +39,8 @@ const FilterCard = (props) => { | |||
| </ContentContainer> | |||
| <FilterFooter | |||
| closeResponsive={props.closeResponsive} | |||
| responsiveOpen={props.responsiveOpen} | |||
| toggleFilters={props.toggleFilters} | |||
| filters={offers} | |||
| /> | |||
| </FilterCardContainer> | |||
| ); | |||
| @@ -43,11 +48,15 @@ const FilterCard = (props) => { | |||
| FilterCard.propTypes = { | |||
| children: PropTypes.node, | |||
| filters: PropTypes.any, | |||
| offers: PropTypes.any, | |||
| responsive: PropTypes.bool, | |||
| responsiveOpen: PropTypes.bool, | |||
| closeResponsive: PropTypes.func, | |||
| myOffers: PropTypes.bool, | |||
| skeleton: PropTypes.bool, | |||
| animationStage: PropTypes.number, | |||
| filtersOpened: PropTypes.bool, | |||
| toggleFilters: PropTypes.func, | |||
| }; | |||
| FilterCard.defaultProps = { | |||
| @@ -8,7 +8,7 @@ export const FilterCardContainer = styled(Box)` | |||
| border-top-right-radius: 4px; | |||
| height: ${(props) => | |||
| props.myOffers ? `calc(100% - 153px)` : `calc(100% - 90px)`}; | |||
| padding: 36px; | |||
| padding: ${props => props.skeleton ? "0" : "36px"}; | |||
| background-color: white; | |||
| width: calc(100% / 12 * 3.5); | |||
| left: 0; | |||
| @@ -23,17 +23,17 @@ export const FilterCardContainer = styled(Box)` | |||
| min-width: 285px !important; | |||
| z-index: 9; | |||
| margin-top: -24px; | |||
| transition: all ease-in-out 0.36s; | |||
| transition: all ease-in-out 1s; | |||
| transition: padding 0s; | |||
| & header { | |||
| position: absolute; | |||
| top: -73px; | |||
| } | |||
| @media (max-width: 900px) { | |||
| margin-left: -400px; | |||
| ${(props) => | |||
| props.responsiveOpen | |||
| props.filtersOpened | |||
| ? ` | |||
| display: "flex"; | |||
| margin-left: 0; | |||
| @@ -45,6 +45,9 @@ export const FilterCardContainer = styled(Box)` | |||
| : "display: none"}; | |||
| transition: all ease-in-out 0.36s; | |||
| } | |||
| & * { | |||
| ${props => props.skeleton && 'display: none;'} | |||
| } | |||
| @media (max-width: 600px) { | |||
| margin-top: -14px; | |||
| } | |||
| @@ -64,6 +64,7 @@ const FilterCheckboxDropdown = (props) => { | |||
| searchPlaceholder={props.searchPlaceholder} | |||
| isOpened={isOpened} | |||
| setIsOpened={setIsOpened} | |||
| setItemsSelected={props.setItemsSelected} | |||
| > | |||
| {dataToShow.map((item) => { | |||
| return ( | |||
| @@ -51,12 +51,11 @@ const FilterRadioDropdown = (props) => { | |||
| setIsOpened((prevState) => !prevState); | |||
| if (props.handleOpen) props.handleOpen(); | |||
| }; | |||
| return ( | |||
| <DropdownList | |||
| title={props.title} | |||
| textcolor={ | |||
| !props.selected || props.selected?._id === 0 | |||
| !props.selected || props.selected?._id === 0 || !props.selected?._id | |||
| ? selectedTheme.primaryText | |||
| : selectedTheme.primaryPurple | |||
| } | |||
| @@ -64,7 +63,7 @@ const FilterRadioDropdown = (props) => { | |||
| toggleIconClosed={<DropdownDown />} | |||
| toggleIconOpened={<DropdownUp />} | |||
| fullWidth | |||
| open={ props?.open !== undefined ? props.open : isOpened} | |||
| open={props?.open !== undefined ? props.open : isOpened} | |||
| disabled={props.disabled} | |||
| setIsOpened={handleOpen} | |||
| toggleIconStyles={{ | |||
| @@ -4,22 +4,23 @@ import { FilterFooterContainer } from "./FilterFooter.styled"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import useFilters from "../../../../hooks/useFilters"; | |||
| import useScreenDimensions from "../../../../hooks/useScreenDimensions"; | |||
| const FilterFooter = (props) => { | |||
| const { t } = useTranslation(); | |||
| const filters = useFilters(); | |||
| const filters = props.filters; | |||
| const screenDimensions = useScreenDimensions(); | |||
| const handleFilters = () => { | |||
| filters.applyFilters(); | |||
| if (props.closeResponsive) props.closeResponsive(); | |||
| filters.apply(); | |||
| if (props.toggleFilters) props.toggleFilters(); | |||
| }; | |||
| return ( | |||
| <FilterFooterContainer responsiveOpen={props.responsiveOpen}> | |||
| {props.responsiveOpen && ( | |||
| <FilterFooterContainer responsiveOpen={screenDimensions.width < 600}> | |||
| {screenDimensions.width < 600 && ( | |||
| <PrimaryButton | |||
| variant="outlined" | |||
| fullWidth | |||
| onClick={props.closeResponsive} | |||
| onClick={props.toggleFilters} | |||
| textcolor={selectedTheme.primaryPurple} | |||
| font="Open Sans" | |||
| style={{ | |||
| @@ -52,7 +53,8 @@ const FilterFooter = (props) => { | |||
| (FilterFooter.propTypes = { | |||
| responsiveOpen: PropTypes.bool, | |||
| closeResponsive: PropTypes.func, | |||
| toggleFilters: PropTypes.func, | |||
| filters: PropTypes.any, | |||
| }), | |||
| (FilterFooter.defaultProps = { | |||
| responsiveOpen: false, | |||
| @@ -2,14 +2,13 @@ import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { FilterHeaderContainer, Title } from "./FilterHeader.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import useFilters from "../../../../hooks/useFilters"; | |||
| import Link from "../../../Link/Link"; | |||
| const FilterHeader = () => { | |||
| const filters = useFilters(); | |||
| const FilterHeader = (props) => { | |||
| const filters = props.filters; | |||
| const { t } = useTranslation(); | |||
| const clearFilters = () => { | |||
| filters.clearFilters(); | |||
| filters.clear(); | |||
| }; | |||
| return ( | |||
| <FilterHeaderContainer> | |||
| @@ -23,6 +22,7 @@ const FilterHeader = () => { | |||
| FilterHeader.propTypes = { | |||
| children: PropTypes.node, | |||
| filters: PropTypes.any, | |||
| }; | |||
| export default FilterHeader; | |||
| @@ -0,0 +1,28 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| CircleOne, | |||
| CircleSecond, | |||
| LeftContainer, | |||
| Line, | |||
| SkeletonChooserContainer, | |||
| } from "./SkeletonChooserHeader.styled"; | |||
| const SkeletonChooserHeader = (props) => { | |||
| return ( | |||
| <SkeletonChooserContainer> | |||
| <LeftContainer> | |||
| <CircleOne animationStage={props.animationStage}/> | |||
| <Line animationStage={props.animationStage}/> | |||
| </LeftContainer> | |||
| <CircleSecond animationStage={props.animationStage}/> | |||
| </SkeletonChooserContainer> | |||
| ); | |||
| }; | |||
| SkeletonChooserHeader.propTypes = { | |||
| children: PropTypes.node, | |||
| animationStage: PropTypes.any, | |||
| }; | |||
| export default SkeletonChooserHeader; | |||
| @@ -0,0 +1,33 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import { ItemsTransition } from "../../../OfferCard/SkeletonOfferCard/SkeletonOfferCard.styled"; | |||
| export const SkeletonChooserContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| margin-top: 39px; | |||
| margin-bottom: 3px; | |||
| `; | |||
| export const CircleOne = styled(ItemsTransition)` | |||
| width: 24px; | |||
| height: 24px; | |||
| border-radius: 100% !important; | |||
| position: relative; | |||
| bottom: 3px; | |||
| `; | |||
| export const Line = styled(ItemsTransition)` | |||
| width: 117px; | |||
| height: 18px; | |||
| `; | |||
| export const CircleSecond = styled(ItemsTransition)` | |||
| width: 18px; | |||
| height: 18px; | |||
| border-radius: 100% !important; | |||
| `; | |||
| export const LeftContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 9px; | |||
| `; | |||
| @@ -0,0 +1,19 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { SkeletonChooserTitleContainer, SkeletonChooserTitleLine } from './SkeletonChooserTitle.styled' | |||
| const SkeletonChooserTitle = (props) => { | |||
| return ( | |||
| <SkeletonChooserTitleContainer center={props.center} animationStage={props.animationStage} > | |||
| <SkeletonChooserTitleLine center={props.center} animationStage={props.animationStage}/> | |||
| </SkeletonChooserTitleContainer> | |||
| ) | |||
| } | |||
| SkeletonChooserTitle.propTypes = { | |||
| children: PropTypes.any, | |||
| center: PropTypes.bool, | |||
| animationStage: PropTypes.number, | |||
| } | |||
| export default SkeletonChooserTitle | |||
| @@ -0,0 +1,19 @@ | |||
| import styled from "styled-components"; | |||
| import { BackgroundTransition } from "../../../../MarketPlace/Header/SkeletonHeader/SkeletonHeader.styled"; | |||
| import { ItemsTransition } from "../../../OfferCard/SkeletonOfferCard/SkeletonOfferCard.styled"; | |||
| export const SkeletonChooserTitleContainer = styled(ItemsTransition)` | |||
| margin-top: ${(props) => (props.center ? "44px" : "18px")}; | |||
| width: 100%; | |||
| height: 40px; | |||
| padding: 13px 18px; | |||
| `; | |||
| export const SkeletonChooserTitleLine = styled(BackgroundTransition)` | |||
| width: 108px; | |||
| height: 14px; | |||
| ${(props) => | |||
| props.center && | |||
| ` | |||
| margin: auto; | |||
| `} | |||
| `; | |||
| @@ -0,0 +1,39 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| SkeletonFilterCardContainer, | |||
| SkeletonHeader, | |||
| SkeletonHeaderLineOne, | |||
| SkeletonHeaderLineSecond, | |||
| } from "./SkeletonFilterCard.styled"; | |||
| import SkeletonChooserHeader from "./SkeletonChooserHeader/SkeletonChooserHeader"; | |||
| import SkeletonChooserTitle from "./SkeletonChooserTitle/SkeletonChooserTitle"; | |||
| import SkeletonSection from "./SkeletonSection/SkeletonSection"; | |||
| const SkeletonFilterCard = (props) => { | |||
| return ( | |||
| <SkeletonFilterCardContainer animationStage={props.animationStage} skeleton={props.skeleton}> | |||
| <SkeletonHeader> | |||
| <SkeletonHeaderLineOne animationStage={props.animationStage} /> | |||
| <SkeletonHeaderLineSecond animationStage={props.animationStage} /> | |||
| </SkeletonHeader> | |||
| <SkeletonChooserHeader animationStage={props.animationStage}/> | |||
| <SkeletonChooserTitle animationStage={props.animationStage} /> | |||
| <SkeletonSection numberOfOptions={14} animationStage={props.animationStage} /> | |||
| <SkeletonChooserHeader animationStage={props.animationStage} /> | |||
| <SkeletonChooserHeader animationStage={props.animationStage} /> | |||
| <SkeletonChooserTitle animationStage={props.animationStage} /> | |||
| <SkeletonSection numberOfOptions={3} animationStage={props.animationStage} /> | |||
| <SkeletonChooserTitle center animationStage={props.animationStage} /> | |||
| </SkeletonFilterCardContainer> | |||
| ); | |||
| }; | |||
| SkeletonFilterCard.propTypes = { | |||
| children: PropTypes.any, | |||
| animationStage: PropTypes.number, | |||
| skeleton: PropTypes.bool, | |||
| }; | |||
| export default SkeletonFilterCard; | |||
| @@ -0,0 +1,30 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import { BackgroundTransition } from "../../../MarketPlace/Header/SkeletonHeader/SkeletonHeader.styled"; | |||
| import { ItemsTransition } from "../../OfferCard/SkeletonOfferCard/SkeletonOfferCard.styled"; | |||
| export const SkeletonFilterCardContainer = styled(BackgroundTransition)` | |||
| display: ${props => props.skeleton ? "block" : "none"}; | |||
| width: 100%; | |||
| height: 100%; | |||
| padding: 36px; | |||
| & * { | |||
| display: flex; | |||
| border-radius: 4px; | |||
| } | |||
| `; | |||
| export const SkeletonHeader = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| `; | |||
| export const SkeletonHeaderLineOne = styled(ItemsTransition)` | |||
| width: 90px; | |||
| height: 27px; | |||
| `; | |||
| export const SkeletonHeaderLineSecond = styled(ItemsTransition)` | |||
| width: 78px; | |||
| height: 14px; | |||
| position: relative; | |||
| top: 7px; | |||
| `; | |||
| @@ -0,0 +1,25 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { SkeletonSectionContainer } from "./SkeletonSection.styled"; | |||
| import SkeletonSectionOption from "./SkeletonSectionOption/SkeletonSectionOption"; | |||
| const SkeletonSection = (props) => { | |||
| const arrayForMapping = Array.apply(null, Array(props.numberOfOptions)).map( | |||
| () => {} | |||
| ); | |||
| return ( | |||
| <SkeletonSectionContainer> | |||
| {arrayForMapping.map((item, index) => ( | |||
| <SkeletonSectionOption key={index} animationStage={props.animationStage} /> | |||
| ))} | |||
| </SkeletonSectionContainer> | |||
| ); | |||
| }; | |||
| SkeletonSection.propTypes = { | |||
| children: PropTypes.node, | |||
| numberOfOptions: PropTypes.number, | |||
| animationStage: PropTypes.number, | |||
| }; | |||
| export default SkeletonSection; | |||
| @@ -0,0 +1,10 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const SkeletonSectionContainer = styled(Box)` | |||
| padding-left: 18px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 9px; | |||
| margin-top: 18px; | |||
| ` | |||
| @@ -0,0 +1,22 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { Circle, EndLine, Line, OptionLeftContainer, SkeletonSectionOptionContainer } from './SkeletonSectionOption.styled' | |||
| const SkeletonSectionOption = (props) => { | |||
| return ( | |||
| <SkeletonSectionOptionContainer> | |||
| <OptionLeftContainer> | |||
| <Circle animationStage={props.animationStage} /> | |||
| <Line animationStage={props.animationStage} /> | |||
| </OptionLeftContainer> | |||
| <EndLine animationStage={props.animationStage} /> | |||
| </SkeletonSectionOptionContainer> | |||
| ) | |||
| } | |||
| SkeletonSectionOption.propTypes = { | |||
| children: PropTypes.any, | |||
| animationStage: PropTypes.number, | |||
| } | |||
| export default SkeletonSectionOption | |||
| @@ -0,0 +1,27 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import { ItemsTransition } from "../../../../OfferCard/SkeletonOfferCard/SkeletonOfferCard.styled"; | |||
| export const SkeletonSectionOptionContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| `; | |||
| export const OptionLeftContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 9px; | |||
| `; | |||
| export const Circle = styled(ItemsTransition)` | |||
| width: 14px; | |||
| height: 14px; | |||
| border-radius: 100% !important; | |||
| `; | |||
| export const Line = styled(ItemsTransition)` | |||
| width: 86px; | |||
| height: 14px; | |||
| `; | |||
| export const EndLine = styled(ItemsTransition)` | |||
| width: 23px; | |||
| height: 14px; | |||
| `; | |||
| @@ -1,15 +1,22 @@ | |||
| import React, { useEffect, useMemo } from "react"; | |||
| import React, { useEffect, useMemo, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| CheckButton, | |||
| ItemDetailsCardContainer, | |||
| OfferInfo, | |||
| Info, | |||
| DateButtonsContainer, | |||
| ButtonsContainer, | |||
| PostDate, | |||
| CategoryIcon, | |||
| SubcategoryIcon, | |||
| QuantityIcon, | |||
| EyeIcon, | |||
| EditIconContainer, | |||
| EditIcon, | |||
| RemoveIconContainer, | |||
| RemoveIcon, | |||
| EditDeleteButtons, | |||
| } from "./ItemDetailsCard.styled"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| @@ -21,14 +28,20 @@ import { formatDateLocale } from "../../../util/helpers/dateHelpers"; | |||
| import { startChat } from "../../../util/helpers/chatHelper"; | |||
| import Information from "./Information/Information"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import DeleteOffer from "../OfferCard/DeleteOffer/DeleteOffer"; | |||
| import CreateOffer from "../CreateOfferCard/CreateOffer"; | |||
| import OfferDetails from "./OfferDetails/OfferDetails"; | |||
| const ItemDetailsCard = (props) => { | |||
| const [showModalRemove, setShowModalRemove] = useState(false); | |||
| const [showModalEdit, setShowModalEdit] = useState(false); | |||
| const [isMyProfile, setIsMyProfile] = useState(false); | |||
| const offer = props.offer; | |||
| const chats = useSelector(selectLatestChats); | |||
| const userId = useSelector(selectUserId); | |||
| const { t } = useTranslation(); | |||
| const dispatch = useDispatch(); | |||
| const idProfile = offer?.offer?.userId; | |||
| const increaseOfferCounter = useMemo(() => { | |||
| return _.once(function (id) { | |||
| @@ -42,53 +55,97 @@ const ItemDetailsCard = (props) => { | |||
| } | |||
| }, [offer]); | |||
| useEffect(() => { | |||
| if (userId === idProfile) setIsMyProfile(true); | |||
| }, [userId, idProfile]); | |||
| const date = formatDateLocale(new Date(offer?.offer?._created)); | |||
| const startExchange = () => { | |||
| startChat(chats, offer?.offer, userId); | |||
| }; | |||
| return ( | |||
| <ItemDetailsCardContainer | |||
| sponsored={props.sponsored.toString()} | |||
| halfwidth={props.halfwidth ? 1 : 0} | |||
| className={props.className} | |||
| > | |||
| <OfferInfo> | |||
| <Info> | |||
| <Information | |||
| icon={<CategoryIcon />} | |||
| value={offer?.offer?.category?.name} | |||
| /> | |||
| <Information | |||
| icon={<SubcategoryIcon />} | |||
| value={offer?.offer?.subcategory} | |||
| /> | |||
| <Information | |||
| icon={<QuantityIcon />} | |||
| value={offer?.offer?.condition} | |||
| /> | |||
| <Information icon={<EyeIcon />} value={offer?.offer?.views?.count} /> | |||
| </Info> | |||
| <PostDate>{date}</PostDate> | |||
| </OfferInfo> | |||
| <OfferDetails | |||
| offer={offer} | |||
| showExchangeButton={props.showExchangeButton} | |||
| showPublishButton={props.showPublishButton} | |||
| /> | |||
| const closeEditModalHandler = () => { | |||
| setShowModalEdit(false); | |||
| }; | |||
| {!props.halfwidth && props.showExchangeButton && ( | |||
| <CheckButton | |||
| variant={props.sponsored ? "contained" : "outlined"} | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor={props.sponsored ? "white" : selectedTheme.primaryPurple} | |||
| onClick={startExchange} | |||
| > | |||
| {t("itemDetailsCard.startExchangeButton")} | |||
| </CheckButton> | |||
| const closeRemoveModalHandler = () => { | |||
| setShowModalRemove(false); | |||
| }; | |||
| return ( | |||
| <> | |||
| <ItemDetailsCardContainer | |||
| sponsored={props.sponsored.toString()} | |||
| halfwidth={props.halfwidth ? 1 : 0} | |||
| className={props.className} | |||
| > | |||
| <OfferInfo> | |||
| <Info> | |||
| <Information | |||
| icon={<CategoryIcon />} | |||
| value={offer?.offer?.category?.name} | |||
| /> | |||
| <Information | |||
| icon={<SubcategoryIcon />} | |||
| value={offer?.offer?.subcategory} | |||
| /> | |||
| <Information | |||
| icon={<QuantityIcon />} | |||
| value={offer?.offer?.condition} | |||
| /> | |||
| <Information | |||
| icon={<EyeIcon />} | |||
| value={offer?.offer?.views?.count} | |||
| /> | |||
| </Info> | |||
| <DateButtonsContainer> | |||
| <PostDate>{date}</PostDate> | |||
| {isMyProfile && ( | |||
| <ButtonsContainer> | |||
| <EditDeleteButtons> | |||
| <EditIconContainer onClick={() => setShowModalEdit(true)}> | |||
| <EditIcon /> | |||
| </EditIconContainer> | |||
| <RemoveIconContainer onClick={() => setShowModalRemove(true)}> | |||
| <RemoveIcon /> | |||
| </RemoveIconContainer> | |||
| </EditDeleteButtons> | |||
| </ButtonsContainer> | |||
| )} | |||
| </DateButtonsContainer> | |||
| </OfferInfo> | |||
| <OfferDetails | |||
| offer={offer} | |||
| showExchangeButton={props.showExchangeButton} | |||
| showPublishButton={props.showPublishButton} | |||
| /> | |||
| {!props.halfwidth && props.showExchangeButton && ( | |||
| <CheckButton | |||
| variant={props.sponsored ? "contained" : "outlined"} | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor={props.sponsored ? "white" : selectedTheme.primaryPurple} | |||
| onClick={startExchange} | |||
| > | |||
| {t("itemDetailsCard.startExchangeButton")} | |||
| </CheckButton> | |||
| )} | |||
| </ItemDetailsCardContainer> | |||
| {showModalRemove && ( | |||
| <DeleteOffer | |||
| offer={offer.offer} | |||
| closeModalHandler={closeRemoveModalHandler} | |||
| /> | |||
| )} | |||
| {showModalEdit && ( | |||
| <CreateOffer | |||
| editOffer | |||
| offer={offer.offer} | |||
| closeCreateOfferModal={closeEditModalHandler} | |||
| /> | |||
| )} | |||
| </ItemDetailsCardContainer> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -8,6 +8,9 @@ import { ReactComponent as Category } from "../../../assets/images/svg/category. | |||
| import { ReactComponent as Subcategory } from "../../../assets/images/svg/subcategory.svg"; | |||
| import { ReactComponent as Quantity } from "../../../assets/images/svg/quantity.svg"; | |||
| import { ReactComponent as Eye } from "../../../assets/images/svg/eye-striked.svg"; | |||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import { ReactComponent as Edit } from "../../../assets/images/svg/edit.svg"; | |||
| import { ReactComponent as Remove } from "../../../assets/images/svg/trash.svg"; | |||
| export const ItemDetailsCardContainer = styled(Container)` | |||
| display: flex; | |||
| @@ -37,11 +40,84 @@ export const OfferInfo = styled(Box)` | |||
| flex: 2; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| margin: 18px 0; | |||
| @media (max-width: 600px) { | |||
| margin: 0; | |||
| } | |||
| `; | |||
| export const ButtonsContainer = styled(Box)` | |||
| display: flex; | |||
| align-items: center; | |||
| @media screen and (max-width: 600px) { | |||
| position: absolute; | |||
| top: 75px; | |||
| right: 143px; | |||
| } | |||
| `; | |||
| export const EditDeleteButtons = styled(Box)` | |||
| display: flex; | |||
| @media screen and (max-width: 600px) { | |||
| position: absolute; | |||
| } | |||
| `; | |||
| export const EditIconContainer = styled(IconButton)` | |||
| width: 40px; | |||
| height: 40px; | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| border-radius: 100%; | |||
| padding-top: 2px; | |||
| text-align: center; | |||
| margin-left: 18px; | |||
| @media screen and (max-width: 600px) { | |||
| width: 32px; | |||
| height: 32px; | |||
| } | |||
| `; | |||
| export const EditIcon = styled(Edit)` | |||
| @media screen and (max-width: 600px) { | |||
| width: 16px; | |||
| height: 16px; | |||
| position: relative; | |||
| top: -4px; | |||
| left: -2px; | |||
| } | |||
| `; | |||
| export const RemoveIconContainer = styled(IconButton)` | |||
| width: 40px; | |||
| height: 40px; | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| border-radius: 100%; | |||
| padding-top: 2px; | |||
| text-align: center; | |||
| margin-left: 18px; | |||
| @media screen and (max-width: 600px) { | |||
| width: 32px; | |||
| height: 32px; | |||
| } | |||
| `; | |||
| export const RemoveIcon = styled(Remove)` | |||
| @media screen and (max-width: 600px) { | |||
| width: 16px; | |||
| height: 16px; | |||
| position: relative; | |||
| top: -4px; | |||
| left: -2px; | |||
| } | |||
| `; | |||
| export const DateButtonsContainer = styled(Box)` | |||
| display: flex; | |||
| align-items: center; | |||
| `; | |||
| export const PostDate = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| font-size: 12px; | |||
| @@ -58,6 +134,8 @@ export const PostDate = styled(Typography)` | |||
| bottom: 23px; | |||
| left: 18px; | |||
| width: 70px; | |||
| flex-direction: column; | |||
| align-items: start; | |||
| } | |||
| `; | |||
| export const Info = styled(Box)` | |||
| @@ -112,6 +190,35 @@ export const OfferViews = styled(Box)` | |||
| font-size: 12px; | |||
| width: 34%; | |||
| `; | |||
| export const OfferDescriptionTitle = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 12px; | |||
| color: ${selectedTheme.primaryDarkText}; | |||
| line-height: 16px; | |||
| @media (max-width: 600px) { | |||
| font-size: 9px; | |||
| line-height: 13px; | |||
| } | |||
| `; | |||
| export const OfferDescriptionText = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 16px; | |||
| color: ${selectedTheme.primaryDarkText}; | |||
| line-height: 22px; | |||
| padding-bottom: 20px; | |||
| max-width: ${(props) => | |||
| props.showBarterButton ? "calc(100% - 230px)" : "100%"}; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| max-width: 100%; | |||
| max-height: 100px; | |||
| } | |||
| /* max-width: calc(100% - 230px); */ | |||
| /* overflow: hidden; */ | |||
| /* display: -webkit-box; | |||
| -webkit-line-clamp: 5; | |||
| -webkit-box-orient: vertical; */ | |||
| `; | |||
| export const OfferDescription = styled(Box)` | |||
| flex: 3; | |||
| `; | |||
| @@ -162,16 +269,16 @@ export const PublishButtonContainer = styled(Box)` | |||
| `; | |||
| export const CategoryIcon = styled(Category)` | |||
| width: 14px; | |||
| ` | |||
| `; | |||
| export const SubcategoryIcon = styled(Subcategory)` | |||
| width: 14px; | |||
| ` | |||
| `; | |||
| export const QuantityIcon = styled(Quantity)` | |||
| width: 22px; | |||
| height: 16px; | |||
| ` | |||
| `; | |||
| export const EyeIcon = styled(Eye)` | |||
| width: 18px; | |||
| height: 20px; | |||
| ` | |||
| `; | |||
| @@ -15,20 +15,22 @@ import { | |||
| RemoveIcon, | |||
| SaveButton, | |||
| CategoryIcon, | |||
| } from "./DeleteOffer.styles"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { ReactComponent as Category } from "../../../assets/images/svg/category.svg"; | |||
| import BackdropComponent from "../../MUI/BackdropComponent"; | |||
| } from "./DeleteOffer.styled"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { ReactComponent as Category } from "../../../../assets/images/svg/category.svg"; | |||
| import BackdropComponent from "../../../MUI/BackdropComponent"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { | |||
| fetchProfileOffers, | |||
| removeOffer, | |||
| } from "../../../store/actions/offers/offersActions"; | |||
| } from "../../../../store/actions/offers/offersActions"; | |||
| import { useTranslation, Trans } from "react-i18next"; | |||
| import { useHistory } from "react-router-dom"; | |||
| const DeleteOffer = (props) => { | |||
| const dispatch = useDispatch(); | |||
| const { t } = useTranslation(); | |||
| const history = useHistory(); | |||
| const userId = props.offer.userId; | |||
| const offerId = props.offer._id; | |||
| const closeDeleteModalHandler = () => { | |||
| @@ -39,9 +41,14 @@ const DeleteOffer = (props) => { | |||
| dispatch(fetchProfileOffers(userId)); | |||
| }; | |||
| console.log(history); | |||
| const removeOfferHandler = () => { | |||
| dispatch(removeOffer({ offerId, handleApiResponseSuccess })); | |||
| props.closeModalHandler(); | |||
| if (history.location.pathname.includes("proizvodi")) { | |||
| history.goBack(); | |||
| } | |||
| }; | |||
| return ( | |||
| @@ -1,11 +1,11 @@ | |||
| import { Typography } from "@mui/material"; | |||
| import { Box } from "@mui/system"; | |||
| import styled from "styled-components"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { Icon } from "../../Icon/Icon"; | |||
| import { ReactComponent as Remove } from "../../../assets/images/svg/trash-gold.svg"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { Icon } from "../../../Icon/Icon"; | |||
| import { ReactComponent as Remove } from "../../../../assets/images/svg/trash-gold.svg"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { IconButton } from "../../../Buttons/IconButton/IconButton"; | |||
| export const DeleteOfferContainer = styled(Box)` | |||
| width: 537px; | |||
| @@ -30,7 +30,7 @@ import { | |||
| StarIcon, | |||
| StarIconContainer, | |||
| } from "./OfferCard.styled"; | |||
| import DeleteOffer from "./DeleteOffer"; | |||
| import DeleteOffer from "./DeleteOffer/DeleteOffer"; | |||
| import { ReactComponent as Category } from "../../../assets/images/svg/category.svg"; | |||
| import { ReactComponent as Message } from "../../../assets/images/svg/mail.svg"; | |||
| import selectedTheme from "../../../themes"; | |||
| @@ -235,6 +235,7 @@ OfferCard.propTypes = { | |||
| messageUser: PropTypes.func, | |||
| makeReview: PropTypes.func, | |||
| dontShowViews: PropTypes.bool, | |||
| skeleton: PropTypes.bool, | |||
| }; | |||
| OfferCard.defaultProps = { | |||
| halfwidth: false, | |||
| @@ -10,7 +10,7 @@ import { ReactComponent as Edit } from "../../../assets/images/svg/edit.svg"; | |||
| import { ReactComponent as Star } from "../../../assets/images/svg/star.svg"; | |||
| export const OfferCardContainer = styled(Container)` | |||
| display: flex; | |||
| display: ${(props) => (props.skeleton ? "none" : "flex")}; | |||
| flex-direction: column; | |||
| width: ${(props) => (!props.halfwidth ? "100%" : "49%")}; | |||
| box-sizing: border-box; | |||
| @@ -323,6 +323,10 @@ export const EyeIcon = styled(Eye)` | |||
| } | |||
| `; | |||
| export const RemoveIconContainer = styled(MessageIcon)` | |||
| display: block; | |||
| top: 18px; | |||
| right: 18px; | |||
| @media screen and (max-width: 600px) { | |||
| position: absolute; | |||
| display: block; | |||
| @@ -333,7 +337,15 @@ export const RemoveIconContainer = styled(MessageIcon)` | |||
| export const RemoveIcon = styled(Remove)``; | |||
| export const EditIconContainer = styled(MessageIcon)` | |||
| display: block; | |||
| right: 70px; | |||
| top: 18px; | |||
| right: 76px; | |||
| @media screen and (max-width: 600px) { | |||
| position: absolute; | |||
| display: block; | |||
| right: 20px; | |||
| top: 60%; | |||
| } | |||
| `; | |||
| export const EditIcon = styled(Edit)``; | |||
| export const StarIconContainer = styled(MessageIcon)` | |||
| @@ -0,0 +1,63 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| LeftPart, | |||
| RightPart, | |||
| SkeletonAuthor, | |||
| SkeletonColumnContainer, | |||
| SkeletonDescription, | |||
| SkeletonDescriptionLine, | |||
| SkeletonDetail, | |||
| SkeletonExchangeButton, | |||
| SkeletonExchangeLine, | |||
| SkeletonGroup, | |||
| SkeletonImage, | |||
| SkeletonLocation, | |||
| SkeletonMessageButton, | |||
| SkeletonOfferCardContainer, | |||
| SkeletonRowGroup, | |||
| SkeletonTitle, | |||
| SpreadLine, | |||
| } from "./SkeletonOfferCard.styled"; | |||
| const SkeletonOfferCard = (props) => { | |||
| return ( | |||
| <SkeletonOfferCardContainer skeleton={props.skeleton} animationStage={props.animationStage}> | |||
| <LeftPart animationStage={props.animationStage}> | |||
| <SkeletonImage animationStage={props.animationStage} /> | |||
| <SkeletonColumnContainer animationStage={props.animationStage}> | |||
| <SkeletonTitle animationStage={props.animationStage} /> | |||
| <SkeletonGroup animationStage={props.animationStage}> | |||
| <SkeletonAuthor animationStage={props.animationStage} /> | |||
| <SkeletonLocation animationStage={props.animationStage} /> | |||
| </SkeletonGroup> | |||
| <SkeletonRowGroup animationStage={props.animationStage}> | |||
| <SkeletonDetail animationStage={props.animationStage} /> | |||
| <SkeletonDetail animationStage={props.animationStage} /> | |||
| <SkeletonDetail animationStage={props.animationStage} /> | |||
| </SkeletonRowGroup> | |||
| </SkeletonColumnContainer> | |||
| </LeftPart> | |||
| <SpreadLine /> | |||
| <RightPart animationStage={props.animationStage}> | |||
| <SkeletonDescription animationStage={props.animationStage} /> | |||
| <SkeletonDescriptionLine animationStage={props.animationStage} /> | |||
| <SkeletonDescriptionLine animationStage={props.animationStage} /> | |||
| <SkeletonDescriptionLine animationStage={props.animationStage} /> | |||
| <SkeletonDescriptionLine animationStage={props.animationStage} /> | |||
| </RightPart> | |||
| <SkeletonExchangeButton animationStage={props.animationStage}> | |||
| <SkeletonExchangeLine animationStage={props.animationStage} /> | |||
| </SkeletonExchangeButton> | |||
| <SkeletonMessageButton /> | |||
| </SkeletonOfferCardContainer> | |||
| ); | |||
| }; | |||
| SkeletonOfferCard.propTypes = { | |||
| children: PropTypes.node, | |||
| skeleton: PropTypes.bool, | |||
| animationStage: PropTypes.number, | |||
| }; | |||
| export default SkeletonOfferCard; | |||
| @@ -0,0 +1,133 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { BackgroundTransition } from "../../../MarketPlace/Header/SkeletonHeader/SkeletonHeader.styled"; | |||
| export const ItemsTransition = styled(Box)` | |||
| transition-duration: 0.4s; | |||
| transition-property: background-color; | |||
| background-color: ${props => props.animationStage === 1 ? selectedTheme.filterSkeletonItems : selectedTheme.filterSkeletonItemsSecond} !important; | |||
| `; | |||
| export const SkeletonOfferCardContainer = styled(BackgroundTransition)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| width: 100%; | |||
| box-sizing: border-box; | |||
| margin: 14px 0; | |||
| border-radius: 4px; | |||
| ${(props) => | |||
| props.sponsored === "true" && | |||
| `border: 1px solid ${selectedTheme.borderSponsoredColor};`} | |||
| padding: 16px; | |||
| max-width: 2000px; | |||
| height: 180px; | |||
| position: relative; | |||
| & * { | |||
| border-radius: 4px; | |||
| } | |||
| @media (max-width: 550px) { | |||
| height: 184px; | |||
| padding: 18px; | |||
| padding-top: 12px; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| height: 330px; | |||
| width: 180px; | |||
| margin: 0 18px; | |||
| `} | |||
| } | |||
| `; | |||
| export const LeftPart = styled(Box)` | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: row; | |||
| margin-right: 40px; | |||
| `; | |||
| export const SpreadLine = styled(Box)` | |||
| height: 108px; | |||
| margin-top: auto; | |||
| margin-bottom: auto; | |||
| opacity: 0.12; | |||
| border: 1px solid black; | |||
| `; | |||
| export const RightPart = styled(Box)` | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| gap: 4px; | |||
| margin-left: 36px; | |||
| padding-top: 20px; | |||
| `; | |||
| export const SkeletonImage = styled(ItemsTransition)` | |||
| width: 144px; | |||
| height: 144px; | |||
| `; | |||
| export const SkeletonColumnContainer = styled(Box)` | |||
| display: flex; | |||
| margin-left: 18px; | |||
| justify-content: space-between; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| `; | |||
| export const SkeletonTitle = styled(ItemsTransition)` | |||
| width: 90px; | |||
| height: 27px; | |||
| `; | |||
| export const SkeletonGroup = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 4px; | |||
| `; | |||
| export const SkeletonAuthor = styled(ItemsTransition)` | |||
| width: 117px; | |||
| height: 18px; | |||
| `; | |||
| export const SkeletonLocation = styled(ItemsTransition)` | |||
| width: 90px; | |||
| height: 18px; | |||
| `; | |||
| export const SkeletonRowGroup = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| `; | |||
| export const SkeletonDetail = styled(ItemsTransition)` | |||
| width: 72px; | |||
| height: 14px; | |||
| background-color: ${selectedTheme.filterSkeletonItems}; | |||
| `; | |||
| export const SkeletonDescription = styled(ItemsTransition)` | |||
| width: 72px; | |||
| height: 14px; | |||
| background-color: ${selectedTheme.filterSkeletonItems}; | |||
| `; | |||
| export const SkeletonDescriptionLine = styled(ItemsTransition)` | |||
| width: 221px; | |||
| height: 18px; | |||
| background-color: ${selectedTheme.filterSkeletonItems}; | |||
| `; | |||
| export const SkeletonMessageButton = styled(ItemsTransition)` | |||
| width: 40px; | |||
| height: 40px; | |||
| border-radius: 100%; | |||
| background-color: ${selectedTheme.filterSkeletonItems}; | |||
| top: 18px; | |||
| right: 18px; | |||
| `; | |||
| export const SkeletonExchangeButton = styled(ItemsTransition)` | |||
| width: 180px; | |||
| height: 48px; | |||
| background-color: ${selectedTheme.filterSkeletonItems}; | |||
| bottom: 18px; | |||
| right: 18px; | |||
| position: absolute; | |||
| padding-top: 17px; | |||
| `; | |||
| export const SkeletonExchangeLine = styled(BackgroundTransition)` | |||
| width: 108px; | |||
| height: 14px; | |||
| background-color: ${selectedTheme.filterSkeletonBackground}; | |||
| margin: auto; | |||
| `; | |||
| @@ -10,7 +10,6 @@ import { | |||
| TitleSortContainer, | |||
| } from "./ChatColumn.styled"; | |||
| import { sortEnum } from "../../enums/sortEnum"; | |||
| import useSorting from "../../hooks/useSorting"; | |||
| import { ReactComponent as Down } from "../../assets/images/svg/down-arrow.svg"; | |||
| import { IconStyled } from "../Icon/Icon.styled"; | |||
| import { Grid } from "@mui/material"; | |||
| @@ -20,6 +19,7 @@ import { useTranslation } from "react-i18next"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { selectLatestChats } from "../../store/selectors/chatSelectors"; | |||
| import { fetchChats } from "../../store/actions/chat/chatActions"; | |||
| import useSorting from "../../hooks/useOffers/useSorting"; | |||
| export const DownArrow = (props) => { | |||
| <IconStyled {...props}> | |||
| @@ -31,7 +31,7 @@ const DirectChatHeader = (props) => { | |||
| if (exchange.buyer?.givenReview) return true; | |||
| } | |||
| return false; | |||
| }, [exchange, userId]) | |||
| }, [exchange, userId]); | |||
| useEffect(() => { | |||
| if (showReviewModal) { | |||
| @@ -39,17 +39,17 @@ const DirectChatHeader = (props) => { | |||
| } else { | |||
| document.body.style.overflow = "auto"; | |||
| } | |||
| }, [showReviewModal]) | |||
| }, [showReviewModal]); | |||
| const makeReview = () => { | |||
| setShowReviewModal(true); | |||
| }; | |||
| const handleGiveReviewSuccess = () => { | |||
| refetchExchange(); | |||
| } | |||
| }; | |||
| const refetchExchange = () => { | |||
| dispatch(fetchExchange(chat.chat.exchangeId)); | |||
| } | |||
| }; | |||
| return ( | |||
| <DirectChatHeaderContainer> | |||
| {showReviewModal && ( | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useState } from "react"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| DirectChatNewMessageContainer, | |||
| @@ -17,6 +17,7 @@ import { useHistory, useLocation } from "react-router-dom"; | |||
| const DirectChatNewMessage = (props) => { | |||
| const [typedValue, setTypedValue] = useState(""); | |||
| const [isFocused, setIsFocused] = useState(false); | |||
| const dispatch = useDispatch(); | |||
| const { t } = useTranslation(); | |||
| const location = useLocation(); | |||
| @@ -24,8 +25,8 @@ const DirectChatNewMessage = (props) => { | |||
| const handleApiResponseSuccess = () => { | |||
| props.refreshChat(); | |||
| }; | |||
| const handleSend = () => { | |||
| console.log(location.state?.offerId); | |||
| const handleSend = useCallback(() => { | |||
| console.log(typedValue); | |||
| if (location.state?.offerId) { | |||
| initiateNewChat(typedValue); | |||
| } else { | |||
| @@ -38,11 +39,20 @@ const DirectChatNewMessage = (props) => { | |||
| ); | |||
| } | |||
| setTypedValue(""); | |||
| }; | |||
| }, [typedValue]); | |||
| const handleMessageSendSuccess = (newChatId) => { | |||
| history.replace(`${newChatId}`); | |||
| dispatch(fetchChats()); | |||
| }; | |||
| useEffect(() => { | |||
| const listener = (event) => { | |||
| if (event.keyCode === 13) handleSend(); | |||
| }; | |||
| if (isFocused) window.addEventListener("keypress", listener); | |||
| return () => window.removeEventListener("keypress", listener); | |||
| }, [typedValue]); | |||
| const initiateNewChat = (typedValue) => { | |||
| const offerId = location.state.offerId; | |||
| dispatch( | |||
| @@ -54,6 +64,8 @@ const DirectChatNewMessage = (props) => { | |||
| <NewMessageField | |||
| placeholder={t("messages.sendPlaceholder")} | |||
| fullWidth | |||
| onFocus={() => setIsFocused(true)} | |||
| onBlur={() => setIsFocused(false)} | |||
| italicPlaceholder | |||
| value={typedValue} | |||
| onChange={(typed) => setTypedValue(typed.target.value)} | |||
| @@ -3,8 +3,8 @@ import { | |||
| AddOfferButton, | |||
| AuthButtonsContainer, | |||
| EndIcon, | |||
| FilterContainer, | |||
| FilterIcon, | |||
| // FilterContainer, | |||
| // FilterIcon, | |||
| HeaderContainer, | |||
| LoginButton, | |||
| LogoContainer, | |||
| @@ -36,10 +36,10 @@ import { useTranslation } from "react-i18next"; | |||
| import { IconButton } from "../Buttons/IconButton/IconButton"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { selectUserId } from "../../store/selectors/loginSelectors"; | |||
| import { useSearch } from "../../hooks/useSearch"; | |||
| import { selectProfileName } from "../../store/selectors/profileSelectors"; | |||
| import { useHistory, useRouteMatch } from "react-router-dom"; | |||
| import { | |||
| BASE_PAGE, | |||
| FORGOT_PASSWORD_MAIL_SENT, | |||
| FORGOT_PASSWORD_PAGE, | |||
| HOME_PAGE, | |||
| @@ -48,32 +48,28 @@ import { | |||
| REGISTER_SUCCESSFUL_PAGE, | |||
| RESET_PASSWORD_PAGE, | |||
| } from "../../constants/pages"; | |||
| import useFilters from "../../hooks/useFilters"; | |||
| import FilterCard from "../Cards/FilterCard/FilterCard"; | |||
| import { useQueryString } from "../../hooks/useQueryString"; | |||
| import { convertQueryStringFrontend } from "../../util/helpers/queryHelpers"; | |||
| // import { convertQueryStringForFrontend } from "../../util/helpers/queryHelpers"; | |||
| import { fetchMineProfile } from "../../store/actions/profile/profileActions"; | |||
| import CreateOffer from "../Cards/CreateOfferCard/CreateOffer"; | |||
| import { Drawer as HeaderDrawer } from "./Drawer/Drawer"; | |||
| import useSearch from "../../hooks/useOffers/useSearch"; | |||
| // import useQueryString from "../../hooks/useOffers/useQueryString"; | |||
| const Header = (props) => { | |||
| const [openFilters, setOpenFilters] = useState(false); | |||
| const Header = () => { | |||
| // const setOpenFilters = useState(false)[1]; | |||
| const [showSearchBar, setShowSearchBar] = useState(true); | |||
| const [numberOfFilters, setNumberOfFilters] = useState(0); | |||
| const [showCreateOfferModal, setShowCreateOfferModal] = useState(false); | |||
| const { t } = useTranslation(); | |||
| const theme = useTheme(); | |||
| const searchRef = useRef(null); | |||
| const matches = useMediaQuery(theme.breakpoints.down("md")); | |||
| const user = useSelector(selectUserId); | |||
| const search = useSearch(); | |||
| const search = useSearch(() => {}); | |||
| const dispatch = useDispatch(); | |||
| const name = useSelector(selectProfileName); | |||
| const history = useHistory(); | |||
| const routeMatch = useRouteMatch(); | |||
| const filters = useFilters(); | |||
| const searchMobileRef = useRef(null); | |||
| const queryStringHook = useQueryString(); | |||
| const [openDrawer, setOpenDrawer] = useState(false); | |||
| useEffect(() => { | |||
| @@ -87,6 +83,9 @@ const Header = (props) => { | |||
| setUserAnchorEl(null); | |||
| }; | |||
| }, []); | |||
| useEffect(() => { | |||
| searchRef.current.value = search.searchString ?? ""; | |||
| }, [search.searchString]); | |||
| useEffect(() => { | |||
| if (history.location.pathname !== "/home") { | |||
| setShowSearchBar(false); | |||
| @@ -94,25 +93,18 @@ const Header = (props) => { | |||
| setShowSearchBar(true); | |||
| } | |||
| }, [history.location.pathname]); | |||
| useEffect(() => { | |||
| setNumberOfFilters(filters.calculateFiltersChosen()); | |||
| }, [ | |||
| filters.selectedCategory, | |||
| filters.selectedLocations, | |||
| filters.selectedSubcategory, | |||
| ]); | |||
| useEffect(() => { | |||
| if (queryStringHook.loadedFromURL) { | |||
| const queryObject = new URLSearchParams( | |||
| convertQueryStringFrontend(queryStringHook.queryString) | |||
| ); | |||
| if (queryObject.has("search")) { | |||
| searchRef.current.value = queryObject.get("search"); | |||
| searchMobileRef.current.value = queryObject.get("search"); | |||
| } | |||
| } | |||
| }); | |||
| // useEffect(() => { | |||
| // if (queryStringHook.loadedFromURL) { | |||
| // const queryObject = new URLSearchParams( | |||
| // convertQueryStringForFrontend(queryStringHook.queryString) | |||
| // ); | |||
| // if (queryObject.has("search")) { | |||
| // searchRef.current.value = queryObject.get("search"); | |||
| // searchMobileRef.current.value = queryObject.get("search"); | |||
| // } | |||
| // } | |||
| // }); | |||
| const closeCreateOfferModal = () => { | |||
| setShowCreateOfferModal(false); | |||
| @@ -137,15 +129,13 @@ const Header = (props) => { | |||
| useEffect(() => { | |||
| let shouldShowHeader = true; | |||
| console.log(props); | |||
| if ( | |||
| location.pathname === LOGIN_PAGE || | |||
| location.pathname === REGISTER_PAGE || | |||
| location.pathname === REGISTER_SUCCESSFUL_PAGE || | |||
| location.pathname === FORGOT_PASSWORD_PAGE || | |||
| location.pathname === FORGOT_PASSWORD_MAIL_SENT || | |||
| location.pathname === RESET_PASSWORD_PAGE || | |||
| location.pathname === "/" | |||
| location.pathname === RESET_PASSWORD_PAGE | |||
| ) { | |||
| shouldShowHeader = false; | |||
| } | |||
| @@ -183,11 +173,22 @@ const Header = (props) => { | |||
| searchRef.current.removeEventListener("keyup", listener); | |||
| }; | |||
| const handleSearch = (value) => { | |||
| search.searchOffers(value); | |||
| }; | |||
| const toggleFilters = () => { | |||
| setOpenFilters((prevState) => !prevState); | |||
| if ( | |||
| history.location.pathname !== HOME_PAGE && | |||
| history.location.pathname !== BASE_PAGE | |||
| ) { | |||
| const newQueryString = new URLSearchParams({ search: value }); | |||
| history.push({ | |||
| pathname: HOME_PAGE, | |||
| search: newQueryString.toString(), | |||
| }); | |||
| } else { | |||
| search.searchOffers(value); | |||
| } | |||
| }; | |||
| // const toggleFilters = () => { | |||
| // setOpenFilters((prevState) => !prevState); | |||
| // }; | |||
| const handleToggleDrawer = () => { | |||
| setOpenDrawer(!openDrawer); | |||
| @@ -383,30 +384,25 @@ const Header = (props) => { | |||
| fullWidth | |||
| shouldShow={showSearchBar} | |||
| ref={searchMobileRef} | |||
| placeholder={t("header.searchOffers")} | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <React.Fragment> | |||
| <FilterContainer number={numberOfFilters}> | |||
| <FilterIcon onClick={toggleFilters} /> | |||
| </FilterContainer> | |||
| <EndIcon size="36px"> | |||
| <SearchIcon | |||
| onClick={() => handleSearch(searchMobileRef.current.value)} | |||
| /> | |||
| </EndIcon> | |||
| </React.Fragment> | |||
| <EndIcon size="36px"> | |||
| <SearchIcon | |||
| onClick={() => handleSearch(searchMobileRef.current.value)} | |||
| /> | |||
| </EndIcon> | |||
| ), | |||
| }} | |||
| placeholder={t("header.searchOffers")} | |||
| italicPlaceholder | |||
| onFocus={handleFocusSearch} | |||
| onBlur={handleBlurSearch} | |||
| /> | |||
| <FilterCard | |||
| {/* <FilterCard | |||
| responsive={true} | |||
| responsiveOpen={openFilters} | |||
| closeResponsive={toggleFilters} | |||
| /> | |||
| /> */} | |||
| {showCreateOfferModal && ( | |||
| <CreateOffer closeCreateOfferModal={closeCreateOfferModal} /> | |||
| )} | |||
| @@ -3,10 +3,8 @@ import styled from "styled-components"; | |||
| import { PrimaryButton } from "../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { TextField } from "../TextFields/TextField/TextField"; | |||
| import { ReactComponent as Search } from "../../assets/images/svg/magnifying-glass.svg"; | |||
| import { ReactComponent as Filter } from "../../assets/images/svg/filter.svg"; | |||
| import selectedTheme from "../../themes"; | |||
| import { Icon } from "../Icon/Icon"; | |||
| import IconWithNumber from "../Icon/IconWithNumber/IconWithNumber"; | |||
| export const SearchInput = styled(TextField)` | |||
| background-color: #f4f4f4; | |||
| @@ -195,25 +193,4 @@ export const SearchInputMobile = styled(SearchInput)` | |||
| width: 0; | |||
| } | |||
| `; | |||
| export const FilterContainer = styled(IconWithNumber)` | |||
| position: relative; | |||
| top: 8px; | |||
| left: 95px; | |||
| cursor: pointer; | |||
| background-color: ${selectedTheme.offerBackgroundColor} !important; | |||
| & div { | |||
| width: 16px; | |||
| height: 16px; | |||
| background-color: ${selectedTheme.primaryPurple}; | |||
| position: absolute; | |||
| top: -5px; | |||
| right: -5px; | |||
| line-height: 15px; | |||
| text-align: center; | |||
| padding-right: 2px; | |||
| } | |||
| `; | |||
| export const FilterIcon = styled(Filter)` | |||
| background-color: ${selectedTheme.offerBackgroundColor}; | |||
| `; | |||
| export const HeaderContainer = styled(Box)``; | |||
| @@ -1,20 +1,24 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { IconWithNumberContainer, Number } from './IconWithNumber.styled' | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { IconWithNumberContainer, Number } from "./IconWithNumber.styled"; | |||
| const IconWithNumber = (props) => { | |||
| return ( | |||
| <IconWithNumberContainer className={props.className}> | |||
| {props.children} | |||
| {props.number > 0 && <Number>{props.number}</Number>} | |||
| <IconWithNumberContainer | |||
| className={props.className} | |||
| onClick={props.onClick} | |||
| > | |||
| {props.children} | |||
| {props.number > 0 && <Number>{props.number}</Number>} | |||
| </IconWithNumberContainer> | |||
| ) | |||
| } | |||
| ); | |||
| }; | |||
| IconWithNumber.propTypes = { | |||
| children: PropTypes.node, | |||
| number: PropTypes.number, | |||
| className: PropTypes.string, | |||
| } | |||
| children: PropTypes.node, | |||
| number: PropTypes.number, | |||
| className: PropTypes.string, | |||
| onClick: PropTypes.func, | |||
| }; | |||
| export default IconWithNumber | |||
| export default IconWithNumber; | |||
| @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; | |||
| const Header = (props) => { | |||
| const history = useHistory(); | |||
| const {t} = useTranslation(); | |||
| const { t } = useTranslation(); | |||
| const handleBackButton = () => { | |||
| history.goBack(); | |||
| @@ -29,17 +29,19 @@ const ItemDetailsHeaderCard = (props) => { | |||
| history.push(`/profile/${offer?.offer?.userId}`); | |||
| }; | |||
| const messageUser = (offer) => { | |||
| const chatItem = chats.find(item => item.chat.offerId === offer?.offer?._id); | |||
| const chatItem = chats.find( | |||
| (item) => item.chat.offerId === offer?.offer?._id | |||
| ); | |||
| if (chatItem !== undefined) { | |||
| history.push(`/messages/${chatItem.chat._id}`) | |||
| history.push(`/messages/${chatItem.chat._id}`); | |||
| } else { | |||
| if (offer?.offer?.userId !== userId) { | |||
| history.push(`/messages/newMessage`, { | |||
| offerId: offer?.offer?._id | |||
| }) | |||
| offerId: offer?.offer?._id, | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| return ( | |||
| <ItemDetailsHeaderContainer | |||
| isMyProfile={props.isMyProfile} | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useEffect, useState } from "react"; | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| HeaderAltLocation, | |||
| @@ -18,15 +18,11 @@ import { ReactComponent as GridLine } from "../../../assets/images/svg/offer-gri | |||
| import { ReactComponent as Down } from "../../../assets/images/svg/down-arrow.svg"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { sortEnum } from "../../../enums/sortEnum"; | |||
| import useFilters from "../../../hooks/useFilters"; | |||
| import useSorting from "../../../hooks/useSorting"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Tooltip } from "@mui/material"; | |||
| import { | |||
| ALL_CATEGORIES, | |||
| COMMA, | |||
| SPREAD, | |||
| } from "../../../constants/marketplaceHeaderTitle"; | |||
| import SkeletonHeader from "./SkeletonHeader/SkeletonHeader"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectHeaderString } from "../../../store/selectors/filtersSelectors"; | |||
| const DownArrow = (props) => ( | |||
| <IconStyled {...props}> | |||
| @@ -35,137 +31,108 @@ const DownArrow = (props) => ( | |||
| ); | |||
| const Header = (props) => { | |||
| const filters = useFilters(); | |||
| const sorting = useSorting(); | |||
| const { t } = useTranslation(); | |||
| const [sortOption, setSortOption] = useState(sortEnum.INITIAL); | |||
| const [headerString, setHeaderString] = useState(ALL_CATEGORIES); | |||
| //Changing shown sort option on select menu | |||
| useEffect(() => { | |||
| setSortOption(sorting.selectedSortOption); | |||
| }, [sorting.selectedSortOption]); | |||
| const sorting = props.sorting; | |||
| const headerString = useSelector(selectHeaderString); | |||
| // Changing header string on refresh or on load | |||
| useEffect(() => { | |||
| let headerStringLocal = ALL_CATEGORIES; | |||
| if (filters.isApplied) { | |||
| // Adding category to header string | |||
| if (filters.selectedCategory?.name) { | |||
| headerStringLocal = filters.selectedCategory.name; | |||
| // Adding subcategories to header string | |||
| if (filters.selectedSubcategory?.name) { | |||
| headerStringLocal += `${SPREAD}${filters.selectedSubcategory.name}`; | |||
| } | |||
| } | |||
| // Adding locations to header string | |||
| if (filters.selectedLocations && filters.selectedLocations?.length > 0) { | |||
| headerStringLocal += SPREAD; | |||
| filters.selectedLocations.forEach((location, index) => { | |||
| // Checking if item is last | |||
| if (index + 1 === filters.selectedLocations.length) { | |||
| headerStringLocal += location.city; | |||
| } else { | |||
| headerStringLocal += location.city + COMMA; | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| setHeaderString(headerStringLocal); | |||
| }, [ | |||
| filters.isApplied, | |||
| filters.selectedCategory, | |||
| filters.selectedSubcategory, | |||
| filters.selectedLocations, | |||
| ]); | |||
| const handleChangeSelect = (event) => { | |||
| let chosenOption; | |||
| for (const sortOption in sortEnum) { | |||
| if (sortEnum[sortOption].value === event.target.value) { | |||
| chosenOption = sortEnum[sortOption]; | |||
| sorting.changeSorting(chosenOption); | |||
| } | |||
| } | |||
| // let chosenOption; | |||
| sorting.changeSorting(event.target.value); | |||
| // for (const sortOption in sortEnum) { | |||
| // if (sortEnum[sortOption].value === event.target.value) { | |||
| // chosenOption = sortEnum[sortOption]; | |||
| // sorting.changeSorting(chosenOption); | |||
| // } | |||
| // } | |||
| }; | |||
| return ( | |||
| <HeaderContainer> | |||
| {/* Setting appropriate header title if page is market place or my offers */} | |||
| <Tooltip title={headerString}> | |||
| {!props.myOffers ? ( | |||
| headerString === "Sve kategorije" && | |||
| (sorting.selectedSortOption === sortEnum.INITIAL || | |||
| sorting.selectedSortOption === sortEnum.NEW) ? ( | |||
| <React.Fragment> | |||
| <HeaderLocation initial>{headerString}</HeaderLocation> | |||
| <HeaderAltLocation>{t("header.newOffers")}</HeaderAltLocation> | |||
| </React.Fragment> | |||
| <> | |||
| <SkeletonHeader | |||
| skeleton={props.skeleton} | |||
| animationStage={props.animationStage} | |||
| /> | |||
| <HeaderContainer skeleton={props.skeleton}> | |||
| {/* Setting appropriate header title if page is market place or my offers */} | |||
| <Tooltip title={headerString}> | |||
| {!props.myOffers ? ( | |||
| headerString === "Sve kategorije" && | |||
| (sorting.selectedSortOption === sortEnum.INITIAL || | |||
| sorting.selectedSortOption === sortEnum.NEW) ? ( | |||
| <React.Fragment> | |||
| <HeaderLocation initial>{headerString}</HeaderLocation> | |||
| <HeaderAltLocation>{t("header.newOffers")}</HeaderAltLocation> | |||
| </React.Fragment> | |||
| ) : ( | |||
| <HeaderLocation>{headerString}</HeaderLocation> | |||
| ) | |||
| ) : ( | |||
| <HeaderLocation>{headerString}</HeaderLocation> | |||
| ) | |||
| ) : ( | |||
| <MySwapsTitle> | |||
| <RefreshIcon /> {t("header.myOffers")} | |||
| </MySwapsTitle> | |||
| )} | |||
| </Tooltip> | |||
| {/* ^^^^^^ */} | |||
| <MySwapsTitle> | |||
| <RefreshIcon /> {t("header.myOffers")} | |||
| </MySwapsTitle> | |||
| )} | |||
| </Tooltip> | |||
| {/* ^^^^^^ */} | |||
| <HeaderOptions> | |||
| <HeaderButtons> | |||
| {/* Setting display of offer cards to full width */} | |||
| <HeaderButton | |||
| iconColor={ | |||
| props.isGrid | |||
| ? selectedTheme.iconStrokeColor | |||
| : selectedTheme.primaryPurple | |||
| } | |||
| onClick={() => props.setIsGrid(false)} | |||
| > | |||
| <GridLine /> | |||
| </HeaderButton> | |||
| {/* ^^^^^^ */} | |||
| <HeaderOptions> | |||
| <HeaderButtons> | |||
| {/* Setting display of offer cards to full width */} | |||
| <HeaderButton | |||
| iconColor={ | |||
| props.isGrid | |||
| ? selectedTheme.iconStrokeColor | |||
| : selectedTheme.primaryPurple | |||
| } | |||
| onClick={() => props.setIsGrid(false)} | |||
| > | |||
| <GridLine /> | |||
| </HeaderButton> | |||
| {/* ^^^^^^ */} | |||
| {/* Setting display of offer cards to half width (Grid) */} | |||
| <HeaderButton | |||
| iconColor={ | |||
| props.isGrid | |||
| ? selectedTheme.primaryPurple | |||
| : selectedTheme.iconStrokeColor | |||
| } | |||
| onClick={() => props.setIsGrid(true)} | |||
| > | |||
| <GridSquare /> | |||
| </HeaderButton> | |||
| {/* ^^^^^^ */} | |||
| </HeaderButtons> | |||
| {/* Setting display of offer cards to half width (Grid) */} | |||
| <HeaderButton | |||
| iconColor={ | |||
| props.isGrid | |||
| ? selectedTheme.primaryPurple | |||
| : selectedTheme.iconStrokeColor | |||
| {/* Select option to choose sorting */} | |||
| <HeaderSelect | |||
| value={ | |||
| sorting.selectedSortOption?.value | |||
| ? sorting.selectedSortOption | |||
| : "default" | |||
| } | |||
| onClick={() => props.setIsGrid(true)} | |||
| IconComponent={DownArrow} | |||
| onChange={handleChangeSelect} | |||
| > | |||
| <GridSquare /> | |||
| </HeaderButton> | |||
| <SelectOption style={{ display: "none" }} value="default"> | |||
| Sortiraj po | |||
| </SelectOption> | |||
| {Object.keys(sortEnum).map((property) => { | |||
| if (sortEnum[property].value === 0) return; | |||
| return ( | |||
| <SelectOption | |||
| value={sortEnum[property]} | |||
| key={sortEnum[property].value} | |||
| > | |||
| {sortEnum[property].mainText} | |||
| </SelectOption> | |||
| ); | |||
| })} | |||
| </HeaderSelect> | |||
| {/* ^^^^^^ */} | |||
| </HeaderButtons> | |||
| {/* Select option to choose sorting */} | |||
| <HeaderSelect | |||
| value={sortOption?.value ? sortOption.value : "default"} | |||
| IconComponent={DownArrow} | |||
| onChange={handleChangeSelect} | |||
| > | |||
| <SelectOption style={{ display: "none" }} value="default"> | |||
| Sortiraj po | |||
| </SelectOption> | |||
| {Object.keys(sortEnum).map((property) => { | |||
| if(sortEnum[property].value === 0) return; | |||
| return ( | |||
| <SelectOption | |||
| value={sortEnum[property].value} | |||
| key={sortEnum[property].value} | |||
| > | |||
| {sortEnum[property].mainText} | |||
| </SelectOption> | |||
| ); | |||
| })} | |||
| </HeaderSelect> | |||
| {/* ^^^^^^ */} | |||
| </HeaderOptions> | |||
| </HeaderContainer> | |||
| </HeaderOptions> | |||
| </HeaderContainer> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -176,6 +143,9 @@ Header.propTypes = { | |||
| filters: PropTypes.any, | |||
| category: PropTypes.string, | |||
| myOffers: PropTypes.bool, | |||
| skeleton: PropTypes.bool, | |||
| animationStage: PropTypes.number, | |||
| sorting: PropTypes.any, | |||
| }; | |||
| Header.defaultProps = { | |||
| isGrid: false, | |||
| @@ -8,7 +8,7 @@ import { ReactComponent as Refresh } from "../../../assets/images/svg/refresh.sv | |||
| export const HeaderContainer = styled(Box)` | |||
| margin-top: 20px; | |||
| display: flex; | |||
| display: ${props => props.skeleton ? "none" : "flex"}; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| `; | |||
| @@ -0,0 +1,25 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { CircleGroup, SkeletonHeaderCircle, SkeletonHeaderContainer, SkeletonHeaderLine, SkeletonHeaderRightLine, SkeletonRowGroup } from './SkeletonHeader.styled' | |||
| const SkeletonHeader = (props) => { | |||
| return ( | |||
| <SkeletonHeaderContainer skeleton={props.skeleton}> | |||
| <SkeletonHeaderLine animationStage={props.animationStage} /> | |||
| <SkeletonRowGroup> | |||
| <CircleGroup> | |||
| <SkeletonHeaderCircle animationStage={props.animationStage} /> | |||
| <SkeletonHeaderCircle animationStage={props.animationStage} /> | |||
| </CircleGroup> | |||
| <SkeletonHeaderRightLine animationStage={props.animationStage} /> | |||
| </SkeletonRowGroup> | |||
| </SkeletonHeaderContainer> | |||
| ) | |||
| } | |||
| SkeletonHeader.propTypes = { | |||
| skeleton: PropTypes.bool, | |||
| animationStage: PropTypes.number, | |||
| } | |||
| export default SkeletonHeader | |||
| @@ -0,0 +1,48 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| export const BackgroundTransition = styled(Box)` | |||
| transition-duration: 0.4s; | |||
| transition-property: background-color; | |||
| background-color: ${props => props.animationStage === 1 ? selectedTheme.filterSkeletonBackground : selectedTheme.filterSkeletonBackgroundSecond} !important; | |||
| `; | |||
| export const SkeletonHeaderContainer = styled(Box)` | |||
| display: ${(props) => (props.skeleton ? "flex" : "none")}; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| margin-top: 36px; | |||
| `; | |||
| export const SkeletonHeaderLine = styled(BackgroundTransition)` | |||
| background-color: ${selectedTheme.filterSkeletonBackground}; | |||
| width: 234px; | |||
| height: 18px; | |||
| `; | |||
| export const SkeletonRowGroup = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| position: relative; | |||
| top: -11px; | |||
| `; | |||
| export const CircleGroup = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: space-between; | |||
| gap: 18px; | |||
| position: relative; | |||
| top: -3px; | |||
| margin-right: 46px; | |||
| `; | |||
| export const SkeletonHeaderCircle = styled(BackgroundTransition)` | |||
| width: 40px; | |||
| height: 40px; | |||
| background-color: ${selectedTheme.filterSkeletonBackground}; | |||
| border-radius: 100%; | |||
| `; | |||
| export const SkeletonHeaderRightLine = styled(BackgroundTransition)` | |||
| width: 209px; | |||
| height: 34px; | |||
| background-color: ${selectedTheme.filterSkeletonBackground}; | |||
| `; | |||
| @@ -6,11 +6,26 @@ import Offers from "./Offers/Offers"; | |||
| const MarketPlace = (props) => { | |||
| const [isGrid, setIsGrid] = useState(false); | |||
| const offers = props.offers; | |||
| return ( | |||
| <MarketPlaceContainer> | |||
| <Header isGrid={isGrid} setIsGrid={setIsGrid} myOffers={props.myOffers}/> | |||
| <Offers isGrid={isGrid} myOffers={props.myOffers} /> | |||
| <Header | |||
| isGrid={isGrid} | |||
| setIsGrid={setIsGrid} | |||
| myOffers={props.myOffers} | |||
| sorting={props.offers.sorting} | |||
| skeleton={props.skeleton} | |||
| animationStage={props.animationStage} | |||
| /> | |||
| <Offers | |||
| isGrid={isGrid} | |||
| myOffers={props.myOffers} | |||
| animationStage={props.animationStage} | |||
| skeleton={props.skeleton} | |||
| offers={offers} | |||
| toggleFilters={props.toggleFilters} | |||
| /> | |||
| </MarketPlaceContainer> | |||
| ); | |||
| }; | |||
| @@ -18,6 +33,10 @@ const MarketPlace = (props) => { | |||
| MarketPlace.propTypes = { | |||
| children: PropTypes.node, | |||
| myOffers: PropTypes.bool, | |||
| animationStage: PropTypes.number, | |||
| skeleton: PropTypes.bool, | |||
| offers: PropTypes.any, | |||
| toggleFilters: PropTypes.func | |||
| }; | |||
| export default MarketPlace; | |||
| @@ -1,7 +1,6 @@ | |||
| import React, { useCallback, useRef } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { TextField } from "../../../TextFields/TextField/TextField"; | |||
| import { EndIcon, SearchIcon } from "./HeadersMyOffers.styled"; | |||
| import { EndIcon, SearchIcon, SearchInput } from "./HeadersMyOffers.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const HeadersMyOffers = (props) => { | |||
| @@ -21,9 +20,10 @@ const HeadersMyOffers = (props) => { | |||
| }; | |||
| const handleSearch = () => { | |||
| props.searchMyOffers(searchRef.current.value); | |||
| props.handleSearch(); | |||
| }; | |||
| return ( | |||
| <TextField | |||
| <SearchInput | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| @@ -43,6 +43,7 @@ const HeadersMyOffers = (props) => { | |||
| HeadersMyOffers.propTypes = { | |||
| children: PropTypes.node, | |||
| searchMyOffers: PropTypes.func, | |||
| handleSearch: PropTypes.func, | |||
| }; | |||
| export default HeadersMyOffers; | |||
| @@ -3,7 +3,7 @@ import styled from "styled-components"; | |||
| import { Icon } from "../../../Icon/Icon"; | |||
| import { ReactComponent as Search } from "../../../../assets/images/svg/magnifying-glass.svg"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { TextField } from "../../../TextFields/TextField/TextField"; | |||
| export const HeadersMyOffersContainer = styled(Box)``; | |||
| export const EndIcon = styled(Icon)``; | |||
| @@ -23,3 +23,12 @@ export const SearchIcon = styled(Search)` | |||
| left: 11px; | |||
| } | |||
| `; | |||
| export const SearchInput = styled(TextField)` | |||
| & div { | |||
| height: 40px; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 90%; | |||
| height: 36px; | |||
| } | |||
| `; | |||
| @@ -1,53 +1,80 @@ | |||
| import React, { useRef } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { OffersContainer } from "./Offers.styled"; | |||
| import { FilterContainer, FilterIcon, OffersContainer } from "./Offers.styled"; | |||
| import OfferCard from "../../Cards/OfferCard/OfferCard"; | |||
| import { useSelector } from "react-redux"; | |||
| import Paging from "../../Paging/Paging"; | |||
| import { selectLatestChats } from "../../../store/selectors/chatSelectors"; | |||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | |||
| import { startChat } from "../../../util/helpers/chatHelper"; | |||
| import useOffers from "../../../hooks/useOffers"; | |||
| import OffersNotFound from "./OffersNotFound"; | |||
| import HeadersMyOffers from "./HeaderMyOffers.js/HeadersMyOffers"; | |||
| import SkeletonOfferCard from "../../Cards/OfferCard/SkeletonOfferCard/SkeletonOfferCard"; | |||
| const Offers = (props) => { | |||
| const chats = useSelector(selectLatestChats); | |||
| const offersRef = useRef(null); | |||
| const userId = useSelector(selectUserId); | |||
| const offers = useOffers(props.myOffers); | |||
| const offers = props.offers; | |||
| const arrayForMapping = Array.apply(null, Array(4)).map(() => {}); | |||
| const messageOneUser = (offer) => { | |||
| startChat(chats, offer, userId); | |||
| }; | |||
| const toggleFilters = () => { | |||
| props.toggleFilters(); | |||
| }; | |||
| console.log(offers.allOffersToShow); | |||
| return ( | |||
| <> | |||
| {props.myOffers && ( | |||
| <HeadersMyOffers searchMyOffers={offers.searchMyOffers} /> | |||
| )} | |||
| {offers.allOffersToShow.length === 0 ? ( | |||
| <OffersNotFound /> | |||
| ) : ( | |||
| <OffersContainer ref={offersRef}> | |||
| {offers.allOffersToShow.map((item) => { | |||
| return ( | |||
| <OfferCard | |||
| key={item._id} | |||
| offer={item} | |||
| halfwidth={props.isGrid} | |||
| messageUser={messageOneUser} | |||
| <FilterContainer | |||
| onClick={toggleFilters} | |||
| number={offers.filters.numOfFiltersChosen} | |||
| myOffers={props.myOffers} | |||
| > | |||
| <FilterIcon /> | |||
| </FilterContainer> | |||
| {!props.skeleton ? ( | |||
| <> | |||
| {props.myOffers && ( | |||
| <HeadersMyOffers | |||
| searchMyOffers={offers.search.searchOffers} | |||
| handleSearch={offers.apply} | |||
| /> | |||
| )} | |||
| {offers.allOffersToShow.length === 0 ? ( | |||
| <OffersNotFound /> | |||
| ) : ( | |||
| <OffersContainer ref={offersRef}> | |||
| {offers.allOffersToShow.map((item) => { | |||
| return ( | |||
| <OfferCard | |||
| key={item._id} | |||
| offer={item} | |||
| halfwidth={props.isGrid} | |||
| messageUser={messageOneUser} | |||
| /> | |||
| ); | |||
| })} | |||
| <Paging | |||
| totalElements={offers.totalOffers} | |||
| elementsPerPage={10} | |||
| current={parseInt(offers.paging.currentPage)} | |||
| changePage={offers.paging.changePage} | |||
| /> | |||
| ); | |||
| })} | |||
| <Paging | |||
| totalElements={offers.totalOffers} | |||
| elementsPerPage={10} | |||
| current={offers.page} | |||
| changePage={offers.handleDifferentPage} | |||
| /> | |||
| </OffersContainer> | |||
| </OffersContainer> | |||
| )} | |||
| </> | |||
| ) : ( | |||
| <> | |||
| {arrayForMapping.map((item, index) => ( | |||
| <SkeletonOfferCard | |||
| key={index} | |||
| skeleton | |||
| animationStage={props.animationStage} | |||
| /> | |||
| ))} | |||
| </> | |||
| )} | |||
| </> | |||
| ); | |||
| @@ -57,6 +84,10 @@ Offers.propTypes = { | |||
| children: PropTypes.node, | |||
| isGrid: PropTypes.bool, | |||
| myOffers: PropTypes.bool, | |||
| skeleton: PropTypes.bool, | |||
| animationStage: PropTypes.number, | |||
| offers: PropTypes.any, | |||
| toggleFilters: PropTypes.func, | |||
| }; | |||
| Offers.defaultProps = { | |||
| @@ -1,5 +1,9 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| import IconWithNumber from "../../Icon/IconWithNumber/IconWithNumber"; | |||
| import { ReactComponent as Filter } from "../../../assets/images/svg/filter.svg"; | |||
| export const OffersContainer = styled(Box)` | |||
| display: flex; | |||
| @@ -10,3 +14,26 @@ export const OffersContainer = styled(Box)` | |||
| position: relative; | |||
| padding-bottom: 60px; | |||
| `; | |||
| export const FilterContainer = styled(IconWithNumber)` | |||
| position: absolute; | |||
| top: ${props => props.myOffers ? "126px" : "93px"}; | |||
| right: 18px; | |||
| cursor: pointer; | |||
| background-color: ${selectedTheme.offerBackgroundColor} !important; | |||
| & div { | |||
| width: 16px; | |||
| height: 16px; | |||
| background-color: ${selectedTheme.primaryPurple}; | |||
| position: absolute; | |||
| top: -5px; | |||
| right: -5px; | |||
| line-height: 15px; | |||
| text-align: center; | |||
| } | |||
| @media (min-width: 600px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const FilterIcon = styled(Filter)` | |||
| background-color: ${selectedTheme.offerBackgroundColor}; | |||
| `; | |||
| @@ -17,6 +17,7 @@ const Paging = (props) => { | |||
| : 1; | |||
| let moving = 0; | |||
| console.log(props.current) | |||
| // Making array of pages which contains 2 pages before and after current page | |||
| const pagesAsArray = Array.apply(null, Array(5)).map(() => {}); | |||
| @@ -41,7 +41,7 @@ export const MyMessages = () => { | |||
| } | |||
| }, [chats]); | |||
| const goToMessages = () => { | |||
| history.push(`messages/${chats[0].chat?._id}`); | |||
| history.push(`/messages/${chats[0].chat?._id}`); | |||
| }; | |||
| return ( | |||
| <HeaderPopover | |||
| @@ -39,18 +39,20 @@ const ProfileOffers = (props) => { | |||
| const userId = useSelector(selectUserId); | |||
| const messageUser = (offer) => { | |||
| const chatItem = chats.find(item => item.chat.offerId === offer?.offer?._id); | |||
| const chatItem = chats.find( | |||
| (item) => item.chat.offerId === offer?.offer?._id | |||
| ); | |||
| if (chatItem !== undefined) { | |||
| history.push(`/messages/${chatItem.chat._id}`) | |||
| history.push(`/messages/${chatItem.chat._id}`); | |||
| } else { | |||
| if (offer?.offer?.userId !== userId) { | |||
| history.push(`/messages/newMessage`, { | |||
| offerId: offer?.offer?._id | |||
| }) | |||
| offerId: offer?.offer?._id, | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| useEffect(() => { | |||
| let newOffersToShow = [...offersToShow]; | |||
| if (sortOption.value === sortEnum.OLD.value) { | |||
| @@ -134,7 +136,9 @@ const ProfileOffers = (props) => { | |||
| sx={{ mb: 1.4 }} | |||
| > | |||
| <OffersIcon /> | |||
| <HeaderTitle>{props.isMyProfile ? "Moje objave" : "Objave kompanije"}</HeaderTitle> | |||
| <HeaderTitle> | |||
| {props.isMyProfile ? "Moje objave" : "Objave kompanije"} | |||
| </HeaderTitle> | |||
| </Grid> | |||
| <SearchInput | |||
| fullWidth | |||
| @@ -155,12 +159,26 @@ const ProfileOffers = (props) => { | |||
| <OffersContainer> | |||
| {dimensions.width > 600 ? ( | |||
| offersToShow.map((item) => ( | |||
| <OfferCard isMyOffer={props.isMyProfile} offer={item} key={JSON.stringify(item)} pinned messageUser={messageUser} /> | |||
| <OfferCard | |||
| isMyOffer={props.isMyProfile} | |||
| offer={item} | |||
| key={JSON.stringify(item)} | |||
| pinned | |||
| messageUser={messageUser} | |||
| /> | |||
| )) | |||
| ) : ( | |||
| <OffersScroller hideArrows> | |||
| {offersToShow.map((item) => ( | |||
| <OfferCard vertical isMyOffer={props.isMyProfile} offer={item} key={JSON.stringify(item)} pinned messageUser={messageUser} />))} | |||
| <OfferCard | |||
| vertical | |||
| isMyOffer={props.isMyProfile} | |||
| offer={item} | |||
| key={JSON.stringify(item)} | |||
| pinned | |||
| messageUser={messageUser} | |||
| /> | |||
| ))} | |||
| </OffersScroller> | |||
| )} | |||
| </OffersContainer> | |||
| @@ -1,6 +1,6 @@ | |||
| import React, { useState, useEffect } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import BackdropComponent from "../MUI/BackdropComponent"; | |||
| import BackdropComponent from "../../MUI/BackdropComponent"; | |||
| import { | |||
| EditProfileContainer, | |||
| ProfileImageContainer, | |||
| @@ -16,19 +16,19 @@ import { | |||
| ErrorMessage, | |||
| ProfileImagePicker, | |||
| } from "./EditProfile.styled"; | |||
| import selectedTheme from "../../themes"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { useFormik } from "formik"; | |||
| import * as Yup from "yup"; | |||
| import { ReactComponent as ArrowBack } from "../../assets/images/svg/arrow-back.svg"; | |||
| import { ReactComponent as CloseIcon } from "../../assets/images/svg/close-modal.svg"; | |||
| import { ReactComponent as ArrowBack } from "../../../assets/images/svg/arrow-back.svg"; | |||
| import { ReactComponent as CloseIcon } from "../../../assets/images/svg/close-modal.svg"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| editMineProfile, | |||
| fetchMineProfile, | |||
| } from "../../store/actions/profile/profileActions"; | |||
| import { useDispatch } from "react-redux"; | |||
| import useScreenDimensions from "../../hooks/useScreenDimensions"; | |||
| import { useRouteMatch } from "react-router-dom"; | |||
| } from "../../../store/actions/profile/profileActions"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | |||
| import useScreenDimensions from "../../../hooks/useScreenDimensions"; | |||
| import editProfileValidation from "../../../validations/editProfileValidation"; | |||
| const EditProfile = (props) => { | |||
| const [profileImage, setProfileImage] = useState(props.profile.image); | |||
| @@ -37,8 +37,7 @@ const EditProfile = (props) => { | |||
| const { t } = useTranslation(); | |||
| const dispatch = useDispatch(); | |||
| const dimensions = useScreenDimensions(); | |||
| const routeMatch = useRouteMatch(); | |||
| const userId = routeMatch.params.idProfile; | |||
| const userId = useSelector(selectUserId); | |||
| useEffect(() => { | |||
| if (dimensions.width < 600) { | |||
| @@ -68,18 +67,7 @@ const EditProfile = (props) => { | |||
| firmPhone: `${props.profile.company.contacts.telephone}`, | |||
| firmLogo: profileImage, | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| firmName: Yup.string().required(t("editProfile.labelNameRequired")), | |||
| firmPIB: Yup.string() | |||
| .required(t("editProfile.labelPIBRequired")) | |||
| .min(9, t("register.PIBnoOfCharacters")), | |||
| firmLocation: Yup.string().required( | |||
| t("editProfile.labelLocationRequired") | |||
| ), | |||
| firmWebsite: Yup.string(), | |||
| firmApplink: Yup.string(), | |||
| firmPhone: Yup.string().required(t("editProfile.labelPhoneRequired")), | |||
| }), | |||
| validationSchema: editProfileValidation, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| @@ -129,17 +117,16 @@ const EditProfile = (props) => { | |||
| value={formik.values.firmName} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.firmName && formik.errors.firmName} | |||
| // helperText={formik.touched.firmName && formik.errors.firmName} | |||
| margin="normal" | |||
| fullWidth | |||
| /> | |||
| <InputFieldLabel leftText={t("common.labelPIB")} /> | |||
| <InputField | |||
| name="firmPIB" | |||
| type="number" | |||
| value={formik.values.firmPIB} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.firmPIB && formik.errors.firmPIB} | |||
| // helperText={formik.touched.firmPIB && formik.errors.firmPIB} | |||
| margin="normal" | |||
| fullWidth | |||
| /> | |||
| @@ -151,9 +138,6 @@ const EditProfile = (props) => { | |||
| value={formik.values.firmLocation} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.firmLocation && formik.errors.firmLocation} | |||
| // helperText={ | |||
| // formik.touched.firmLocation && formik.errors.firmLocation | |||
| // } | |||
| margin="normal" | |||
| fullWidth | |||
| /> | |||
| @@ -184,13 +168,11 @@ const EditProfile = (props) => { | |||
| leftText={t("editProfile.phoneNumber").toUpperCase()} | |||
| /> | |||
| <InputField | |||
| type="number" | |||
| name="firmPhone" | |||
| value={formik.values.firmPhone} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.firmPhone && formik.errors.firmPhone} | |||
| // helperText={ | |||
| // formik.touched.firmPhone && formik.errors.firmPhone | |||
| // } | |||
| margin="normal" | |||
| fullWidth | |||
| /> | |||
| @@ -225,8 +207,6 @@ const EditProfile = (props) => { | |||
| ) : ( | |||
| <ButtonsContainer> | |||
| <SaveButton | |||
| // type="submit" | |||
| // variant="outlined" | |||
| height="44px" | |||
| width="155px" | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| @@ -260,8 +240,6 @@ EditProfile.propTypes = { | |||
| closeModalHandler: PropTypes.func, | |||
| setImage: PropTypes.func, | |||
| reFetchProfile: PropTypes.func, | |||
| // error: PropTypes.string, | |||
| // errorMessage: PropTypes.string, | |||
| }; | |||
| export default EditProfile; | |||
| @@ -1,8 +1,8 @@ | |||
| import styled from "styled-components"; | |||
| import { Box, TextField, Typography } from "@mui/material"; | |||
| import ImagePicker from "../ImagePicker/ImagePicker"; | |||
| import { PrimaryButton } from "../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { Label } from "../CheckBox/Label"; | |||
| import ImagePicker from "../../ImagePicker/ImagePicker"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { Label } from "../../CheckBox/Label"; | |||
| export const EditProfileContainer = styled(Box)` | |||
| background-color: #fff; | |||
| @@ -12,15 +12,17 @@ export const EditProfileContainer = styled(Box)` | |||
| z-index: 150; | |||
| padding: 36px 144px; | |||
| width: 623px; | |||
| max-height: 90vh; | |||
| max-height: 95vh; | |||
| overflow-y: auto; | |||
| @media screen and (max-width: 600px) { | |||
| width: 375px; | |||
| height: 653px; | |||
| height: 100vh; | |||
| max-height: 100vh; | |||
| min-height: 90vh; | |||
| width: 100vw; | |||
| top: 0; | |||
| left: 0; | |||
| padding: 38px 18px; | |||
| top: 60px; | |||
| left: calc(50% - 187px); | |||
| } | |||
| `; | |||
| @@ -3,28 +3,14 @@ import PropTypes from "prop-types"; | |||
| import { | |||
| EditButton, | |||
| ProfileCardWrapper, | |||
| ProfileName, | |||
| ProfilePIB, | |||
| ProfileMainInfo, | |||
| ProfileContact, | |||
| ContactItem, | |||
| ProfileStats, | |||
| StatsItem, | |||
| ProfileCardContainer, | |||
| AvatarImage, | |||
| ProfileCardHeader, | |||
| HeaderTitle, | |||
| PocketIcon, | |||
| LocationIcon, | |||
| MailIcon, | |||
| GlobeIcon, | |||
| ProfilePIBContainer, | |||
| EditIcon, | |||
| MessageIcon, | |||
| MessageButton, | |||
| ProfileInfoContainer, | |||
| } from "./ProfileCard.styled"; | |||
| import { Grid, Stack } from "@mui/material"; | |||
| import PersonOutlineIcon from "@mui/icons-material/PersonOutline"; | |||
| import { useRouteMatch } from "react-router-dom"; | |||
| import { fetchProfile } from "../../store/actions/profile/profileActions"; | |||
| @@ -34,7 +20,11 @@ import { selectProfile } from "../../store/selectors/profileSelectors"; | |||
| import { selectUserId } from "../../store/selectors/loginSelectors"; | |||
| import { useState } from "react"; | |||
| import { fetchProfileOffers } from "../../store/actions/offers/offersActions"; | |||
| import EditProfile from "./EditProfile"; | |||
| import EditProfile from "./EditProfile/EditProfile"; | |||
| import ProfileMainInfo from "./ProfileMainInfo/ProfileMainInfo"; | |||
| import ProfileContact from "./ProfileContact/ProfileContact"; | |||
| import ProfileStats from "./ProfileStats/ProfileStats"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const ProfileCard = () => { | |||
| const [isMyProfile, setIsMyProfile] = useState(false); | |||
| @@ -44,7 +34,8 @@ const ProfileCard = () => { | |||
| const profile = useSelector(selectProfile); | |||
| const userId = useSelector(selectUserId); | |||
| const idProfile = routeMatch.params.idProfile; | |||
| console.log(idProfile); | |||
| const { t } = useTranslation(); | |||
| useEffect(() => { | |||
| if (idProfile?.length > 0) { | |||
| reFetchProfile(); | |||
| @@ -78,20 +69,13 @@ const ProfileCard = () => { | |||
| document.body.style.overflow = "auto"; | |||
| } | |||
| console.log(profile); | |||
| return ( | |||
| <> | |||
| <ProfileCardContainer> | |||
| <Grid | |||
| container | |||
| direction="row" | |||
| justifyContent="start" | |||
| alignItems="center" | |||
| sx={{ mb: 1.4 }} | |||
| > | |||
| <ProfileCardHeader> | |||
| <PersonOutlineIcon color="action" sx={{ mr: 0.9 }} /> | |||
| <HeaderTitle>Moj Profil</HeaderTitle> | |||
| </Grid> | |||
| <HeaderTitle>{t("profile.myProfile")}</HeaderTitle> | |||
| </ProfileCardHeader> | |||
| <ProfileCardWrapper variant="outlined" isMyProfile={isMyProfile}> | |||
| {isMyProfile ? ( | |||
| <EditButton onClick={() => setEditProfileModal(true)}> | |||
| @@ -102,112 +86,17 @@ const ProfileCard = () => { | |||
| <MessageIcon /> | |||
| </MessageButton> | |||
| )} | |||
| <Grid | |||
| container | |||
| direction="column" | |||
| justifyContent="center" | |||
| alignItems="start" | |||
| > | |||
| {/* Profile Main Info */} | |||
| <ProfileMainInfo | |||
| container | |||
| direction="row" | |||
| justifyContent="start" | |||
| alignItems="start" | |||
| > | |||
| <Grid | |||
| direction="column" | |||
| justifyContent="start" | |||
| alignItems="center" | |||
| > | |||
| <AvatarImage alt="Player.rs" src={profile?.image} /> | |||
| </Grid> | |||
| <Grid | |||
| direction="column" | |||
| justifyContent="center" | |||
| alignItems="start" | |||
| sx={{ ml: 2 }} | |||
| > | |||
| <ProfileName isMyProfile={isMyProfile} variant="h5"> | |||
| {profile?.company?.name} | |||
| </ProfileName> | |||
| <ProfilePIBContainer | |||
| container | |||
| direction="row" | |||
| justifyContent="center" | |||
| alignItems="center" | |||
| > | |||
| <PocketIcon /> | |||
| <ProfilePIB isMyProfile={isMyProfile} variant="subtitle2"> | |||
| PIB: {profile?.company?.PIB} | |||
| </ProfilePIB> | |||
| </ProfilePIBContainer> | |||
| </Grid> | |||
| </ProfileMainInfo> | |||
| <ProfileInfoContainer> | |||
| {/* Profile Main Info */} | |||
| <ProfileMainInfo profile={profile} isMyProfile={isMyProfile} /> | |||
| {/* Profile Contact */} | |||
| <ProfileContact | |||
| container | |||
| direction={{ xs: "column", sm: "row" }} | |||
| justifyContent={{ xs: "center", sm: "start" }} | |||
| alignItems={{ xs: "start", sm: "center" }} | |||
| > | |||
| <Stack direction="row"> | |||
| <LocationIcon isMyProfile={isMyProfile} /> | |||
| <ContactItem isMyProfile={isMyProfile} variant="subtitle2"> | |||
| {profile?.company?.contacts?.location} | |||
| </ContactItem> | |||
| </Stack> | |||
| <Stack direction="row"> | |||
| <MailIcon isMyProfile={isMyProfile} /> | |||
| <ContactItem isMyProfile={isMyProfile} variant="subtitle2"> | |||
| {profile?.email} | |||
| </ContactItem> | |||
| </Stack> | |||
| <Stack direction="row"> | |||
| <GlobeIcon isMyProfile={isMyProfile} /> | |||
| <ContactItem isMyProfile={isMyProfile} variant="subtitle2"> | |||
| {profile?.company?.contacts?.web} | |||
| </ContactItem> | |||
| </Stack> | |||
| </ProfileContact> | |||
| <ProfileContact profile={profile} isMyProfile={isMyProfile} /> | |||
| {/* Profile Stats */} | |||
| <ProfileStats | |||
| container | |||
| direction="row" | |||
| justifyContent="start" | |||
| alignItems="center" | |||
| > | |||
| <Grid | |||
| container | |||
| direction="column" | |||
| justifyContent="center" | |||
| alignItems="start" | |||
| sx={{ width: "fit-content" }} | |||
| > | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{profile?.statistics?.publishes?.count}</b> objava | |||
| </StatsItem> | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{percentOfSucceededExchanges}%</b> uspešna komunikacija | |||
| </StatsItem> | |||
| </Grid> | |||
| <Grid | |||
| container | |||
| direction="column" | |||
| justifyContent="center" | |||
| alignItems="start" | |||
| sx={{ width: "fit-content" }} | |||
| > | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{profile?.statistics?.views?.count}</b> ukupnih pregleda | |||
| </StatsItem> | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{percentOfSucceededExchanges}%</b> korektna saradnja | |||
| </StatsItem> | |||
| </Grid> | |||
| </ProfileStats> | |||
| </Grid> | |||
| profile={profile} | |||
| percentOfSucceededExchanges={percentOfSucceededExchanges} | |||
| /> | |||
| </ProfileInfoContainer> | |||
| </ProfileCardWrapper> | |||
| </ProfileCardContainer> | |||
| {editProfileModal && ( | |||
| @@ -2,10 +2,10 @@ import styled from "styled-components"; | |||
| import { Card, Typography, Grid, Box } from "@mui/material"; | |||
| import selectedTheme from "../../themes"; | |||
| import { ReactComponent as Edit } from "../../assets/images/svg/edit.svg"; | |||
| import { ReactComponent as Pocket } from "../../assets/images/svg/pocket.svg"; | |||
| import { ReactComponent as Globe } from "../../assets/images/svg/globe.svg"; | |||
| // import { ReactComponent as Pocket } from "../../assets/images/svg/pocket.svg"; | |||
| // import { ReactComponent as Globe } from "../../assets/images/svg/globe.svg"; | |||
| import { ReactComponent as Mail } from "../../assets/images/svg/mail.svg"; | |||
| import { ReactComponent as Location } from "../../assets/images/svg/location.svg"; | |||
| // import { ReactComponent as Location } from "../../assets/images/svg/location.svg"; | |||
| // import { PRIMARY_PURPLE_COLOR, PRIMARY_YELLOW_COLOR } from '../../constants/stylesConstants'; | |||
| export const ProfileCardContainer = styled(Box)` | |||
| @@ -52,97 +52,128 @@ export const ProfileCardWrapper = styled(Card)` | |||
| position: relative; | |||
| `; | |||
| export const ProfileName = styled(Typography)` | |||
| color: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.primaryYellow | |||
| : selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 24px; | |||
| font-family: "Open Sans"; | |||
| margin-bottom: 5px; | |||
| @media (max-width: 600px) { | |||
| font-size: 18px; | |||
| } | |||
| `; | |||
| // export const ProfileName = styled(Typography)` | |||
| // color: ${(props) => | |||
| // props.isMyProfile | |||
| // ? selectedTheme.primaryYellow | |||
| // : selectedTheme.primaryPurple}; | |||
| // font-weight: 700; | |||
| // font-size: 24px; | |||
| // font-family: "Open Sans"; | |||
| // margin-bottom: 5px; | |||
| // @media (max-width: 600px) { | |||
| // font-size: 18px; | |||
| // } | |||
| // `; | |||
| export const ProfilePIB = styled(Typography)` | |||
| color: ${(props) => | |||
| props.isMyProfile ? "white" : selectedTheme.primaryDarkText}; | |||
| margin-top: 0.18rem; | |||
| font-family: "Open Sans"; | |||
| font-size: 16px; | |||
| padding-top: 1px; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| } | |||
| `; | |||
| export const ProfilePIBContainer = styled(Grid)` | |||
| position: relative; | |||
| left: 5px; | |||
| `; | |||
| // export const ProfilePIB = styled(Typography)` | |||
| // color: ${(props) => | |||
| // props.isMyProfile ? "white" : selectedTheme.primaryDarkText}; | |||
| // margin-top: 0.18rem; | |||
| // font-family: "Open Sans"; | |||
| // font-size: 16px; | |||
| // padding-top: 1px; | |||
| // @media (max-width: 600px) { | |||
| // font-size: 14px; | |||
| // } | |||
| // `; | |||
| // export const ProfilePIBContainer = styled(Grid)` | |||
| // display: flex; | |||
| // justify-content: center; | |||
| // align-items: center; | |||
| // position: relative; | |||
| // left: 5px; | |||
| // `; | |||
| export const ProfileMainInfo = styled(Grid)``; | |||
| // export const ProfileMainInfo = styled(Grid)` | |||
| // display: flex; | |||
| // justify-content: start; | |||
| // align-items: start; | |||
| // `; | |||
| export const ProfileContact = styled(Grid)` | |||
| padding-top: 2rem; | |||
| padding-bottom: 2rem; | |||
| @media (max-width: 600px) { | |||
| padding-bottom: 1rem; | |||
| } | |||
| `; | |||
| // export const AvatarImageContainer = styled(Grid)` | |||
| // display: flex; | |||
| // justify-content: start; | |||
| // align-items: center; | |||
| // `; | |||
| export const ContactItem = styled(Typography)` | |||
| margin-right: 2rem; | |||
| margin-left: 0.4rem; | |||
| color: ${(props) => | |||
| props.isMyProfile ? "white" : selectedTheme.primaryDarkText}; | |||
| display: unset; | |||
| font-family: "Open Sans"; | |||
| letter-spacing: 0.02em; | |||
| font-size: 16px; | |||
| position: relative; | |||
| bottom: 1px; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| bottom: 4px; | |||
| } | |||
| `; | |||
| // export const ProfileMainInfoGrid = styled(Grid)` | |||
| // display: flex; | |||
| // flex-direction: column; | |||
| // align-items: start; | |||
| // margin-left: 16px; | |||
| // `; | |||
| export const StatsItem = styled(Typography)` | |||
| margin-right: 2rem; | |||
| display: unset; | |||
| margin-left: 1rem; | |||
| font-family: "Open Sans"; | |||
| font-size: 16px; | |||
| margin-bottom: 2px; | |||
| @media (max-width: 600px) { | |||
| font-size: 12px; | |||
| } | |||
| `; | |||
| // export const ProfileContact = styled(Grid)` | |||
| // padding-top: 2rem; | |||
| // padding-bottom: 2rem; | |||
| // @media (max-width: 600px) { | |||
| // padding-bottom: 1rem; | |||
| // } | |||
| // `; | |||
| export const ProfileStats = styled(Grid)` | |||
| background: ${selectedTheme.primaryDarkTextSecond}; | |||
| width: calc(100% + 2rem); | |||
| padding-top: 1.3rem; | |||
| padding-bottom: 1.3rem; | |||
| margin-bottom: -1rem; | |||
| margin-left: -1rem; | |||
| border-radius: 0 0 4px 4px; | |||
| `; | |||
| export const AvatarImage = styled.img` | |||
| min-height: 144px; | |||
| min-width: 144px; | |||
| width: 144px; | |||
| height: 144px; | |||
| border-radius: 100%; | |||
| @media (max-width: 600px) { | |||
| min-height: 90px; | |||
| min-width: 90px; | |||
| width: 90px; | |||
| height: 90px; | |||
| } | |||
| // export const ContactItem = styled(Typography)` | |||
| // margin-right: 2rem; | |||
| // margin-left: 0.4rem; | |||
| // color: ${(props) => | |||
| // props.isMyProfile ? "white" : selectedTheme.primaryDarkText}; | |||
| // display: unset; | |||
| // font-family: "Open Sans"; | |||
| // letter-spacing: 0.02em; | |||
| // font-size: 16px; | |||
| // position: relative; | |||
| // bottom: 1px; | |||
| // @media (max-width: 600px) { | |||
| // font-size: 14px; | |||
| // bottom: 4px; | |||
| // } | |||
| // `; | |||
| // export const StatsItem = styled(Typography)` | |||
| // margin-right: 2rem; | |||
| // display: unset; | |||
| // margin-left: 1rem; | |||
| // font-family: "Open Sans"; | |||
| // font-size: 16px; | |||
| // margin-bottom: 2px; | |||
| // @media (max-width: 600px) { | |||
| // font-size: 12px; | |||
| // } | |||
| // `; | |||
| // export const ProfileStats = styled(Grid)` | |||
| // display: flex; | |||
| // justify-content: start; | |||
| // align-items: center; | |||
| // background: ${selectedTheme.primaryDarkTextSecond}; | |||
| // width: calc(100% + 2rem); | |||
| // padding-top: 1.3rem; | |||
| // padding-bottom: 1.3rem; | |||
| // margin-bottom: -1rem; | |||
| // margin-left: -1rem; | |||
| // border-radius: 0 0 4px 4px; | |||
| // `; | |||
| // export const AvatarImage = styled.img` | |||
| // min-height: 144px; | |||
| // min-width: 144px; | |||
| // width: 144px; | |||
| // height: 144px; | |||
| // border-radius: 100%; | |||
| // @media (max-width: 600px) { | |||
| // min-height: 90px; | |||
| // min-width: 90px; | |||
| // width: 90px; | |||
| // height: 90px; | |||
| // } | |||
| // `; | |||
| export const ProfileCardHeader = styled(Grid)` | |||
| display: flex; | |||
| justify-content: start; | |||
| align-items: center; | |||
| margin-bottom: 11px; | |||
| `; | |||
| export const HeaderTitle = styled(Typography)` | |||
| font-size: 16px; | |||
| font-family: "Open Sans"; | |||
| @@ -152,62 +183,62 @@ export const HeaderTitle = styled(Typography)` | |||
| font-size: 12px; | |||
| } | |||
| `; | |||
| export const PocketIcon = styled(Pocket)` | |||
| width: 22px; | |||
| height: 22px; | |||
| position: relative; | |||
| left: -5px; | |||
| top: 2px; | |||
| & path { | |||
| stroke: #b4b4b4; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| export const MailIcon = styled(Mail)` | |||
| height: 24px; | |||
| width: 24px; | |||
| & path { | |||
| stroke: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.iconMineProfileColor | |||
| : selectedTheme.iconProfileColor}; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| export const GlobeIcon = styled(Globe)` | |||
| height: 22px; | |||
| width: 22px; | |||
| & path { | |||
| stroke: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.iconMineProfileColor | |||
| : selectedTheme.iconProfileColor}; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| export const LocationIcon = styled(Location)` | |||
| height: 22px; | |||
| width: 22px; | |||
| & path { | |||
| stroke: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.iconMineProfileColor | |||
| : selectedTheme.iconProfileColor}; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| // export const PocketIcon = styled(Pocket)` | |||
| // width: 22px; | |||
| // height: 22px; | |||
| // position: relative; | |||
| // left: -5px; | |||
| // top: 2px; | |||
| // & path { | |||
| // stroke: #b4b4b4; | |||
| // } | |||
| // @media (max-width: 600px) { | |||
| // width: 14px; | |||
| // height: 14px; | |||
| // } | |||
| // `; | |||
| // export const MailIcon = styled(Mail)` | |||
| // height: 24px; | |||
| // width: 24px; | |||
| // & path { | |||
| // stroke: ${(props) => | |||
| // props.isMyProfile | |||
| // ? selectedTheme.iconMineProfileColor | |||
| // : selectedTheme.iconProfileColor}; | |||
| // } | |||
| // @media (max-width: 600px) { | |||
| // width: 14px; | |||
| // height: 14px; | |||
| // } | |||
| // `; | |||
| // export const GlobeIcon = styled(Globe)` | |||
| // height: 22px; | |||
| // width: 22px; | |||
| // & path { | |||
| // stroke: ${(props) => | |||
| // props.isMyProfile | |||
| // ? selectedTheme.iconMineProfileColor | |||
| // : selectedTheme.iconProfileColor}; | |||
| // } | |||
| // @media (max-width: 600px) { | |||
| // width: 14px; | |||
| // height: 14px; | |||
| // } | |||
| // `; | |||
| // export const LocationIcon = styled(Location)` | |||
| // height: 22px; | |||
| // width: 22px; | |||
| // & path { | |||
| // stroke: ${(props) => | |||
| // props.isMyProfile | |||
| // ? selectedTheme.iconMineProfileColor | |||
| // : selectedTheme.iconProfileColor}; | |||
| // } | |||
| // @media (max-width: 600px) { | |||
| // width: 14px; | |||
| // height: 14px; | |||
| // } | |||
| // `; | |||
| export const MessageIcon = styled(Mail)` | |||
| width: 19.5px; | |||
| height: 19.5px; | |||
| @@ -223,3 +254,17 @@ export const MessageIcon = styled(Mail)` | |||
| right: 0.5px; | |||
| } | |||
| `; | |||
| export const ProfileInfoContainer = styled(Grid)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| align-items: start; | |||
| `; | |||
| // export const ProfileStatsGrid = styled(Grid)` | |||
| // display: flex; | |||
| // flex-direction: column; | |||
| // justify-content: center; | |||
| // align-items: start; | |||
| // `; | |||
| @@ -0,0 +1,48 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| ProfileContactContainer, | |||
| LocationIcon, | |||
| ContactItem, | |||
| MailIcon, | |||
| GlobeIcon, | |||
| } from "./ProfileContact.styled"; | |||
| import { Stack } from "@mui/material"; | |||
| const ProfileContact = (props) => { | |||
| return ( | |||
| <ProfileContactContainer | |||
| container | |||
| direction={{ xs: "column", sm: "row" }} | |||
| justifyContent={{ xs: "center", sm: "start" }} | |||
| alignItems={{ xs: "start", sm: "center" }} | |||
| > | |||
| <Stack direction="row"> | |||
| <LocationIcon isMyProfile={props.isMyProfile} /> | |||
| <ContactItem isMyProfile={props.isMyProfile} variant="subtitle2"> | |||
| {props.profile?.company?.contacts?.location} | |||
| </ContactItem> | |||
| </Stack> | |||
| <Stack direction="row"> | |||
| <MailIcon isMyProfile={props.isMyProfile} /> | |||
| <ContactItem isMyProfile={props.isMyProfile} variant="subtitle2"> | |||
| {props.profile?.email} | |||
| </ContactItem> | |||
| </Stack> | |||
| <Stack direction="row"> | |||
| <GlobeIcon isMyProfile={props.isMyProfile} /> | |||
| <ContactItem isMyProfile={props.isMyProfile} variant="subtitle2"> | |||
| {props.profile?.company?.contacts?.web} | |||
| </ContactItem> | |||
| </Stack> | |||
| </ProfileContactContainer> | |||
| ); | |||
| }; | |||
| ProfileContact.propTypes = { | |||
| profile: PropTypes.object, | |||
| isMyProfile: PropTypes.bool, | |||
| children: PropTypes.node, | |||
| }; | |||
| export default ProfileContact; | |||
| @@ -0,0 +1,72 @@ | |||
| import styled from "styled-components"; | |||
| import { Grid, Typography } from "@mui/material"; | |||
| import { ReactComponent as Location } from "../../../assets/images/svg/location.svg"; | |||
| import { ReactComponent as Mail } from "../../../assets/images/svg/mail.svg"; | |||
| import { ReactComponent as Globe } from "../../../assets/images/svg/globe.svg"; | |||
| import selectedTheme from "../../../themes"; | |||
| export const ProfileContactContainer = styled(Grid)` | |||
| padding-top: 2rem; | |||
| padding-bottom: 2rem; | |||
| @media (max-width: 600px) { | |||
| padding-bottom: 1rem; | |||
| } | |||
| `; | |||
| export const LocationIcon = styled(Location)` | |||
| height: 22px; | |||
| width: 22px; | |||
| & path { | |||
| stroke: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.iconMineProfileColor | |||
| : selectedTheme.iconProfileColor}; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| export const ContactItem = styled(Typography)` | |||
| margin-right: 2rem; | |||
| margin-left: 0.4rem; | |||
| color: ${(props) => | |||
| props.isMyProfile ? "white" : selectedTheme.primaryDarkText}; | |||
| display: unset; | |||
| font-family: "Open Sans"; | |||
| letter-spacing: 0.02em; | |||
| font-size: 16px; | |||
| position: relative; | |||
| bottom: 1px; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| bottom: 4px; | |||
| } | |||
| `; | |||
| export const MailIcon = styled(Mail)` | |||
| height: 24px; | |||
| width: 24px; | |||
| & path { | |||
| stroke: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.iconMineProfileColor | |||
| : selectedTheme.iconProfileColor}; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| export const GlobeIcon = styled(Globe)` | |||
| height: 22px; | |||
| width: 22px; | |||
| & path { | |||
| stroke: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.iconMineProfileColor | |||
| : selectedTheme.iconProfileColor}; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| @@ -0,0 +1,46 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| ProfileMainInfoContainer, | |||
| AvatarImageContainer, | |||
| AvatarImage, | |||
| ProfileMainInfoGrid, | |||
| ProfileName, | |||
| ProfilePIBContainer, | |||
| PocketIcon, | |||
| ProfilePIB, | |||
| } from "./ProfileMainInfo.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const ProfileMainInfo = (props) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <ProfileMainInfoContainer> | |||
| <AvatarImageContainer> | |||
| <AvatarImage | |||
| alt={props.profile?.company?.name} | |||
| src={props.profile?.image} | |||
| /> | |||
| </AvatarImageContainer> | |||
| <ProfileMainInfoGrid> | |||
| <ProfileName isMyProfile={props.isMyProfile} variant="h5"> | |||
| {props.profile?.company?.name} | |||
| </ProfileName> | |||
| <ProfilePIBContainer> | |||
| <PocketIcon /> | |||
| <ProfilePIB isMyProfile={props.isMyProfile} variant="subtitle2"> | |||
| {t("profile.PIB")} {props.profile?.company?.PIB} | |||
| </ProfilePIB> | |||
| </ProfilePIBContainer> | |||
| </ProfileMainInfoGrid> | |||
| </ProfileMainInfoContainer> | |||
| ); | |||
| }; | |||
| ProfileMainInfo.propTypes = { | |||
| profile: PropTypes.object, | |||
| isMyProfile: PropTypes.bool, | |||
| children: PropTypes.node, | |||
| }; | |||
| export default ProfileMainInfo; | |||
| @@ -0,0 +1,79 @@ | |||
| import styled from "styled-components"; | |||
| import { Grid, Typography } from "@mui/material"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { ReactComponent as Pocket } from "../../../assets/images/svg/pocket.svg"; | |||
| export const ProfileMainInfoContainer = styled(Grid)` | |||
| display: flex; | |||
| justify-content: start; | |||
| align-items: start; | |||
| `; | |||
| export const AvatarImageContainer = styled(Grid)` | |||
| display: flex; | |||
| justify-content: start; | |||
| align-items: center; | |||
| `; | |||
| export const AvatarImage = styled.img` | |||
| min-height: 144px; | |||
| min-width: 144px; | |||
| width: 144px; | |||
| height: 144px; | |||
| border-radius: 100%; | |||
| @media (max-width: 600px) { | |||
| min-height: 90px; | |||
| min-width: 90px; | |||
| width: 90px; | |||
| height: 90px; | |||
| } | |||
| `; | |||
| export const ProfileMainInfoGrid = styled(Grid)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: start; | |||
| margin-left: 16px; | |||
| `; | |||
| export const ProfileName = styled(Typography)` | |||
| color: ${(props) => | |||
| props.isMyProfile | |||
| ? selectedTheme.primaryYellow | |||
| : selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 24px; | |||
| font-family: "Open Sans"; | |||
| margin-bottom: 5px; | |||
| @media (max-width: 600px) { | |||
| font-size: 18px; | |||
| } | |||
| `; | |||
| export const ProfilePIBContainer = styled(Grid)` | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| position: relative; | |||
| left: 5px; | |||
| `; | |||
| export const PocketIcon = styled(Pocket)` | |||
| width: 22px; | |||
| height: 22px; | |||
| position: relative; | |||
| left: -5px; | |||
| top: 2px; | |||
| & path { | |||
| stroke: #b4b4b4; | |||
| } | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| `; | |||
| export const ProfilePIB = styled(Typography)` | |||
| color: ${(props) => | |||
| props.isMyProfile ? "white" : selectedTheme.primaryDarkText}; | |||
| margin-top: 0.18rem; | |||
| font-family: "Open Sans"; | |||
| font-size: 16px; | |||
| padding-top: 1px; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| } | |||
| `; | |||
| @@ -0,0 +1,44 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| ProfileStatsContainer, | |||
| ProfileStatsGrid, | |||
| StatsItem, | |||
| } from "./ProfileStats.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const ProfileStats = (props) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <ProfileStatsContainer> | |||
| <ProfileStatsGrid> | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{props.profile?.statistics?.publishes?.count}</b> | |||
| {t("profile.publishes")} | |||
| </StatsItem> | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{props.percentOfSucceededExchanges}%</b> | |||
| {t("profile.successExchange")} | |||
| </StatsItem> | |||
| </ProfileStatsGrid> | |||
| <ProfileStatsGrid> | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{props.profile?.statistics?.views?.count}</b> | |||
| {t("profile.numberOfViews")} | |||
| </StatsItem> | |||
| <StatsItem variant="subtitle2"> | |||
| <b>{props.percentOfSucceededExchanges}%</b> | |||
| {t("profile.successComunication")} | |||
| </StatsItem> | |||
| </ProfileStatsGrid> | |||
| </ProfileStatsContainer> | |||
| ); | |||
| }; | |||
| ProfileStats.propTypes = { | |||
| profile: PropTypes.object, | |||
| percentOfSucceededExchanges: PropTypes.number, | |||
| }; | |||
| export default ProfileStats; | |||
| @@ -0,0 +1,33 @@ | |||
| import styled from "styled-components"; | |||
| import { Grid, Typography } from "@mui/material"; | |||
| import selectedTheme from "../../../themes"; | |||
| export const ProfileStatsContainer = styled(Grid)` | |||
| display: flex; | |||
| justify-content: start; | |||
| align-items: center; | |||
| background: ${selectedTheme.primaryDarkTextSecond}; | |||
| width: calc(100% + 2rem); | |||
| padding-top: 1.3rem; | |||
| padding-bottom: 1.3rem; | |||
| margin-bottom: -1rem; | |||
| margin-left: -1rem; | |||
| border-radius: 0 0 4px 4px; | |||
| `; | |||
| export const ProfileStatsGrid = styled(Grid)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| align-items: start; | |||
| `; | |||
| export const StatsItem = styled(Typography)` | |||
| margin-right: 2rem; | |||
| display: unset; | |||
| margin-left: 1rem; | |||
| font-family: "Open Sans"; | |||
| font-size: 16px; | |||
| margin-bottom: 2px; | |||
| @media (max-width: 600px) { | |||
| font-size: 12px; | |||
| } | |||
| `; | |||
| @@ -42,9 +42,9 @@ export const TextField = React.forwardRef((props, ref) => { | |||
| label={props.showAnimation ? props.placeholder : ""} | |||
| onFocus={props.onFocus} | |||
| onBlur={props.onBlur} | |||
| italicplaceholder={(props.italicPlaceholder && isFieldEmpty) ? "true" : "false"} | |||
| italicplaceholder={ | |||
| props.italicPlaceholder && isFieldEmpty ? "true" : "false" | |||
| } | |||
| focused={props.focused} | |||
| > | |||
| {props.children} | |||
| @@ -6,6 +6,9 @@ export const KEY_SORTBY = "sortBy"; | |||
| export const KEY_SORT_DATE = "_des_date"; | |||
| export const KEY_SORT_POPULAR = "_des_popular"; | |||
| export const KEY_LOCATION = "location" | |||
| export const KEY_NAME = "name"; | |||
| export const KEY_SEARCH = "search" | |||
| export const VALUE_SORTBY_NEW = "newest"; | |||
| export const VALUE_SORTBY_OLD = "oldest"; | |||
| export const VALUE_SORTBY_POPULAR = "popular"; | |||
| export const initialSize = "10"; | |||
| @@ -1,177 +0,0 @@ | |||
| import { useEffect, useState } from "react"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { useSelector } from "react-redux"; | |||
| import { fetchCategories } from "../store/actions/categories/categoriesActions"; | |||
| import { | |||
| setFilteredCategory, | |||
| setFilteredLocations, | |||
| setFilteredSubcategory, | |||
| setIsAppliedStatus, | |||
| } from "../store/actions/filters/filtersActions"; | |||
| import { fetchLocations } from "../store/actions/locations/locationsActions"; | |||
| import { | |||
| selectCategories, | |||
| selectSubcategories, | |||
| } from "../store/selectors/categoriesSelectors"; | |||
| import { | |||
| selectAppliedStatus, | |||
| selectSelectedCategory, | |||
| selectSelectedLocations, | |||
| selectSelectedSubcategory, | |||
| } from "../store/selectors/filtersSelectors"; | |||
| import { selectLocations } from "../store/selectors/locationsSelectors"; | |||
| import { useQueryString } from "./useQueryString"; | |||
| const useFilters = (myOffers) => { | |||
| const selectedCategory = useSelector(selectSelectedCategory); | |||
| const selectedSubcategory = useSelector(selectSelectedSubcategory); | |||
| const selectedLocations = useSelector(selectSelectedLocations); | |||
| const [loadedFromQS, setLoadedFromQS] = useState(false); | |||
| const [loaded, setLoadedStatus] = useState(false); | |||
| const isApplied = useSelector(selectAppliedStatus); | |||
| const categories = useSelector(selectCategories); | |||
| const subcategories = useSelector( | |||
| selectSubcategories(selectedCategory?.name) | |||
| ); | |||
| const locations = useSelector(selectLocations); | |||
| const dispatch = useDispatch(); | |||
| const queryStringHook = useQueryString(); | |||
| useEffect(() => { | |||
| if (!loaded) { | |||
| dispatch(fetchCategories()); | |||
| dispatch(fetchLocations()); | |||
| setLoadedStatus(true); | |||
| } | |||
| }, [categories, locations]); | |||
| useEffect(() => { | |||
| const queryObject = new URLSearchParams(queryStringHook.queryString); | |||
| if (categories?.length > 0 && locations?.length > 0) { | |||
| let category; | |||
| if (queryObject.has("category")) { | |||
| category = categories.find( | |||
| (item) => item.name === queryObject.get("category").toString() | |||
| ); | |||
| setSelectedCategory(category); | |||
| } else { | |||
| if (!myOffers) { | |||
| setSelectedCategory(); | |||
| } | |||
| } | |||
| if (queryObject.has("subcategory")) { | |||
| setSelectedSubcategory( | |||
| category?.subcategories?.find( | |||
| (item) => | |||
| item.name.toString() === queryObject.get("subcategory").toString() | |||
| ) | |||
| ); | |||
| } else { | |||
| if (!myOffers) { | |||
| setSelectedSubcategory(); | |||
| } | |||
| } | |||
| } | |||
| if (queryObject.has("location")) { | |||
| let locationsToPush = []; | |||
| queryObject.getAll("location").forEach((item) => { | |||
| locationsToPush.push(locations.find((p) => p.city === item)); | |||
| }); | |||
| setSelectedLocations([...locationsToPush]); | |||
| } else { | |||
| if (!myOffers) { | |||
| setSelectedLocations([]); | |||
| } | |||
| } | |||
| dispatch(setIsAppliedStatus(true)); | |||
| }, [queryStringHook.queryString, categories, locations]); | |||
| // Apply everything | |||
| const applyFilters = () => { | |||
| makeQueryString(); | |||
| }; | |||
| // Clear function | |||
| const clearFilters = () => { | |||
| setSelectedLocations([]); | |||
| setSelectedSubcategory(); | |||
| setSelectedCategory(); | |||
| applyFilters(); | |||
| }; | |||
| // Helper function | |||
| const makeQueryString = () => { | |||
| let qsArray = []; | |||
| qsArray.push({ key: "category", value: selectedCategory?.name }); | |||
| qsArray.push({ key: "subcategory", value: selectedSubcategory?.name }); | |||
| selectedLocations?.forEach((location) => { | |||
| qsArray.push({ key: "location", value: location?.city }); | |||
| }); | |||
| qsArray.push({ key: "page", value: "1" }); | |||
| queryStringHook.appendMultipleToQueryString(qsArray); | |||
| }; | |||
| //Calculate chosen categories for number above filter icon on mobile responsive version | |||
| const calculateFiltersChosen = () => { | |||
| let sum = 0; | |||
| if (selectedCategory && selectedCategory?._id !== 0) { | |||
| sum++; | |||
| } | |||
| if (selectedSubcategory && selectedSubcategory?._id !== 0) { | |||
| sum++; | |||
| } | |||
| if (selectedLocations && selectedLocations?.length > 0) { | |||
| sum += selectedLocations.length; | |||
| } | |||
| return sum; | |||
| }; | |||
| // Setters | |||
| const setSelectedCategory = (payload) => { | |||
| if (isApplied !== false) { | |||
| dispatch(setIsAppliedStatus(false)); | |||
| } | |||
| if (JSON.stringify(payload) !== JSON.stringify(selectedCategory)) { | |||
| dispatch(setFilteredCategory(payload)); | |||
| } | |||
| }; | |||
| const setSelectedSubcategory = (payload) => { | |||
| if (isApplied !== false) { | |||
| dispatch(setIsAppliedStatus(false)); | |||
| } | |||
| if (JSON.stringify(payload) !== JSON.stringify(selectedSubcategory)) { | |||
| dispatch(setFilteredSubcategory(payload)); | |||
| } | |||
| }; | |||
| const clearSelectedSubcategory = () => { | |||
| setSelectedSubcategory(); | |||
| }; | |||
| const setSelectedLocations = (payload) => { | |||
| if (isApplied !== false) { | |||
| dispatch(setIsAppliedStatus(false)); | |||
| } | |||
| if (JSON.stringify(payload) !== JSON.stringify(selectedLocations)) { | |||
| dispatch(setFilteredLocations(payload)); | |||
| } | |||
| }; | |||
| return { | |||
| selectedCategory, | |||
| setSelectedCategory, | |||
| selectedSubcategory, | |||
| setSelectedSubcategory, | |||
| clearSelectedSubcategory, | |||
| selectedLocations, | |||
| setSelectedLocations, | |||
| categories, | |||
| subcategories, | |||
| locations, | |||
| applyFilters, | |||
| clearFilters, | |||
| isApplied, | |||
| makeQueryString, | |||
| calculateFiltersChosen, | |||
| loadedFromQS, | |||
| setLoadedFromQS, | |||
| }; | |||
| }; | |||
| export default useFilters; | |||
| @@ -1,253 +0,0 @@ | |||
| import { useEffect, useMemo, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { HOME_PAGE } from "../constants/pages"; | |||
| import { KEY_PAGE, KEY_SIZE } from "../constants/queryStringConstants"; | |||
| import { sortEnum } from "../enums/sortEnum"; | |||
| import { fetchChats } from "../store/actions/chat/chatActions"; | |||
| import { | |||
| fetchMineOffers, | |||
| fetchOffers, | |||
| } from "../store/actions/offers/offersActions"; | |||
| import { | |||
| selectAppliedStatus, | |||
| selectSelectedCategory, | |||
| selectSelectedLocations, | |||
| selectSelectedSortOption, | |||
| selectSelectedSubcategory, | |||
| } from "../store/selectors/filtersSelectors"; | |||
| import { | |||
| selectMineOffers, | |||
| selectOffers, | |||
| selectPinnedOffers, | |||
| selectTotalOffers, | |||
| } from "../store/selectors/offersSelectors"; | |||
| import { useQueryString } from "./useQueryString"; | |||
| const useOffers = (myOffers) => { | |||
| const history = useHistory(); | |||
| const pinnedOffers = useSelector(selectPinnedOffers); | |||
| const offers = useSelector(selectOffers); | |||
| const mineOffers = useSelector(selectMineOffers); | |||
| const dispatch = useDispatch(); | |||
| const queryStringHook = useQueryString(); | |||
| const selectedCategory = useSelector(selectSelectedCategory); | |||
| const selectedSubcategory = useSelector(selectSelectedSubcategory); | |||
| const selectedLocations = useSelector(selectSelectedLocations); | |||
| const selectedSortOption = useSelector(selectSelectedSortOption); | |||
| const isApplied = useSelector(selectAppliedStatus); | |||
| const total = useSelector(selectTotalOffers); | |||
| const [page, setPage] = useState(1); | |||
| const [searchQuery, setSearchQuery] = useState(""); | |||
| const [myOffersLength, setMyOffersLength] = useState(0); | |||
| //Fetching chats | |||
| useEffect(() => { | |||
| dispatch(fetchChats()); | |||
| }, []); | |||
| //Setting appropriate page based on query string | |||
| useEffect(() => { | |||
| let queryObject = new URLSearchParams(queryStringHook.queryString); | |||
| if (queryObject.has(KEY_PAGE) && queryObject.get(KEY_PAGE) !== 1) { | |||
| setPage(parseInt(queryObject.get(KEY_PAGE))); | |||
| } | |||
| }, [history.location.search]); | |||
| // Checking if page is opened by clicking on logo on header, and fetching offers | |||
| // with empty query string if previous statement is true | |||
| useEffect(() => { | |||
| if (history?.location?.state?.logo || history?.location?.state?.refetch) { | |||
| dispatch(fetchOffers({ queryString: "" })); | |||
| queryStringHook.setQueryString(""); | |||
| setPage(1); | |||
| history.location.state = undefined; | |||
| } | |||
| }, [history.location.state]); | |||
| // Initialy loading offers with filters from query string | |||
| useEffect(() => { | |||
| if (queryStringHook.loadedFromURL) { | |||
| refetch(); | |||
| } else { | |||
| queryStringHook.appendMultipleToQueryString([ | |||
| { key: KEY_SIZE, value: "10" }, | |||
| { key: KEY_PAGE, value: "1" }, | |||
| ]); | |||
| } | |||
| }, [queryStringHook.loadedFromURL, queryStringHook.queryString]); | |||
| // Changing offers when page changes | |||
| useEffect(() => { | |||
| const queryObject = new URLSearchParams(queryStringHook.queryString); | |||
| if (queryObject.has(KEY_PAGE)) { | |||
| if (queryObject.get(KEY_PAGE) !== page.toString()) { | |||
| queryStringHook.appendToQueryString(KEY_PAGE, page); | |||
| } else { | |||
| refetch(); | |||
| } | |||
| } else { | |||
| queryStringHook.appendToQueryString(KEY_PAGE, page); | |||
| } | |||
| }, [page]); | |||
| // All pinned to show when market place is opened | |||
| const pinnedOffersToShow = useMemo(() => { | |||
| if (myOffers) { | |||
| return mineOffers.filter((item) => item.pinned); | |||
| } | |||
| return pinnedOffers; | |||
| }, [pinnedOffers, mineOffers, page, myOffers]); | |||
| // Normal offers to show when market place is opened | |||
| const offersToShow = useMemo(() => { | |||
| if (myOffers) { | |||
| return mineOffers.filter((item) => item.pinned === false); | |||
| } | |||
| return offers; | |||
| }, [offers, mineOffers, page, myOffers]); | |||
| // Offers to show when market place is opened and when my offers are opened | |||
| const allOffersToShow = useMemo(() => { | |||
| let newOffers = [...pinnedOffersToShow, ...offersToShow]; | |||
| if (myOffers) { | |||
| // Filtering my offers based on category | |||
| if (selectedCategory && selectedCategory?._id !== 0) { | |||
| newOffers = newOffers.filter( | |||
| (item) => item.category.name === selectedCategory.name | |||
| ); | |||
| } | |||
| // Filtering my offers based on subcategory | |||
| if (selectedSubcategory && selectedSubcategory?._id !== 0) { | |||
| newOffers = newOffers.filter( | |||
| (item) => item.subcategory === selectedSubcategory.name | |||
| ); | |||
| } | |||
| // Filtering my offers based on locations | |||
| if (selectedLocations && selectedLocations?.length > 0) { | |||
| newOffers = newOffers.filter((item) => { | |||
| let isInOneOfLocations = false; | |||
| selectedLocations?.forEach((location) => { | |||
| if (item.location.city === location.city) { | |||
| isInOneOfLocations = true; | |||
| } | |||
| }); | |||
| return isInOneOfLocations; | |||
| }); | |||
| } | |||
| // Sorting my offers based on chosen sorting option | |||
| // Old offers are arrays used for sorting | |||
| let oldOffers = [...offersToShow]; | |||
| let oldPinnedOffers = [...pinnedOffersToShow]; | |||
| if ( | |||
| selectedSortOption && | |||
| selectedSortOption.value === sortEnum.NEW.value | |||
| ) { | |||
| newOffers = [ | |||
| ...oldPinnedOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemB._created) - new Date(itemA._created) | |||
| ), | |||
| ...oldOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemB._created) - new Date(itemA._created) | |||
| ), | |||
| ]; | |||
| } | |||
| if ( | |||
| selectedSortOption && | |||
| selectedSortOption.value === sortEnum.OLD.value | |||
| ) { | |||
| newOffers = newOffers.sort( | |||
| (itemA, itemB) => new Date(itemA._created) - new Date(itemB._created) | |||
| ); | |||
| newOffers = [ | |||
| ...oldPinnedOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemA._created) - new Date(itemB._created) | |||
| ), | |||
| ...oldOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemA._created) - new Date(itemB._created) | |||
| ), | |||
| ]; | |||
| } | |||
| if ( | |||
| selectedSortOption && | |||
| selectedSortOption.value === sortEnum.POPULAR.value | |||
| ) { | |||
| newOffers = [ | |||
| ...oldPinnedOffers.sort( | |||
| (itemA, itemB) => itemB.views.count - itemA.views.count | |||
| ), | |||
| ...oldOffers.sort( | |||
| (itemA, itemB) => itemB.views.count - itemA.views.count | |||
| ), | |||
| ]; | |||
| } | |||
| newOffers = newOffers.filter((item) => | |||
| item?.name?.toLowerCase().includes(searchQuery.toLowerCase(), 0) | |||
| ); | |||
| setMyOffersLength(newOffers?.length); | |||
| newOffers = newOffers.slice((page - 1) * 10, page * 10); | |||
| } | |||
| return newOffers; | |||
| }, [ | |||
| pinnedOffersToShow, | |||
| offersToShow, | |||
| myOffers, | |||
| page, | |||
| searchQuery, | |||
| isApplied, | |||
| ]); | |||
| // Total number of all offers that can be shown | |||
| const totalOffers = useMemo(() => { | |||
| if (myOffers) { | |||
| return myOffersLength; | |||
| } | |||
| return total; | |||
| }, [total, myOffersLength]); | |||
| const searchMyOffers = (searchValue) => { | |||
| setSearchQuery(searchValue); | |||
| }; | |||
| // Changing page | |||
| const handleDifferentPage = (pageNum) => { | |||
| setPage(pageNum); | |||
| }; | |||
| // Refetching offers based on query string | |||
| const refetch = () => { | |||
| if (!myOffers) { | |||
| dispatch(fetchOffers({ queryString: "?" + queryStringHook.queryString })); | |||
| history.replace({ | |||
| pathname: HOME_PAGE, | |||
| search: queryStringHook.getGlobalQueryString(), | |||
| }); | |||
| } else { | |||
| dispatch(fetchMineOffers()); | |||
| } | |||
| window.scrollTo({ | |||
| top: 0, | |||
| behavior: "smooth", | |||
| }); | |||
| const queryObject = new URLSearchParams(queryStringHook.queryString); | |||
| if (queryObject.has(KEY_PAGE)) { | |||
| if (queryObject.get(KEY_PAGE) !== page.toString()) | |||
| setPage(parseInt(queryObject.get(KEY_PAGE))); | |||
| } else { | |||
| setPage(1); | |||
| } | |||
| }; | |||
| return { | |||
| handleDifferentPage, | |||
| totalOffers, | |||
| allOffersToShow, | |||
| page, | |||
| searchMyOffers, | |||
| }; | |||
| }; | |||
| export default useOffers; | |||
| @@ -0,0 +1,66 @@ | |||
| import { useEffect, useMemo, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { setFilteredCategory } from "../../store/actions/filters/filtersActions"; | |||
| import { selectCategories } from "../../store/selectors/categoriesSelectors"; | |||
| import { selectSelectedCategory } from "../../store/selectors/filtersSelectors"; | |||
| const useCategoryFilter = () => { | |||
| const selectedCategory = useSelector(selectSelectedCategory); | |||
| const allCategories = useSelector(selectCategories); | |||
| const dispatch = useDispatch(); | |||
| const [selectedCategoryLocally, setSelectedCategoryLocally] = useState({}); | |||
| const initialOption = useMemo(() => { | |||
| return { | |||
| _id: 0, | |||
| }; | |||
| }, []); | |||
| useEffect(() => { | |||
| setSelectedCategoryLocally(selectedCategory); | |||
| }, [selectedCategory]); | |||
| // Set selected category locally in state | |||
| // If second argument is true, then selected category is also updated in redux | |||
| const setSelectedCategory = (category, immediateApply = false) => { | |||
| setSelectedCategoryLocally(category); | |||
| if (immediateApply) { | |||
| dispatch(setFilteredCategory(category)); | |||
| } | |||
| }; | |||
| // Find category object by providing its name | |||
| const findCategory = (categoryName) => { | |||
| return allCategories.find((category) => category.name === categoryName); | |||
| }; | |||
| // Get all subcategories by providing its category name | |||
| const getSubcategories = (categoryName) => { | |||
| let category = findCategory(categoryName); | |||
| return category?.subcategories ? category.subcategories : []; | |||
| }; | |||
| // Update selected category in redux | |||
| const apply = () => { | |||
| dispatch(setFilteredCategory(selectedCategoryLocally)); | |||
| }; | |||
| // Clear category chosen | |||
| const clear = () => { | |||
| setSelectedCategoryLocally(initialOption); | |||
| dispatch(setFilteredCategory(initialOption)); | |||
| }; | |||
| return { | |||
| selectedCategory, | |||
| selectedCategoryLocally, | |||
| setSelectedCategory, | |||
| getSubcategories, | |||
| findCategory, | |||
| allCategories, | |||
| apply, | |||
| clear, | |||
| }; | |||
| }; | |||
| export default useCategoryFilter; | |||
| @@ -0,0 +1,50 @@ | |||
| import { useEffect, useMemo } from "react"; | |||
| import useCategoryFilter from "./useCategoryFilter"; | |||
| import useLocationsFilter from "./useLocationsFilter"; | |||
| import useSubcategoryFilter from "./useSubcategoryFilter"; | |||
| const useFilters = (clearAll = false) => { | |||
| const category = useCategoryFilter(); | |||
| const subcategory = useSubcategoryFilter(); | |||
| const locations = useLocationsFilter(); | |||
| useEffect(() => { | |||
| if (clearAll) { | |||
| clear(); | |||
| } | |||
| }, []); | |||
| const numOfFiltersChosen = useMemo(() => { | |||
| let sumOfFiltersChosen = 0; | |||
| if (category.selectedCategoryLocally?._id) sumOfFiltersChosen++; | |||
| if (subcategory.selectedSubcategoryLocally?._id) sumOfFiltersChosen++; | |||
| sumOfFiltersChosen += locations.selectedLocationsLocally.length; | |||
| return sumOfFiltersChosen; | |||
| }, [ | |||
| category.selectedCategoryLocally, | |||
| subcategory.selectedSubcategoryLocally, | |||
| locations.selectedLocationsLocally, | |||
| ]); | |||
| const apply = () => { | |||
| category.apply(); | |||
| subcategory.apply(); | |||
| locations.apply(); | |||
| }; | |||
| const clear = () => { | |||
| category.clear(); | |||
| subcategory.clear(); | |||
| locations.clear(); | |||
| }; | |||
| return { | |||
| category, | |||
| subcategory, | |||
| locations, | |||
| numOfFiltersChosen, | |||
| apply, | |||
| clear, | |||
| }; | |||
| }; | |||
| export default useFilters; | |||
| @@ -0,0 +1,54 @@ | |||
| import { useEffect, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { setFilteredLocations } from "../../store/actions/filters/filtersActions"; | |||
| import { selectSelectedLocations } from "../../store/selectors/filtersSelectors"; | |||
| import { selectLocations } from "../../store/selectors/locationsSelectors"; | |||
| const useLocationsFilter = () => { | |||
| const selectedLocations = useSelector(selectSelectedLocations); | |||
| const dispatch = useDispatch(); | |||
| const allLocations = useSelector(selectLocations); | |||
| const [selectedLocationsLocally, setSelectedLocationsLocally] = useState([]); | |||
| useEffect(() => { | |||
| setSelectedLocationsLocally(selectedLocations); | |||
| }, [selectedLocations]); | |||
| // Set selected locations globally | |||
| const setSelectedLocations = (locations, immediateApply = false) => { | |||
| setSelectedLocationsLocally(locations); | |||
| if (immediateApply) { | |||
| dispatch(setFilteredLocations(locations)); | |||
| } | |||
| }; | |||
| // Find locations from array made from query string, and set locations globally | |||
| const setSelectedLocationsFromArray = (locations) => { | |||
| let locationsToPush = []; | |||
| locations.forEach((locationName) => { | |||
| locationsToPush.push(allLocations.find((p) => p.city === locationName)); | |||
| }); | |||
| setSelectedLocations([...locationsToPush]) | |||
| }; | |||
| const apply = () => { | |||
| dispatch(setFilteredLocations(selectedLocationsLocally)); | |||
| }; | |||
| const clear = () => { | |||
| setSelectedLocationsLocally([]); | |||
| dispatch(setFilteredLocations([])); | |||
| }; | |||
| return { | |||
| selectedLocations, | |||
| selectedLocationsLocally, | |||
| setSelectedLocations, | |||
| setSelectedLocationsFromArray, | |||
| allLocations, | |||
| apply, | |||
| clear, | |||
| }; | |||
| }; | |||
| export default useLocationsFilter; | |||
| @@ -0,0 +1,101 @@ | |||
| import { useEffect, useMemo, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { sortEnum } from "../../enums/sortEnum"; | |||
| import { fetchMineOffers } from "../../store/actions/offers/offersActions"; | |||
| import { selectMineOffers } from "../../store/selectors/offersSelectors"; | |||
| import useFilters from "./useFilters"; | |||
| import usePaging from "./usePaging"; | |||
| import useSearch from "./useSearch"; | |||
| import useSorting from "./useSorting"; | |||
| const useMyOffers = () => { | |||
| const filters = useFilters(true); | |||
| const sorting = useSorting(); | |||
| const mineOffers = useSelector(selectMineOffers); | |||
| const search = useSearch(); | |||
| const dispatch = useDispatch(); | |||
| const paging = usePaging(); | |||
| const [appliedFilters, setAppliedFilters] = useState(false); | |||
| const [totalOffers, setTotalOffers] = useState(0); | |||
| useEffect(() => { | |||
| dispatch(fetchMineOffers()); | |||
| }, []); | |||
| const apply = () => { | |||
| paging.changePage(1); | |||
| setAppliedFilters(false); | |||
| }; | |||
| // Filter, search and sort all mine offers | |||
| const allOffersToShow = useMemo(() => { | |||
| let mineOffersFiltered = [...mineOffers]; | |||
| // Filter mine offers by category | |||
| if (filters.category.selectedCategoryLocally?.name) | |||
| mineOffersFiltered = mineOffersFiltered.filter( | |||
| (offer) => | |||
| offer?.category?.name === | |||
| filters.category.selectedCategoryLocally.name | |||
| ); | |||
| // Filter mine offers by subcategory | |||
| if (filters.subcategory.selectedSubcategoryLocally?.name) { | |||
| mineOffersFiltered = mineOffersFiltered.filter( | |||
| (offer) => | |||
| offer?.subcategory === | |||
| filters.subcategory.selectedSubcategoryLocally?.name | |||
| ); | |||
| } | |||
| // Filter mine offers by locations | |||
| if (filters.locations.selectedLocationsLocally?.length > 0) { | |||
| mineOffersFiltered = mineOffersFiltered.filter((offer) => | |||
| filters.locations.selectedLocationsLocally.find( | |||
| (location) => location?.city === offer?.location?.city | |||
| ) | |||
| ); | |||
| } | |||
| // Sort mine offers | |||
| if (sorting.selectedSortOptionLocally.value !== sortEnum.INITIAL.value) { | |||
| if (sorting.selectedSortOptionLocally.value === sortEnum.OLD.value) { | |||
| mineOffersFiltered.sort( | |||
| (a, b) => new Date(a._created) - new Date(b._created) | |||
| ); | |||
| } | |||
| if (sorting.selectedSortOptionLocally.value === sortEnum.NEW.value) { | |||
| mineOffersFiltered.sort( | |||
| (a, b) => new Date(b._created) - new Date(a._created) | |||
| ); | |||
| } | |||
| if (sorting.selectedSortOptionLocally.value === sortEnum.POPULAR.value) { | |||
| mineOffersFiltered.sort((a, b) => b.views.count - a.views.count); | |||
| } | |||
| } | |||
| mineOffersFiltered = mineOffersFiltered.filter((offer) => | |||
| offer?.name?.toLowerCase()?.includes(search.searchStringLocally) | |||
| ); | |||
| setTotalOffers(mineOffersFiltered?.length); | |||
| mineOffersFiltered = mineOffersFiltered.slice( | |||
| (paging.currentPage - 1) * 10, | |||
| paging.currentPage * 10 | |||
| ); | |||
| if (!appliedFilters) { | |||
| setAppliedFilters(true); | |||
| } | |||
| return [...mineOffersFiltered]; | |||
| }, [ | |||
| appliedFilters, | |||
| sorting.selectedSortOptionLocally, | |||
| mineOffers, | |||
| paging.currentPage, | |||
| ]); | |||
| return { | |||
| filters, | |||
| paging, | |||
| sorting, | |||
| search, | |||
| allOffersToShow, | |||
| totalOffers, | |||
| apply, | |||
| }; | |||
| }; | |||
| export default useMyOffers; | |||
| @@ -0,0 +1,144 @@ | |||
| import { useEffect, useMemo } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { | |||
| KEY_CATEGORY, | |||
| KEY_LOCATION, | |||
| KEY_PAGE, | |||
| KEY_SEARCH, | |||
| KEY_SORTBY, | |||
| KEY_SUBCATEGORY, | |||
| } from "../../constants/queryStringConstants"; | |||
| import { fetchCategories } from "../../store/actions/categories/categoriesActions"; | |||
| import { fetchLocations } from "../../store/actions/locations/locationsActions"; | |||
| import { | |||
| selectOffers, | |||
| selectTotalOffers, | |||
| } from "../../store/selectors/offersSelectors"; | |||
| import useFilters from "./useFilters"; | |||
| import useQueryString from "./useQueryString"; | |||
| import { setQueryString } from "../../store/actions/queryString/queryStringActions"; | |||
| import { | |||
| convertQueryStringForBackend, | |||
| makeHeaderStringHelper, | |||
| makeQueryStringHelper, | |||
| } from "../../util/helpers/queryHelpers"; | |||
| import useSorting from "./useSorting"; | |||
| import useSearch from "./useSearch"; | |||
| import { | |||
| setHeaderString, | |||
| setSearchString, | |||
| } from "../../store/actions/filters/filtersActions"; | |||
| import usePaging from "./usePaging"; | |||
| import { useHistory } from "react-router-dom"; | |||
| const useOffers = () => { | |||
| const dispatch = useDispatch(); | |||
| const filters = useFilters(); | |||
| const queryStringHook = useQueryString(); | |||
| const offers = useSelector(selectOffers); | |||
| const totalOffers = useSelector(selectTotalOffers); | |||
| const history = useHistory(); | |||
| // Always fetch categories and locations, | |||
| // becouse count of total offers change over time | |||
| useEffect(() => { | |||
| dispatch(fetchCategories()); | |||
| dispatch(fetchLocations()); | |||
| return () => clear(); | |||
| }, []); | |||
| useEffect(() => { | |||
| console.log('location: ', history.location) | |||
| if (history.location.state?.logo) { | |||
| console.log('logo'); | |||
| clear(); | |||
| } | |||
| }, [history.location]) | |||
| // On every change of query string, new header string should be created | |||
| // Header string is shown on Home page above offers | |||
| useEffect(() => { | |||
| const headerStringLocal = makeHeaderStringHelper(filters); | |||
| dispatch(setHeaderString(headerStringLocal)); | |||
| }, [queryStringHook.queryString]); | |||
| // Initially set category, location and subcategory based on query string | |||
| useEffect(() => { | |||
| if (queryStringHook.isInitiallyLoaded) { | |||
| const queryObject = queryStringHook.queryObject; | |||
| if (KEY_CATEGORY in queryObject) { | |||
| const category = filters.category.findCategory( | |||
| queryObject[KEY_CATEGORY] | |||
| ); | |||
| filters.category.setSelectedCategory(category); | |||
| if (KEY_SUBCATEGORY in queryObject) { | |||
| const subcategory = filters.category | |||
| .getSubcategories(category?.name) | |||
| .find( | |||
| (subcategory) => subcategory.name === queryObject[KEY_SUBCATEGORY] | |||
| ); | |||
| filters.subcategory.setSelectedSubcategory(subcategory); | |||
| } | |||
| } | |||
| if (KEY_LOCATION in queryObject) { | |||
| filters.locations.setSelectedLocationsFromArray( | |||
| queryObject[KEY_LOCATION] | |||
| ); | |||
| } | |||
| if (KEY_SORTBY in queryObject) { | |||
| sorting.changeSortingFromName(queryObject[KEY_SORTBY]); | |||
| } | |||
| if (KEY_PAGE in queryObject) { | |||
| if (queryObject[KEY_PAGE] !== 1) | |||
| paging.changePage(queryObject[KEY_PAGE]); | |||
| } | |||
| dispatch(setSearchString(queryObject[KEY_SEARCH])); | |||
| } | |||
| }, [queryStringHook.isInitiallyLoaded]); | |||
| const allOffersToShow = useMemo(() => { | |||
| return offers; | |||
| }, [offers]); | |||
| const apply = () => { | |||
| filters.apply(); | |||
| const newQueryString = makeQueryStringHelper( | |||
| filters, | |||
| paging, | |||
| search, | |||
| sorting | |||
| ); | |||
| dispatch(setQueryString(convertQueryStringForBackend(newQueryString))); | |||
| }; | |||
| // Those hooks are below becouse function apply cannot be put on props before initialization | |||
| const sorting = useSorting(apply); | |||
| const paging = usePaging(apply); | |||
| const search = useSearch(apply); | |||
| // On every change of search string, offers should be immediately searched | |||
| useEffect(() => { | |||
| if (queryStringHook.isInitiallyLoaded) { | |||
| search.searchOffers(search.searchString); | |||
| } | |||
| }, [search.searchString]); | |||
| const clear = () => { | |||
| filters.clear(); | |||
| sorting.clear(); | |||
| paging.changePage(1); | |||
| }; | |||
| return { | |||
| filters, | |||
| sorting, | |||
| paging, | |||
| queryStringHook, | |||
| allOffersToShow, | |||
| totalOffers, | |||
| apply, | |||
| clear, | |||
| }; | |||
| }; | |||
| export default useOffers; | |||
| @@ -0,0 +1,38 @@ | |||
| import { useEffect, useState } from "react"; | |||
| const usePaging = (applyAllFilters) => { | |||
| const [currentPage, setCurrentPage] = useState(1); | |||
| const [isInitallyLoaded, setIsInitiallyLoaded] = useState(false); | |||
| // If state currentPage is changed, new request to backend should be sent, | |||
| // except on initial load | |||
| useEffect(() => { | |||
| if (isInitallyLoaded && applyAllFilters) { | |||
| applyAllFilters(); | |||
| } | |||
| window.scrollTo({ | |||
| top: 0, | |||
| behavior: "smooth", | |||
| }); | |||
| }, [currentPage]); | |||
| const changePage = (pageNumber) => { | |||
| setCurrentPage(pageNumber); | |||
| setIsInitiallyLoaded(true); | |||
| }; | |||
| const goToNextPage = () => { | |||
| setCurrentPage((prevPage) => prevPage + 1); | |||
| }; | |||
| const goToPrevPage = () => { | |||
| setCurrentPage((prevPage) => prevPage - 1); | |||
| }; | |||
| return { | |||
| currentPage, | |||
| changePage, | |||
| goToNextPage, | |||
| goToPrevPage, | |||
| }; | |||
| }; | |||
| export default usePaging; | |||
| @@ -0,0 +1,60 @@ | |||
| import { useEffect, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { fetchOffers } from "../../store/actions/offers/offersActions"; | |||
| import { setQueryString } from "../../store/actions/queryString/queryStringActions"; | |||
| import { selectQueryString } from "../../store/selectors/queryStringSelectors"; | |||
| import { | |||
| convertQueryStringForBackend, | |||
| convertQueryStringForFrontend, | |||
| getQueryObjectHelper, | |||
| } from "../../util/helpers/queryHelpers"; | |||
| const useQueryString = () => { | |||
| const queryString = useSelector(selectQueryString); | |||
| const history = useHistory(); | |||
| const dispatch = useDispatch(); | |||
| const [isInitiallyLoaded, setIsInitallyLoaded] = useState(false); | |||
| const [queryObject, setQueryObject] = useState({}); | |||
| // Initially read filters, sorting and paging from querystring | |||
| useEffect(() => { | |||
| if ((!isInitiallyLoaded || history.location?.state?.logo) && !history.location?.state?.from) { | |||
| const queryStringFromUrl = history.location?.search; | |||
| setQueryObject(getQueryObjectHelper(queryStringFromUrl)); | |||
| dispatch(setQueryString(queryStringFromUrl)); | |||
| } | |||
| history.location.state = {} | |||
| }, [history.location]); | |||
| // Set initially loaded to true on initial load | |||
| useEffect(() => { | |||
| if ( | |||
| convertQueryStringForFrontend(queryString) === | |||
| convertQueryStringForFrontend(history.location.search) && | |||
| !isInitiallyLoaded | |||
| ) { | |||
| setIsInitallyLoaded(true); | |||
| } | |||
| }, [queryString]); | |||
| // Updating offers on query string change | |||
| useEffect(() => { | |||
| if (isInitiallyLoaded) { | |||
| dispatch( | |||
| fetchOffers({ queryString: convertQueryStringForBackend(queryString) }) | |||
| ); | |||
| setQueryObject(getQueryObjectHelper(queryString)); | |||
| history.replace({ | |||
| search: convertQueryStringForFrontend(queryString), | |||
| }); | |||
| } | |||
| }, [queryString, isInitiallyLoaded]); | |||
| return { | |||
| queryString, | |||
| queryObject, | |||
| isInitiallyLoaded, | |||
| }; | |||
| }; | |||
| export default useQueryString; | |||
| @@ -0,0 +1,46 @@ | |||
| import { useEffect, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { setSearchString } from "../../store/actions/filters/filtersActions"; | |||
| import { selectSearchString } from "../../store/selectors/filtersSelectors"; | |||
| const useSearch = (applyAllFilters) => { | |||
| const [searchStringLocally, setSearchStringLocally] = useState(""); | |||
| const [isInitallyLoaded, setIsInitiallyLoaded] = useState(false); | |||
| const dispatch = useDispatch(); | |||
| const searchString = useSelector(selectSearchString); | |||
| // On every global change of search string, new request to backend should be sent | |||
| useEffect(() => { | |||
| if (searchStringLocally !== searchString && applyAllFilters) { | |||
| setSearchStringLocally(searchString); | |||
| } | |||
| if (isInitallyLoaded) { | |||
| if (applyAllFilters) applyAllFilters(); | |||
| } | |||
| }, [searchString]); | |||
| // On every local change of search string, global state of search string should be also updated | |||
| useEffect(() => { | |||
| if (isInitallyLoaded && applyAllFilters) { | |||
| dispatch(setSearchString(searchStringLocally)); | |||
| } | |||
| }, [searchStringLocally]); | |||
| const searchOffers = (searchValue) => { | |||
| setIsInitiallyLoaded(true); | |||
| setSearchStringLocally(searchValue); | |||
| }; | |||
| const clear = () => { | |||
| setSearchStringLocally(""); | |||
| }; | |||
| return { | |||
| searchOffers, | |||
| setSearchStringLocally, | |||
| searchStringLocally, | |||
| searchString, | |||
| clear, | |||
| }; | |||
| }; | |||
| export default useSearch; | |||
| @@ -0,0 +1,65 @@ | |||
| import { useEffect, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { | |||
| VALUE_SORTBY_NEW, | |||
| VALUE_SORTBY_OLD, | |||
| VALUE_SORTBY_POPULAR, | |||
| } from "../../constants/queryStringConstants"; | |||
| import { sortEnum } from "../../enums/sortEnum"; | |||
| import { setFilteredSortOption } from "../../store/actions/filters/filtersActions"; | |||
| import { selectSelectedSortOption } from "../../store/selectors/filtersSelectors"; | |||
| const useSorting = (applyAllFilters) => { | |||
| const selectedSortOption = useSelector(selectSelectedSortOption); | |||
| const [selectedSortOptionLocally, setSelectedSortOptionLocally] = useState( | |||
| sortEnum.INITIAL | |||
| ); | |||
| const [isInitiallyLoaded, setIsInitallyLoaded] = useState(false); | |||
| const dispatch = useDispatch(); | |||
| // On every change of sorting option, new request to backend should be sent | |||
| useEffect(() => { | |||
| if (isInitiallyLoaded) { | |||
| if (applyAllFilters) applyAllFilters(); | |||
| } | |||
| }, [isInitiallyLoaded, selectedSortOption]); | |||
| const changeSorting = (newSortOption) => { | |||
| dispatch(setFilteredSortOption(newSortOption)); | |||
| setSelectedSortOptionLocally(newSortOption); | |||
| if (!isInitiallyLoaded) { | |||
| setIsInitallyLoaded(true); | |||
| } | |||
| }; | |||
| // Change sorting by name of sorting option that is shown on frontned | |||
| const changeSortingFromName = (sortingName) => { | |||
| if (sortingName === VALUE_SORTBY_NEW) { | |||
| changeSorting(sortEnum.NEW, true); | |||
| } | |||
| if (sortingName === VALUE_SORTBY_OLD) { | |||
| changeSorting(sortEnum.OLD, true); | |||
| } | |||
| if (sortingName === VALUE_SORTBY_POPULAR) { | |||
| changeSorting(sortEnum.POPULAR, true); | |||
| } | |||
| }; | |||
| const apply = () => { | |||
| // For future changes | |||
| }; | |||
| const clear = () => { | |||
| dispatch(setFilteredSortOption(sortEnum.INITIAL)); | |||
| }; | |||
| return { | |||
| selectedSortOption, | |||
| selectedSortOptionLocally, | |||
| changeSorting, | |||
| changeSortingFromName, | |||
| apply, | |||
| clear, | |||
| }; | |||
| }; | |||
| export default useSorting; | |||
| @@ -0,0 +1,56 @@ | |||
| import { useEffect, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { setFilteredSubcategory } from "../../store/actions/filters/filtersActions"; | |||
| import { selectSelectedSubcategory } from "../../store/selectors/filtersSelectors"; | |||
| const useSubcategoryFilter = () => { | |||
| const selectedSubcategory = useSelector(selectSelectedSubcategory); | |||
| const dispatch = useDispatch(); | |||
| const initialOption = { | |||
| label: "SVE PODKATEGORIJE", | |||
| _id: 0, | |||
| }; | |||
| const [selectedSubcategoryLocally, setSelectedSubcategoryLocally] = | |||
| useState(initialOption); | |||
| useEffect(() => { | |||
| if (selectedSubcategory) | |||
| if ("_id" in selectedSubcategory) { | |||
| setSelectedSubcategoryLocally(selectedSubcategory); | |||
| } | |||
| }, [selectedSubcategory]); | |||
| useEffect(() => { | |||
| if (selectedSubcategoryLocally) | |||
| if (Object.keys(selectedSubcategoryLocally)?.length === 0) { | |||
| setSelectedSubcategoryLocally(initialOption); | |||
| } | |||
| }, [initialOption]); | |||
| const setSelectedSubcategory = (subcategory, immediateApply = false) => { | |||
| setSelectedSubcategoryLocally(subcategory); | |||
| if (immediateApply) { | |||
| dispatch(setFilteredSubcategory(subcategory)); | |||
| } | |||
| }; | |||
| const apply = () => { | |||
| dispatch(setFilteredSubcategory(selectedSubcategoryLocally)); | |||
| }; | |||
| const clear = () => { | |||
| setSelectedSubcategoryLocally(initialOption); | |||
| dispatch(setFilteredSubcategory(initialOption)); | |||
| }; | |||
| return { | |||
| selectedSubcategory, | |||
| selectedSubcategoryLocally, | |||
| setSelectedSubcategory, | |||
| initialOption, | |||
| apply, | |||
| clear, | |||
| }; | |||
| }; | |||
| export default useSubcategoryFilter; | |||
| @@ -1,174 +0,0 @@ | |||
| /* eslint-disable */ | |||
| import _ from "lodash"; | |||
| import { useEffect, useMemo, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import PropTypes from "prop-types"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { HOME_PAGE } from "../constants/pages"; | |||
| import { setQueryString as setQueryStringSaga } from "../store/actions/queryString/queryStringActions"; | |||
| import { selectQueryString } from "../store/selectors/queryStringSelectors"; | |||
| import { | |||
| convertQueryStringBackend, | |||
| convertQueryStringFrontend, | |||
| } from "../util/helpers/queryHelpers"; | |||
| import { KEY_CATEGORY, KEY_LOCATION, KEY_PAGE, KEY_SIZE, KEY_SORTBY, KEY_SORT_DATE, KEY_SORT_POPULAR, KEY_SUBCATEGORY } from "../constants/queryStringConstants"; | |||
| export const useQueryString = () => { | |||
| const queryString = useSelector(selectQueryString); | |||
| const history = useHistory(); | |||
| const [globalQueryString, setGlobalQueryString] = useState(""); | |||
| const [initial, setInitial] = useState(true); | |||
| const [loadedFromURL, setLoadedFromURL] = useState(false); | |||
| const dispatch = useDispatch(); | |||
| // Initially loading query string and putting it into redux | |||
| useEffect(() => { | |||
| // Substring(1) used becouse query string in initial state is ?key=value | |||
| // and in redux query string is stored as only key=value | |||
| const queryStringLocal = history.location.search.substring(1); | |||
| setQueryString(convertQueryStringBackend(queryStringLocal)); | |||
| setGlobalQueryString(queryStringLocal); | |||
| }, []); | |||
| // Setting loadedFromUrl to true when query string is loaded and filters/sorting/search | |||
| // has been changed so global query string matches current query string | |||
| useEffect(() => { | |||
| if (globalQueryString === history.location.search.substring(1)) | |||
| setLoadedFromURL(true); | |||
| }, [globalQueryString]); | |||
| // Making function to initially set global query string when all filters and sorting | |||
| // has been set | |||
| const fun = useMemo(() => { | |||
| return _.once(() => { | |||
| setGlobalQueryString(convertQueryStringFrontend(queryString)); | |||
| setInitial(false); | |||
| }); | |||
| }, [queryString]); | |||
| // Setting global query string when all filters and sorting has been set | |||
| useEffect(() => { | |||
| if (initial && loadedFromURL) { | |||
| if (queryString?.length > 0) { | |||
| fun(); | |||
| } | |||
| } else { | |||
| setGlobalQueryString(convertQueryStringFrontend(queryString)); | |||
| } | |||
| }, [queryString, loadedFromURL]); | |||
| // When global query string is changed, updating query string that user sees | |||
| useEffect(() => { | |||
| if (!initial && history.location.pathname === HOME_PAGE) { | |||
| history.replace({ | |||
| pathname: HOME_PAGE, | |||
| search: "?" + globalQueryString, | |||
| }); | |||
| } | |||
| }, [globalQueryString, initial]); | |||
| const getQueryString = () => { | |||
| return queryString; | |||
| }; | |||
| const setQueryString = (newQueryString) => { | |||
| dispatch(setQueryStringSaga(newQueryString)); | |||
| }; | |||
| const getQueryObject = () => { | |||
| const urlParams = new URLSearchParams(queryString); | |||
| return Object.fromEntries(urlParams); | |||
| }; | |||
| // Adding key-value pairs to query string, deletes its previous duplicates, except | |||
| // when working with locations, then just appends it, or doesnt append if query string | |||
| // already contains provided location | |||
| const appendToQueryString = (key, value) => { | |||
| if (loadedFromURL) { | |||
| let urlParams = new URLSearchParams(queryString); | |||
| if (key === KEY_LOCATION) { | |||
| if (urlParams.has(key)) { | |||
| let arrayOfLocations = urlParams.getAll(key); | |||
| if (arrayOfLocations.includes(value)) { | |||
| arrayOfLocations = arrayOfLocations.filter( | |||
| (item) => item?.toString() !== value?.toString() | |||
| ); | |||
| urlParams.delete(key); | |||
| arrayOfLocations.forEach((item) => { | |||
| urlParams.append(key, item); | |||
| }); | |||
| } | |||
| } | |||
| } else { | |||
| if (urlParams.has(key)) { | |||
| urlParams.delete(key); | |||
| } | |||
| } | |||
| if (!value) setQueryString(urlParams.toString()); | |||
| urlParams.append(key, value); | |||
| setQueryString(urlParams.toString()); | |||
| return urlParams.toString(); | |||
| } | |||
| }; | |||
| // Same as appendToQueryString, just adds multiple key-value pairs at once | |||
| const appendMultipleToQueryString = (array = []) => { | |||
| if (loadedFromURL) { | |||
| let urlParams = new URLSearchParams(queryString); | |||
| if ( | |||
| array.find((item) => item.key === KEY_CATEGORY) || | |||
| array.find((item) => item.key === KEY_SUBCATEGORY) | |||
| ) { | |||
| urlParams.delete(KEY_LOCATION); | |||
| } | |||
| array.forEach((item) => { | |||
| if (urlParams.has(item.key) && item.key !== KEY_LOCATION) { | |||
| urlParams.delete(item.key); | |||
| } | |||
| if (!item.value) return; | |||
| urlParams.append(item.key, item.value); | |||
| }); | |||
| setQueryString(urlParams.toString()); | |||
| return urlParams.toString(); | |||
| } | |||
| }; | |||
| const deleteFromQueryString = (key, value = null) => { | |||
| let urlParams = new URLSearchParams(queryString); | |||
| if (key === KEY_LOCATION) { | |||
| let arrayOfLocations = urlParams.getAll(key); | |||
| arrayOfLocations = arrayOfLocations.filter((item) => item !== value); | |||
| urlParams.delete(key); | |||
| arrayOfLocations.forEach((item) => { | |||
| urlParams.append(key, item); | |||
| }); | |||
| } else if (key === KEY_SORTBY) { | |||
| urlParams.delete(KEY_SORT_DATE); | |||
| urlParams.delete(KEY_SORT_POPULAR); | |||
| } else { | |||
| urlParams.delete(key); | |||
| } | |||
| setQueryString(urlParams.toString()); | |||
| return urlParams.toString(); | |||
| }; | |||
| const getInitialQueryString = () => { | |||
| let urlParams = new URLSearchParams(queryString); | |||
| urlParams = new URLSearchParams(appendToQueryString(KEY_SIZE, 10)); | |||
| urlParams = new URLSearchParams(appendToQueryString(KEY_PAGE, 1)); | |||
| return urlParams; | |||
| }; | |||
| const getGlobalQueryString = () => { | |||
| return globalQueryString; | |||
| }; | |||
| return { | |||
| queryString, | |||
| globalQueryString, | |||
| getQueryString, | |||
| setQueryString, | |||
| getQueryObject, | |||
| initial, | |||
| loadedFromURL, | |||
| appendMultipleToQueryString, | |||
| getGlobalQueryString, | |||
| appendToQueryString, | |||
| getInitialQueryString, | |||
| deleteFromQueryString, | |||
| }; | |||
| }; | |||
| @@ -1,16 +1,16 @@ | |||
| import { useQueryString } from "./useQueryString"; | |||
| // import useQueryString from "./useOffers/useQueryString"; | |||
| export const useSearch = () => { | |||
| const queryStringHook = useQueryString(); | |||
| const searchOffers = (searchString) => { | |||
| if (searchString?.length !== 0) { | |||
| queryStringHook.appendToQueryString("oname", searchString); | |||
| } else { | |||
| const newQueryString = new URLSearchParams(queryStringHook.queryString); | |||
| if (newQueryString.has("oname")) { | |||
| queryStringHook.deleteFromQueryString("oname"); | |||
| } | |||
| } | |||
| // const queryStringHook = useQueryString(); | |||
| const searchOffers = () => { | |||
| // if (searchString?.length !== 0) { | |||
| // queryStringHook.appendToQueryString("oname", searchString); | |||
| // } else { | |||
| // const newQueryString = new URLSearchParams(queryStringHook.queryString); | |||
| // if (newQueryString.has("oname")) { | |||
| // queryStringHook.deleteFromQueryString("oname"); | |||
| // } | |||
| // } | |||
| }; | |||
| return { | |||
| @@ -0,0 +1,36 @@ | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| const useSkeleton = (skeletonOptions) => { | |||
| const [transitionStage, setTransitionStage] = useState(1); | |||
| // After how long skeleton changes screen | |||
| const timeoutInterval = useMemo(() => { | |||
| return skeletonOptions?.timeoutInterval || 900; | |||
| }); | |||
| // isLoadingIndicator is boolean that indicates should skeleton screen be shown | |||
| const isLoadingIndicator = useMemo(() => { | |||
| return skeletonOptions?.isLoadingIndicator; | |||
| }, [skeletonOptions]); | |||
| // Timeout function to change transition stage | |||
| const timeout = useCallback(() => { | |||
| setTransitionStage((prevTransitionStage) => { | |||
| if (prevTransitionStage === 2) return 1; | |||
| return prevTransitionStage + 1; | |||
| }); | |||
| }, [transitionStage]); | |||
| useEffect(() => { | |||
| let newTimeout; | |||
| if (isLoadingIndicator) { | |||
| newTimeout = setTimeout(timeout, timeoutInterval); | |||
| } | |||
| return () => clearTimeout(newTimeout); | |||
| }, [timeout, isLoadingIndicator]); | |||
| return { | |||
| transitionStage, | |||
| }; | |||
| }; | |||
| export default useSkeleton; | |||
| @@ -1,86 +0,0 @@ | |||
| import { useEffect } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { sortEnum } from "../enums/sortEnum"; | |||
| import { setFilteredSortOption } from "../store/actions/filters/filtersActions"; | |||
| import { selectSelectedSortOption } from "../store/selectors/filtersSelectors"; | |||
| import { useQueryString } from "./useQueryString"; | |||
| import { convertQueryStringFrontend } from "../util/helpers/queryHelpers"; | |||
| import { | |||
| KEY_PAGE, | |||
| KEY_SORTBY, | |||
| KEY_SORT_DATE, | |||
| KEY_SORT_POPULAR, | |||
| VALUE_SORTBY_NEW, | |||
| VALUE_SORTBY_OLD, | |||
| VALUE_SORTBY_POPULAR, | |||
| } from "../constants/queryStringConstants"; | |||
| const useSorting = () => { | |||
| const dispatch = useDispatch(); | |||
| const selectedSortOption = useSelector(selectSelectedSortOption); | |||
| const sortOptions = sortEnum; | |||
| const queryStringHook = useQueryString(); | |||
| // Setting sort option on initially load or refresh page | |||
| useEffect(() => { | |||
| if (queryStringHook.loadedFromURL) { | |||
| const queryString = queryStringHook.queryString; | |||
| let queryObject = new URLSearchParams( | |||
| convertQueryStringFrontend(queryString) | |||
| ); | |||
| if (queryObject.has(KEY_SORTBY)) { | |||
| if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_NEW) { | |||
| setSelectedSortOption(sortEnum.NEW); | |||
| } | |||
| if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_OLD) { | |||
| setSelectedSortOption(sortEnum.OLD); | |||
| } | |||
| if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_POPULAR) { | |||
| setSelectedSortOption(sortEnum.POPULAR); | |||
| } | |||
| } else { | |||
| setSelectedSortOption(sortOptions.INITIAL); | |||
| } | |||
| } | |||
| }, [queryStringHook.queryString, queryStringHook.loadedFromURL]); | |||
| const setSelectedSortOption = (payload, shouldGoFirstPage = false) => { | |||
| dispatch(setFilteredSortOption(payload)); | |||
| let _des_date = null; | |||
| let _des_popular = null; | |||
| if (payload.value === sortOptions.NEW.value) { | |||
| _des_date = true; | |||
| } | |||
| if (payload.value === sortOptions.OLD.value) { | |||
| _des_date = false; | |||
| } | |||
| if (payload.value === sortOptions.POPULAR.value) { | |||
| _des_popular = true; | |||
| } | |||
| let queryArray = []; | |||
| if (_des_date !== null) { | |||
| queryArray.push({ key: KEY_SORT_DATE, value: `${_des_date}` }); | |||
| queryArray.push({ key: KEY_SORT_POPULAR }); | |||
| } | |||
| if (_des_popular !== null) { | |||
| queryArray.push({ key: KEY_SORT_POPULAR, value: `${_des_popular}` }); | |||
| queryArray.push({ key: KEY_SORT_DATE }); | |||
| } | |||
| if (shouldGoFirstPage) { | |||
| queryArray.push({ key: KEY_PAGE, value: "1" }); | |||
| } | |||
| queryStringHook.appendMultipleToQueryString(queryArray); | |||
| }; | |||
| const changeSorting = (payload) => { | |||
| setSelectedSortOption(payload, true); | |||
| }; | |||
| return { | |||
| selectedSortOption, | |||
| setSelectedSortOption, | |||
| sortOptions, | |||
| changeSorting, | |||
| }; | |||
| }; | |||
| export default useSorting; | |||
| @@ -96,7 +96,7 @@ export default { | |||
| PIBnoOfCharacters: "PIB mora imati 9 karaktera!", | |||
| welcome: "Dobro došli na trampu, želimo vam uspešno trampovanje!", | |||
| imageError: "Slika je obavezna!", | |||
| serverError: "Greška sa serverom!" | |||
| serverError: "Greška sa serverom!", | |||
| }, | |||
| forgotPassword: { | |||
| title: "Povrati lozinku", | |||
| @@ -160,6 +160,9 @@ export default { | |||
| "Podržani formati fotografija: <strong>.JPG</strong> | <strong>.JPEG</strong> | <strong>.PNG</strong>", | |||
| continue: "NASTAVI", | |||
| publish: "OBJAVI", | |||
| review: "Pregled", | |||
| changeOffer: "Izmena Objave", | |||
| newOffer: "Nova Objava", | |||
| }, | |||
| apiErrors: { | |||
| somethingWentWrong: "Greska sa serverom!", | |||
| @@ -195,7 +198,7 @@ export default { | |||
| miniChatHeaderTitle: "Moje Poruke", | |||
| send: "Pošalji", | |||
| sendPlaceholder: "Poruka...", | |||
| seeChats: "Pogledaj ćaskanje" | |||
| seeChats: "Pogledaj ćaskanje", | |||
| }, | |||
| editProfile: { | |||
| website: "Web Sajt*", | |||
| @@ -207,6 +210,7 @@ export default { | |||
| labelNameRequired: "Ime firme je obavezno!", | |||
| labelPIBRequired: "PIB firme je obavezan!", | |||
| labelLocationRequired: "Lokacija je obavezna!", | |||
| labelPhoneValid: "Unesite validan broj telefona", | |||
| labelPhoneRequired: "Broj telefona je obavezan!", | |||
| }, | |||
| deleteOffer: { | |||
| @@ -222,7 +226,7 @@ export default { | |||
| totalViews: " ukupnih pregleda", | |||
| successfulExchanges: " uspešnih trampi", | |||
| correctCommunications: " korektna komunikacija", | |||
| headerTitle: "Nazad na objave" | |||
| headerTitle: "Nazad na objave", | |||
| }, | |||
| notFound: { | |||
| error404: "Greška 404", | |||
| @@ -236,4 +240,13 @@ export default { | |||
| "Nažalost ne postoji ni jedna objava <br /> za unete kriterijume.", | |||
| showAllOffers: "Pogledaj sve objave", | |||
| }, | |||
| profile: { | |||
| myProfile: "Moj profil", | |||
| PIB: "PIB:", | |||
| publishes: " objava", | |||
| successExchange: " uspešna trampa", | |||
| numberOfViews: " ukupnih pregleda", | |||
| successComunication: " korektna komunikacija", | |||
| back: "Nazad na objave", | |||
| }, | |||
| }; | |||
| @@ -3,6 +3,7 @@ import { Trans, useTranslation } from "react-i18next"; | |||
| import { | |||
| Container, | |||
| ErrorContainer, | |||
| ErrorImageContainer, | |||
| ErrorHeading, | |||
| ErrorMessage, | |||
| Button, | |||
| @@ -28,17 +29,16 @@ const NotFoundPage = () => { | |||
| return ( | |||
| <Container> | |||
| <ErrorContainer> | |||
| <Error404 /> | |||
| <ErrorImageContainer> | |||
| <Error404 /> | |||
| </ErrorImageContainer> | |||
| <ErrorHeading>{t("notFound.error404")}</ErrorHeading> | |||
| <ErrorMessage> | |||
| <Trans i18nKey="notFound.errorMessage" /> | |||
| </ErrorMessage> | |||
| <Button | |||
| variant="contained" | |||
| width="190px" | |||
| height="49px" | |||
| buttoncolor={selectedTheme.primaryYellow} | |||
| textcolor="black" | |||
| onClick={showAllOffersHandler} | |||
| > | |||
| {t("notFound.showAllOffers")} | |||
| @@ -2,7 +2,6 @@ import { Typography } from "@mui/material"; | |||
| import { Box } from "@mui/system"; | |||
| import styled from "styled-components"; | |||
| import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton"; | |||
| // import Section from "../../components/Section/Section"; | |||
| import selectedTheme from "../../themes"; | |||
| export const Container = styled(Box)` | |||
| @@ -19,14 +18,24 @@ export const ErrorContainer = styled(Box)` | |||
| height: 100vh; | |||
| `; | |||
| export const ErrorImageContainer = styled(Box)` | |||
| @media screen and (max-width: 600px) { | |||
| width: 196px; | |||
| svg { | |||
| width: 196px; | |||
| height: 90px; | |||
| } | |||
| } | |||
| `; | |||
| export const ErrorHeading = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| font-size: 72px; | |||
| font-weight: 700; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| @media screen and (max-width: 420px) { | |||
| font-size: 62px; | |||
| @media screen and (max-width: 600px) { | |||
| font-size: 36px; | |||
| } | |||
| `; | |||
| @@ -36,8 +45,20 @@ export const ErrorMessage = styled(Typography)` | |||
| font-weight: 400; | |||
| color: #818181; | |||
| margin-bottom: 45px; | |||
| @media screen and (max-width: 600px) { | |||
| font-size: 14px; | |||
| } | |||
| `; | |||
| export const Button = styled(PrimaryButton)` | |||
| width: 190px; | |||
| height: 49px; | |||
| font-weight: 600; | |||
| color: #000; | |||
| @media screen and (max-width: 600px) { | |||
| width: 180px; | |||
| height: 44px; | |||
| } | |||
| `; | |||
| @@ -1,15 +1,50 @@ | |||
| import React from "react"; | |||
| import React, { useState } from "react"; | |||
| import { HomePageContainer } from "./HomePage.styled"; | |||
| import FilterCard from "../../components/Cards/FilterCard/FilterCard"; | |||
| import MainLayout from "../../layouts/MainLayout/MainLayout"; | |||
| import MarketPlace from "../../components/MarketPlace/MarketPlace"; | |||
| import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors"; | |||
| import { useSelector } from "react-redux"; | |||
| import { OFFERS_SCOPE } from "../../store/actions/offers/offersActionConstants"; | |||
| import useOffers from "../../hooks/useOffers/useOffers"; | |||
| import useSkeleton from "../../hooks/useSkeleton"; | |||
| const HomePage = () => { | |||
| const isLoadingOffers = useSelector( | |||
| selectIsLoadingByActionType(OFFERS_SCOPE) | |||
| ); | |||
| const [filtersOpened, setFiltersOpened] = useState(false); | |||
| const offers = useOffers(); | |||
| const { transitionStage } = useSkeleton({ | |||
| timeoutInterval: 900, | |||
| isLoadingIndicator: isLoadingOffers, | |||
| }); | |||
| const toggleFilters = () => { | |||
| setFiltersOpened((prevFiltersOpened) => !prevFiltersOpened); | |||
| }; | |||
| return ( | |||
| <HomePageContainer> | |||
| <MainLayout leftCard={<FilterCard />} content={<MarketPlace />} /> | |||
| <MainLayout | |||
| leftCard={ | |||
| <FilterCard | |||
| offers={offers} | |||
| filtersOpened={filtersOpened} | |||
| skeleton={isLoadingOffers} | |||
| animationStage={transitionStage} | |||
| toggleFilters={toggleFilters} | |||
| /> | |||
| } | |||
| content={ | |||
| <MarketPlace | |||
| offers={offers} | |||
| skeleton={isLoadingOffers} | |||
| animationStage={transitionStage} | |||
| toggleFilters={toggleFilters} | |||
| /> | |||
| } | |||
| /> | |||
| </HomePageContainer> | |||
| ); | |||
| } | |||
| }; | |||
| export default HomePage; | |||
| @@ -1,22 +1,24 @@ | |||
| import React, { useEffect } from "react"; | |||
| import { PropTypes } from "prop-types"; | |||
| import { ItemDetailsPageContainer } from "./ItemDetailsPage.styled"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import ItemDetails from "../../components/ItemDetails/ItemDetails"; | |||
| import ItemDetailsLayout from "../../layouts/ItemDetailsLayout/ItemDetailsLayout"; | |||
| import { fetchOneOffer } from "../../store/actions/offers/offersActions"; | |||
| import UserReviews from "../../components/UserReviews/UserReviews"; | |||
| import { selectOffer } from "../../store/selectors/offersSelectors"; | |||
| const ItemDetailsPage = (props) => { | |||
| const dispatch = useDispatch(); | |||
| const selectedOffer = useSelector(selectOffer); | |||
| const offerId = props.match.params.idProizvod; | |||
| useEffect(() => { | |||
| if (offerId) { | |||
| if (offerId && !selectedOffer?.offer) { | |||
| dispatch(fetchOneOffer(offerId)); | |||
| } | |||
| }, [offerId]); | |||
| }, [offerId, selectedOffer?.offer]); | |||
| return ( | |||
| <ItemDetailsPageContainer> | |||
| @@ -1,16 +1,51 @@ | |||
| import React from "react"; | |||
| import React, { useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { MyOffersContainer } from "./MyOffers.styled"; | |||
| import MainLayout from "../../layouts/MainLayout/MainLayout"; | |||
| import FilterCard from "../../components/Cards/FilterCard/FilterCard"; | |||
| import MarketPlace from "../../components/MarketPlace/MarketPlace"; | |||
| import useMyOffers from "../../hooks/useOffers/useMyOffers"; | |||
| import useSkeleton from "../../hooks/useSkeleton"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors"; | |||
| import { OFFERS_MINE_SCOPE } from "../../store/actions/offers/offersActionConstants"; | |||
| const MyOffers = () => { | |||
| const offers = useMyOffers(); | |||
| const isLoadingMineOffers = useSelector( | |||
| selectIsLoadingByActionType(OFFERS_MINE_SCOPE) | |||
| ); | |||
| const [filtersOpened, setFiltersOpened] = useState(false); | |||
| const { transitionStage } = useSkeleton({ | |||
| timeoutInterval: 900, | |||
| isLoadingIndicator: isLoadingMineOffers, | |||
| }); | |||
| const toggleFilters = () => { | |||
| setFiltersOpened((prevFiltersOpened) => !prevFiltersOpened); | |||
| }; | |||
| return ( | |||
| <MyOffersContainer> | |||
| <MainLayout | |||
| leftCard={<FilterCard myOffers />} | |||
| content={<MarketPlace myOffers={true} />} | |||
| leftCard={ | |||
| <FilterCard | |||
| myOffers | |||
| offers={offers} | |||
| animationStage={transitionStage} | |||
| filtersOpened={filtersOpened} | |||
| toggleFilters={toggleFilters} | |||
| skeleton={isLoadingMineOffers} | |||
| /> | |||
| } | |||
| content={ | |||
| <MarketPlace | |||
| myOffers={true} | |||
| offers={offers} | |||
| animationStage={transitionStage} | |||
| skeleton={isLoadingMineOffers} | |||
| toggleFilters={toggleFilters} | |||
| /> | |||
| } | |||
| /> | |||
| </MyOffersContainer> | |||
| ); | |||
| @@ -1,4 +1,5 @@ | |||
| import { createLoadingType } from '../actionHelpers'; | |||
| export const APP_LOADING = createLoadingType('APP_LOADING'); | |||
| export const UPDATE_LOADER = createLoadingType("UPDATE_LOADER") | |||
| export const ADD_LOADER = createLoadingType("ADD_LOADER"); | |||
| export const REMOVE_LOADER = createLoadingType("REMOVE_LOADER"); | |||
| @@ -1,6 +1,10 @@ | |||
| import { APP_LOADING } from './appActionConstants'; | |||
| import { ADD_LOADER, REMOVE_LOADER } from "./appActionConstants"; | |||
| export const setAppReady = (payload) => ({ | |||
| type: APP_LOADING, | |||
| payload: payload | |||
| export const addLoader = (payload) => ({ | |||
| type: ADD_LOADER, | |||
| payload | |||
| }); | |||
| export const removeLoader = (payload) => ({ | |||
| type: REMOVE_LOADER, | |||
| payload | |||
| }) | |||
| @@ -1,6 +1,8 @@ | |||
| import { createFetchType } from "../actionHelpers"; | |||
| import { createErrorType, createFetchType, createSetType, createSuccessType } from "../actionHelpers"; | |||
| const CATEGORIES_SCOPE = "CATEGORIES"; | |||
| export const CATEGORIES_FETCH = createFetchType(CATEGORIES_SCOPE); | |||
| export const CATEGORIES_FETCH_SUCCESS = createSuccessType(CATEGORIES_SCOPE); | |||
| export const CATEGORIES_FETCH_ERROR = createErrorType(CATEGORIES_SCOPE); | |||
| export const CATEGORIES_SET = "CATEGORIES_SET"; | |||
| export const CATEGORIES_SET = createSetType("CATEGORIES_SET"); | |||
| @@ -1,4 +1,4 @@ | |||
| import { CATEGORIES_FETCH, CATEGORIES_SET } from "./categoriesActionConstants"; | |||
| import { CATEGORIES_FETCH, CATEGORIES_FETCH_ERROR, CATEGORIES_FETCH_SUCCESS, CATEGORIES_SET } from "./categoriesActionConstants"; | |||
| export const fetchCategories = () => ({ | |||
| type: CATEGORIES_FETCH | |||
| @@ -7,4 +7,10 @@ export const fetchCategories = () => ({ | |||
| export const setCategories = (payload) => ({ | |||
| type: CATEGORIES_SET, | |||
| payload | |||
| }) | |||
| export const fetchCategoriesSuccess = () => ({ | |||
| type: CATEGORIES_FETCH_SUCCESS | |||
| }) | |||
| export const fetchCategoriesError = () => ({ | |||
| type: CATEGORIES_FETCH_ERROR | |||
| }) | |||