| @@ -4912,6 +4912,11 @@ | |||
| } | |||
| } | |||
| }, | |||
| "classnames": { | |||
| "version": "2.3.1", | |||
| "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", | |||
| "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" | |||
| }, | |||
| "clean-css": { | |||
| "version": "4.2.3", | |||
| "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", | |||
| @@ -5744,6 +5749,11 @@ | |||
| "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", | |||
| "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" | |||
| }, | |||
| "debounce": { | |||
| "version": "1.2.1", | |||
| "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", | |||
| "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" | |||
| }, | |||
| "debug": { | |||
| "version": "4.3.1", | |||
| "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", | |||
| @@ -6252,6 +6262,11 @@ | |||
| } | |||
| } | |||
| }, | |||
| "easy-bem": { | |||
| "version": "1.1.1", | |||
| "resolved": "https://registry.npmjs.org/easy-bem/-/easy-bem-1.1.1.tgz", | |||
| "integrity": "sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==" | |||
| }, | |||
| "ecdsa-sig-formatter": { | |||
| "version": "1.0.11", | |||
| "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", | |||
| @@ -14219,6 +14234,16 @@ | |||
| } | |||
| } | |||
| }, | |||
| "react-indiana-drag-scroll": { | |||
| "version": "2.2.0", | |||
| "resolved": "https://registry.npmjs.org/react-indiana-drag-scroll/-/react-indiana-drag-scroll-2.2.0.tgz", | |||
| "integrity": "sha512-+W/3B2OQV0FrbdnsoIo4dww/xpH0MUQJz6ziQb7H+oBko3OCbXuzDFYnho6v6yhGrYDNWYPuFUewb89IONEl/A==", | |||
| "requires": { | |||
| "classnames": "^2.2.6", | |||
| "debounce": "^1.2.0", | |||
| "easy-bem": "^1.1.1" | |||
| } | |||
| }, | |||
| "react-input-autosize": { | |||
| "version": "3.0.0", | |||
| "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", | |||
| @@ -29,6 +29,7 @@ | |||
| "react-dom": "^17.0.2", | |||
| "react-helmet-async": "^1.0.9", | |||
| "react-i18next": "^11.10.0", | |||
| "react-indiana-drag-scroll": "^2.2.0", | |||
| "react-redux": "^7.2.4", | |||
| "react-router-dom": "^5.2.0", | |||
| "react-scripts": "4.0.3", | |||
| @@ -9,19 +9,21 @@ import { StyledEngineProvider } from "@mui/material"; | |||
| const App = () => { | |||
| return ( | |||
| <> | |||
| <Router history={history}> | |||
| <Helmet> | |||
| <title>{i18next.t("app.title")}</title> | |||
| </Helmet> | |||
| {/* <main className="l-page"> */} | |||
| <> | |||
| <Router history={history}> | |||
| <Helmet> | |||
| <title>{i18next.t("app.title")}</title> | |||
| </Helmet> | |||
| {/* <main className="l-page"> */} | |||
| <StyledEngineProvider injectFirst> | |||
| <GlobalStyle /> | |||
| <AppRoutes /> | |||
| </StyledEngineProvider> | |||
| {/* </main> */} | |||
| </Router> | |||
| </> | |||
| )}; | |||
| {/* </main> */} | |||
| </Router> | |||
| </> | |||
| ); | |||
| }; | |||
| export default App; | |||
| @@ -15,11 +15,11 @@ import { | |||
| CREATE_OFFER_PAGE, | |||
| ITEM_DETAILS_PAGE | |||
| } from './constants/pages'; | |||
| import LoginPage from './pages/LoginPage/LoginPageMUI'; | |||
| 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/ForgotPasswordPageMUI'; | |||
| 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'; | |||
| @@ -0,0 +1,4 @@ | |||
| <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M3.75 9L14.25 9" stroke="#5A3984" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M9 3.75L14.25 9L9 14.25" stroke="#5A3984" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/> | |||
| </svg> | |||
| @@ -0,0 +1,7 @@ | |||
| <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M9 4H9.5C10.0304 4 10.5391 4.21071 10.9142 4.58579C11.2893 4.96086 11.5 5.46957 11.5 6C11.5 6.53043 11.2893 7.03914 10.9142 7.41421C10.5391 7.78929 10.0304 8 9.5 8H9" stroke="#FEB005" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M1 4H9V8.5C9 9.03043 8.78929 9.53914 8.41421 9.91421C8.03914 10.2893 7.53043 10.5 7 10.5H3C2.46957 10.5 1.96086 10.2893 1.58579 9.91421C1.21071 9.53914 1 9.03043 1 8.5V4Z" stroke="#FEB005" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M3 0.5V2" stroke="#FEB005" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M5 0.5V2" stroke="#FEB005" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M7 0.5V2" stroke="#FEB005" stroke-linecap="round" stroke-linejoin="round"/> | |||
| </svg> | |||
| @@ -0,0 +1,4 @@ | |||
| <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M1.5 4.5L6 1L10.5 4.5V10C10.5 10.2652 10.3946 10.5196 10.2071 10.7071C10.0196 10.8946 9.76522 11 9.5 11H2.5C2.23478 11 1.98043 10.8946 1.79289 10.7071C1.60536 10.5196 1.5 10.2652 1.5 10V4.5Z" stroke="#5A3984" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M4.5 11V6H7.5V11" stroke="#5A3984" stroke-linecap="round" stroke-linejoin="round"/> | |||
| </svg> | |||
| @@ -0,0 +1,6 @@ | |||
| <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M8 1.5H0.5V8H8V1.5Z" stroke="#5A3984" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M8 4H10L11.5 5.5V8H8V4Z" stroke="#5A3984" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M2.75 10.5C3.44036 10.5 4 9.94036 4 9.25C4 8.55964 3.44036 8 2.75 8C2.05964 8 1.5 8.55964 1.5 9.25C1.5 9.94036 2.05964 10.5 2.75 10.5Z" stroke="#5A3984" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M9.25 10.5C9.94036 10.5 10.5 9.94036 10.5 9.25C10.5 8.55964 9.94036 8 9.25 8C8.55964 8 8 8.55964 8 9.25C8 9.94036 8.55964 10.5 9.25 10.5Z" stroke="#5A3984" stroke-linecap="round" stroke-linejoin="round"/> | |||
| </svg> | |||
| @@ -6,9 +6,7 @@ import { useDispatch, useSelector } from "react-redux"; | |||
| import { NavLink } from "react-router-dom"; | |||
| import * as Yup from "yup"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| fetchUser, | |||
| } from "../../../store/actions/login/loginActions"; | |||
| import { fetchUser } 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"; | |||
| @@ -25,15 +23,20 @@ import { | |||
| CreateOfferFormContainer, | |||
| RegisterAltText, | |||
| RegisterTextContainer, | |||
| FieldLabel, | |||
| } 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"; | |||
| const CreateOffer = ({ history }) => { | |||
| 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); | |||
| @@ -72,101 +75,21 @@ const CreateOffer = ({ history }) => { | |||
| ); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| nameOfProduct: "", | |||
| description: "", | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| nameOfProduct: Yup.string().required(t("login.nameOfProductRequired")), | |||
| description: Yup.string().required(t("login.descriptionRequired")).min(8), | |||
| }), | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| const handleNext = (values) => { | |||
| setInformations({...values}); | |||
| setCurrentStep(prevState => prevState+1) | |||
| } | |||
| return ( | |||
| <CreateOfferContainer> | |||
| <CreateOfferTitle component="h1" variant="h5"> | |||
| Nova Objava | |||
| </CreateOfferTitle> | |||
| <StepProgress current={1} numberOfSteps={3}/> | |||
| <CreateOfferFormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| {/* <Backdrop position="absolute" isLoading={isLoading} /> */} | |||
| <TextField | |||
| name="nameOfProduct" | |||
| placeholder={"Naziv proizvoda..."} | |||
| italicPlaceholder | |||
| margin="normal" | |||
| value={formik.values.nameOfProduct} | |||
| onChange={formik.handleChange} | |||
| error={(formik.touched.nameOfProduct && formik.errors.nameOfProduct)} | |||
| helperText={formik.touched.nameOfProduct && formik.errors.nameOfProduct} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="description" | |||
| placeholder={"Opis..."} | |||
| margin="normal" | |||
| italicPlaceholder | |||
| value={formik.values.description} | |||
| onChange={formik.handleChange} | |||
| error={(formik.touched.description && formik.errors.description)} | |||
| helperText={formik.touched.description && formik.errors.description} | |||
| fullWidth | |||
| multiline | |||
| minRows={4} | |||
| height={"100px"} | |||
| /> | |||
| <Link | |||
| to={FORGOT_PASSWORD_PAGE} | |||
| textsize="12px" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="right" | |||
| style={{ marginTop: "18px", marginBottom: "18px" }} | |||
| > | |||
| {t("login.forgotYourPassword")} | |||
| </Link> | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| // disabled={ | |||
| // formik.values.username.length === 0 || | |||
| // formik.values.password.length === 0 | |||
| // } | |||
| > | |||
| {t("login.logIn")} | |||
| </PrimaryButton> | |||
| <RegisterTextContainer> | |||
| <RegisterAltText> | |||
| {t("login.dontHaveAccount").padEnd(2, " ")} | |||
| </RegisterAltText> | |||
| <Link | |||
| to="/register" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="center" | |||
| > | |||
| {t("login.signUp")} | |||
| </Link> | |||
| </RegisterTextContainer> | |||
| </CreateOfferFormContainer> | |||
| <StepProgress current={currentStep} numberOfSteps={3} /> | |||
| {currentStep === 1 && <FirstPartCreateOffer handleNext={handleNext}/>} | |||
| {currentStep === 2 && <SecondPartCreateOffer handleNext={handleNext} />} | |||
| </CreateOfferContainer> | |||
| ); | |||
| }; | |||
| @@ -1,6 +1,8 @@ | |||
| import { Box, Container, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { Label } from "../../CheckBox/Label"; | |||
| import Select from "../../Select/Select"; | |||
| export const CreateOfferContainer = styled(Container)` | |||
| margin-top: 0px; | |||
| @@ -40,6 +42,7 @@ export const CreateOfferDescription = styled(Typography)` | |||
| export const CreateOfferFormContainer = styled(Box)` | |||
| width: 335px; | |||
| height: 700px; | |||
| padding-top: 20px; | |||
| `; | |||
| export const RegisterAltText = styled(Typography)` | |||
| font-family: "Poppins"; | |||
| @@ -53,4 +56,21 @@ export const RegisterTextContainer = styled(Box)` | |||
| flex-direction: row; | |||
| margin-top: 36px; | |||
| justify-content: center; | |||
| ` | |||
| export const FieldLabel = styled(Label)` | |||
| position: relative; | |||
| bottom: -14px; | |||
| & label { | |||
| font-size: 12px; | |||
| font-weight: 600; | |||
| line-height: 20px; | |||
| color: ${selectedTheme.primaryGrayText}; | |||
| cursor: auto; | |||
| letter-spacing: 0.2px; | |||
| } | |||
| ` | |||
| export const SelectField = styled(Select)` | |||
| position: relative; | |||
| top: 15px; | |||
| margin-bottom: 18px; | |||
| ` | |||
| @@ -0,0 +1,119 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useFormik } from "formik"; | |||
| import { | |||
| CreateOfferFormContainer, | |||
| DescriptionField, | |||
| FieldLabel, | |||
| NextButton, | |||
| TitleField, | |||
| } from "./FirstPartCreateOffer.styled"; | |||
| // import { TextField } from "../../../TextFields/TextField/TextField"; | |||
| // import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import * as Yup from "yup"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import Option from "../../../Select/Option/Option"; | |||
| import { SelectField } from "../CreateOffer.styled"; | |||
| const FirstPartCreateOffer = (props) => { | |||
| const { t } = useTranslation(); | |||
| const handleSubmit = (values) => { | |||
| console.log(values); | |||
| props.handleNext(values); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| nameOfProduct: "", | |||
| description: "", | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| nameOfProduct: Yup.string().required(t("login.nameOfProductRequired")), | |||
| description: Yup.string().required(t("login.descriptionRequired")).min(8), | |||
| }), | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <CreateOfferFormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| {/* <Backdrop position="absolute" isLoading={isLoading} /> */} | |||
| <FieldLabel leftText={"NASLOV"} /> | |||
| <TitleField | |||
| name="nameOfProduct" | |||
| placeholder={"Naziv proizvoda..."} | |||
| italicPlaceholder | |||
| margin="normal" | |||
| value={formik.values.nameOfProduct} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.nameOfProduct && formik.errors.nameOfProduct} | |||
| helperText={formik.touched.nameOfProduct && formik.errors.nameOfProduct} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <FieldLabel leftText={"OPIS PROIZVODA"} /> | |||
| <DescriptionField | |||
| name="description" | |||
| placeholder={"Opis..."} | |||
| margin="normal" | |||
| italicPlaceholder | |||
| value={formik.values.description} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.description && formik.errors.description} | |||
| helperText={formik.touched.description && formik.errors.description} | |||
| fullWidth | |||
| multiline | |||
| minRows={4} | |||
| height={"100px"} | |||
| /> | |||
| <FieldLabel leftText={"LOKACIJA"} /> | |||
| <SelectField defaultValue={1}> | |||
| <Option value={1}>Opcija 1</Option> | |||
| <Option value={2}>Opcija 2</Option> | |||
| <Option value={3}>Opcija 3</Option> | |||
| <Option value={4}>Opcija 4</Option> | |||
| </SelectField> | |||
| <FieldLabel leftText={"KATEGORIJA"} /> | |||
| <SelectField defaultValue={1}> | |||
| <Option value={1}>Opcija 1</Option> | |||
| <Option value={2}>Opcija 2</Option> | |||
| <Option value={3}>Opcija 3</Option> | |||
| <Option value={4}>Opcija 4</Option> | |||
| </SelectField> | |||
| <FieldLabel leftText={"PODKATEGORIJA"} /> | |||
| <SelectField defaultValue={1}> | |||
| <Option value={1}>Opcija 1</Option> | |||
| <Option value={2}>Opcija 2</Option> | |||
| <Option value={3}>Opcija 3</Option> | |||
| <Option value={4}>Opcija 4</Option> | |||
| </SelectField> | |||
| <NextButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| // disabled={ | |||
| // formik.values.username.length === 0 || | |||
| // formik.values.password.length === 0 | |||
| // } | |||
| > | |||
| NASTAVI | |||
| </NextButton> | |||
| </CreateOfferFormContainer> | |||
| ); | |||
| }; | |||
| FirstPartCreateOffer.propTypes = { | |||
| children: PropTypes.any, | |||
| handleNext: PropTypes.func, | |||
| }; | |||
| export default FirstPartCreateOffer; | |||
| @@ -0,0 +1,62 @@ | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { Label } from "../../../CheckBox/Label"; | |||
| import { TextField } from "../../../TextFields/TextField/TextField"; | |||
| export const CreateOfferTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| width: 328px; | |||
| height: 33px; | |||
| text-align: center; | |||
| flex: 1; | |||
| font-style: normal; | |||
| font-weight: 700; | |||
| font-size: 24px; | |||
| line-height: 33px; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| margin-top: 36px; | |||
| margin-bottom: 40px; | |||
| position: relative; | |||
| `; | |||
| export const CreateOfferDescription = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| margin-top: 9px; | |||
| width: 221px; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 22px; | |||
| display: flex; | |||
| align-items: center; | |||
| text-align: center; | |||
| color: ${selectedTheme.primaryGrayText}; | |||
| margin-bottom: 18px; | |||
| `; | |||
| export const CreateOfferFormContainer = styled(Box)` | |||
| width: 335px; | |||
| height: 700px; | |||
| padding-top: 20px; | |||
| `; | |||
| export const FieldLabel = styled(Label)` | |||
| position: relative; | |||
| bottom: -14px; | |||
| & label { | |||
| font-size: 12px; | |||
| font-weight: 600; | |||
| line-height: 20px; | |||
| color: ${selectedTheme.primaryGrayText}; | |||
| cursor: auto; | |||
| letter-spacing: 0.2px; | |||
| } | |||
| ` | |||
| export const DescriptionField = styled(TextField)` | |||
| margin-bottom: 4px; | |||
| ` | |||
| export const TitleField = styled(TextField)` | |||
| ` | |||
| export const NextButton = styled(PrimaryButton)` | |||
| margin-top: 16px; | |||
| ` | |||
| @@ -0,0 +1,14 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { CreateOfferFormContainer } from "./SecondPartCreateOffer.styled"; | |||
| const SecondPartCreateOffer = () => { | |||
| return <CreateOfferFormContainer>Aaaa</CreateOfferFormContainer>; | |||
| }; | |||
| SecondPartCreateOffer.propTypes = { | |||
| children: PropTypes.node, | |||
| handleOffer: PropTypes.func, | |||
| }; | |||
| export default SecondPartCreateOffer; | |||
| @@ -0,0 +1,8 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const CreateOfferFormContainer = styled(Box)` | |||
| width: 335px; | |||
| height: 700px; | |||
| padding-top: 20px; | |||
| `; | |||
| @@ -3,19 +3,29 @@ import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| export const FilterCardContainer = styled(Box)` | |||
| position: fixed; | |||
| border-radius: 0; | |||
| border-top-right-radius: 4px; | |||
| margin-top: -24px; | |||
| height: calc(100% - 90px); | |||
| padding: 36px; | |||
| background-color: white; | |||
| width: calc(100% / 12 * 2.4); | |||
| position: fixed; | |||
| left: 0; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| background-color: white; | |||
| min-width: fit-content; | |||
| z-index: 9; | |||
| margin-top: -24px; | |||
| transition: all ease-in-out .36s; | |||
| @media (max-width: 900px) { | |||
| margin-left: -400px; | |||
| transition: all ease-in-out .36s; | |||
| } | |||
| @media (max-width: 600px) { | |||
| margin-top: -14px; | |||
| } | |||
| `; | |||
| export const Title = styled(Typography)` | |||
| font-size: 24px; | |||
| @@ -34,7 +44,6 @@ export const Header = styled(Box)` | |||
| `; | |||
| export const Footer = styled(Box)` | |||
| position: "sticky"; | |||
| bottom: 0; | |||
| & div button { | |||
| @@ -62,5 +71,4 @@ export const ContentContainer = styled(Box)` | |||
| scrollbar-width: thin; | |||
| scrollbar-color: #ddd; | |||
| ${() => window.scrollbars.visible && `padding-right: 15px;`} | |||
| ` | |||
| `; | |||
| @@ -19,6 +19,7 @@ import { CheckBox } from "../../../../CheckBox/CheckBox"; | |||
| const FilterCheckboxDropdown = (props) => { | |||
| const [toSearch, setToSearch] = useState(""); | |||
| const [dataToShow, setDataToShow] = useState([]); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const { data } = props; | |||
| useEffect(() => { | |||
| @@ -77,8 +78,9 @@ const FilterCheckboxDropdown = (props) => { | |||
| toggleIconClosed={<DropdownDown />} | |||
| toggleIconOpened={<DropdownUp />} | |||
| fullWidth | |||
| setIsOpened={setIsOpened} | |||
| toggleIconStyles={{ | |||
| backgroundColor: selectedTheme.primaryIconBackgroundColor, | |||
| backgroundColor: isOpened ? "white" : selectedTheme.primaryIconBackgroundColor, | |||
| }} | |||
| headerOptions={ | |||
| <React.Fragment> | |||
| @@ -14,6 +14,7 @@ import RadioGroup from "../../../../Radio/Group/RadioGroup"; | |||
| const FilterRadioDropdown = (props) => { | |||
| const [toSearch, setToSearch] = useState(""); | |||
| const [dataToShow, setDataToShow] = useState([]); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const { data } = props; | |||
| useEffect(() => { | |||
| @@ -51,8 +52,9 @@ const FilterRadioDropdown = (props) => { | |||
| toggleIconClosed={<DropdownDown />} | |||
| toggleIconOpened={<DropdownUp />} | |||
| fullWidth | |||
| setIsOpened={setIsOpened} | |||
| toggleIconStyles={{ | |||
| backgroundColor: selectedTheme.primaryIconBackgroundColor, | |||
| backgroundColor: isOpened ? "white" : selectedTheme.primaryIconBackgroundColor, | |||
| }} | |||
| headerOptions={ | |||
| <React.Fragment> | |||
| @@ -35,32 +35,32 @@ const OfferCard = (props) => { | |||
| history.push(`/proizvodi/${id}`) | |||
| } | |||
| return ( | |||
| <OfferCardContainer sponsored={props.sponsored.toString()} halfwidth={props.halfwidth ? 1 : 0}> | |||
| <OfferImage>{props.image}</OfferImage> | |||
| <OfferCardContainer sponsored={props.offer.pinned.toString()} halfwidth={props.halfwidth ? 1 : 0}> | |||
| <OfferImage src={props.offer.images[0]}></OfferImage> | |||
| <OfferInfo> | |||
| <OfferTitle>{props.title}</OfferTitle> | |||
| <OfferTitle>{props.offer.name}</OfferTitle> | |||
| <OfferAuthor> | |||
| <OfferAuthorName>{props.author}</OfferAuthorName> | |||
| <OfferLocation>{props.location}</OfferLocation> | |||
| <OfferAuthorName>{props.offer.description}</OfferAuthorName> | |||
| <OfferLocation>{props.offer.location.city}</OfferLocation> | |||
| </OfferAuthor> | |||
| <OfferDetails> | |||
| <OfferCategory> | |||
| <DetailIcon color="black" component="span" size="16px"> | |||
| <Category width={"14px"} /> | |||
| </DetailIcon> | |||
| <DetailText>{props.category}</DetailText> | |||
| <DetailText>{props.offer.category.name}</DetailText> | |||
| </OfferCategory> | |||
| <OfferPackage> | |||
| <DetailIcon color="black" component="span" size="16px"> | |||
| <Quantity width={"12px"} height={"12px"} /> | |||
| </DetailIcon> | |||
| <DetailText>{props.quantity} pakovanja</DetailText> | |||
| <DetailText>{props.offer.quantity} pakovanja</DetailText> | |||
| </OfferPackage> | |||
| <OfferViews> | |||
| <DetailIcon color="black" component="span" size="16px"> | |||
| <Eye width={"12px"} height={"11px"} /> | |||
| </DetailIcon> | |||
| <DetailText>{props.numberOfViews} pregleda</DetailText> | |||
| <DetailText>{props.offer.views.viewers.length} pregleda</DetailText> | |||
| </OfferViews> | |||
| </OfferDetails> | |||
| </OfferInfo> | |||
| @@ -69,7 +69,7 @@ const OfferCard = (props) => { | |||
| <Line/> | |||
| <OfferDescription> | |||
| <OfferDescriptionTitle>Opis:</OfferDescriptionTitle> | |||
| <OfferDescriptionText>{props.description}</OfferDescriptionText> | |||
| <OfferDescriptionText>{props.offer.description}</OfferDescriptionText> | |||
| </OfferDescription> | |||
| <CheckButton | |||
| @@ -117,6 +117,7 @@ OfferCard.propTypes = { | |||
| numberOfViews: PropTypes.number, | |||
| halfwidth: PropTypes.bool, | |||
| sponsored: PropTypes.bool, | |||
| offer: PropTypes.any, | |||
| }; | |||
| OfferCard.defaultProps = { | |||
| halfwidth: false, | |||
| @@ -22,7 +22,7 @@ export const OfferCardContainer = styled(Container)` | |||
| height: 180px; | |||
| position: relative; | |||
| `; | |||
| export const OfferImage = styled(Box)``; | |||
| export const OfferImage = styled.img``; | |||
| export const OfferInfo = styled(Box)` | |||
| display: flex; | |||
| flex: 2; | |||
| @@ -58,28 +58,32 @@ export const OfferDetails = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: ${(props) => (!props.halfwidth ? "no-wrap" : "wrap")}; | |||
| justify-content: space-between; | |||
| justify-content: start; | |||
| gap: 1rem; | |||
| `; | |||
| export const OfferCategory = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 12px; | |||
| width: 33%; | |||
| @media (max-width: 1000px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const OfferPackage = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 12px; | |||
| width: 34%; | |||
| `; | |||
| export const OfferViews = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 12px; | |||
| width: 34%; | |||
| @media (max-width: 1200px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const OfferDescriptionTitle = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| @@ -98,17 +102,26 @@ export const OfferDescriptionText = styled(Box)` | |||
| display: -webkit-box; | |||
| -webkit-line-clamp: 5; | |||
| -webkit-box-orient: vertical; | |||
| @media (max-width: 1500px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const OfferDescription = styled(Box)` | |||
| flex: 3; | |||
| margin: auto 0; | |||
| padding-left: 35px; | |||
| @media (max-width: 1500px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const Line = styled(Box)` | |||
| border-left: 1px solid rgba(0, 0, 0, 0.15); | |||
| height: 100px; | |||
| width: 0; | |||
| margin: auto 0; | |||
| @media (max-width: 1500px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const DetailIcon = styled(Icon)` | |||
| & svg { | |||
| @@ -136,6 +149,9 @@ export const CheckButton = styled(PrimaryButton)` | |||
| background-color: ${selectedTheme.primaryPurple} !important; | |||
| color: white !important; | |||
| } | |||
| @media (max-width: 650px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const MessageIcon = styled(IconButton)` | |||
| width: 40px; | |||
| @@ -4,8 +4,13 @@ import PropTypes from "prop-types"; | |||
| export const Label = (props) => { | |||
| return ( | |||
| <LabelContainer onClick={props.onClick} maxWidth={props.maxWidth}> | |||
| <LeftLabel>{props.leftText}</LeftLabel> | |||
| <LabelContainer | |||
| onClick={props.onClick} | |||
| maxWidth={props.maxWidth} | |||
| style={props.containerStyle} | |||
| className={props.className} | |||
| > | |||
| <LeftLabel style={props.leftTextStyle}>{props.leftText}</LeftLabel> | |||
| {props.rightText && <RightLabel>{props.rightText}</RightLabel>} | |||
| </LabelContainer> | |||
| ); | |||
| @@ -16,4 +21,8 @@ Label.propTypes = { | |||
| leftText: PropTypes.string, | |||
| rightText: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), | |||
| maxWidth: PropTypes.string, | |||
| leftTextStyle: PropTypes.any, | |||
| rightTextStyle: PropTypes.any, | |||
| containerStyle: PropTypes.any, | |||
| className: PropTypes.any, | |||
| }; | |||
| @@ -14,31 +14,34 @@ import PropTypes from "prop-types"; | |||
| const DropdownList = (props) => { | |||
| const [listShown, setListShown] = useState(props.defaultOpen); | |||
| const handleShow = () => { | |||
| if (props.setIsOpened) { | |||
| props.setIsOpened(!listShown); | |||
| } | |||
| setListShown((prevState) => !prevState); | |||
| }; | |||
| return ( | |||
| <DropdownListContainer fullwidth={props.fullWidth ? 1 : 0}> | |||
| <DropdownHeader> | |||
| {props.dropdownIcon && ( | |||
| <DropdownIcon onClick={() => setListShown((prevState) => !prevState)}> | |||
| <DropdownIcon onClick={() => handleShow()}> | |||
| {props.dropdownIcon} | |||
| </DropdownIcon> | |||
| )} | |||
| <DropdownTitle | |||
| onClick={() => setListShown((prevState) => !prevState)} | |||
| textcolor={props.textcolor} | |||
| > | |||
| <DropdownTitle onClick={() => handleShow()} textcolor={props.textcolor}> | |||
| {props.title} | |||
| </DropdownTitle> | |||
| {listShown ? ( | |||
| <ToggleIconOpened | |||
| style={props.toggleIconStyles} | |||
| onClick={() => setListShown((prevState) => !prevState)} | |||
| onClick={() => handleShow()} | |||
| > | |||
| {props.toggleIconOpened} | |||
| </ToggleIconOpened> | |||
| ) : ( | |||
| <ToggleIconClosed | |||
| style={props.toggleIconStyles} | |||
| onClick={() => setListShown((prevState) => !prevState)} | |||
| onClick={() => handleShow()} | |||
| > | |||
| {props.toggleIconClosed} | |||
| </ToggleIconClosed> | |||
| @@ -65,6 +68,7 @@ DropdownList.propTypes = { | |||
| toggleIconStyles: PropTypes.any, | |||
| headerOptions: PropTypes.node, | |||
| textcolor: PropTypes.string, | |||
| setIsOpened: PropTypes.func, | |||
| }; | |||
| DropdownList.defaultProps = { | |||
| @@ -18,6 +18,7 @@ export const DropdownTitle = styled(Typography)` | |||
| font-weight: 400; | |||
| padding-bottom: 10px; | |||
| padding-top: 5px; | |||
| padding-right: 0.9rem; | |||
| font-family: "Open Sans"; | |||
| color: ${props => props.textcolor ? props.textcolor : selectedTheme.primaryText}; | |||
| `; | |||
| @@ -25,11 +26,17 @@ export const DropdownTitle = styled(Typography)` | |||
| export const ToggleIconOpened = styled(IconButton)` | |||
| cursor: pointer; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| & span { | |||
| ${props => props.backgroundColor ? `background-color: ${props.backgroundColor}` : ``} | |||
| } | |||
| `; | |||
| export const ToggleIconClosed = styled(IconButton)` | |||
| cursor: pointer; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| & span { | |||
| ${props => props.backgroundColor ? `background-color: ${props.backgroundColor}` : ``} | |||
| } | |||
| `; | |||
| export const DropdownIcon = styled(IconButton)` | |||
| @@ -12,11 +12,12 @@ import { | |||
| import { ReactComponent as GridSquare } from "../../../assets/images/svg/offer-grid-square.svg"; | |||
| import { ReactComponent as GridLine } from "../../../assets/images/svg/offer-grid-line.svg"; | |||
| import { ReactComponent as Down } from "../../../assets/images/svg/down-arrow.svg"; | |||
| import { MenuItem } from "@mui/material"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectFilters } from "../../../store/selectors/filtersSelectors"; | |||
| import Mockupdata from "../../Cards/FilterCard/Mockupdata"; | |||
| import Option from "../../Select/Option/Option"; | |||
| import { useHistory, useLocation, useRouteMatch } from "react-router-dom"; | |||
| const DownArrow = (props) => ( | |||
| <IconStyled {...props}> | |||
| @@ -31,11 +32,15 @@ const MockupdataForSelect = [ | |||
| }, | |||
| { | |||
| id: 1, | |||
| string: "Ceni rastuce", | |||
| string: "Najpopularnije", | |||
| }, | |||
| { | |||
| id: 2, | |||
| string: "Ceni opadajuce", | |||
| string: "Najnovije", | |||
| }, | |||
| { | |||
| id: 3, | |||
| string: "Najstarije", | |||
| }, | |||
| ]; | |||
| @@ -43,6 +48,9 @@ const Header = (props) => { | |||
| const [categoryString, setCategoryString] = useState(""); | |||
| const [filtersString, setFiltersString] = useState(""); | |||
| const { category, cities } = useSelector(selectFilters); | |||
| const history = useHistory(); | |||
| const location = useLocation(); | |||
| const routeMatch = useRouteMatch(); | |||
| useEffect(() => { | |||
| let categorystring = ""; | |||
| @@ -67,6 +75,15 @@ const Header = (props) => { | |||
| setCategoryString(categorystring); | |||
| setFiltersString(filtersstring); | |||
| }, [category, cities]); | |||
| const handleChangeSelect = (value) => { | |||
| let chosenOption = MockupdataForSelect.find(item => item.id === value); | |||
| console.log(location); | |||
| console.log(history); | |||
| console.log(chosenOption); | |||
| console.log(routeMatch) | |||
| } | |||
| return ( | |||
| <HeaderContainer> | |||
| <HeaderLocation>{categoryString + filtersString} </HeaderLocation> | |||
| @@ -93,15 +110,21 @@ const Header = (props) => { | |||
| <GridSquare /> | |||
| </HeaderButton> | |||
| </HeaderButtons> | |||
| <HeaderSelect defaultValue={0} IconComponent={DownArrow}> | |||
| <HeaderSelect | |||
| defaultValue={0} | |||
| IconComponent={DownArrow} | |||
| width="209px" | |||
| height="34px" | |||
| onChange={handleChangeSelect} | |||
| > | |||
| {MockupdataForSelect.map((item) => ( | |||
| <MenuItem | |||
| <Option | |||
| value={item.id} | |||
| key={item.id} | |||
| style={{ display: item.id === 0 ? "none" : "flex" }} | |||
| > | |||
| {item.string} | |||
| </MenuItem> | |||
| </Option> | |||
| ))} | |||
| </HeaderSelect> | |||
| </HeaderOptions> | |||
| @@ -1,15 +1,16 @@ | |||
| import { Box, MenuItem, Select } from "@mui/material"; | |||
| import { Box, MenuItem } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import Select from "../../Select/Select"; | |||
| export const HeaderContainer = styled(Box)` | |||
| margin-top: 20px; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| ` | |||
| export const HeaderLocation = styled(Box)` | |||
| padding-top: 10px; | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| @@ -19,6 +20,9 @@ export const HeaderLocation = styled(Box)` | |||
| ` | |||
| export const HeaderButton = styled(IconButton)` | |||
| padding: 2px 10px; | |||
| @media (max-width: 1500px) { | |||
| display: none; | |||
| } | |||
| ` | |||
| export const HeaderOptions = styled(Box)` | |||
| display: flex; | |||
| @@ -31,9 +35,11 @@ export const HeaderSelect = styled(Select)` | |||
| height: 35px; | |||
| font-family: "Open Sans"; | |||
| margin-top: 3px; | |||
| & div span { | |||
| position: relative; | |||
| top: -4px; | |||
| font-weight: 400; | |||
| position: relative; | |||
| left: -5px; | |||
| & div { | |||
| padding-left: 8px; | |||
| } | |||
| ` | |||
| export const SelectItem = styled(MenuItem)` | |||
| @@ -4,4 +4,7 @@ import styled from "styled-components"; | |||
| export const MarketPlaceContainer = styled(Box)` | |||
| height: 100%; | |||
| margin: 0 70px; | |||
| @media (max-width: 600px) { | |||
| margin: 0 1.8rem; | |||
| } | |||
| `; | |||
| @@ -1,8 +1,10 @@ | |||
| import React from "react"; | |||
| import React, { useEffect } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { OffersContainer } from "./Offers.styled"; | |||
| import OfferCard from "../../Cards/OfferCard/OfferCard"; | |||
| import MockupdataOffers from "../MockupdataOffers"; | |||
| import { fetchOffers } from "../../../store/actions/offers/offersActions"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { selectOffers } from "../../../store/selectors/offersSelectors"; | |||
| // import { fetchOffers } from "../../../store/actions/offers/offersActions"; | |||
| // import { useDispatch, useSelector } from "react-redux"; | |||
| // import { selectOffers } from "../../../store/selectors/offersSelectors"; | |||
| @@ -11,17 +13,17 @@ const Offers = (props) => { | |||
| // Market place nije zavrsen | |||
| // Koriste se Mockup podaci | |||
| // const dispatch = useDispatch(); | |||
| // const offers = useSelector(selectOffers); | |||
| const dispatch = useDispatch(); | |||
| const offers = useSelector(selectOffers); | |||
| // useEffect(() => { | |||
| // dispatch(fetchOffers()); | |||
| // }, []) | |||
| useEffect(() => { | |||
| dispatch(fetchOffers()); | |||
| }, []) | |||
| return ( | |||
| <OffersContainer> | |||
| {MockupdataOffers.map((item) => { | |||
| return <OfferCard {...item} key={item.id} halfwidth={props.isGrid} />; | |||
| {offers.map((item) => { | |||
| return <OfferCard key={item._id} offer={item} halfwidth={props.isGrid} />; | |||
| })} | |||
| </OffersContainer> | |||
| ); | |||
| @@ -0,0 +1,83 @@ | |||
| import React, { useRef, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| HorizontalScrollerContainer, | |||
| Arrow, | |||
| ListContainer, | |||
| ArrowIcon, | |||
| } from "./HorizontalScroller.styled"; | |||
| const HorizontalScroller = (props) => { | |||
| const scrollRef = useRef(null); | |||
| const [isDisabledLeftButton, setIsDisabledLeftButton] = useState(true); | |||
| const [isDisabledRightButton, setIsDisabledRightButton] = useState(false); | |||
| const handleScroll = (event) => { | |||
| if (!event.external) { | |||
| if (scrollRef.current.scrollLeft === 0) { | |||
| if (isDisabledLeftButton !== true) setIsDisabledLeftButton(true); | |||
| } else { | |||
| if (isDisabledLeftButton !== false) setIsDisabledLeftButton(false); | |||
| } | |||
| if ( | |||
| scrollRef.current.scrollWidth - scrollRef.current.scrollLeft === | |||
| scrollRef.current.clientWidth | |||
| ) { | |||
| if (isDisabledRightButton !== true) setIsDisabledRightButton(true); | |||
| } else { | |||
| if (isDisabledRightButton !== false) setIsDisabledRightButton(false); | |||
| } | |||
| } | |||
| }; | |||
| const handleRight = () => { | |||
| if (scrollRef.current.scrollLeft + 50 !== 0) { | |||
| if (isDisabledLeftButton !== false) setIsDisabledLeftButton(false); | |||
| } | |||
| if ( | |||
| scrollRef.current.scrollWidth - scrollRef.current.scrollLeft - 50 <= | |||
| scrollRef.current.clientWidth | |||
| ) { | |||
| if (isDisabledRightButton !== true) setIsDisabledRightButton(true); | |||
| } | |||
| scrollRef.current.scrollBy({ left: 50, behaviour: "smooth" }); | |||
| }; | |||
| const handleLeft = () => { | |||
| if (scrollRef.current.scrollLeft - 50 <= 0) { | |||
| if (isDisabledLeftButton !== true) setIsDisabledLeftButton(true); | |||
| } | |||
| if ( | |||
| scrollRef.current.scrollWidth - scrollRef.current.scrollLeft + 50 !== | |||
| scrollRef.current.clientWidth | |||
| ) { | |||
| if (isDisabledRightButton !== false) setIsDisabledRightButton(false); | |||
| } | |||
| scrollRef.current.scrollBy({ left: -50, behaviour: "smooth" }); | |||
| }; | |||
| return ( | |||
| <HorizontalScrollerContainer style={props.containerStyle}> | |||
| <Arrow onClick={handleLeft} disabled={isDisabledLeftButton}> | |||
| <ArrowIcon side={"left"} /> | |||
| </Arrow> | |||
| <ListContainer | |||
| innerRef={scrollRef} | |||
| style={props.listStyle} | |||
| onScroll={handleScroll} | |||
| > | |||
| {props.children} | |||
| </ListContainer> | |||
| <Arrow onClick={handleRight} disabled={isDisabledRightButton}> | |||
| <ArrowIcon side={"right"} /> | |||
| </Arrow> | |||
| </HorizontalScrollerContainer> | |||
| ); | |||
| }; | |||
| HorizontalScroller.propTypes = { | |||
| children: PropTypes.node, | |||
| className: PropTypes.string, | |||
| containerStyle: PropTypes.any, | |||
| listStyle: PropTypes.any, | |||
| }; | |||
| export default HorizontalScroller; | |||
| @@ -0,0 +1,65 @@ | |||
| import { Box, Button } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import {ReactComponent as DownArrow} from "../../assets/images/svg/arrow-down.svg" | |||
| import selectedTheme from "../../themes"; | |||
| import ScrollContainer from 'react-indiana-drag-scroll' | |||
| export const HorizontalScrollerContainer = styled(Box)` | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: row; | |||
| flex-wrap: nowrap; | |||
| overflow: hidden; | |||
| ` | |||
| export const Arrow = styled(Button)` | |||
| border: 1px solid ${selectedTheme.primaryPurple}; | |||
| border-radius: 100%; | |||
| min-width: 40px; | |||
| width: 40px; | |||
| height: 40px; | |||
| display: block; | |||
| box-sizing: border-box; | |||
| cursor: pointer; | |||
| padding-left: 8px; | |||
| padding-top: 10px; | |||
| margin-top: auto; | |||
| margin-bottom: auto; | |||
| transition: 0.2s all ease; | |||
| &:hover { | |||
| background-color: ${selectedTheme.primaryPurple}; | |||
| & svg path { | |||
| stroke: white; | |||
| } | |||
| } | |||
| ${props => props.disabled && ` | |||
| border 1px solid ${selectedTheme.iconStrokeDisabledColor}; | |||
| & svg path { | |||
| stroke: ${selectedTheme.iconStrokeDisabledColor}; | |||
| transition: 0.2s all ease; | |||
| } | |||
| `} | |||
| ` | |||
| export const ListContainer = styled(ScrollContainer)` | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: row; | |||
| flex-wrap: nowrap; | |||
| cursor: grab; | |||
| scroll-behavior: smooth; | |||
| margin: 0 18px; | |||
| user-select: none; | |||
| ` | |||
| export const ArrowIcon = styled(DownArrow)` | |||
| ${props => props.side === 'left' && ` | |||
| transform: rotate(180deg); | |||
| `} | |||
| width: 18px; | |||
| height: 18px; | |||
| & path { | |||
| ${props => props.disabled && ` | |||
| stroke: ${selectedTheme.iconStrokeDisabledColor} | |||
| `} | |||
| } | |||
| ` | |||
| @@ -0,0 +1,27 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { OptionIcon, OptionStyled } from './Option.styled' | |||
| const Option = props => { | |||
| console.log(props) | |||
| return ( | |||
| <OptionStyled {...props}> | |||
| {props.startIcon ? ( | |||
| <OptionIcon color={props.color}> | |||
| {props.startIcon} | |||
| </OptionIcon> | |||
| ) :(<> | |||
| </>)} | |||
| {props.children} | |||
| </OptionStyled> | |||
| ) | |||
| } | |||
| Option.propTypes = { | |||
| children: PropTypes.node, | |||
| color: PropTypes.any, | |||
| startIcon: PropTypes.any, | |||
| } | |||
| export default Option | |||
| @@ -0,0 +1,16 @@ | |||
| import { Box, MenuItem } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| export const OptionStyled = styled(MenuItem)` | |||
| background-color: ${props => props.selected ? selectedTheme.primaryPurple : "white"} !important; | |||
| color: ${props => props.selected ? "white" : selectedTheme.selectOptionTextColor}; | |||
| margin:2px 9px; | |||
| border-radius: 4px; | |||
| &:hover { | |||
| background-color: ${selectedTheme.primaryPurple} !important; | |||
| color: white; | |||
| } | |||
| ` | |||
| export const OptionIcon = styled(Box)` | |||
| ` | |||
| @@ -0,0 +1,42 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { SelectIcon, SelectStyled } from "./Select.styled"; | |||
| import { ReactComponent as Down } from "../../assets/images/svg/down-arrow.svg"; | |||
| // import {Select as SelectMUI} from "@mui/material"; | |||
| const Select = (props) => { | |||
| return ( | |||
| <SelectStyled | |||
| defaultValue={props.defaultValue} | |||
| fullWidth={props.fullwidth} | |||
| width={props.width} | |||
| height={props.height} | |||
| className={props.className} | |||
| onChange={props.onChange} | |||
| IconComponent={(iconProps) => ( | |||
| <SelectIcon {...iconProps}> | |||
| <Down /> | |||
| </SelectIcon> | |||
| )} | |||
| > | |||
| {props.children} | |||
| </SelectStyled> | |||
| ); | |||
| }; | |||
| Select.propTypes = { | |||
| children: PropTypes.node, | |||
| width: PropTypes.string, | |||
| height: PropTypes.string, | |||
| fullwidth: PropTypes.bool, | |||
| defaultValue: PropTypes.number, | |||
| className: PropTypes.string, | |||
| onChange: PropTypes.func, | |||
| }; | |||
| Select.defaultProps = { | |||
| fullwidth: true, | |||
| height: "48px" | |||
| }; | |||
| export default Select; | |||
| @@ -0,0 +1,16 @@ | |||
| import { Box, Select } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const SelectStyled = styled(Select)` | |||
| width: ${props => props.width}; | |||
| height: ${props => props.height}; | |||
| padding: 2px; | |||
| font-size: 16px; | |||
| font-weight: 600; | |||
| font-family: "Open Sans"; | |||
| ` | |||
| export const SelectIcon = styled(Box)` | |||
| position: relative; | |||
| top: 0px; | |||
| left: -15px; | |||
| ` | |||
| @@ -11,11 +11,19 @@ const StepProgress = (props) => { | |||
| current: i === props.current, | |||
| }); | |||
| } | |||
| const functions = []; | |||
| steps.forEach((item,index) => { | |||
| if (props.functions[index]) { | |||
| functions.push(props.functions[index]) | |||
| } else { | |||
| functions.push(() => {}) | |||
| } | |||
| }) | |||
| return ( | |||
| <StepProgressContainer done> | |||
| {steps.map((item, index) => | |||
| index === 0 ? ( | |||
| <StepBar current={item.current} done={item.done} key={index}> | |||
| <StepBar current={item.current} done={item.done} key={index} onClick={item.done ? props.functions[index] : () => {console.log("neuspeh")}}> | |||
| {item.done ? <Checkmark /> : index+1} | |||
| </StepBar> | |||
| ) : ( | |||
| @@ -23,7 +31,7 @@ const StepProgress = (props) => { | |||
| <StepLine done={item.done || item.current} > | |||
| <Progress done={item.done || item.current} /> | |||
| </StepLine> | |||
| <StepBar current={item.current} done={item.done}> | |||
| <StepBar current={item.current} done={item.done} onClick={item.done ? props.functions[index] : () => {}} > | |||
| {item.done ? <Checkmark /> : index+1} | |||
| </StepBar> | |||
| </React.Fragment> | |||
| @@ -38,6 +46,10 @@ StepProgress.propTypes = { | |||
| handleNext: PropTypes.node, | |||
| current: PropTypes.number, | |||
| numberOfSteps: PropTypes.number, | |||
| functions: PropTypes.array | |||
| }; | |||
| StepProgress.defaultProps = { | |||
| functions: [] | |||
| } | |||
| export default StepProgress; | |||
| @@ -35,6 +35,7 @@ export const StepBar = styled(Box)` | |||
| color: #1d1d1d; | |||
| z-index: 1; | |||
| transition: background-color 1s ease; | |||
| ${props => props.done && `cursor: pointer;`} | |||
| `; | |||
| export const Progress = styled(Box)` | |||
| height: 9px; | |||
| @@ -42,7 +42,8 @@ export const TextField = (props) => { | |||
| sx={props.style} | |||
| label={props.showAnimation ? props.placeholder : ""} | |||
| italicplaceholder={props.italicPlaceholder && isFieldEmpty} | |||
| italicplaceholder={(props.italicPlaceholder && isFieldEmpty) ? "true" : "false"} | |||
| ref={textInputRef} | |||
| focused={props.focused} | |||
| > | |||
| @@ -14,12 +14,13 @@ export const TextFieldStyled = styled(TextField)` | |||
| background-color: ${selectedTheme.primaryBackgroundColor}; | |||
| width: ${(props) => props.width}; | |||
| font-style: ${(props) => | |||
| props.italicplaceholder === true ? "italic" : "normal"}; | |||
| props.italicplaceholder === "true" ? "italic" : "normal"}; | |||
| padding-left: 0; | |||
| margin: 0; | |||
| padding: 0; | |||
| height: ${(props) => props.height}; | |||
| box-sizing: border-box; | |||
| overflow-y: hidden; | |||
| & div { | |||
| padding-left: 2px; | |||
| @@ -28,16 +29,21 @@ export const TextFieldStyled = styled(TextField)` | |||
| ` | |||
| padding: 10px 16px; | |||
| max-height: ${props.height}; | |||
| overflow-y: auto; | |||
| & fieldset { | |||
| border: 1px solid rgba(0, 0, 0, 0.23); | |||
| border-radius: 4px; | |||
| position: relative; | |||
| height: 100%; | |||
| & textarea { | |||
| height: 100% !important; | |||
| width: 100% ; | |||
| overflow: auto; | |||
| font-size: 16px; | |||
| } | |||
| `} | |||
| } | |||
| & div input { | |||
| height: ${(props) => props.height}; | |||
| box-sizing: border-box; | |||
| font-size: ${(props) => | |||
| props.textsize ? props.textsize : "16px"} !important; | |||
| font-family: ${(props) => (props.font ? props.font : "")}; | |||
| @@ -7,6 +7,11 @@ export const NOT_FOUND_PAGE = '/not-found'; | |||
| export const FORGOT_PASSWORD_MAIL_SENT = '/forgot-password/mail-sent' | |||
| export const REGISTER_PAGE = "/register" | |||
| export const REGISTER_SUCCESSFUL_PAGE = "/register/success"; | |||
| <<<<<<< HEAD | |||
| export const RESET_PASSWORD_PAGE = "/reset-password" | |||
| export const CREATE_OFFER_PAGE = "/create-offer" | |||
| export const ITEM_DETAILS_PAGE = "/proizvodi/:idProizvod" | |||
| export const ITEM_DETAILS_PAGE = "/proizvodi/:idProizvod" | |||
| ======= | |||
| export const RESET_PASSWORD_PAGE = "/reset-password/:token" | |||
| export const CREATE_OFFER_PAGE = "/create-offer" | |||
| >>>>>>> horizontal-scroller | |||
| @@ -55,7 +55,7 @@ export default { | |||
| emailRequired: 'Email adresa je obavezna!', | |||
| noUsers: 'Ne postoji korisnik sa zadatom email adresom.', | |||
| passwordStrength: 'Your password is {{strength}}.', | |||
| passwordLength: 'Lozinka mora imati izmedju 8 i 50 karaktera!', | |||
| passwordLength: 'Lozinka mora imati najmanje 8 karaktera!', | |||
| signUpRecommendation: 'Registrujte se.', | |||
| email: 'Unesite email adresu kako biste se prijavili', | |||
| logInTitle: 'Uloguj se', | |||
| @@ -66,6 +66,7 @@ export default { | |||
| forgotYourPassword: 'Zaboravili ste lozinku?', | |||
| forgotPasswordEmail:'Email', | |||
| useDifferentEmail: 'Iskoristite drugačiju lozinku.', | |||
| wrongCredentials: 'Pogrešan mail ili lozinka!' | |||
| }, | |||
| password: { | |||
| weak: 'slaba', | |||
| @@ -80,11 +81,14 @@ export default { | |||
| descriptionSecond: 'Ovaj korak nije obavezan za razgledanje artikla ali za proces trampe je obavezan. Uvek možete popuniti ova polja u podešavanjima naloga kasnije', | |||
| descriptionThird: 'Ovaj korak nije obavezan za razgledanje artikla ali za proces trampe je obavezan. Uvek možete popuniti ova polja u podešavanjima naloga kasnije', | |||
| loginText: "Već posedujete nalog?", | |||
| emailFormat: 'Nevalidan format email adrese!', | |||
| emailTaken: "E-mail je zauzet!", | |||
| login: "Ulogujte se.", | |||
| acceptTerms: `Pri klikom na dugme "Registruj se", prihvatate naše`, | |||
| terms: "Uslove Korišćenja", | |||
| success: 'Registracija Uspešna', | |||
| welcome: 'Dobro došli na trampu, želimo vam uspešno trampovanje!' | |||
| PIBTaken: "PIB je zauzet!", | |||
| welcome: 'Dobro došli na trampu, želimo vam uspešno trampovanje!', | |||
| }, | |||
| forgotPassword: { | |||
| title: 'Povrati lozinku', | |||
| @@ -94,6 +98,7 @@ export default { | |||
| emailFormat: 'Nevalidan format email adrese!', | |||
| mailSent: "E-Mail poslat!", | |||
| mailSentDescription: "Poslat vam je email sa instrukcijama kako da resetujete lozinku", | |||
| mailNotFound: "Mejl nije povezan ni sa jednim nalogom!", | |||
| notRecievedMail: "Niste dobili email?", | |||
| checkSpam: "U slučaju da Vam ne stigne email, pogledajte <strong>Spam</strong> folder email provajdera", | |||
| forgotPassword: { | |||
| @@ -1,38 +0,0 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { Content, LeftCard, RightCard, GridLayoutContainer, HeaderCard, MiddleCard } from "./GridLayout.styled"; | |||
| import { Grid } from "@mui/material"; | |||
| const GridLayout = (props) => { | |||
| return ( | |||
| <GridLayoutContainer> | |||
| {props.children} | |||
| <Grid container maxHeight="lg"> | |||
| <LeftCard item xs={2} lg={2.5} xl={2.4} md={3}> | |||
| {props.leftCard} | |||
| </LeftCard> | |||
| <MiddleCard item xs={7.5} lg={6.5} xl={6.6} md={6}> | |||
| <HeaderCard> | |||
| {props.headerCard} | |||
| </HeaderCard> | |||
| <Content> | |||
| {props.content} | |||
| </Content> | |||
| </MiddleCard> | |||
| <RightCard item xs={2.5} lg={3} xl={3} md={3}> | |||
| {props.rightCard} | |||
| </RightCard> | |||
| </Grid> | |||
| </GridLayoutContainer> | |||
| ); | |||
| }; | |||
| GridLayout.propTypes = { | |||
| children: PropTypes.node, | |||
| leftCard: PropTypes.node, | |||
| content: PropTypes.node, | |||
| rightCard: PropTypes.node, | |||
| headerCard: PropTypes.node, | |||
| }; | |||
| export default GridLayout; | |||
| @@ -1,28 +0,0 @@ | |||
| import { Box, Container, Grid } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const GridLayoutContainer = styled(Container)` | |||
| padding-left: 0; | |||
| padding-right: 0; | |||
| margin: 0; | |||
| width: 100%; | |||
| display: flex; | |||
| flex: 1; | |||
| height: 100%; | |||
| ` | |||
| export const LeftCard = styled(Grid)` | |||
| margin-top: 30px; | |||
| border-top-right-radius: 4px; | |||
| ` | |||
| export const Content = styled(Grid)` | |||
| ` | |||
| export const RightCard = styled(Grid)` | |||
| margin-top: 30px; | |||
| border-top-left-radius: 4px; | |||
| ` | |||
| export const MiddleCard = styled(Grid)` | |||
| ` | |||
| export const HeaderCard = styled(Box)` | |||
| height: 450px; | |||
| ` | |||
| @@ -8,10 +8,10 @@ const MainLayout = (props) => { | |||
| <MainLayoutContainer> | |||
| {props.children} | |||
| <Grid container maxHeight="xl"> | |||
| <LeftCard item xs={2} lg={3} xl={2.4} md={4} > | |||
| <LeftCard item xs={0} sm={0} md={3} lg={3} xl={2.4} > | |||
| {props.leftCard} | |||
| </LeftCard> | |||
| <Content item xs={10} lg={9} xl={9.6} md={8} > | |||
| <Content item xs={12} sm={12} md={9} lg={9} xl={9.6} > | |||
| {props.content} | |||
| </Content> | |||
| </Grid> | |||
| @@ -7,7 +7,7 @@ const ProfileLayout = (props) => { | |||
| return ( | |||
| <ProfileLayoutContainer> | |||
| {props.children} | |||
| <Grid container maxHeight="xl"> | |||
| <Grid container maxHeight maxWidth={1900}> | |||
| <LeftCard item xs={2} lg={2.5} xl={2.4} md={3}> | |||
| {props.leftCard} | |||
| </LeftCard> | |||
| @@ -7,6 +7,7 @@ export const ProfileLayoutContainer = styled(Container)` | |||
| margin: 0; | |||
| width: 100%; | |||
| display: flex; | |||
| max-width: none; | |||
| flex: 1; | |||
| height: 100%; | |||
| ` | |||
| @@ -14,15 +15,20 @@ export const ProfileLayoutContainer = styled(Container)` | |||
| export const LeftCard = styled(Grid)` | |||
| margin-top: 30px; | |||
| border-top-right-radius: 4px; | |||
| background-color: green; | |||
| ` | |||
| export const Content = styled(Grid)` | |||
| background-color: yellow; | |||
| height: 100%; | |||
| ` | |||
| export const RightCard = styled(Grid)` | |||
| margin-top: 30px; | |||
| border-top-left-radius: 4px; | |||
| background-color: blue; | |||
| ` | |||
| export const MiddleCard = styled(Grid)` | |||
| ` | |||
| export const HeaderCard = styled(Box)` | |||
| height: 450px; | |||
| background-color: orange; | |||
| ` | |||
| @@ -7,6 +7,12 @@ export const ForgotPasswordPageContainer = styled(Container)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @media (max-height: 900px) { | |||
| margin-top: 160px; | |||
| } | |||
| @media (max-height: 800px) { | |||
| margin-top: 120px; | |||
| } | |||
| `; | |||
| export const ForgotPasswordTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| @@ -38,4 +44,11 @@ export const ForgotPasswordDescription = styled(Typography)` | |||
| export const FormContainer = styled(Box)` | |||
| width: 335px; | |||
| height: 216px; | |||
| `; | |||
| `; | |||
| export const ErrorMessage = styled(Typography)` | |||
| color: red; | |||
| font-family: "Open Sans"; | |||
| position: relative; | |||
| top: -7px; | |||
| font-size: 14px; | |||
| ` | |||
| @@ -1,4 +1,4 @@ | |||
| import React from "react"; | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| MailSentContainer, | |||
| @@ -21,22 +21,26 @@ import { useDispatch } from "react-redux"; | |||
| import { forgotPassword } from "../../../store/actions/user/userActions"; | |||
| const MailSent = () => { | |||
| const [mail, setEmail] = useState(""); | |||
| const { t } = useTranslation(); | |||
| const history = useHistory(); | |||
| const location = useLocation(); | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| setEmail(location.state.email); | |||
| }, []); | |||
| const navigateLogin = () => { | |||
| history.replace(LOGIN_PAGE); | |||
| }; | |||
| const handleResend = () => { | |||
| dispatch(forgotPassword(location.state.email)) | |||
| } | |||
| dispatch(forgotPassword({ email: mail })); | |||
| }; | |||
| return ( | |||
| <MailSentContainer> | |||
| <MailSentImage /> | |||
| <Title component="h1" variant="h5"> | |||
| @@ -48,12 +52,11 @@ const MailSent = () => { | |||
| </Description> | |||
| <FormContainer> | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| onClick={navigateLogin} | |||
| @@ -62,27 +65,26 @@ const MailSent = () => { | |||
| </PrimaryButton> | |||
| <SendAgainTextContainer> | |||
| <StandardText> | |||
| {t("forgotPassword.notRecievedMail")} | |||
| </StandardText> | |||
| <Link to="#" component={NavLink} underline="hover" align="center" textsize="16px" onClick={handleResend}> | |||
| <StandardText>{t("forgotPassword.notRecievedMail")}</StandardText> | |||
| <Link | |||
| to="#" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="center" | |||
| textsize="16px" | |||
| onClick={handleResend} | |||
| > | |||
| {t("common.sendAgain")} | |||
| </Link> | |||
| </SendAgainTextContainer> | |||
| </FormContainer> | |||
| <Footer> | |||
| <FooterText> | |||
| <Trans i18nKey="forgotPassword.checkSpam" /> | |||
| <Trans i18nKey="forgotPassword.checkSpam" /> | |||
| </FooterText> | |||
| </Footer> | |||
| </MailSentContainer> | |||
| ); | |||
| }; | |||
| @@ -7,6 +7,12 @@ export const MailSentContainer = styled(Container)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @media (max-height: 900px) { | |||
| margin-top: 200px; | |||
| } | |||
| @media (max-height: 800px) { | |||
| margin-top: 150px; | |||
| } | |||
| `; | |||
| export const Title = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| @@ -1,62 +1,116 @@ | |||
| import React from 'react'; | |||
| import { Formik, Form, Field } from 'formik'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import * as Yup from 'yup'; | |||
| import i18next from 'i18next'; | |||
| import Auth from '../../components/Auth/Auth'; | |||
| import AuthCard from '../../components/AuthCards/AuthCard'; | |||
| import TextField from '../../components/InputFields/TextField'; | |||
| import Button from '../../components/Buttons/Button'; | |||
| import Section from '../../components/Section/Section'; | |||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||
| email: Yup.string().required( | |||
| i18next.t('login.securityQuestion.answerRequired'), | |||
| ), | |||
| }); | |||
| import React, { useState } from "react"; | |||
| import { useFormik } from "formik"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import * as Yup from "yup"; | |||
| // import i18next from "i18next"; | |||
| import { ReactComponent as Logo } from "../../assets/images/svg/logo-vertical.svg"; | |||
| import { | |||
| ForgotPasswordPageContainer, | |||
| ForgotPasswordDescription, | |||
| ForgotPasswordTitle, | |||
| FormContainer, | |||
| ErrorMessage, | |||
| } from "./ForgotPassword.styled"; | |||
| import { TextField } from "../../components/TextFields/TextField/TextField"; | |||
| import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { FORGOT_PASSWORD_MAIL_SENT } from "../../constants/pages"; | |||
| import selectedTheme from "../../themes"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { forgotPassword } from "../../store/actions/user/userActions"; | |||
| const ForgotPasswordPage = () => { | |||
| const history = useHistory(); | |||
| const { t } = useTranslation(); | |||
| const dispatch = useDispatch(); | |||
| const [emailNotFoundStatus, setEmailNotFoundStatus] = useState(false); | |||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||
| email: Yup.string() | |||
| .required(t("forgotPassword.emailRequired")) | |||
| .email(t("forgotPassword.emailFormat")), | |||
| }); | |||
| const handleResponseSuccess = () => { | |||
| history.push({ | |||
| pathname: FORGOT_PASSWORD_MAIL_SENT, | |||
| state: { email: formik.values.email }, | |||
| }); | |||
| }; | |||
| const handleResponseError = () => { | |||
| setEmailNotFoundStatus(true); | |||
| }; | |||
| const handleSubmit = (values) => { | |||
| console.log("Values",values) | |||
| dispatch( | |||
| forgotPassword({ | |||
| email: values.email, | |||
| handleResponseSuccess, | |||
| handleResponseError, | |||
| }) | |||
| ); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: "", | |||
| }, | |||
| validationSchema: forgotPasswordValidationSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Auth> | |||
| <AuthCard | |||
| title={t('forgotPassword.title')} | |||
| > | |||
| <Section> | |||
| <div className="c-reset-security"> | |||
| <div className="c-reset-security__form"> | |||
| <Formik | |||
| onSubmit={handleSubmit} | |||
| initialValues={{ email: '' }} | |||
| validationSchema={forgotPasswordValidationSchema} | |||
| > | |||
| <Form> | |||
| <Field | |||
| label={t('login.forgotPasswordEmail')} | |||
| name="email" | |||
| component={TextField} | |||
| /> | |||
| <Button | |||
| className="c-reset-security__button" | |||
| authButton | |||
| variant="primary" | |||
| type="submit" | |||
| > | |||
| {t('forgotPassword.label')} | |||
| </Button> | |||
| </Form> | |||
| </Formik> | |||
| </div> | |||
| </div> | |||
| </Section> | |||
| </AuthCard> | |||
| </Auth> | |||
| <ForgotPasswordPageContainer> | |||
| <Logo /> | |||
| <ForgotPasswordTitle component="h1" variant="h5"> | |||
| {t("forgotPassword.title")} | |||
| </ForgotPasswordTitle> | |||
| <ForgotPasswordDescription component="h1" variant="h6"> | |||
| {t("forgotPassword.description")} | |||
| </ForgotPasswordDescription> | |||
| <FormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| {/* <Backdrop position="absolute" isLoading={isLoading} /> */} | |||
| <TextField | |||
| name="email" | |||
| placeholder={t("common.labelEmail")} | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| error={ | |||
| (formik.touched.email && Boolean(formik.errors.email)) || | |||
| emailNotFoundStatus | |||
| } | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| onChange={formik.handleChange} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| {formik.errors.email && formik.touched.email && ( | |||
| <ErrorMessage>{formik.errors.email}</ErrorMessage> | |||
| )} | |||
| {emailNotFoundStatus && ( | |||
| <ErrorMessage>{t("forgotPassword.mailNotFound")}</ErrorMessage> | |||
| )} | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={formik.values.email?.length === 0} | |||
| > | |||
| {t("common.send")} | |||
| </PrimaryButton> | |||
| </FormContainer> | |||
| </ForgotPasswordPageContainer> | |||
| ); | |||
| }; | |||
| @@ -1,103 +0,0 @@ | |||
| import React, { useState } from "react"; | |||
| import { useFormik } from "formik"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import * as Yup from "yup"; | |||
| import i18next from "i18next"; | |||
| import { ReactComponent as Logo } from "../../assets/images/svg/logo-vertical.svg"; | |||
| import { | |||
| ForgotPasswordPageContainer, | |||
| ForgotPasswordDescription, | |||
| ForgotPasswordTitle, | |||
| FormContainer, | |||
| } from "./ForgotPassword.styled"; | |||
| import { TextField } from "../../components/TextFields/TextField/TextField"; | |||
| import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { FORGOT_PASSWORD_MAIL_SENT } from "../../constants/pages"; | |||
| import selectedTheme from "../../themes"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { forgotPassword } from "../../store/actions/user/userActions"; | |||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||
| email: Yup.string() | |||
| .required(i18next.t("forgotPassword.emailRequired")) | |||
| .email(i18next.t("forgotPassword.emailFormat")), | |||
| }); | |||
| const ForgotPasswordPage = () => { | |||
| const history = useHistory(); | |||
| const { t } = useTranslation(); | |||
| const dispatch = useDispatch(); | |||
| const [emailNotFoundStatus, setEmailNotFoundStatus] = useState(false); | |||
| const handleResponseSuccess = () => { | |||
| history.push({ | |||
| pathname: FORGOT_PASSWORD_MAIL_SENT, | |||
| state: {email: formik.values.email} | |||
| }); | |||
| setEmailNotFoundStatus(true); | |||
| } | |||
| // const handleResponseError = () => { | |||
| // setEmailNotFoundStatus(true); | |||
| // console.log("greska!"); | |||
| // } | |||
| const handleSubmit = (values) => { | |||
| // validate email | |||
| dispatch(forgotPassword({email: values.email, handleResponseSuccess, handleResponseError: handleResponseSuccess})); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: "", | |||
| }, | |||
| validationSchema: forgotPasswordValidationSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <ForgotPasswordPageContainer> | |||
| <Logo /> | |||
| <ForgotPasswordTitle component="h1" variant="h5"> | |||
| {t("forgotPassword.title")} | |||
| </ForgotPasswordTitle> | |||
| <ForgotPasswordDescription component="h1" variant="h6"> | |||
| {t("forgotPassword.description")} | |||
| </ForgotPasswordDescription> | |||
| <FormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| {/* <Backdrop position="absolute" isLoading={isLoading} /> */} | |||
| <TextField | |||
| name="email" | |||
| placeholder={t("common.labelEmail")} | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| error={(formik.touched.email && Boolean(formik.errors.email)) || emailNotFoundStatus} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| onChange={formik.handleChange} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={formik.values.email?.length === 0} | |||
| > | |||
| {t("common.send")} | |||
| </PrimaryButton> | |||
| </FormContainer> | |||
| </ForgotPasswordPageContainer> | |||
| ); | |||
| }; | |||
| export default ForgotPasswordPage; | |||
| @@ -1,30 +1,39 @@ | |||
| import React, { useEffect } from "react"; | |||
| import Navbar from "../../components/MUI/NavbarComponent"; | |||
| import FilterCard from "../../components/Cards/FilterCard/FilterCard"; | |||
| // import FilterCard from "../../components/Cards/FilterCard/FilterCard"; | |||
| import { HomePageContainer } from "./HomePage.styled"; | |||
| import MarketPlace from "../../components/MarketPlace/MarketPlace"; | |||
| import MainLayout from "../../layouts/MainLayout/MainLayout"; | |||
| // import MarketPlace from "../../components/MarketPlace/MarketPlace"; | |||
| // import MainLayout from "../../layouts/MainLayout/MainLayout"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { logoutUser } from "../../store/actions/login/loginActions"; | |||
| // import { logoutUser } from "../../store/actions/login/loginActions"; | |||
| import Mockupdata from "../../components/Cards/FilterCard/Mockupdata"; | |||
| import qs from "query-string"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { setFilters } from "../../store/actions/filters/filtersActions"; | |||
| // import ProfileLayout from "../../layouts/ProfileLayout/ProfileLayout"; | |||
| import FilterCard from "../../components/Cards/FilterCard/FilterCard"; | |||
| import MainLayout from "../../layouts/MainLayout/MainLayout"; | |||
| import MarketPlace from "../../components/MarketPlace/MarketPlace"; | |||
| const HomePage = () => { | |||
| const dispatch = useDispatch(); | |||
| const history = useHistory(); | |||
| useEffect(() => { | |||
| let category = null, subcategory = null, cities = [], _des_date = false, _des_popular = false, page, size; | |||
| const queryString = history.location.search.substring(1); | |||
| const queryObject = qs.parse(queryString); | |||
| let category = null; | |||
| if (queryObject.category) { | |||
| category = Mockupdata[1].find( | |||
| (item) => item.string === queryObject.category.toString() | |||
| ).id; | |||
| } | |||
| let cities = []; | |||
| if (queryObject.subcategory) { | |||
| subcategory = Mockupdata[1].find( | |||
| (item) => item.string === queryObject.subcategory.toString() | |||
| ).id; | |||
| } | |||
| if (queryObject.city) { | |||
| if (Array.isArray(queryObject.city)) { | |||
| queryObject.city.forEach((item) => { | |||
| @@ -36,45 +45,25 @@ const HomePage = () => { | |||
| ); | |||
| } | |||
| } | |||
| let subcategory = null; | |||
| if (queryObject.subcategory) { | |||
| subcategory = Mockupdata[1].find( | |||
| (item) => item.string === queryObject.subcategory.toString() | |||
| ).id; | |||
| if (queryObject.sortBy) { | |||
| if (queryObject.sortBy === "dateAsc") _des_date = false; | |||
| if (queryObject.sortBy === "dateDesc") _des_date = true; | |||
| if (queryObject.sortBy === "popular") _des_popular = true; | |||
| } | |||
| console.log("iz useeffect: ", { category, subcategory, cities }); | |||
| dispatch(setFilters({ category, subcategory, cities })); | |||
| if (queryObject.page) { | |||
| page = queryObject.page; | |||
| } | |||
| if (queryObject.size) { | |||
| size = queryObject.size; | |||
| } | |||
| dispatch(setFilters({ category, subcategory, cities, _des_date, _des_popular, page, size })); | |||
| }, [history.location.search]); | |||
| const handleCl = () => { | |||
| dispatch(logoutUser()); | |||
| }; | |||
| return ( | |||
| <HomePageContainer> | |||
| <button onClick={handleCl}>Dugme</button> | |||
| <Navbar /> | |||
| <MainLayout leftCard={<FilterCard />} content={<MarketPlace />} /> | |||
| {/* <Box sx={{ mt: 4, mx: 4 }}> | |||
| <GridStyled container justifyContent="space-between"> | |||
| <GridStyled item xs={12} md={3}> | |||
| asdasdasd | |||
| </GridStyled> | |||
| <GridStyled item xs={12} md={6}> | |||
| <GridStyled xs={12} md={12}> | |||
| <HomeListCard></HomeListCard> | |||
| </GridStyled> | |||
| </GridStyled> */} | |||
| {/* <GridStyled item xs={12} md={9}> | |||
| <PagingSortingFiltering /> | |||
| </GridStyled> | |||
| <GridStyled item xs={12} md={9}> | |||
| {/* Move to higher components? */} | |||
| {/* <RandomDataProvider> | |||
| <PagingSortingFilteringServerSide /> | |||
| </RandomDataProvider> | |||
| </GridStyled> */} | |||
| {/* </GridStyled> | |||
| </Box> */} | |||
| </HomePageContainer> | |||
| ); | |||
| }; | |||
| @@ -7,6 +7,12 @@ export const LoginPageContainer = styled(Container)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @media (max-height: 900px) { | |||
| margin-top: 110px; | |||
| } | |||
| @media (max-height: 800px) { | |||
| margin-top: 70px; | |||
| } | |||
| `; | |||
| export const LoginTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| @@ -45,10 +51,18 @@ export const RegisterAltText = styled(Typography)` | |||
| font-size: 14px; | |||
| padding-right: 6px; | |||
| line-height: 14px; | |||
| ` | |||
| `; | |||
| export const RegisterTextContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| margin-top: 36px; | |||
| justify-content: center; | |||
| ` | |||
| `; | |||
| export const ErrorMessage = styled(Typography)` | |||
| color: red; | |||
| font-family: "Open Sans"; | |||
| position: relative; | |||
| top: -12px; | |||
| height: 20px; | |||
| font-size: 14px; | |||
| `; | |||
| @@ -1,135 +1,213 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Field, Form, Formik } from 'formik'; | |||
| import { useDispatch, useSelector } from 'react-redux'; | |||
| import { NavLink } from 'react-router-dom'; | |||
| import * as Yup from 'yup'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import i18next from 'i18next'; | |||
| import PasswordField from '../../components/InputFields/PasswordField'; | |||
| import Button from '../../components/Buttons/Button'; | |||
| import TextField from '../../components/InputFields/TextField'; | |||
| import Auth from '../../components/Auth/Auth'; | |||
| import AuthCard from '../../components/AuthCards/AuthCard'; | |||
| /* eslint-disable */ | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useFormik } from "formik"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { NavLink } from "react-router-dom"; | |||
| import * as Yup from "yup"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| clearLoginErrors, | |||
| fetchUser, | |||
| } from '../../store/actions/login/loginActions'; | |||
| } from "../../store/actions/login/loginActions"; | |||
| import { selectLoginError } from "../../store/selectors/loginSelectors"; | |||
| 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 "../../components/MUI/BackdropComponent"; | |||
| import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors"; | |||
| import { LOGIN_USER_LOADING } from "../../store/actions/login/loginActionConstants"; | |||
| import { TextField } from "../../components/TextFields/TextField/TextField"; | |||
| import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton"; | |||
| import { IconButton } from "../../components/Buttons/IconButton/IconButton"; | |||
| import Link from "../../components/Link/Link"; | |||
| import { ReactComponent as Logo } from "../../assets/images/svg/logo-vertical.svg"; | |||
| import { | |||
| selectLoginError, | |||
| } from '../../store/selectors/loginSelectors'; | |||
| import { | |||
| FORGOT_PASSWORD_PAGE, HOME_PAGE, | |||
| } from '../../constants/pages'; | |||
| import { selectIsLoadingByActionType } from '../../store/selectors/loadingSelectors'; | |||
| import { LOGIN_USER_LOADING } from '../../store/actions/login/loginActionConstants'; | |||
| const LoginValidationSchema = Yup.object().shape({ | |||
| username: Yup.string().required(i18next.t('login.usernameRequired')), | |||
| password: Yup.string().required(i18next.t('login.passwordRequired')), | |||
| }); | |||
| LoginPageContainer, | |||
| LoginTitle, | |||
| LoginDescription, | |||
| LoginFormContainer, | |||
| RegisterAltText, | |||
| RegisterTextContainer, | |||
| ErrorMessage, | |||
| } from "./Login.styled"; | |||
| import selectedTheme from "../../themes"; | |||
| const LoginPage = ({ history }) => { | |||
| const dispatch = useDispatch(); | |||
| const { t } = useTranslation(); | |||
| const error = useSelector(selectLoginError); | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| // When user refreshes page | |||
| // useEffect(() => { | |||
| // function redirectClient() { | |||
| // if (!tokens.RefreshToken && !tokens.JwtToken) { | |||
| // return | |||
| // } | |||
| // } | |||
| // function redirectClient() { | |||
| // if (!tokens.RefreshToken && !tokens.JwtToken) { | |||
| // return; | |||
| // } | |||
| // } | |||
| // redirectClient(); | |||
| // redirectClient(); | |||
| // }, [history, tokens]); | |||
| const isLoading = useSelector( | |||
| selectIsLoadingByActionType(LOGIN_USER_LOADING), | |||
| selectIsLoadingByActionType(LOGIN_USER_LOADING) | |||
| ); | |||
| const handleApiResponseSuccess =()=>{ | |||
| useEffect(() => { | |||
| dispatch(clearLoginErrors()); | |||
| }, []); | |||
| const handleApiResponseSuccess = () => { | |||
| history.push({ | |||
| pathname: HOME_PAGE, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| } | |||
| }; | |||
| const handleSubmit = (values) => { | |||
| // destructure value as username. | |||
| const { username: Username } = values; | |||
| const { password: Password } = values; | |||
| dispatch(clearLoginErrors()); | |||
| dispatch( | |||
| fetchUser({ | |||
| Username, | |||
| Password, | |||
| handleApiResponseSuccess | |||
| }, | |||
| ), | |||
| ); | |||
| const { email, password: password } = values; | |||
| dispatch(clearLoginErrors()); | |||
| dispatch( | |||
| fetchUser({ | |||
| email, | |||
| password, | |||
| handleApiResponseSuccess, | |||
| }) | |||
| ); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: "", | |||
| password: "", | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| email: Yup.string().required(t("login.mailRequired")), | |||
| password: Yup.string() | |||
| .required(t("login.passwordRequired")) | |||
| .min(8, t("login.passwordLength")), | |||
| }), | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| useEffect(() => { | |||
| if (error) { | |||
| if (formik.errors.email || formik.errors.password) { | |||
| dispatch(clearLoginErrors()); | |||
| } | |||
| } | |||
| }, [formik.errors.email, formik.errors.password]) | |||
| return ( | |||
| <Auth> | |||
| <AuthCard | |||
| title="Log In" | |||
| isLoading={isLoading} | |||
| > | |||
| <div className="c-login c-login--user"> | |||
| <div className="c-login__form"> | |||
| <Formik | |||
| initialValues={{ | |||
| username: '', | |||
| password: '', | |||
| }} | |||
| onSubmit={handleSubmit} | |||
| validationSchema={LoginValidationSchema} | |||
| validateOnBlur | |||
| enableReinitialize | |||
| > | |||
| {({ values }) => ( | |||
| <Form> | |||
| <Field | |||
| label={t('common.labelUsername')} | |||
| value={values.username.value} | |||
| component={TextField} | |||
| name="username" | |||
| /> | |||
| <Field | |||
| label={ | |||
| <div className="c-login--password__label"> | |||
| {t('common.labelPassword')} | |||
| </div> | |||
| } | |||
| link={ | |||
| <NavLink | |||
| to={FORGOT_PASSWORD_PAGE} | |||
| > | |||
| {t('login.forgotYourPassword')} | |||
| </NavLink> | |||
| } | |||
| name="password" | |||
| component={PasswordField} | |||
| errorMessage={error} | |||
| autoFocus | |||
| /> | |||
| <Button | |||
| className="c-login__button" | |||
| authButton | |||
| variant="primary" | |||
| type="submit" | |||
| > | |||
| {t('common.continue')} | |||
| </Button> | |||
| </Form> | |||
| )} | |||
| </Formik> | |||
| </div> | |||
| </div> | |||
| </AuthCard> | |||
| </Auth> | |||
| <LoginPageContainer> | |||
| <Logo /> | |||
| <LoginTitle component="h1" variant="h5"> | |||
| {t("login.logInTitle")} | |||
| </LoginTitle> | |||
| <LoginDescription component="h1" variant="h6"> | |||
| {t("login.welcomeText")} | |||
| </LoginDescription> | |||
| <LoginFormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| <Backdrop position="absolute" isLoading={isLoading} /> | |||
| <TextField | |||
| name="email" | |||
| placeholder={t("common.labelEmail")} | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={ | |||
| (formik.touched.email && formik.errors.email) || error.length > 0 | |||
| } | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="password" | |||
| placeholder={t("common.labelPassword")} | |||
| margin="normal" | |||
| type={showPassword ? "text" : "password"} | |||
| value={formik.values.password} | |||
| onChange={formik.handleChange} | |||
| error={ | |||
| (formik.touched.password && formik.errors.password) || | |||
| error.length > 0 | |||
| } | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <IconButton | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| > | |||
| {showPassword ? <VisibilityOn /> : <VisibilityOff />} | |||
| </IconButton> | |||
| ), | |||
| }} | |||
| /> | |||
| {formik.errors.password && formik.touched.password && ( | |||
| <ErrorMessage>{formik.errors.password}</ErrorMessage> | |||
| )} | |||
| {error.length > 0 && !formik.errors.password && <ErrorMessage>{error}</ErrorMessage>} | |||
| <Link | |||
| to={FORGOT_PASSWORD_PAGE} | |||
| textsize="12px" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="right" | |||
| style={{ | |||
| marginTop: error.length > 0 ? "0" : "18px", | |||
| marginBottom: "18px", | |||
| }} | |||
| > | |||
| {t("login.forgotYourPassword")} | |||
| </Link> | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={ | |||
| formik.values.email.length === 0 || | |||
| formik.values.password.length === 0 | |||
| } | |||
| > | |||
| {t("login.logIn")} | |||
| </PrimaryButton> | |||
| <RegisterTextContainer> | |||
| <RegisterAltText> | |||
| {t("login.dontHaveAccount").padEnd(2, " ")} | |||
| </RegisterAltText> | |||
| <Link | |||
| to="/register" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="center" | |||
| > | |||
| {t("login.signUp")} | |||
| </Link> | |||
| </RegisterTextContainer> | |||
| </LoginFormContainer> | |||
| </LoginPageContainer> | |||
| ); | |||
| }; | |||
| @@ -1,201 +0,0 @@ | |||
| /* eslint-disable */ | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useFormik } from "formik"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { NavLink } from "react-router-dom"; | |||
| import * as Yup from "yup"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| clearLoginErrors, | |||
| fetchUser, | |||
| } from "../../store/actions/login/loginActions"; | |||
| import { selectLoginError } from "../../store/selectors/loginSelectors"; | |||
| 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 "../../components/MUI/BackdropComponent"; | |||
| import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors"; | |||
| import { LOGIN_USER_LOADING } from "../../store/actions/login/loginActionConstants"; | |||
| import { TextField } from "../../components/TextFields/TextField/TextField"; | |||
| import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton"; | |||
| import { IconButton } from "../../components/Buttons/IconButton/IconButton"; | |||
| import Link from "../../components/Link/Link"; | |||
| import { ReactComponent as Logo } from "../../assets/images/svg/logo-vertical.svg"; | |||
| import { | |||
| LoginPageContainer, | |||
| LoginTitle, | |||
| LoginDescription, | |||
| LoginFormContainer, | |||
| RegisterAltText, | |||
| RegisterTextContainer, | |||
| } from "./Login.styled"; | |||
| import selectedTheme from "../../themes"; | |||
| const LoginPage = ({ history }) => { | |||
| const dispatch = useDispatch(); | |||
| const { t } = useTranslation(); | |||
| const error = useSelector(selectLoginError); | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| // When user refreshes page | |||
| // useEffect(() => { | |||
| // function redirectClient() { | |||
| // if (!tokens.RefreshToken && !tokens.JwtToken) { | |||
| // return; | |||
| // } | |||
| // } | |||
| // redirectClient(); | |||
| // }, [history, tokens]); | |||
| const isLoading = useSelector( | |||
| selectIsLoadingByActionType(LOGIN_USER_LOADING) | |||
| ); | |||
| useEffect(() => { | |||
| dispatch(clearLoginErrors()); | |||
| }, []); | |||
| const handleApiResponseSuccess = (status) => { | |||
| history.push({ | |||
| pathname: HOME_PAGE, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| }; | |||
| const handleSubmit = (values) => { | |||
| const { username: email, password: password } = values; | |||
| dispatch(clearLoginErrors()); | |||
| dispatch( | |||
| fetchUser({ | |||
| email, | |||
| password, | |||
| handleApiResponseSuccess, | |||
| }) | |||
| ); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| username: "", | |||
| password: "", | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| username: Yup.string().required(t("login.usernameRequired")), | |||
| password: Yup.string().required(t("login.passwordRequired")).min(8), | |||
| }), | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <LoginPageContainer> | |||
| <Logo /> | |||
| <LoginTitle component="h1" variant="h5"> | |||
| {t("login.logInTitle")} | |||
| </LoginTitle> | |||
| <LoginDescription component="h1" variant="h6"> | |||
| {t("login.welcomeText")} | |||
| </LoginDescription> | |||
| <LoginFormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| <Backdrop position="absolute" isLoading={isLoading} /> | |||
| <TextField | |||
| name="username" | |||
| placeholder={t("common.labelEmail")} | |||
| margin="normal" | |||
| value={formik.values.username} | |||
| onChange={formik.handleChange} | |||
| error={(formik.touched.password && formik.errors.password) || error.length > 0} | |||
| helperText={formik.touched.username && formik.errors.username} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="password" | |||
| placeholder={t("common.labelPassword")} | |||
| margin="normal" | |||
| type={showPassword ? "text" : "password"} | |||
| value={formik.values.password} | |||
| onChange={formik.handleChange} | |||
| error={(formik.touched.password && formik.errors.password) || error.length > 0} | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth={true} | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <IconButton | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| > | |||
| {showPassword ? <VisibilityOn /> : <VisibilityOff />} | |||
| </IconButton> | |||
| ), | |||
| }} | |||
| /> | |||
| <Link | |||
| to={FORGOT_PASSWORD_PAGE} | |||
| textsize="12px" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="right" | |||
| style={{ marginTop: "18px", marginBottom: "18px" }} | |||
| > | |||
| {t("login.forgotYourPassword")} | |||
| </Link> | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={ | |||
| formik.values.username.length === 0 || | |||
| formik.values.password.length === 0 | |||
| } | |||
| > | |||
| {t("login.logIn")} | |||
| </PrimaryButton> | |||
| <RegisterTextContainer> | |||
| <RegisterAltText> | |||
| {t("login.dontHaveAccount").padEnd(2, " ")} | |||
| </RegisterAltText> | |||
| <Link | |||
| to="/register" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="center" | |||
| > | |||
| {t("login.signUp")} | |||
| </Link> | |||
| </RegisterTextContainer> | |||
| </LoginFormContainer> | |||
| </LoginPageContainer> | |||
| ); | |||
| }; | |||
| LoginPage.propTypes = { | |||
| history: PropTypes.shape({ | |||
| replace: PropTypes.func, | |||
| push: PropTypes.func, | |||
| location: PropTypes.shape({ | |||
| pathname: PropTypes.string, | |||
| }), | |||
| }), | |||
| }; | |||
| export default LoginPage; | |||
| @@ -1,6 +1,7 @@ | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| ErrorMessage, | |||
| FormContainer, | |||
| RegisterDescription, | |||
| } from "./FirstPartOfRegistration.styled"; | |||
| @@ -20,7 +21,6 @@ const FirstPartOfRegistration = (props) => { | |||
| const { t } = useTranslation(); | |||
| useEffect(() => { | |||
| console.log(props.error); | |||
| if (props.error.length > 0) { | |||
| setEmailTakenStatus(true); | |||
| } | |||
| @@ -32,8 +32,12 @@ const FirstPartOfRegistration = (props) => { | |||
| password: "", | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| mail: Yup.string().email().required(t("login.usernameRequired")), | |||
| password: Yup.string().required(t("login.passwordRequired")).min(8), | |||
| mail: Yup.string() | |||
| .email(t("forgotPassword.emailFormat")) | |||
| .required(t("login.usernameRequired")), | |||
| password: Yup.string() | |||
| .required(t("login.passwordRequired")) | |||
| .min(8, t("login.passwordLength")), | |||
| }), | |||
| onSubmit: props.handleSubmit, | |||
| validateOnBlur: true, | |||
| @@ -54,15 +58,22 @@ const FirstPartOfRegistration = (props) => { | |||
| placeholder={t("common.labelEmail")} | |||
| margin="normal" | |||
| value={formik.values.mail} | |||
| onChange={(value) => | |||
| formik.setFieldValue("mail", value.target.value) | |||
| onChange={(value) => formik.setFieldValue("mail", value.target.value)} | |||
| error={ | |||
| (formik.touched.mail && Boolean(formik.errors.mail)) || | |||
| emailTakenStatus | |||
| } | |||
| error={(formik.touched.mail && Boolean(formik.errors.mail)) || emailTakenStatus} | |||
| helperText={formik.touched.mail && formik.errors.mail} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| {formik.errors.mail && formik.touched.mail ? ( | |||
| <ErrorMessage>{formik.errors.mail}</ErrorMessage> | |||
| ) : ( | |||
| <></> | |||
| )} | |||
| <TextField | |||
| name="password" | |||
| placeholder={t("common.labelPassword")} | |||
| @@ -72,14 +83,8 @@ const FirstPartOfRegistration = (props) => { | |||
| onChange={(value) => | |||
| formik.setFieldValue("password", value.target.value) | |||
| } | |||
| error={ | |||
| formik.touched.password && | |||
| Boolean(formik.errors.password) | |||
| } | |||
| helperText={ | |||
| formik.touched.password && | |||
| formik.errors.password | |||
| } | |||
| error={formik.touched.password && Boolean(formik.errors.password)} | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| @@ -89,12 +94,18 @@ const FirstPartOfRegistration = (props) => { | |||
| ), | |||
| }} | |||
| /> | |||
| {formik.errors.password && formik.touched.password ? ( | |||
| <ErrorMessage>{formik.errors.password}</ErrorMessage> | |||
| ) : ( | |||
| <></> | |||
| )} | |||
| {props.error && <ErrorMessage>{props.errorMessage}</ErrorMessage>} | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={ | |||
| @@ -113,6 +124,7 @@ FirstPartOfRegistration.propTypes = { | |||
| children: PropTypes.node, | |||
| handleSubmit: PropTypes.func, | |||
| error: PropTypes.string, | |||
| errorMessage: PropTypes.string, | |||
| }; | |||
| export default FirstPartOfRegistration; | |||
| @@ -19,4 +19,15 @@ export const RegisterDescription = styled(Typography)` | |||
| margin-top: 31px; | |||
| margin-bottom: 2px; | |||
| letter-spacing: 0.02em; | |||
| @media (max-height: 800px) { | |||
| margin-top: 14px; | |||
| } | |||
| `; | |||
| export const ErrorMessage = styled(Typography)` | |||
| color: red; | |||
| font-family: "Open Sans"; | |||
| position: relative; | |||
| top: -7px; | |||
| font-size: 14px; | |||
| ` | |||
| @@ -27,32 +27,42 @@ const Register = () => { | |||
| const history = useHistory(); | |||
| const dispatch = useDispatch(); | |||
| const [currentStep, setCurrentStep] = useState(1); | |||
| const [informations, setInformations] = useState({}); | |||
| const [mailError, setMailError] = useState(""); | |||
| const [PIBError, setPIBError] = useState(""); | |||
| const [informations, setInformations] = useState({}); // Values of fields typed in all steps | |||
| const [mailError, setMailError] = useState(""); // Wrong mail typed | |||
| const [mailErrorMessage, setMailErrorMessage] = useState(""); // Error message caused by typing wrong mail | |||
| const [PIBError, setPIBError] = useState(""); // Wrong PIB typed | |||
| const [PIBErrorMessage, setPIBErrorMessage] = useState(""); // Error message caused by typing wrong PIB | |||
| const handleResponseSuccess = () => { | |||
| history.push(REGISTER_SUCCESSFUL_PAGE); | |||
| }; | |||
| const handleResponseError = (error) => { | |||
| console.log("handleResponse: ", error); | |||
| if (error.type === "mail") { | |||
| const { mail } = informations; | |||
| setInformations({}); | |||
| setCurrentStep(1); | |||
| setMailError(mail); | |||
| if ( | |||
| error.error.response.data.toString() === | |||
| "User with email already exists" | |||
| ) { | |||
| setMailErrorMessage(t("register.emailTaken")); | |||
| } else { | |||
| setMailErrorMessage(t("register.emailFormat")); | |||
| } | |||
| } else { | |||
| const { mail, password, PIB } = informations; | |||
| setInformations({ mail, password }); | |||
| setCurrentStep(2); | |||
| setPIBError(PIB.toString()); | |||
| setPIBErrorMessage(t("register.PIBTaken")); | |||
| } | |||
| }; | |||
| const registerUser = (values) => { | |||
| dispatch( | |||
| fetchRegisterUser({values, handleResponseSuccess, handleResponseError}) | |||
| fetchRegisterUser({ values, handleResponseSuccess, handleResponseError }) | |||
| ); | |||
| }; | |||
| @@ -64,8 +74,20 @@ const Register = () => { | |||
| } | |||
| setInformations({ ...informations, ...values }); | |||
| }; | |||
| const goStepBack = (stepNumber) => { | |||
| setCurrentStep(stepNumber); | |||
| if (stepNumber === 1) { | |||
| setInformations({}); | |||
| } | |||
| if (stepNumber === 2) { | |||
| const { mail, password } = informations; | |||
| setInformations({ mail, password }); | |||
| } | |||
| }; | |||
| return ( | |||
| <RegisterPageContainer> | |||
| <RegisterPageContainer currentstep={currentStep}> | |||
| <Logo /> | |||
| <RegisterTitle component="h1" variant="h5"> | |||
| @@ -77,19 +99,25 @@ const Register = () => { | |||
| </RegisterDescription> | |||
| <ProgressContainer> | |||
| <StepProgress current={currentStep} numberOfSteps={3} /> | |||
| <StepProgress | |||
| functions={[() => goStepBack(1), () => goStepBack(2)]} | |||
| current={currentStep} | |||
| numberOfSteps={3} | |||
| /> | |||
| </ProgressContainer> | |||
| {currentStep === 1 && ( | |||
| <FirstPartOfRegistration | |||
| handleSubmit={handleSubmit} | |||
| error={mailError} | |||
| errorMessage={mailErrorMessage} | |||
| /> | |||
| )} | |||
| {currentStep === 2 && ( | |||
| <SecondPartOfRegistration | |||
| handleSubmit={handleSubmit} | |||
| error={PIBError} | |||
| errorMessage={PIBErrorMessage} | |||
| /> | |||
| )} | |||
| {currentStep === 3 && ( | |||
| @@ -9,19 +9,33 @@ export const RegisterPageContainer = styled(Container)` | |||
| align-items: center; | |||
| width: 335px; | |||
| padding: 0; | |||
| flex: 1; | |||
| position: relative; | |||
| @media (max-height: 900px) { | |||
| margin-top: 60px; | |||
| } | |||
| @media (max-height: 800px) { | |||
| margin-top: 30px; | |||
| flex: none; | |||
| height: 95vh; | |||
| ${props => props.currentstep === 3 && `height: 105vh`}; | |||
| ${props => props.currentstep === 2 && `height: 100vh`}; | |||
| } | |||
| `; | |||
| export const RegisterTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| width: 328px; | |||
| height: 33px; | |||
| text-align: center; | |||
| flex: 1; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 24px; | |||
| line-height: 33px; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| margin-top: 34px; | |||
| @media (max-height: 800px) { | |||
| margin-top: 26px; | |||
| } | |||
| `; | |||
| export const RegisterDescription = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| @@ -36,6 +50,10 @@ export const RegisterDescription = styled(Typography)` | |||
| text-align: center; | |||
| color: ${selectedTheme.primaryGrayText}; | |||
| margin-bottom: 20px; | |||
| @media (max-height: 800px) { | |||
| margin-bottom: 14px; | |||
| margin-top: 6px; | |||
| } | |||
| `; | |||
| export const FormContainer = styled(Box)` | |||
| width: 335px; | |||
| @@ -52,6 +70,9 @@ export const LoginTextContainer = styled(Box)` | |||
| flex-direction: row; | |||
| margin-top: 36px; | |||
| justify-content: center; | |||
| @media (max-height: 800px) { | |||
| margin-top: 26px; | |||
| } | |||
| ` | |||
| export const ProgressContainer = styled(Container)` | |||
| width: 100%; | |||
| @@ -64,6 +85,9 @@ export const Footer = styled(Box)` | |||
| width: 100%; | |||
| flex-direction: row; | |||
| justify-content: center; | |||
| @media (max-height: 800px) { | |||
| bottom: 10px; | |||
| } | |||
| ` | |||
| export const FooterText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| @@ -75,12 +99,3 @@ export const FooterText = styled(Typography)` | |||
| padding: 0; | |||
| font-size: 12px; | |||
| ` | |||
| export const RegisterDescriptionPart = styled(RegisterDescription)` | |||
| font-size: 12px; | |||
| width: 100%; | |||
| text-align: left; | |||
| line-height: 16px; | |||
| margin-top: 31px; | |||
| margin-bottom: 2px; | |||
| letter-spacing: 0.02em; | |||
| ` | |||
| @@ -1,6 +1,7 @@ | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| ErrorMessage, | |||
| FormContainer, | |||
| RegisterDescription, | |||
| } from "./SecondPartOfRegistration.styled"; | |||
| @@ -19,7 +20,7 @@ const SecondPartOfRegistration = (props) => { | |||
| if (props.error.length > 0) { | |||
| setPIBTakenStatus(true); | |||
| } | |||
| }, [props.error]) | |||
| }, [props.error]); | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| @@ -28,7 +29,10 @@ const SecondPartOfRegistration = (props) => { | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| nameOfFirm: Yup.string().required(t("login.usernameRequired")), | |||
| PIB: Yup.number().required(t("login.passwordRequired")).min(100000000).max(999999999), | |||
| PIB: Yup.number() | |||
| .required(t("login.passwordRequired")) | |||
| .min(100000000, "PIB mora imati 9 karaktera!") | |||
| .max(999999999, "PIB mora imati 9 karaktera!"), | |||
| }), | |||
| onSubmit: props.handleSubmit, | |||
| validateOnBlur: true, | |||
| @@ -59,16 +63,22 @@ const SecondPartOfRegistration = (props) => { | |||
| type="number" | |||
| value={formik.values.PIB} | |||
| onChange={formik.handleChange} | |||
| error={(formik.touched.PIB && Boolean(formik.errors.PIB)) || PIBTakenStatus} | |||
| error={ | |||
| (formik.touched.PIB && Boolean(formik.errors.PIB)) || PIBTakenStatus | |||
| } | |||
| helperText={formik.touched.PIB && formik.errors.PIB} | |||
| fullWidth={true} | |||
| fullWidth | |||
| /> | |||
| {formik.errors.PIB && formik.touched.PIB && ( | |||
| <ErrorMessage>{formik.errors.PIB}</ErrorMessage> | |||
| )} | |||
| {props.error && <ErrorMessage>{props.errorMessage}</ErrorMessage>} | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={ | |||
| @@ -87,6 +97,7 @@ SecondPartOfRegistration.propTypes = { | |||
| children: PropTypes.node, | |||
| handleSubmit: PropTypes.func, | |||
| error: PropTypes.string, | |||
| errorMessage: PropTypes.string, | |||
| }; | |||
| export default SecondPartOfRegistration; | |||
| @@ -20,4 +20,14 @@ export const RegisterDescription = styled(Typography)` | |||
| margin-top: 31px; | |||
| margin-bottom: 2px; | |||
| letter-spacing: 0.02em; | |||
| @media (max-height: 800px) { | |||
| margin-top: 14px; | |||
| } | |||
| `; | |||
| export const ErrorMessage = styled(Typography)` | |||
| color: red; | |||
| font-family: "Open Sans"; | |||
| position: relative; | |||
| top: -7px; | |||
| font-size: 14px; | |||
| ` | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useState } from "react"; | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| FormContainer, | |||
| @@ -13,16 +13,6 @@ import selectedTheme from "../../../../themes"; | |||
| const ThirdPartOfRegistration = (props) => { | |||
| const { t } = useTranslation(); | |||
| const [phoneNumberTakenStatus, setPhoneNumberTakenStatus] = useState(false); | |||
| const handleForm = (values) => { | |||
| // validate email | |||
| if (true) { // eslint-disable-line | |||
| props.handleSubmit(values) | |||
| } else { | |||
| setPhoneNumberTakenStatus(true); | |||
| } | |||
| } | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| @@ -37,7 +27,7 @@ const ThirdPartOfRegistration = (props) => { | |||
| /^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm | |||
| ), | |||
| }), | |||
| onSubmit: handleForm, | |||
| onSubmit: props.handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| @@ -55,7 +45,9 @@ const ThirdPartOfRegistration = (props) => { | |||
| type="number" | |||
| value={formik.values.phoneNumber} | |||
| onChange={formik.handleChange} | |||
| error={(formik.touched.phoneNumber && Boolean(formik.errors.phoneNumber)) || phoneNumberTakenStatus} | |||
| error={ | |||
| (formik.touched.phoneNumber && Boolean(formik.errors.phoneNumber)) | |||
| } | |||
| helperText={formik.touched.phoneNumber && formik.errors.phoneNumber} | |||
| autoFocus | |||
| fullWidth | |||
| @@ -70,7 +62,7 @@ const ThirdPartOfRegistration = (props) => { | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.location && Boolean(formik.errors.location)} | |||
| helperText={formik.touched.location && formik.errors.location} | |||
| fullWidth={true} | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| @@ -82,14 +74,14 @@ const ThirdPartOfRegistration = (props) => { | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.website && Boolean(formik.errors.website)} | |||
| helperText={formik.touched.website && formik.errors.website} | |||
| fullWidth={true} | |||
| fullWidth | |||
| /> | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={ | |||
| @@ -19,4 +19,7 @@ export const RegisterDescription = styled(Typography)` | |||
| margin-top: 31px; | |||
| margin-bottom: 2px; | |||
| letter-spacing: 0.02em; | |||
| @media (max-height: 800px) { | |||
| margin-top: 14px; | |||
| } | |||
| `; | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useState } from "react"; | |||
| import React, { useEffect, useState } from "react"; | |||
| import { useFormik } from "formik"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import * as Yup from "yup"; | |||
| @@ -13,16 +13,16 @@ import { | |||
| } from "./ResetPasswordPage.styled"; | |||
| import { TextField } from "../../components/TextFields/TextField/TextField"; | |||
| import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { LOGIN_PAGE } from "../../constants/pages"; | |||
| import { useHistory, useRouteMatch } from "react-router-dom"; | |||
| import { LOGIN_PAGE } from "../../constants/pages"; | |||
| import selectedTheme from "../../themes"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { resetPassword } from "../../store/actions/user/userActions"; | |||
| import { resetPassword } from "../../store/actions/user/userActions"; | |||
| import { Trans } from "react-i18next"; | |||
| import { ReactComponent as VisibilityOn } from "../../assets/images/svg/eye-striked.svg"; | |||
| import { ReactComponent as VisibilityOff } from "../../assets/images/svg/eye.svg"; | |||
| import { IconButton } from "../../components/Buttons/IconButton/IconButton"; | |||
| import jwt from "jsonwebtoken"; | |||
| const ResetPasswordPage = () => { | |||
| const history = useHistory(); | |||
| @@ -30,6 +30,24 @@ const ResetPasswordPage = () => { | |||
| const dispatch = useDispatch(); | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const [showPasswordConfirm, setShowPasswordConfirm] = useState(false); | |||
| const [error, setError] = useState(false); | |||
| const [token, setToken] = useState(""); | |||
| const routeMatch = useRouteMatch(); | |||
| useEffect(() => { | |||
| const tokenFromParams = routeMatch.params.token | |||
| setToken(tokenFromParams); | |||
| const data = jwt.decode(tokenFromParams); | |||
| if (!data || new Date() > new Date(data?.exp * 1000)) { | |||
| setError(true); | |||
| } | |||
| }, []); | |||
| useEffect(() => { | |||
| if (error) { | |||
| history.replace("/"); | |||
| } | |||
| }, [error]); | |||
| const handleClickShowPassword = () => { | |||
| setShowPassword((prevState) => !prevState); | |||
| @@ -37,21 +55,18 @@ const ResetPasswordPage = () => { | |||
| const handleClickShowPasswordConfirm = () => { | |||
| setShowPasswordConfirm((prevState) => !prevState); | |||
| }; | |||
| const handleResponseSuccess = () => { | |||
| history.push(LOGIN_PAGE); | |||
| } | |||
| const handleResponseError = () => { | |||
| console.log("error"); | |||
| } | |||
| }; | |||
| const handleSubmit = (values) => { | |||
| // validate email | |||
| dispatch( | |||
| resetPassword({ | |||
| token: token, | |||
| password: values.password, | |||
| password2: values.passwordConfirm, | |||
| handleResponseSuccess, | |||
| handleResponseError | |||
| }) | |||
| ); | |||
| }; | |||
| @@ -110,8 +125,13 @@ const ResetPasswordPage = () => { | |||
| type={showPasswordConfirm ? "text" : "password"} | |||
| value={formik.values.passwordConfirm} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.passwordConfirm && formik.errors.passwordConfirm?.length > 0} | |||
| helperText={formik.touched.passwordConfirm && formik.errors.passwordConfirm} | |||
| error={ | |||
| formik.touched.passwordConfirm && | |||
| formik.errors.passwordConfirm?.length > 0 | |||
| } | |||
| helperText={ | |||
| formik.touched.passwordConfirm && formik.errors.passwordConfirm | |||
| } | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| @@ -126,7 +146,7 @@ const ResetPasswordPage = () => { | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth={true} | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={ | |||
| @@ -7,6 +7,12 @@ export const ResetPasswordPageContainer = styled(Container)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @media (max-height: 900px) { | |||
| margin-top: 140px; | |||
| } | |||
| @media (max-height: 800px) { | |||
| margin-top: 70px; | |||
| } | |||
| `; | |||
| export const ResetPasswordTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| @@ -2,7 +2,7 @@ export default { | |||
| accounts: { | |||
| get: 'accounts/{accountUid}', | |||
| forgotPassword: 'forgot-password', | |||
| resetPassword: 'resetPassword', | |||
| resetPassword: 'reset-password', | |||
| getCurrentUserPermissions: | |||
| 'accounts/{currentAccountUid}/users/{currentUserUid}/permissions', | |||
| getAddresses: 'accounts/{accountUid}/addresses', | |||
| @@ -20,11 +20,13 @@ export default { | |||
| authentications: { | |||
| getUsernames: 'authenticate/usernames', | |||
| login: 'auth/token', | |||
| logout: 'auth/logout', | |||
| getUserSecurityQuestion: 'users/username/securityquestion', | |||
| confirmSecurityQuestion: 'authenticate/confirm', | |||
| confirmForgotPassword: 'users/passwords/reset_token', | |||
| resetPassword: 'users/passwords', | |||
| refreshToken: '/authenticate/refresh', | |||
| resetPassword: 'reset-password', | |||
| forgotPassword: 'forgot-password', | |||
| refreshToken: '/auth/refresh', | |||
| generateToken: '/authenticate/generate', | |||
| authenticate: | |||
| '/authenticate?fp={fp}&offer={offer}&landingPageUrl={landingPageUrl}®istrationFlowType={registrationFlowType}', | |||
| @@ -156,6 +158,7 @@ export default { | |||
| setFingerprint: '/affiliate/fingerprint', | |||
| }, | |||
| offers: { | |||
| getOffers: 'offers' | |||
| getOffers: 'offers', | |||
| addOffer: 'offers', | |||
| } | |||
| }; | |||
| @@ -1,8 +1,15 @@ | |||
| import { postRequest } from "."; | |||
| import { postRequest, getRequest } from "."; | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const forgotPasswordRequest = (payload) => | |||
| postRequest(apiEndpoints.accounts.forgotPassword, payload); | |||
| export const forgotPasswordRequest = (payload) => { | |||
| const encodedMail = encodeURIComponent(payload); | |||
| return getRequest( | |||
| `${apiEndpoints.authentications.forgotPassword}/${encodedMail}` | |||
| ); | |||
| }; | |||
| export const resetPasswordRequest = (payload) => | |||
| postRequest(apiEndpoints.accounts.resetPassword, payload); | |||
| postRequest( | |||
| `${apiEndpoints.authentications.resetPassword}/${payload.token}`, | |||
| { password: payload.password, password2: payload.password2 } | |||
| ); | |||
| @@ -1,14 +1,14 @@ | |||
| import axios from 'axios'; | |||
| import queryString from 'qs'; | |||
| import axios from "axios"; | |||
| import queryString from "qs"; | |||
| const request = axios.create({ | |||
| baseURL: "http://192.168.88.175:3005/", | |||
| baseURL: "http://192.168.88.150:3001/", | |||
| headers: { | |||
| 'Content-Type': 'application/json', | |||
| "Content-Type": "application/json", | |||
| }, | |||
| // withCredentials: true, | |||
| paramsSerializer: (params) => | |||
| queryString.stringify(params, { arrayFormat: 'comma' }), | |||
| queryString.stringify(params, { arrayFormat: "comma" }), | |||
| }); | |||
| export const getRequest = (url, params = null, options = null) => | |||
| @@ -27,7 +27,7 @@ export const deleteRequest = (url, params = null, options = null) => | |||
| request.delete(url, { params, ...options }); | |||
| export const downloadRequest = (url, params = null, options = null) => | |||
| request.get(url, { params, ...options, responseType: 'blob' }); | |||
| request.get(url, { params, ...options, responseType: "blob" }); | |||
| export const replaceInUrl = (url, pathVariables = {}) => { | |||
| const keys = Object.keys(pathVariables); | |||
| @@ -37,12 +37,11 @@ export const replaceInUrl = (url, pathVariables = {}) => { | |||
| return keys.reduce( | |||
| (acc, key) => acc.replace(`{${key}}`, pathVariables[`${key}`]), | |||
| url, | |||
| url | |||
| ); | |||
| }; | |||
| export const addHeaderToken = (token) => { | |||
| console.log(`Bearer ${token.toString()}`) | |||
| request.defaults.headers.Authorization = `Bearer ${token}`; | |||
| }; | |||
| @@ -54,11 +53,64 @@ export const removeHeaderToken = () => { | |||
| delete request.defaults.headers.Authorization; | |||
| }; | |||
| export const attachPostRequestListener = (postRequestListener) => { | |||
| request.interceptors.response.use( | |||
| // If you pass function to interceptor of axios, it only adds that function | |||
| // to existing array of interceptor functions. That causes that same function | |||
| // of interceptors getting called multiple times instead of just one time, as it | |||
| // is supposed to do. Thats why there is 'global' axios interceptor array, which indicates | |||
| // axios to eject previous interceptor. This approach requires that every middleware has its | |||
| // unique name from which it is being recognized. Every object in those arrays contains | |||
| // interceptor name and ID of interceptor function. | |||
| let axiosInterceptorRequests = []; | |||
| let axiosInterceptorResponses = []; | |||
| export const attachPostRequestListener = ( | |||
| postRequestListener, | |||
| interceptorName | |||
| ) => { | |||
| let previousAxiosInterceptor = axiosInterceptorResponses.find( | |||
| (item) => item.name === interceptorName | |||
| ); | |||
| let previousAxiosInterceptorResponses = axiosInterceptorResponses; | |||
| if (previousAxiosInterceptor !== undefined) { | |||
| request.interceptors.response.eject(previousAxiosInterceptor.interceptorID); | |||
| previousAxiosInterceptorResponses = axiosInterceptorResponses.filter( | |||
| (item) => item.interceptorID !== previousAxiosInterceptor.interceptorID | |||
| ); | |||
| } | |||
| let axiosInterceptorID = request.interceptors.response.use( | |||
| (response) => response, | |||
| (response) => postRequestListener(response), | |||
| (response) => postRequestListener(response) | |||
| ); | |||
| previousAxiosInterceptorResponses.push({ | |||
| name: interceptorName, | |||
| interceptorID: axiosInterceptorID, | |||
| }); | |||
| axiosInterceptorResponses = [...previousAxiosInterceptorResponses]; | |||
| }; | |||
| export const attachBeforeRequestListener = ( | |||
| beforeRequestListener, | |||
| interceptorName | |||
| ) => { | |||
| let previousAxiosInterceptor = axiosInterceptorRequests.find( | |||
| (item) => item.name === interceptorName | |||
| ); | |||
| let previousAxiosInterceptorRequests = axiosInterceptorRequests; | |||
| if (previousAxiosInterceptor !== undefined) { | |||
| request.interceptors.request.eject(previousAxiosInterceptor.interceptorID); | |||
| previousAxiosInterceptorRequests = axiosInterceptorRequests.filter( | |||
| (item) => item.interceptorID !== previousAxiosInterceptor.interceptorID | |||
| ); | |||
| } | |||
| let axiosInterceptorID = request.interceptors.request.use( | |||
| (response) => beforeRequestListener(response), | |||
| (response) => response | |||
| ); | |||
| previousAxiosInterceptorRequests.push({ | |||
| name: interceptorName, | |||
| interceptorID: axiosInterceptorID, | |||
| }); | |||
| axiosInterceptorRequests = [...previousAxiosInterceptorRequests]; | |||
| }; | |||
| export const apiDefaultUrl = request.defaults.baseURL; | |||
| @@ -1,22 +1,10 @@ | |||
| import { getRequest, postRequest } from "./index"; | |||
| import { postRequest } from "./index"; | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const getUsernames = (emailorusername) => | |||
| getRequest(apiEndpoints.authentications.getUsernames, { | |||
| emailorusername, | |||
| }); | |||
| export const attemptLogin = (payload) => | |||
| postRequest(apiEndpoints.authentications.login, payload); | |||
| export const updateSecurityAnswer = (payload) => | |||
| postRequest(apiEndpoints.authentications.confirmSecurityQuestion, payload); | |||
| export const refreshTokenRequest = (payload) => | |||
| postRequest(apiEndpoints.authentications.refreshToken, payload); | |||
| export const logoutUserRequest = (payload) => | |||
| postRequest(apiEndpoints.users.logout, payload); | |||
| postRequest(apiEndpoints.authentications.logout, payload); | |||
| export const generateTokenRequest = (payload) => | |||
| postRequest(apiEndpoints.authentications.generateToken, payload); | |||
| @@ -1,6 +1,9 @@ | |||
| import { getRequest } from "." | |||
| import { getRequest, postRequest } from "." | |||
| import apiEndpoints from "./apiEndpoints" | |||
| export const attemptFetchOffers = () => { | |||
| return getRequest(apiEndpoints.offers.getOffers) | |||
| } | |||
| export const attemptAddOffer = (payload) => { | |||
| return postRequest(apiEndpoints.offers.addOffer, payload) | |||
| } | |||
| @@ -50,8 +50,9 @@ export const logoutUser = () => ({ | |||
| type: LOGOUT_USER, | |||
| }); | |||
| export const refreshUserToken = () => ({ | |||
| export const refreshUserToken = (payload) => ({ | |||
| type: REFRESH_TOKEN, | |||
| payload | |||
| }); | |||
| export const generateToken = (payload) => ({ | |||
| @@ -7,4 +7,6 @@ export const OFFERS_SUCCESS = createSuccessType(OFFERS_SCOPE); | |||
| export const OFFERS_ERROR = createErrorType(OFFERS_SCOPE); | |||
| export const OFFERS_CLEAR = createClearType(OFFERS_SCOPE); | |||
| export const OFFERS_SET = "OFFERS_SET"; | |||
| export const OFFERS_SET = "OFFERS_SET"; | |||
| export const OFFERS_ADD = "OFFERS_ADD"; | |||
| export const OFFER_ADD = "OFFER_ADD"; | |||
| @@ -1,4 +1,4 @@ | |||
| import { OFFERS_CLEAR, OFFERS_ERROR, OFFERS_FETCH, OFFERS_SET, OFFERS_SUCCESS } from "./offersActionConstants"; | |||
| import { OFFERS_ADD, OFFERS_CLEAR, OFFERS_ERROR, OFFERS_FETCH, OFFERS_SET, OFFERS_SUCCESS, OFFER_ADD } from "./offersActionConstants"; | |||
| export const fetchOffers = (payload) => ({ | |||
| type: OFFERS_FETCH, | |||
| @@ -18,4 +18,12 @@ export const clearOffers = () => ({ | |||
| export const setOffers = (payload) => ({ | |||
| type: OFFERS_SET, | |||
| payload, | |||
| }) | |||
| export const addOffers = (payload) => ({ | |||
| type: OFFERS_ADD, | |||
| payload | |||
| }) | |||
| export const addOffer = (payload) => ({ | |||
| type: OFFER_ADD, | |||
| payload | |||
| }) | |||
| @@ -2,4 +2,6 @@ | |||
| export const SET_USER = 'SET_USER'; | |||
| export const SET_USER_ERROR = 'SET_USER_ERROR'; | |||
| export const FORGOT_PASSWORD = "FORGOT_PASSWORD"; | |||
| export const RESET_PASSWORD = "RESET_PASSWORD"; | |||
| export const RESET_PASSWORD = "RESET_PASSWORD"; | |||
| export const SET_USER_ACCESS_TOKEN = "SET_USER_ACCESS_TOKEN"; | |||
| export const SET_USER_REFRESH_TOKEN = "SET_USER_REFRESH_TOKEN"; | |||
| @@ -2,7 +2,9 @@ import { | |||
| FORGOT_PASSWORD, | |||
| RESET_PASSWORD, | |||
| SET_USER, | |||
| SET_USER_ACCESS_TOKEN, | |||
| SET_USER_ERROR, | |||
| SET_USER_REFRESH_TOKEN, | |||
| } from './userActionConstants'; | |||
| export const setUser = (payload) => ({ | |||
| @@ -21,4 +23,12 @@ export const forgotPassword = (payload) => ({ | |||
| export const resetPassword = (payload) => ({ | |||
| type: RESET_PASSWORD, | |||
| payload | |||
| }) | |||
| export const setUserAccessToken = (payload) => ({ | |||
| type: SET_USER_ACCESS_TOKEN, | |||
| payload | |||
| }) | |||
| export const setUserRefreshToken = (payload) => ({ | |||
| type: SET_USER_REFRESH_TOKEN, | |||
| payload | |||
| }) | |||
| @@ -6,6 +6,7 @@ import loadingMiddleware from './middleware/loadingMiddleware'; | |||
| import requestStatusMiddleware from './middleware/requestStatusMiddleware'; | |||
| import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware'; | |||
| import persistStore from 'redux-persist/es/persistStore'; | |||
| import accessTokensMiddleware from './middleware/accessTokensMiddleware'; | |||
| const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; | |||
| @@ -18,6 +19,7 @@ export const store = createStore( | |||
| loadingMiddleware, | |||
| requestStatusMiddleware, | |||
| internalServerErrorMiddleware, | |||
| accessTokensMiddleware | |||
| ), | |||
| ), | |||
| ); | |||
| @@ -0,0 +1,50 @@ | |||
| import axios from "axios"; | |||
| import jwt from "jsonwebtoken"; | |||
| import { JWT_REFRESH_TOKEN, JWT_TOKEN } from "../../constants/localStorage"; | |||
| import { | |||
| // addHeaderToken, | |||
| attachBeforeRequestListener, | |||
| } from "../../request/index"; | |||
| import { | |||
| authScopeStringGetHelper, | |||
| // authScopeSetHelper, | |||
| } from "../../util/helpers/authScopeHelpers"; | |||
| import { logoutUser, refreshUserToken } from "../actions/login/loginActions"; | |||
| // import { setUserAccessToken } from "../actions/user/userActions"; | |||
| //Change URL with .env | |||
| const baseURL = "http://192.168.88.150:3001/"; | |||
| //Interceptor unique name | |||
| export const accessTokensMiddlewareInterceptorName = "ACCESS_TOKEN_INTERCEPTOR"; | |||
| export default ({ dispatch }) => | |||
| (next) => | |||
| (action) => { | |||
| attachBeforeRequestListener(async (response) => { | |||
| const jwtToken = authScopeStringGetHelper(JWT_TOKEN); | |||
| const refresh = authScopeStringGetHelper(JWT_REFRESH_TOKEN); | |||
| if (!jwtToken || !refresh) return Promise.resolve(response); | |||
| if (!response.headers?.Authorization) { | |||
| response.headers.Authorization = `Bearer ${jwtToken}`; | |||
| } | |||
| const jwtTokenDecoded = jwt.decode(jwtToken); | |||
| const refreshTokenDecoded = jwt.decode(refresh); | |||
| // If refresh token is expired, log out user | |||
| if (new Date() > new Date(refreshTokenDecoded?.exp * 1000)) { | |||
| dispatch(logoutUser()); | |||
| } | |||
| // If access token is expired, refresh access token | |||
| if (new Date() > new Date(jwtTokenDecoded.exp * 1000)) { | |||
| const axiosResponse = await axios.post(`${baseURL}auth/refresh`, { | |||
| token: refresh, | |||
| }); | |||
| const newToken = axiosResponse.data.token; | |||
| dispatch(refreshUserToken(newToken)); | |||
| } | |||
| return Promise.resolve(response); | |||
| }, accessTokensMiddlewareInterceptorName); | |||
| next(action); | |||
| }; | |||
| @@ -2,6 +2,9 @@ import { ERROR_PAGE } from '../../constants/pages'; | |||
| import { attachPostRequestListener } from '../../request'; | |||
| import history from '../utils/history'; | |||
| //Interceptor unique name | |||
| export const serverErrorMiddlewareInterceptorName = "INTERNAL_SERVER_ERROR_MIDDLEWARE_INTERCEPTOR"; | |||
| export default () => (next) => (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| @@ -11,7 +14,7 @@ export default () => (next) => (action) => { | |||
| return history.push(ERROR_PAGE); | |||
| } | |||
| return Promise.reject(error); | |||
| }); | |||
| }, serverErrorMiddlewareInterceptorName); | |||
| next(action); | |||
| }; | |||
| @@ -5,37 +5,41 @@ import { | |||
| SUCCESS, | |||
| UPDATE, | |||
| SUBMIT, | |||
| } from '../actions/actionHelpers'; | |||
| } from "../actions/actionHelpers"; | |||
| const promiseTypes = [FETCH, UPDATE, DELETE, SUBMIT]; | |||
| export default ({ dispatch }) => (next) => (action) => { | |||
| const promiseType = promiseTypes.find((promiseType) => | |||
| action.type.includes(promiseType), | |||
| ); | |||
| if (promiseType) { | |||
| dispatch({ | |||
| type: 'UPDATE_LOADER', | |||
| payload: { | |||
| actionType: action.type.replace(promiseType, '[LOADING]'), | |||
| isLoading: true, | |||
| }, | |||
| }); | |||
| return next(action); | |||
| } | |||
| if (action.type.includes(SUCCESS) || action.type.includes(ERROR)) { | |||
| const actionType = action.type.includes(SUCCESS) | |||
| ? action.type.replace(SUCCESS, '[LOADING]') | |||
| : action.type.replace(ERROR, '[LOADING]'); | |||
| dispatch({ | |||
| type: 'UPDATE_LOADER', | |||
| payload: { | |||
| actionType, | |||
| isLoading: false, | |||
| }, | |||
| }); | |||
| return next(action); | |||
| } | |||
| next(action); | |||
| }; | |||
| export default ({ dispatch }) => | |||
| (next) => | |||
| (action) => { | |||
| const promiseType = promiseTypes.find((promiseType) => | |||
| action.type.includes(promiseType) | |||
| ); | |||
| if (promiseType) { | |||
| dispatch({ | |||
| type: "UPDATE_LOADER", | |||
| payload: { | |||
| actionType: action.type.replace(promiseType, "[LOADING]"), | |||
| isLoading: true, | |||
| }, | |||
| }); | |||
| return next(action); | |||
| } | |||
| if (action.type.includes(SUCCESS) || action.type.includes(ERROR)) { | |||
| const actionType = action.type.includes(SUCCESS) | |||
| ? action.type.replace(SUCCESS, "[LOADING]") | |||
| : action.type.replace(ERROR, "[LOADING]"); | |||
| dispatch({ | |||
| type: "UPDATE_LOADER", | |||
| payload: { | |||
| actionType, | |||
| isLoading: false, | |||
| }, | |||
| }); | |||
| return next(action); | |||
| } | |||
| next(action); | |||
| }; | |||
| @@ -2,6 +2,9 @@ import { attachPostRequestListener } from '../../request'; | |||
| import apiEndpoints from '../../request/apiEndpoints'; | |||
| import { logoutUser } from '../actions/login/loginActions'; | |||
| // Interceptor unique name | |||
| export const requestStatusMiddlewareInterceptorName = "REQUEST_STATUS_MIDDLEWARE_INTERCEPTOR"; | |||
| export default ({ dispatch }) => (next) => (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| @@ -16,7 +19,7 @@ export default ({ dispatch }) => (next) => (action) => { | |||
| return dispatch(logoutUser()); | |||
| } | |||
| return Promise.reject(error); | |||
| }); | |||
| }, requestStatusMiddlewareInterceptorName); | |||
| next(action); | |||
| }; | |||
| @@ -14,6 +14,7 @@ const initialState = { | |||
| token: { | |||
| RefreshToken: '', | |||
| JwtToken: '', | |||
| userId: '' | |||
| }, | |||
| errorMessage: '', | |||
| }; | |||
| @@ -1,13 +1,16 @@ | |||
| import { | |||
| OFFERS_ADD, | |||
| OFFERS_CLEAR, | |||
| OFFERS_ERROR, | |||
| OFFERS_SET, | |||
| OFFER_ADD, | |||
| } from "../../actions/offers/offersActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| const initialState = { | |||
| offers: [], | |||
| error: "", | |||
| newOffer: "", | |||
| }; | |||
| export default createReducer( | |||
| @@ -15,6 +18,8 @@ export default createReducer( | |||
| [OFFERS_ERROR]: fetchOffersError, | |||
| [OFFERS_CLEAR]: clearOffers, | |||
| [OFFERS_SET]: setOffers, | |||
| [OFFERS_ADD]: addOffers, | |||
| [OFFER_ADD]: addOffer, | |||
| }, | |||
| initialState | |||
| ); | |||
| @@ -32,3 +37,15 @@ function setOffers(state, action) { | |||
| offers: action.payload, | |||
| }; | |||
| } | |||
| function addOffers(state, action) { | |||
| return { | |||
| ...state, | |||
| offers: [...state.offers, ...action.payload] | |||
| } | |||
| } | |||
| function addOffer(state, action) { | |||
| return { | |||
| ...state, | |||
| offer: action.payload | |||
| } | |||
| } | |||
| @@ -1,17 +1,23 @@ | |||
| import createReducer from '../../utils/createReducer'; | |||
| import { | |||
| SET_USER, | |||
| SET_USER_ACCESS_TOKEN, | |||
| SET_USER_ERROR, | |||
| SET_USER_REFRESH_TOKEN, | |||
| } from '../../actions/user/userActionConstants'; | |||
| const initialState = { | |||
| user: {}, | |||
| accessToken: {}, | |||
| refreshToken: {}, | |||
| userId: {} | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [SET_USER]: setUser, | |||
| [SET_USER_ERROR]: setUserError, | |||
| [SET_USER_ACCESS_TOKEN]: setUserAccessToken, | |||
| [SET_USER_REFRESH_TOKEN]: setUserRefreshToken | |||
| }, | |||
| initialState, | |||
| ); | |||
| @@ -19,7 +25,7 @@ export default createReducer( | |||
| function setUser(state, action) { | |||
| return { | |||
| ...state, | |||
| user: action.payload, | |||
| userId: action.payload, | |||
| }; | |||
| } | |||
| @@ -29,3 +35,15 @@ function setUserError(state, action) { | |||
| errorMessage: action.payload, | |||
| }; | |||
| } | |||
| function setUserAccessToken(state, action) { | |||
| return { | |||
| ...state, | |||
| accessToken: action.payload | |||
| } | |||
| } | |||
| function setUserRefreshToken(state, action) { | |||
| return { | |||
| ...state, | |||
| refreshToken: action.payload | |||
| } | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| // function* setFilters({ payload }) { | |||
| // } | |||
| @@ -4,7 +4,8 @@ import { FORGOT_PASSWORD, RESET_PASSWORD } from "../actions/user/userActionConst | |||
| function* forgotPassword({payload}) { | |||
| try { | |||
| const data = yield call(forgotPasswordRequest); | |||
| console.log(payload) | |||
| const data = yield call(forgotPasswordRequest, payload.email); | |||
| if (data) { | |||
| if (payload.handleResponseSuccess) { | |||
| yield call(payload.handleResponseSuccess); | |||
| @@ -12,7 +13,6 @@ function* forgotPassword({payload}) { | |||
| } | |||
| } | |||
| catch(e) { | |||
| console.log(e); | |||
| if (payload.handleResponseError) { | |||
| yield call(payload.handleResponseError); | |||
| } | |||
| @@ -20,7 +20,11 @@ function* forgotPassword({payload}) { | |||
| } | |||
| function* resetPassword({payload}) { | |||
| try { | |||
| const data = yield call(resetPasswordRequest) | |||
| const data = yield call(resetPasswordRequest, { | |||
| password: payload.password, | |||
| password2: payload.password2, | |||
| token: payload.token | |||
| }) | |||
| if (data) { | |||
| if (payload.handleResponseSuccess) { | |||
| yield call(payload.handleResponseSuccess); | |||
| @@ -28,7 +32,6 @@ function* resetPassword({payload}) { | |||
| } | |||
| } | |||
| catch(e) { | |||
| console.log(e); | |||
| if (payload.handleResponseError) { | |||
| yield call(payload.handleResponseError); | |||
| } | |||
| @@ -6,27 +6,18 @@ import { | |||
| LOGIN_USER_FETCH, | |||
| LOGOUT_USER, | |||
| REFRESH_TOKEN, | |||
| GENERATE_TOKEN, | |||
| } from "../actions/login/loginActionConstants"; | |||
| import { | |||
| attemptLogin, | |||
| logoutUserRequest, | |||
| refreshTokenRequest, | |||
| generateTokenRequest, | |||
| } from "../../request/loginRequest"; | |||
| import { | |||
| fetchUserError, | |||
| fetchUserSuccess, | |||
| resetLoginState, | |||
| updateUserToken, | |||
| } from "../actions/login/loginActions"; | |||
| import { LOGIN_PAGE } from "../../constants/pages"; | |||
| import { setUser } from "../actions/user/userActions"; | |||
| import { addHeaderToken, removeHeaderToken } from "../../request"; | |||
| import { | |||
| IMPERSONATE_USER_UID, | |||
| REGISTRATION_USER_UID, | |||
| } from "../../constants/sessionStorage"; | |||
| import { | |||
| JWT_REFRESH_TOKEN, | |||
| JWT_TOKEN, | |||
| @@ -38,39 +29,46 @@ import { | |||
| authScopeRemoveHelper, | |||
| authScopeSetHelper, | |||
| } from "../../util/helpers/authScopeHelpers"; | |||
| // import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper"; | |||
| import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper"; | |||
| import { setUserAccessToken } from "../actions/user/userActions"; | |||
| import i18next from "i18next"; | |||
| function* fetchUser({ payload }) { | |||
| try { | |||
| const { data } = yield call(attemptLogin, payload); | |||
| console.log(data); | |||
| if (data.tokens) { | |||
| const token = data.tokens[data.tokens.length - 1].token.toString(); | |||
| // cemu sluzi? | |||
| const user = jwt.decode(token); | |||
| console.log("userJWT: ", user); | |||
| if (data.token) { | |||
| const token = data.token; | |||
| const refresh = data.refresh; | |||
| const tokenDecoded = jwt.decode(token); | |||
| const refreshDecoded = jwt.decode(refresh); | |||
| const accessToken = { | |||
| token: token, | |||
| exp: tokenDecoded.exp, | |||
| }; | |||
| const refreshToken = { | |||
| token: refresh, | |||
| exp: refreshDecoded.exp, | |||
| }; | |||
| const userId = tokenDecoded._id; | |||
| yield call(authScopeSetHelper, JWT_TOKEN, token); | |||
| // yield call(authScopeSetHelper, JWT_REFRESH_TOKEN, data.JwtRefreshToken); | |||
| // yield call(authScopeSetHelper, REFRESH_TOKEN_CONST, data.RefreshToken); | |||
| yield call(authScopeSetHelper, JWT_REFRESH_TOKEN, refresh); | |||
| yield call(addHeaderToken, token); | |||
| yield put(setUser(user)); | |||
| } | |||
| yield put(fetchUserSuccess(data)); | |||
| if (payload.handleApiResponseSuccess) { | |||
| yield call(payload.handleApiResponseSuccess); | |||
| yield put(fetchUserSuccess({JwtToken: accessToken, RefreshToken: refreshToken, userId})); | |||
| if (payload.handleApiResponseSuccess) { | |||
| yield call(payload.handleApiResponseSuccess); | |||
| } | |||
| } | |||
| } catch (e) { | |||
| if (e.response && e.response.data) { | |||
| if (payload.handleApiResponseError) { | |||
| yield call(payload.handleApiResponseError); | |||
| yield call(payload.handleApiResponseError, e.response.status); | |||
| } | |||
| let errorMessage = "Greska!"; | |||
| if (e.response.status === 400) { | |||
| errorMessage = "Pogresan mail ili lozinka!"; | |||
| let errorMessage = yield call(rejectErrorCodeHelper, e); | |||
| if (e.response.status === 401) { | |||
| errorMessage = i18next.t("login.wrongCredentials", { | |||
| lng: "rs" | |||
| }); | |||
| } | |||
| // const errorMessage = yield call(rejectErrorCodeHelper, e); | |||
| yield put(fetchUserError(errorMessage)); | |||
| } | |||
| } | |||
| @@ -89,11 +87,10 @@ function* authenticateUser() { | |||
| }) | |||
| ); | |||
| } catch (error) { | |||
| // const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| // yield put(fetchUserError(errorMessage)); | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| yield put(fetchUserError(errorMessage)); | |||
| yield call(authScopeRemoveHelper, JWT_TOKEN); | |||
| // yield call(authScopeRemoveHelper, JWT_REFRESH_TOKEN); | |||
| // yield call(authScopeRemoveHelper, REFRESH_TOKEN_CONST); | |||
| yield call(authScopeRemoveHelper, JWT_REFRESH_TOKEN); | |||
| } | |||
| } | |||
| @@ -101,9 +98,8 @@ function* logoutUser() { | |||
| try { | |||
| const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||
| const user = jwt.decode(JwtToken); | |||
| console.log({token: JwtToken, userId: user._id}) | |||
| if (user) { | |||
| yield call(logoutUserRequest, {token: JwtToken, userId: user._id}); | |||
| yield call(logoutUserRequest, { token: JwtToken, userId: user._id }); | |||
| } | |||
| } catch (error) { | |||
| console.log(error); // eslint-disable-line | |||
| @@ -114,65 +110,15 @@ function* logoutUser() { | |||
| yield call(history.replace, LOGIN_PAGE); | |||
| } | |||
| } | |||
| export function* refreshToken() { | |||
| function* refreshUserToken({payload}) { | |||
| try { | |||
| const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||
| const JwtRefreshToken = yield call( | |||
| authScopeStringGetHelper, | |||
| JWT_REFRESH_TOKEN | |||
| ); | |||
| if (JwtToken && JwtRefreshToken) { | |||
| const { data } = yield call(refreshTokenRequest, { | |||
| JwtRefreshToken, | |||
| JwtToken, | |||
| }); | |||
| yield call(authScopeSetHelper, JWT_TOKEN, data.JwtToken); | |||
| yield call(authScopeSetHelper, JWT_REFRESH_TOKEN, data.JwtRefreshToken); | |||
| const user = jwt.decode(data.JwtToken); | |||
| addHeaderToken(data.JwtToken); | |||
| yield put(setUser(user)); | |||
| yield put(updateUserToken(data.JwtToken)); | |||
| } | |||
| } catch (error) { | |||
| yield call(logoutUser); | |||
| console.log(error); // eslint-disable-line | |||
| const newTokenDecoded = jwt.decode(payload); | |||
| yield put(setUserAccessToken({token: payload, exp: newTokenDecoded.exp})); | |||
| yield call(addHeaderToken, payload); | |||
| yield call(authScopeSetHelper, JWT_TOKEN, payload); | |||
| } | |||
| } | |||
| export function* generateToken({ payload }) { | |||
| try { | |||
| const { data } = yield call(generateTokenRequest, payload.data); | |||
| const { JwtToken, JwtRefreshToken } = data; | |||
| if (JwtToken && JwtRefreshToken) { | |||
| yield call(authScopeSetHelper, JWT_TOKEN, data.JwtToken); | |||
| yield call(authScopeSetHelper, JWT_REFRESH_TOKEN, data.JwtRefreshToken); | |||
| if (payload.impersonate) { | |||
| sessionStorage.setItem(IMPERSONATE_USER_UID, payload.accountUid); | |||
| } | |||
| if (payload.registration) { | |||
| sessionStorage.setItem(REGISTRATION_USER_UID, payload.accountUid); | |||
| } | |||
| const user = jwt.decode(data.JwtToken); | |||
| addHeaderToken(data.JwtToken); | |||
| if (user) { | |||
| yield put(setUser(user)); | |||
| } | |||
| yield put(updateUserToken(data.JwtToken)); | |||
| if (payload.onSuccess) { | |||
| yield call(payload.onSuccess); | |||
| } | |||
| } | |||
| } catch (error) { | |||
| yield call(logoutUser); | |||
| console.log(error); // eslint-disable-line | |||
| catch(e){ | |||
| console.log(e); | |||
| } | |||
| } | |||
| @@ -181,7 +127,6 @@ export default function* loginSaga() { | |||
| takeLatest(LOGIN_USER_FETCH, fetchUser), | |||
| takeLatest(AUTHENTICATE_USER, authenticateUser), | |||
| takeLatest(LOGOUT_USER, logoutUser), | |||
| takeLatest(REFRESH_TOKEN, refreshToken), | |||
| takeLatest(GENERATE_TOKEN, generateToken), | |||
| takeLatest(REFRESH_TOKEN, refreshUserToken) | |||
| ]); | |||
| } | |||
| @@ -1,16 +1,30 @@ | |||
| import { all, takeLatest, call } from "@redux-saga/core/effects"; | |||
| import { attemptFetchOffers } from "../../request/offersRequest"; | |||
| import { OFFERS_FETCH } from "../actions/offers/offersActionConstants"; | |||
| import { all, takeLatest, call, put } from "@redux-saga/core/effects"; | |||
| import { attemptAddOffer, attemptFetchOffers } from "../../request/offersRequest"; | |||
| import { OFFERS_FETCH, OFFER_ADD } from "../actions/offers/offersActionConstants"; | |||
| import { setOffers } from "../actions/offers/offersActions"; | |||
| function* fetchOffers() { | |||
| try { | |||
| const data = yield call(attemptFetchOffers); | |||
| console.log(data); | |||
| console.log(data.data.items) | |||
| yield put(setOffers(data.data.items)) | |||
| } catch (e) { | |||
| console.log(e); | |||
| } | |||
| } | |||
| function* createOffer(payload) { | |||
| try { | |||
| const data = yield call(attemptAddOffer, payload); | |||
| console.log(data); | |||
| } | |||
| catch (e) { | |||
| console.log(e); | |||
| } | |||
| } | |||
| export default function* offersSaga() { | |||
| yield all([takeLatest(OFFERS_FETCH, fetchOffers)]); | |||
| yield all([takeLatest(OFFERS_FETCH, fetchOffers), | |||
| takeLatest(OFFER_ADD, createOffer)]); | |||
| } | |||
| @@ -2,46 +2,47 @@ import { all, takeLatest, call } from "@redux-saga/core/effects"; | |||
| import { attemptRegister } from "../../request/registerRequest"; | |||
| import { REGISTER_USER_FETCH } from "../actions/register/registerActionConstants"; | |||
| function* fetchRegisterUser({payload}) { | |||
| function* fetchRegisterUser({ payload }) { | |||
| try { | |||
| console.log("payload: ", payload) | |||
| const requestData = { | |||
| email: payload.values.mail.toString(), | |||
| password: payload.values.password.toString(), | |||
| roles: [ | |||
| { | |||
| role: "User" | |||
| } | |||
| ], | |||
| company: { | |||
| name: payload.values.nameOfFirm.toString(), | |||
| PIB: payload.values.PIB.toString(), | |||
| contacts: { | |||
| telephone: payload.values.phoneNumber.toString(), | |||
| location: payload.values.location.toString(), | |||
| web: payload.values.website.toString() | |||
| } | |||
| } | |||
| } | |||
| console.log(requestData); | |||
| const data = yield call(attemptRegister, requestData); | |||
| console.log(data); | |||
| email: payload.values.mail.toString(), | |||
| password: payload.values.password.toString(), | |||
| roles: [ | |||
| { | |||
| role: "User", | |||
| }, | |||
| ], | |||
| company: { | |||
| name: payload.values.nameOfFirm.toString(), | |||
| PIB: payload.values.PIB.toString(), | |||
| contacts: { | |||
| telephone: payload.values.phoneNumber.toString(), | |||
| location: payload.values.location.toString(), | |||
| web: payload.values.website.toString(), | |||
| }, | |||
| }, | |||
| }; | |||
| yield call(attemptRegister, requestData); | |||
| if (payload.handleResponseSuccess) { | |||
| yield call(payload.handleResponseSuccess); | |||
| } | |||
| } catch (e) { | |||
| console.log(e.response.data); | |||
| let type; | |||
| if (e.response?.data?.toString() === 'User with email already exists') { | |||
| type = 'mail' | |||
| } else if (e.response?.data?.toString() === 'Company with PIB already exists') { | |||
| type = "PIB" | |||
| if ( | |||
| e.response?.data?.toString() === "User with email already exists" || | |||
| e.response?.data?.toString() === '"email" must be a valid email' | |||
| ) { | |||
| type = "mail"; | |||
| } else if ( | |||
| e.response?.data?.toString() === "Company with PIB already exists" | |||
| ) { | |||
| type = "PIB"; | |||
| } | |||
| const error = { | |||
| error: e, | |||
| type | |||
| } | |||
| type, | |||
| }; | |||
| if (payload.handleResponseError) { | |||
| yield call(payload.handleResponseError, error); | |||
| } | |||
| @@ -12,6 +12,7 @@ export const primaryThemeColors = { | |||
| borderSponsoredColor: "#E5D0FF", | |||
| backgroundSponsoredColor: "#F5EDFF", | |||
| offerBackgroundColor: "#F5F5F5", | |||
| selectOptionTextColor: "#1D1D1D", | |||
| primaryDarkText: "#505050", | |||
| iconStrokeColor: "#8C8C8C", | |||
| } | |||