Pavle Golubovic 3 лет назад
Родитель
Сommit
7d31883c0a
86 измененных файлов: 1624 добавлений и 1026 удалений
  1. 25
    0
      package-lock.json
  2. 1
    0
      package.json
  3. 12
    10
      src/App.js
  4. 2
    2
      src/AppRoutes.js
  5. 4
    0
      src/assets/images/svg/arrow-down.svg
  6. 7
    0
      src/assets/images/svg/coffee.svg
  7. 4
    0
      src/assets/images/svg/home.svg
  8. 6
    0
      src/assets/images/svg/truck.svg
  9. 15
    92
      src/components/Cards/CreateOfferCard/CreateOffer.js
  10. 20
    0
      src/components/Cards/CreateOfferCard/CreateOffer.styled.js
  11. 119
    0
      src/components/Cards/CreateOfferCard/FirstPart/FirstPartCreateOffer.js
  12. 62
    0
      src/components/Cards/CreateOfferCard/FirstPart/FirstPartCreateOffer.styled.js
  13. 14
    0
      src/components/Cards/CreateOfferCard/SecondPart/SecondPartCreateOffer.js
  14. 8
    0
      src/components/Cards/CreateOfferCard/SecondPart/SecondPartCreateOffer.styled.js
  15. 13
    5
      src/components/Cards/FilterCard/FilterCard.styled.js
  16. 3
    1
      src/components/Cards/FilterCard/FilterDropdown/Checkbox/FilterCheckboxDropdown.js
  17. 3
    1
      src/components/Cards/FilterCard/FilterDropdown/Radio/FilterRadioDropdown.js
  18. 10
    9
      src/components/Cards/OfferCard/OfferCard.js
  19. 21
    5
      src/components/Cards/OfferCard/OfferCard.styled.js
  20. 11
    2
      src/components/CheckBox/Label.js
  21. 11
    7
      src/components/Dropdown/DropdownList/DropdownList.js
  22. 7
    0
      src/components/Dropdown/DropdownList/DropdownList.styled.js
  23. 29
    6
      src/components/MarketPlace/Header/Header.js
  24. 11
    5
      src/components/MarketPlace/Header/Header.styled.js
  25. 3
    0
      src/components/MarketPlace/MarketPlace.styled.js
  26. 11
    9
      src/components/MarketPlace/Offers/Offers.js
  27. 83
    0
      src/components/Scroller/HorizontalScroller.js
  28. 65
    0
      src/components/Scroller/HorizontalScroller.styled.js
  29. 27
    0
      src/components/Select/Option/Option.js
  30. 16
    0
      src/components/Select/Option/Option.styled.js
  31. 42
    0
      src/components/Select/Select.js
  32. 16
    0
      src/components/Select/Select.styled.js
  33. 14
    2
      src/components/StepProgress/StepProgress.js
  34. 1
    0
      src/components/StepProgress/StepProgress.styled.js
  35. 2
    1
      src/components/TextFields/TextField/TextField.js
  36. 11
    5
      src/components/TextFields/TextField/TextField.styled.js
  37. 6
    1
      src/constants/pages.js
  38. 7
    2
      src/i18n/resources/rs.js
  39. 0
    38
      src/layouts/GridLayout/GridLayout.js
  40. 0
    28
      src/layouts/GridLayout/GridLayout.styled.js
  41. 2
    2
      src/layouts/MainLayout/MainLayout.js
  42. 1
    1
      src/layouts/ProfileLayout/ProfileLayout.js
  43. 6
    0
      src/layouts/ProfileLayout/ProfileLayout.styled.js
  44. 14
    1
      src/pages/ForgotPasswordPage/ForgotPassword.styled.js
  45. 20
    18
      src/pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent.js
  46. 6
    0
      src/pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent.styled.js
  47. 105
    51
      src/pages/ForgotPasswordPage/ForgotPasswordPage.js
  48. 0
    103
      src/pages/ForgotPasswordPage/ForgotPasswordPageMUI.js
  49. 28
    39
      src/pages/HomePage/HomePageMUI.js
  50. 16
    2
      src/pages/LoginPage/Login.styled.js
  51. 183
    105
      src/pages/LoginPage/LoginPage.js
  52. 0
    201
      src/pages/LoginPage/LoginPageMUI.js
  53. 27
    15
      src/pages/RegisterPages/Register/FirstPart/FirstPartOfRegistration.js
  54. 11
    0
      src/pages/RegisterPages/Register/FirstPart/FirstPartOfRegistration.styled.js
  55. 35
    7
      src/pages/RegisterPages/Register/Register.js
  56. 25
    10
      src/pages/RegisterPages/Register/Register.styled.js
  57. 16
    5
      src/pages/RegisterPages/Register/SecondPart/SecondPartOfRegistration.js
  58. 10
    0
      src/pages/RegisterPages/Register/SecondPart/SecondPartOfRegistration.styled.js
  59. 8
    16
      src/pages/RegisterPages/Register/ThirdPart/ThirdPartOfRegistration.js
  60. 3
    0
      src/pages/RegisterPages/Register/ThirdPart/ThirdPartOfRegistration.styled.js
  61. 34
    14
      src/pages/ResetPasswordPage/ResetPasswordPage.js
  62. 6
    0
      src/pages/ResetPasswordPage/ResetPasswordPage.styled.js
  63. 7
    4
      src/request/apiEndpoints.js
  64. 11
    4
      src/request/forgotPasswordRequest.js
  65. 63
    11
      src/request/index.js
  66. 2
    14
      src/request/loginRequest.js
  67. 4
    1
      src/request/offersRequest.js
  68. 2
    1
      src/store/actions/login/loginActions.js
  69. 3
    1
      src/store/actions/offers/offersActionConstants.js
  70. 9
    1
      src/store/actions/offers/offersActions.js
  71. 3
    1
      src/store/actions/user/userActionConstants.js
  72. 10
    0
      src/store/actions/user/userActions.js
  73. 2
    0
      src/store/index.js
  74. 50
    0
      src/store/middleware/accessTokensMiddleware.js
  75. 4
    1
      src/store/middleware/internalServerErrorMiddleware.js
  76. 34
    30
      src/store/middleware/loadingMiddleware.js
  77. 4
    1
      src/store/middleware/requestStatusMiddleware.js
  78. 1
    0
      src/store/reducers/login/loginReducer.js
  79. 17
    0
      src/store/reducers/offers/offersReducer.js
  80. 20
    2
      src/store/reducers/user/userReducer.js
  81. 3
    0
      src/store/saga/filtersSaga.js
  82. 7
    4
      src/store/saga/forgotPasswordSaga.js
  83. 40
    95
      src/store/saga/loginSaga.js
  84. 19
    5
      src/store/saga/offersSaga.js
  85. 30
    29
      src/store/saga/registerSaga.js
  86. 1
    0
      src/themes/primaryTheme/primaryThemeColors.js

+ 25
- 0
package-lock.json Просмотреть файл

@@ -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",

+ 1
- 0
package.json Просмотреть файл

@@ -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",

+ 12
- 10
src/App.js Просмотреть файл

@@ -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;

+ 2
- 2
src/AppRoutes.js Просмотреть файл

@@ -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';

+ 4
- 0
src/assets/images/svg/arrow-down.svg Просмотреть файл

@@ -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>

+ 7
- 0
src/assets/images/svg/coffee.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>

+ 4
- 0
src/assets/images/svg/home.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>

+ 6
- 0
src/assets/images/svg/truck.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>

+ 15
- 92
src/components/Cards/CreateOfferCard/CreateOffer.js Просмотреть файл

@@ -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>
);
};

+ 20
- 0
src/components/Cards/CreateOfferCard/CreateOffer.styled.js Просмотреть файл

@@ -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;
`

+ 119
- 0
src/components/Cards/CreateOfferCard/FirstPart/FirstPartCreateOffer.js Просмотреть файл

@@ -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;

+ 62
- 0
src/components/Cards/CreateOfferCard/FirstPart/FirstPartCreateOffer.styled.js Просмотреть файл

@@ -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;
`

+ 14
- 0
src/components/Cards/CreateOfferCard/SecondPart/SecondPartCreateOffer.js Просмотреть файл

@@ -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;

+ 8
- 0
src/components/Cards/CreateOfferCard/SecondPart/SecondPartCreateOffer.styled.js Просмотреть файл

@@ -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;
`;

+ 13
- 5
src/components/Cards/FilterCard/FilterCard.styled.js Просмотреть файл

@@ -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;`}
`

`;

+ 3
- 1
src/components/Cards/FilterCard/FilterDropdown/Checkbox/FilterCheckboxDropdown.js Просмотреть файл

@@ -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>

+ 3
- 1
src/components/Cards/FilterCard/FilterDropdown/Radio/FilterRadioDropdown.js Просмотреть файл

@@ -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>

+ 10
- 9
src/components/Cards/OfferCard/OfferCard.js Просмотреть файл

@@ -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,

+ 21
- 5
src/components/Cards/OfferCard/OfferCard.styled.js Просмотреть файл

@@ -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;

+ 11
- 2
src/components/CheckBox/Label.js Просмотреть файл

@@ -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,
};

+ 11
- 7
src/components/Dropdown/DropdownList/DropdownList.js Просмотреть файл

@@ -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 = {

+ 7
- 0
src/components/Dropdown/DropdownList/DropdownList.styled.js Просмотреть файл

@@ -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)`

+ 29
- 6
src/components/MarketPlace/Header/Header.js Просмотреть файл

@@ -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>

+ 11
- 5
src/components/MarketPlace/Header/Header.styled.js Просмотреть файл

@@ -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)`

+ 3
- 0
src/components/MarketPlace/MarketPlace.styled.js Просмотреть файл

@@ -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;
}
`;

+ 11
- 9
src/components/MarketPlace/Offers/Offers.js Просмотреть файл

@@ -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>
);

+ 83
- 0
src/components/Scroller/HorizontalScroller.js Просмотреть файл

@@ -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;

+ 65
- 0
src/components/Scroller/HorizontalScroller.styled.js Просмотреть файл

@@ -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}
`}
}
`

+ 27
- 0
src/components/Select/Option/Option.js Просмотреть файл

@@ -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

+ 16
- 0
src/components/Select/Option/Option.styled.js Просмотреть файл

@@ -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)`
`

+ 42
- 0
src/components/Select/Select.js Просмотреть файл

@@ -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;

+ 16
- 0
src/components/Select/Select.styled.js Просмотреть файл

@@ -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;
`

+ 14
- 2
src/components/StepProgress/StepProgress.js Просмотреть файл

@@ -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;

+ 1
- 0
src/components/StepProgress/StepProgress.styled.js Просмотреть файл

@@ -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;

+ 2
- 1
src/components/TextFields/TextField/TextField.js Просмотреть файл

@@ -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}
>

+ 11
- 5
src/components/TextFields/TextField/TextField.styled.js Просмотреть файл

@@ -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 : "")};

+ 6
- 1
src/constants/pages.js Просмотреть файл

@@ -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

+ 7
- 2
src/i18n/resources/rs.js Просмотреть файл

@@ -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: {

+ 0
- 38
src/layouts/GridLayout/GridLayout.js Просмотреть файл

@@ -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;

+ 0
- 28
src/layouts/GridLayout/GridLayout.styled.js Просмотреть файл

@@ -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;
`

+ 2
- 2
src/layouts/MainLayout/MainLayout.js Просмотреть файл

@@ -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>

+ 1
- 1
src/layouts/ProfileLayout/ProfileLayout.js Просмотреть файл

@@ -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>

+ 6
- 0
src/layouts/ProfileLayout/ProfileLayout.styled.js Просмотреть файл

@@ -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;
`

+ 14
- 1
src/pages/ForgotPasswordPage/ForgotPassword.styled.js Просмотреть файл

@@ -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;
`

+ 20
- 18
src/pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent.js Просмотреть файл

@@ -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>
);
};

+ 6
- 0
src/pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent.styled.js Просмотреть файл

@@ -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";

+ 105
- 51
src/pages/ForgotPasswordPage/ForgotPasswordPage.js Просмотреть файл

@@ -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>
);
};


+ 0
- 103
src/pages/ForgotPasswordPage/ForgotPasswordPageMUI.js Просмотреть файл

@@ -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;

+ 28
- 39
src/pages/HomePage/HomePageMUI.js Просмотреть файл

@@ -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>
);
};

+ 16
- 2
src/pages/LoginPage/Login.styled.js Просмотреть файл

@@ -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;
`;

+ 183
- 105
src/pages/LoginPage/LoginPage.js Просмотреть файл

@@ -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>
);
};


+ 0
- 201
src/pages/LoginPage/LoginPageMUI.js Просмотреть файл

@@ -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;

+ 27
- 15
src/pages/RegisterPages/Register/FirstPart/FirstPartOfRegistration.js Просмотреть файл

@@ -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;

+ 11
- 0
src/pages/RegisterPages/Register/FirstPart/FirstPartOfRegistration.styled.js Просмотреть файл

@@ -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;
`

+ 35
- 7
src/pages/RegisterPages/Register/Register.js Просмотреть файл

@@ -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 && (

+ 25
- 10
src/pages/RegisterPages/Register/Register.styled.js Просмотреть файл

@@ -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;
`

+ 16
- 5
src/pages/RegisterPages/Register/SecondPart/SecondPartOfRegistration.js Просмотреть файл

@@ -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;

+ 10
- 0
src/pages/RegisterPages/Register/SecondPart/SecondPartOfRegistration.styled.js Просмотреть файл

@@ -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;
`

+ 8
- 16
src/pages/RegisterPages/Register/ThirdPart/ThirdPartOfRegistration.js Просмотреть файл

@@ -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={

+ 3
- 0
src/pages/RegisterPages/Register/ThirdPart/ThirdPartOfRegistration.styled.js Просмотреть файл

@@ -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;
}
`;

+ 34
- 14
src/pages/ResetPasswordPage/ResetPasswordPage.js Просмотреть файл

@@ -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={

+ 6
- 0
src/pages/ResetPasswordPage/ResetPasswordPage.styled.js Просмотреть файл

@@ -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";

+ 7
- 4
src/request/apiEndpoints.js Просмотреть файл

@@ -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}&registrationFlowType={registrationFlowType}',
@@ -156,6 +158,7 @@ export default {
setFingerprint: '/affiliate/fingerprint',
},
offers: {
getOffers: 'offers'
getOffers: 'offers',
addOffer: 'offers',
}
};

+ 11
- 4
src/request/forgotPasswordRequest.js Просмотреть файл

@@ -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 }
);

+ 63
- 11
src/request/index.js Просмотреть файл

@@ -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;

+ 2
- 14
src/request/loginRequest.js Просмотреть файл

@@ -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);

+ 4
- 1
src/request/offersRequest.js Просмотреть файл

@@ -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)
}

+ 2
- 1
src/store/actions/login/loginActions.js Просмотреть файл

@@ -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) => ({

+ 3
- 1
src/store/actions/offers/offersActionConstants.js Просмотреть файл

@@ -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";

+ 9
- 1
src/store/actions/offers/offersActions.js Просмотреть файл

@@ -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
})

+ 3
- 1
src/store/actions/user/userActionConstants.js Просмотреть файл

@@ -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";

+ 10
- 0
src/store/actions/user/userActions.js Просмотреть файл

@@ -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
})

+ 2
- 0
src/store/index.js Просмотреть файл

@@ -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
),
),
);

+ 50
- 0
src/store/middleware/accessTokensMiddleware.js Просмотреть файл

@@ -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);
};

+ 4
- 1
src/store/middleware/internalServerErrorMiddleware.js Просмотреть файл

@@ -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);
};

+ 34
- 30
src/store/middleware/loadingMiddleware.js Просмотреть файл

@@ -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);
};

+ 4
- 1
src/store/middleware/requestStatusMiddleware.js Просмотреть файл

@@ -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);
};

+ 1
- 0
src/store/reducers/login/loginReducer.js Просмотреть файл

@@ -14,6 +14,7 @@ const initialState = {
token: {
RefreshToken: '',
JwtToken: '',
userId: ''
},
errorMessage: '',
};

+ 17
- 0
src/store/reducers/offers/offersReducer.js Просмотреть файл

@@ -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
}
}

+ 20
- 2
src/store/reducers/user/userReducer.js Просмотреть файл

@@ -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
}
}

+ 3
- 0
src/store/saga/filtersSaga.js Просмотреть файл

@@ -0,0 +1,3 @@
// function* setFilters({ payload }) {

// }

+ 7
- 4
src/store/saga/forgotPasswordSaga.js Просмотреть файл

@@ -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);
}

+ 40
- 95
src/store/saga/loginSaga.js Просмотреть файл

@@ -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)
]);
}

+ 19
- 5
src/store/saga/offersSaga.js Просмотреть файл

@@ -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)]);
}


+ 30
- 29
src/store/saga/registerSaga.js Просмотреть файл

@@ -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);
}

+ 1
- 0
src/themes/primaryTheme/primaryThemeColors.js Просмотреть файл

@@ -12,6 +12,7 @@ export const primaryThemeColors = {
borderSponsoredColor: "#E5D0FF",
backgroundSponsoredColor: "#F5EDFF",
offerBackgroundColor: "#F5F5F5",
selectOptionTextColor: "#1D1D1D",
primaryDarkText: "#505050",
iconStrokeColor: "#8C8C8C",
}

Загрузка…
Отмена
Сохранить