Explorar el Código

Finished market place

feature/code-cleanup-joca
Djordje Mitrovic hace 3 años
padre
commit
36a699d7d5
Se han modificado 61 ficheros con 2119 adiciones y 790 borrados
  1. 15
    15
      package-lock.json
  2. 20
    22
      src/App.js
  3. 5
    4
      src/AppRoutes.js
  4. 3
    0
      src/assets/images/svg/filter.svg
  5. 5
    0
      src/assets/images/svg/log-out.svg
  6. 1
    1
      src/assets/styles/_base.scss
  7. 33
    7
      src/components/Cards/FilterCard/FilterCard.js
  8. 25
    5
      src/components/Cards/FilterCard/FilterCard.styled.js
  9. 22
    7
      src/components/Cards/OfferCard/OfferCard.js
  10. 88
    11
      src/components/Cards/OfferCard/OfferCard.styled.js
  11. 365
    188
      src/components/Header/Header.js
  12. 127
    14
      src/components/Header/Header.styled.js
  13. 2
    1
      src/components/Icon/IconWithNumber/IconWithNumber.js
  14. 20
    10
      src/components/MarketPlace/Header/Header.js
  15. 82
    45
      src/components/MarketPlace/Header/Header.styled.js
  16. 2
    2
      src/components/MarketPlace/MarketPlace.styled.js
  17. 83
    46
      src/components/MarketPlace/Offers/Offers.js
  18. 3
    0
      src/components/MarketPlace/Offers/Offers.styled.js
  19. 76
    0
      src/components/Paging/Paging.js
  20. 137
    0
      src/components/Paging/Paging.styled.js
  21. 43
    21
      src/components/Popovers/HeaderPopover/HeaderPopover.js
  22. 15
    1
      src/components/Popovers/HeaderPopover/HeaderPopover.styled.js
  23. 20
    37
      src/components/Popovers/MyMessages/MyMessages.js
  24. 64
    19
      src/components/Popovers/MyPosts/MyPosts.js
  25. 50
    13
      src/components/Popovers/MyProfile/MyProfile.js
  26. 3
    0
      src/components/Popovers/MyProfile/MyProfile.styled.js
  27. 4
    0
      src/components/Select/Select.styled.js
  28. 0
    1
      src/components/TextFields/TextField/TextField.js
  29. 150
    95
      src/hooks/useFilters.js
  30. 8
    0
      src/hooks/usePaging.js
  31. 182
    0
      src/hooks/useQueryString.js
  32. 30
    0
      src/hooks/useScreenDimensions.js
  33. 12
    8
      src/hooks/useSearch.js
  34. 42
    72
      src/hooks/useSorting.js
  35. 3
    0
      src/i18n/resources/rs.js
  36. 0
    44
      src/pages/HomePage/HomePageMUI.js
  37. 2
    2
      src/pages/RegisterPages/Register/Register.styled.js
  38. 2
    1
      src/request/apiEndpoints.js
  39. 3
    0
      src/request/chatRequest.js
  40. 5
    2
      src/request/offersRequest.js
  41. 2
    0
      src/store/actions/chat/chatActionConstants.js
  42. 5
    1
      src/store/actions/chat/chatActions.js
  43. 2
    1
      src/store/actions/login/loginActions.js
  44. 5
    1
      src/store/actions/offers/offersActionConstants.js
  45. 55
    30
      src/store/actions/offers/offersActions.js
  46. 2
    0
      src/store/actions/queryString/queryStringActionConstants.js
  47. 10
    0
      src/store/actions/queryString/queryStringActions.js
  48. 0
    11
      src/store/reducers/filters/filtersReducer.js
  49. 3
    1
      src/store/reducers/index.js
  50. 18
    0
      src/store/reducers/offers/offersReducer.js
  51. 20
    0
      src/store/reducers/queryString/queryStringReducer.js
  52. 13
    3
      src/store/saga/chatSaga.js
  53. 3
    1
      src/store/saga/index.js
  54. 7
    3
      src/store/saga/loginSaga.js
  55. 57
    23
      src/store/saga/offersSaga.js
  56. 30
    0
      src/store/saga/queryStringSaga.js
  57. 0
    4
      src/store/selectors/filtersSelectors.js
  58. 8
    0
      src/store/selectors/offersSelectors.js
  59. 8
    0
      src/store/selectors/queryStringSelectors.js
  60. 0
    2
      src/util/helpers/authScopeHelpers.js
  61. 119
    15
      src/util/helpers/queryHelpers.js

+ 15
- 15
package-lock.json Ver fichero

}, },
"dependencies": { "dependencies": {
"@babel/helper-annotate-as-pure": { "@babel/helper-annotate-as-pure": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz",
"integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==",
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
"integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
"requires": { "requires": {
"@babel/types": "^7.16.7"
"@babel/types": "^7.18.6"
} }
}, },
"@babel/helper-module-imports": { "@babel/helper-module-imports": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"requires": { "requires": {
"@babel/types": "^7.16.7"
"@babel/types": "^7.18.6"
} }
}, },
"@babel/helper-validator-identifier": { "@babel/helper-validator-identifier": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g=="
}, },
"@babel/types": { "@babel/types": {
"version": "7.18.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz",
"integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==",
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz",
"integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==",
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"@babel/helper-validator-identifier": "^7.18.6",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
} }

+ 20
- 22
src/App.js Ver fichero

import React from 'react';
import { Router } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import i18next from 'i18next';
import history from './store/utils/history';
import AppRoutes from './AppRoutes';
import Header from './components/Header/Header';
import { StyledEngineProvider } from '@mui/material';
import GlobalStyle from './components/Styles/globalStyles';
import React from "react";
import { Router } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import i18next from "i18next";
import history from "./store/utils/history";
import AppRoutes from "./AppRoutes";
import Header from "./components/Header/Header";
import { StyledEngineProvider } from "@mui/material";
import GlobalStyle from "./components/Styles/globalStyles";


const App = () => { const App = () => {
return ( return (
<>
<Router history={history}>
<Helmet>
<title>{i18next.t("app.title")}</title>
</Helmet>
<Header/>
<StyledEngineProvider injectFirst>
<GlobalStyle />
<AppRoutes />
</StyledEngineProvider>
{/* </main> */}
</Router>
</>
<Router history={history}>
<Helmet>
<title>{i18next.t("app.title")}</title>
</Helmet>
<StyledEngineProvider injectFirst>
<Header />
<GlobalStyle />
<AppRoutes />
</StyledEngineProvider>
{/* </main> */}
</Router>
); );
}; };



+ 5
- 4
src/AppRoutes.js Ver fichero

import NotFoundPage from './pages/ErrorPages/NotFoundPage'; import NotFoundPage from './pages/ErrorPages/NotFoundPage';
import ErrorPage from './pages/ErrorPages/ErrorPage'; import ErrorPage from './pages/ErrorPages/ErrorPage';
import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPage'; import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPage';
import PrivateRoute from './components/Router/PrivateRoute';
// import PrivateRoute from './components/Router/PrivateRoute';
import MailSent from './pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent'; import MailSent from './pages/ForgotPasswordPage/ForgotPasswordMailSent/MailSent';
import Register from './pages/RegisterPages/Register/Register'; import Register from './pages/RegisterPages/Register/Register';
import RegisterSuccessful from './pages/RegisterPages/RegisterSuccessful.js/RegisterSuccessful'; import RegisterSuccessful from './pages/RegisterPages/RegisterSuccessful.js/RegisterSuccessful';
<Route path={FORGOT_PASSWORD_PAGE} component={ForgotPasswordPage} /> <Route path={FORGOT_PASSWORD_PAGE} component={ForgotPasswordPage} />
<Route path={RESET_PASSWORD_PAGE} component={ResetPasswordPage}/> <Route path={RESET_PASSWORD_PAGE} component={ResetPasswordPage}/>
<Route path={CREATE_OFFER_PAGE} component={CreateOffer}/> <Route path={CREATE_OFFER_PAGE} component={CreateOffer}/>
<PrivateRoute
<Route path={HOME_PAGE} component={HomePage} />

{/* <PrivateRoute
exact exact
path={HOME_PAGE} path={HOME_PAGE}
component={HomePage} component={HomePage}
/>
/> */}
<Redirect from="*" to={NOT_FOUND_PAGE} /> <Redirect from="*" to={NOT_FOUND_PAGE} />
</Switch> </Switch>
)}; )};

+ 3
- 0
src/assets/images/svg/filter.svg Ver fichero

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 3H2L10 12.46V19L14 21V12.46L22 3Z" stroke="#5A3984" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

+ 5
- 0
src/assets/images/svg/log-out.svg Ver fichero

<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 15.75H3.75C3.35218 15.75 2.97064 15.592 2.68934 15.3107C2.40804 15.0294 2.25 14.6478 2.25 14.25V3.75C2.25 3.35218 2.40804 2.97064 2.68934 2.68934C2.97064 2.40804 3.35218 2.25 3.75 2.25H6.75" stroke="#FEB005" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 12.75L15.75 9L12 5.25" stroke="#FEB005" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.75 9H6.75" stroke="#FEB005" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

+ 1
- 1
src/assets/styles/_base.scss Ver fichero

-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overflow-anchor: none; overflow-anchor: none;
background-color: #F1F1F1;
background-color: #F5F5F5;
} }


* { * {

+ 33
- 7
src/components/Cards/FilterCard/FilterCard.js Ver fichero

import selectedTheme from "../../../themes"; import selectedTheme from "../../../themes";
import useFilters from "../../../hooks/useFilters"; import useFilters from "../../../hooks/useFilters";


const FilterCard = () => {
const FilterCard = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isOpened, setIsOpened] = useState(false); const [isOpened, setIsOpened] = useState(false);
const [isDisabled, setIsDisabled] = useState(true); const [isDisabled, setIsDisabled] = useState(true);
} else { } else {
setIsDisabled(false); setIsDisabled(false);
} }
}, [filters.selectedCategory])
}, [filters.selectedCategory]);


const handleSelectCategory = (category) => { const handleSelectCategory = (category) => {
filters.setSelectedCategory(category); filters.setSelectedCategory(category);
filters.setSelectedSubcategory(); filters.setSelectedSubcategory();
}
};


const handleOpen = () => { const handleOpen = () => {
setIsOpened(prevState => !prevState);
}
setIsOpened((prevState) => !prevState);
};


const handleFilters = () => { const handleFilters = () => {
filters.applyFilters(); filters.applyFilters();
if (props.closeResponsive) props.closeResponsive();
}; };
const clearFilters = () => { const clearFilters = () => {
filters.clearFilters(); filters.clearFilters();
}; };


return ( return (
<FilterCardContainer>
<FilterCardContainer responsiveOpen={props.responsiveOpen} responsive={props.responsive}>
<Header> <Header>
<Title>{t("filters.title")}</Title> <Title>{t("filters.title")}</Title>
<Link <Link
/> />
</ContentContainer> </ContentContainer>


<Footer>
<Footer responsiveOpen={props.responsiveOpen}>
{props.responsiveOpen && (
<PrimaryButton
variant="outlined"
fullWidth
onClick={props.closeResponsive}
textcolor={selectedTheme.primaryPurple}
font="Open Sans"
style={{
fontWeight: "600",
fontSize: "12px",
border: "0",
textAlign: "center"
}}
>
ZATVORI
</PrimaryButton>
)}
<PrimaryButton <PrimaryButton
variant="outlined" variant="outlined"
fullWidth fullWidth
FilterCard.propTypes = { FilterCard.propTypes = {
children: PropTypes.node, children: PropTypes.node,
filters: PropTypes.any, filters: PropTypes.any,
responsive: PropTypes.bool,
responsiveOpen: PropTypes.bool,
closeResponsive: PropTypes.func,
}; };


FilterCard.defaultProps = {
responsive: false,
responsiveOpen: false,
}

export default FilterCard; export default FilterCard;

+ 25
- 5
src/components/Cards/FilterCard/FilterCard.styled.js Ver fichero



export const FilterCardContainer = styled(Box)` export const FilterCardContainer = styled(Box)`
position: fixed; position: fixed;
box-sizing: border-box;
border-radius: 0; border-radius: 0;
border-top-right-radius: 4px; border-top-right-radius: 4px;
height: calc(100% - 90px); height: calc(100% - 90px);
padding: 36px; padding: 36px;
background-color: white; background-color: white;
width: calc(100% / 12 * 2.4);
width: calc(100% / 12 * 3.5);
left: 0; left: 0;
display: flex;
max-width: 360px;
display: ${(props) => (props.responsive && !props.responsiveOpen ? "none" : "flex")};
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
background-color: white; background-color: white;
min-width: fit-content; min-width: fit-content;
min-width: 285px !important;
z-index: 9; z-index: 9;
margin-top: -24px; margin-top: -24px;
transition: all ease-in-out .36s;
transition: all ease-in-out 0.36s;
@media (max-width: 900px) { @media (max-width: 900px) {
margin-left: -400px; margin-left: -400px;
transition: all ease-in-out .36s;
${(props) =>
props.responsiveOpen
? `
display: "flex";
margin-left: 0;
max-width: 100vw;
width: 100vw;
bottom: 0;
height: calc(100% - 50px);
` : "display: none"};
transition: all ease-in-out 0.36s;

} }
@media (max-width: 600px) { @media (max-width: 600px) {
margin-top: -14px; margin-top: -14px;


export const Footer = styled(Box)` export const Footer = styled(Box)`
position: "sticky"; position: "sticky";
${(props) =>
props.responsiveOpen &&
`
flex-direction: row;
display: flex;
justify-content: space-around;`}
bottom: 0; bottom: 0;
& div button { & div button {
height: 48px; height: 48px;
} }
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #ddd; scrollbar-color: #ddd;
${() => window.scrollbars.visible && `padding-right: 15px;`}
${() => window.scrollbars.visible && `padding-right: 15px`};
`; `;

+ 22
- 7
src/components/Cards/OfferCard/OfferCard.js Ver fichero

CheckButton, CheckButton,
DetailIcon, DetailIcon,
DetailText, DetailText,
EyeIcon,
Line, Line,
MessageIcon, MessageIcon,
OfferAuthor, OfferAuthor,
OfferDescriptionText, OfferDescriptionText,
OfferDescriptionTitle, OfferDescriptionTitle,
OfferDetails, OfferDetails,
OfferFlexContainer,
OfferImage, OfferImage,
OfferImageContainer,
OfferInfo, OfferInfo,
OfferLocation, OfferLocation,
OfferTitle, OfferTitle,
OfferTitleAboveImage,
OfferViews, OfferViews,
} from "./OfferCard.styled"; } from "./OfferCard.styled";
import { ReactComponent as Category } from "../../../assets/images/svg/category.svg"; import { ReactComponent as Category } from "../../../assets/images/svg/category.svg";
import { ReactComponent as Eye } from "../../../assets/images/svg/eye-striked.svg";
import { ReactComponent as Message } from "../../../assets/images/svg/mail.svg"; import { ReactComponent as Message } from "../../../assets/images/svg/mail.svg";
import selectedTheme from "../../../themes"; import selectedTheme from "../../../themes";


const OfferCard = (props) => { const OfferCard = (props) => {
return ( return (
<OfferCardContainer sponsored={props.offer.pinned.toString()} halfwidth={props.halfwidth ? 1 : 0}>
<OfferImage src={props.offer.images[0]}></OfferImage>
<React.Fragment>
<OfferCardContainer
sponsored={props.offer.pinned.toString()}
halfwidth={props.halfwidth ? 1 : 0}
>
<OfferTitleAboveImage>{props.offer.name}</OfferTitleAboveImage>
<OfferFlexContainer>
<OfferImageContainer>
<OfferImage src={props.offer.images[0]}></OfferImage>
</OfferImageContainer>
<OfferInfo> <OfferInfo>
<OfferTitle>{props.offer.name}</OfferTitle> <OfferTitle>{props.offer.name}</OfferTitle>
<OfferAuthor> <OfferAuthor>
</OfferCategory> </OfferCategory>
<OfferViews> <OfferViews>
<DetailIcon color="black" component="span" size="16px"> <DetailIcon color="black" component="span" size="16px">
<Eye width={"12px"} height={"11px"} />
<EyeIcon />
</DetailIcon> </DetailIcon>
<DetailText>{props.offer.views.viewers.length}</DetailText> <DetailText>{props.offer.views.viewers.length}</DetailText>
</OfferViews> </OfferViews>
</OfferInfo> </OfferInfo>
{!props.halfwidth ? ( {!props.halfwidth ? (
<React.Fragment> <React.Fragment>
<Line/>
<Line />
<OfferDescription> <OfferDescription>
<OfferDescriptionTitle>Opis:</OfferDescriptionTitle> <OfferDescriptionTitle>Opis:</OfferDescriptionTitle>
<OfferDescriptionText>{props.offer.description}</OfferDescriptionText>
<OfferDescriptionText>
{props.offer.description}
</OfferDescriptionText>
</OfferDescription> </OfferDescription>


<CheckButton <CheckButton
variant={props.sponsored ? "contained" : "outlined"} variant={props.sponsored ? "contained" : "outlined"}
buttoncolor={selectedTheme.primaryPurple} buttoncolor={selectedTheme.primaryPurple}
textcolor={props.sponsored ? "white" : selectedTheme.primaryPurple} textcolor={props.sponsored ? "white" : selectedTheme.primaryPurple}
style={{fontWeight: "600"}}
style={{ fontWeight: "600" }}
> >
Pogledaj proizvod Pogledaj proizvod
</CheckButton> </CheckButton>
{props.quantity} {props.quantity}
{props.package} {props.package}
{props.numberOfViews} */} {props.numberOfViews} */}
</OfferFlexContainer>
</OfferCardContainer> </OfferCardContainer>
</React.Fragment>
); );
}; };



+ 88
- 11
src/components/Cards/OfferCard/OfferCard.styled.js Ver fichero

import { IconButton } from "../../Buttons/IconButton/IconButton"; import { IconButton } from "../../Buttons/IconButton/IconButton";
import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton";
import { Icon } from "../../Icon/Icon"; import { Icon } from "../../Icon/Icon";
import { ReactComponent as Eye } from "../../../assets/images/svg/eye-striked.svg";


export const OfferCardContainer = styled(Container)` export const OfferCardContainer = styled(Container)`
display: flex; display: flex;
flex-direction: row;
flex-direction: column;
width: ${(props) => (!props.halfwidth ? "100%" : "49%")}; width: ${(props) => (!props.halfwidth ? "100%" : "49%")};
box-sizing: border-box; box-sizing: border-box;
margin: 10px 0; margin: 10px 0;
background-color: ${(props) => background-color: ${(props) =>
props.sponsored === 'true' ? selectedTheme.backgroundSponsoredColor : "white"};
props.sponsored === "true"
? selectedTheme.backgroundSponsoredColor
: "white"};
border-radius: 4px; border-radius: 4px;
${(props) => ${(props) =>
props.sponsored === 'true' &&
props.sponsored === "true" &&
`border: 1px solid ${selectedTheme.borderSponsoredColor};`} `border: 1px solid ${selectedTheme.borderSponsoredColor};`}
padding: 16px; padding: 16px;
max-width: 2000px; max-width: 2000px;
height: 180px; height: 180px;
position: relative; position: relative;
@media (max-width: 550px) {
height: 184px;
padding: 18px;
padding-top: 12px;
}
`;
export const OfferFlexContainer = styled(Container)`
display: flex;
flex-direction: row;
margin: 0;
padding: 0;
max-height: 184px;
`; `;
export const OfferImage = styled.img` export const OfferImage = styled.img`
max-width: 144px; max-width: 144px;
max-height: 144px; max-height: 144px;
width: 144px;
height: 144px;
@media (max-width: 600px) {
max-width: 108px;
max-height: 108px;
width: 108px;
height: 108px;
}
`; `;
export const OfferInfo = styled(Box)` export const OfferInfo = styled(Box)`
display: flex; display: flex;
color: ${selectedTheme.primaryPurple}; color: ${selectedTheme.primaryPurple};
font-weight: 700; font-weight: 700;
font-size: 24px; font-size: 24px;
@media (max-width: 550px) {
font-size: 18px;
display: none;
}
`; `;
export const OfferAuthor = styled(Box)` export const OfferAuthor = styled(Box)`
display: flex; display: flex;
font-family: "Open Sans"; font-family: "Open Sans";
line-height: 22px; line-height: 22px;
font-size: 16px; font-size: 16px;
color: ${selectedTheme.primaryDarkText};
color: ${selectedTheme.primaryText};
@media (max-width: 600px) {
font-size: 14px;
position: relative;
left: -1px;
}
`; `;
export const OfferLocation = styled(Typography)` export const OfferLocation = styled(Typography)`
font-family: "Open Sans"; font-family: "Open Sans";
color: ${selectedTheme.primaryText};
color: ${selectedTheme.primaryDarkText};
line-height: 16px; line-height: 16px;
font-size: 12px; font-size: 12px;
`; `;
flex-wrap: ${(props) => (!props.halfwidth ? "no-wrap" : "wrap")}; flex-wrap: ${(props) => (!props.halfwidth ? "no-wrap" : "wrap")};
justify-content: start; justify-content: start;
gap: 1rem; gap: 1rem;
@media (max-width: 650px) {
flex-direction: column;
justify-content: center;
gap: 0;
}
`; `;
export const OfferCategory = styled(Box)` export const OfferCategory = styled(Box)`
font-family: "Open Sans"; font-family: "Open Sans";
color: ${selectedTheme.primaryText}; color: ${selectedTheme.primaryText};
line-height: 16px; line-height: 16px;
font-size: 12px; font-size: 12px;
@media (max-width: 1000px) {
display: none;
}
`; `;
export const OfferPackage = styled(Box)` export const OfferPackage = styled(Box)`
font-family: "Open Sans"; font-family: "Open Sans";
color: ${selectedTheme.primaryText}; color: ${selectedTheme.primaryText};
line-height: 16px; line-height: 16px;
font-size: 12px; font-size: 12px;
@media (max-width: 1200px) {
display: none;
}
`; `;
export const OfferDescriptionTitle = styled(Box)` export const OfferDescriptionTitle = styled(Box)`
font-family: "Open Sans"; font-family: "Open Sans";
border-radius: 100%; border-radius: 100%;
padding-top: 2px; padding-top: 2px;
text-align: center; text-align: center;
@media (max-width: 600px) {
width: 30px;
height: 30px;
top: 16px;
right: 16px;
padding: 0;
& button svg {
width: 16px;
height: 16px;
position: relative;
top: -3px;
left: -2.4px;
}
}
`;
export const OfferImageContainer = styled(Box)`
min-width: 144px;
min-height: 144px;
width: 144px;
height: 144px;
@media (max-width: 600px) {
min-width: 108px;
min-height: 108px;
width: 108px;
height: 108px;
border-radius: 4px;
overflow: hidden;
box-shadow: 4px 4px 9px rgba(0, 0, 0, 0.12);
}
`;
export const OfferTitleAboveImage = styled(OfferTitle)`
padding-bottom: 12px;
padding-top: 5px;
padding-left: 1px;
display: block;
@media (min-width: 551px) {
display: none;
}
`;
export const EyeIcon = styled(Eye)`
width: 12px;
height: 11px;
@media (max-width: 600px) {
position: relative;
top: 1px !important;
}
`; `;

+ 365
- 188
src/components/Header/Header.js Ver fichero

import React, { useState, useMemo, useEffect, useRef } from "react"; import React, { useState, useMemo, useEffect, useRef } from "react";
import { import {
AddOfferButton, AddOfferButton,
AuthButtonsContainer,
AuthButtonsDrawerContainer,
DrawerContainer, DrawerContainer,
EndIcon,
FilterContainer,
FilterIcon,
HeaderContainer,
LoginButton,
LogoContainer, LogoContainer,
RegisterButton,
SearchIcon, SearchIcon,
SearchInput, SearchInput,
SearchInputMobile,
ToggleDrawerButton, ToggleDrawerButton,
ToolsButtonsContainer, ToolsButtonsContainer,
ToolsContainer, ToolsContainer,
UserButton, UserButton,
UserName, UserName,
} from "./Header.styled"; } from "./Header.styled";
import PropTypes from "prop-types";
import { import {
AppBar, AppBar,
Badge, Badge,
import selectedTheme from "../../themes"; import selectedTheme from "../../themes";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IconButton } from "../Buttons/IconButton/IconButton"; import { IconButton } from "../Buttons/IconButton/IconButton";
import { Icon } from "../Icon/Icon";
import { useSelector } from "react-redux";
import { selectJWTToken } from "../../store/selectors/loginSelectors";
import { useDispatch, useSelector } from "react-redux";
import { selectUserId } from "../../store/selectors/loginSelectors";
import { useSearch } from "../../hooks/useSearch"; import { useSearch } from "../../hooks/useSearch";
import { selectProfileName } from "../../store/selectors/profileSelectors"; import { selectProfileName } from "../../store/selectors/profileSelectors";
import { fetchProfile } from "../../store/actions/profile/profileActions";
import { useHistory, useRouteMatch } from "react-router-dom";
import { LOGIN_PAGE, REGISTER_PAGE } from "../../constants/pages";
import useFilters from "../../hooks/useFilters";
import FilterCard from "../Cards/FilterCard/FilterCard";
import { useQueryString } from "../../hooks/useQueryString";
import { convertQueryStringFrontend } from "../../util/helpers/queryHelpers";


const Header = () => { const Header = () => {
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [openFilters, setOpenFilters] = useState(false);
const [numberOfFilters, setNumberOfFilters] = useState(0);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const searchRef = useRef(null); const searchRef = useRef(null);
const matches = useMediaQuery(theme.breakpoints.down("md")); const matches = useMediaQuery(theme.breakpoints.down("md"));
const user = useSelector(selectJWTToken);
const user = useSelector(selectUserId);
const search = useSearch(); const search = useSearch();
const dispatch = useDispatch();
const name = useSelector(selectProfileName); const name = useSelector(selectProfileName);
const history = useHistory();
const routeMatch = useRouteMatch();
const filters = useFilters();
const searchMobileRef = useRef(null);
const queryStringHook = useQueryString();
useEffect(() => {
if (user?.length > 1) {
dispatch(fetchProfile(user));
}
}, [user]);
useEffect(() => {
setUserPopoverOpen(false);
setUserAnchorEl(null);
return () => {
setUserPopoverOpen(false);
setUserAnchorEl(null);
};
}, []);
useEffect(() => {
setNumberOfFilters(filters.calculateFiltersChosen());
}, [
filters.selectedCategory,
filters.selectedLocations,
filters.selectedSubcategory,
]);


const handleToggleDrawer = () => {
setOpenDrawer(!openDrawer);
};

useEffect(() => {
if (queryStringHook.loadedFromURL) {
const queryObject = new URLSearchParams(
convertQueryStringFrontend(queryStringHook.queryString)
);
if (queryObject.has("search")) {
searchRef.current.value = queryObject.get("search");
searchMobileRef.current.value = queryObject.get("search");
}
}
});


const [postsPopoverOpen, setPostsPopoverOpen] = useState(false); const [postsPopoverOpen, setPostsPopoverOpen] = useState(false);
const [postsAnchorEl, setPostsAnchorEl] = useState(null); const [postsAnchorEl, setPostsAnchorEl] = useState(null);
location.pathname === "/login" || location.pathname === "/login" ||
location.pathname === "/register" || location.pathname === "/register" ||
location.pathname === "/forgot-password" || location.pathname === "/forgot-password" ||
location.pathname === "/reset-password"
location.pathname === "/reset-password" ||
location.pathname === "/"
) { ) {
shouldShowHeader = false; shouldShowHeader = false;
} }
if (location.pathname === "/" && user.JwtToken?.length === 0) {
if (location.pathname === "/" && user?.length === 0) {
shouldShowHeader = false; shouldShowHeader = false;
} }
setShouldShow(shouldShowHeader); setShouldShow(shouldShowHeader);
}, [location, user]);
}, [location.pathname, user, routeMatch]);


let listener;
const handleFocusSearch = () => {
listener = (event) => {
if (event.keyCode === 13) {
event.preventDefault();
search.searchOffers(searchRef.current.value)
}
}
searchRef.current.addEventListener('keyup', listener);
const handleToggleDrawer = () => {
setOpenDrawer(!openDrawer);
}; };
const handleBlurSearch = () => {
searchRef.current.removeEventListener('keyup', listener);
const handleNavigateLogin = () => {
setShouldShow(false);
history.push(LOGIN_PAGE);
};
const handleNavigateRegister = () => {
setShouldShow(false);
history.push(REGISTER_PAGE);
}; };

const drawerContent = useMemo( const drawerContent = useMemo(
() => ( () => (
<DrawerContainer> <DrawerContainer>
<PrimaryButton
type="submit"
variant="contained"
height="36px"
fullWidth
buttoncolor={selectedTheme.primaryYellow}
textcolor="black"
onClick={() => handleToggleDrawer()}
>
{t("header.addOffer")}
</PrimaryButton>
<ToolsContainer mobile>
<IconButton
onClick={(e) => {
setPostsPopoverOpen(true);
setPostsAnchorEl(e.currentTarget);
}}
sx={{ borderRadius: "4px" }}
>
<Autorenew />
<Typography sx={{ ml: 2 }}>Moje objave</Typography>
</IconButton>
<IconButton
onClick={(e) => {
setMsgPopoverOpen(true);
setMsgAnchorEl(e.currentTarget);
}}
sx={{ borderRadius: "4px" }}
>
<Badge badgeContent={3} color="primary">
<MailIcon color="action" />
</Badge>
<Typography sx={{ ml: 2 }}>Moje poruke</Typography>
</IconButton>
<IconButton
onClick={(e) => {
setUserPopoverOpen(true);
setUserAnchorEl(e.currentTarget);
}}
sx={{ borderRadius: "4px" }}
>
<AccountCircle />
<Typography sx={{ ml: 2 }}>Moj profil</Typography>
</IconButton>
</ToolsContainer>
{user ? (
<React.Fragment>
<PrimaryButton
type="submit"
variant="contained"
height="36px"
fullWidth
buttoncolor={selectedTheme.primaryYellow}
textcolor="black"
onClick={() => handleToggleDrawer()}
>
{t("header.addOffer")}
</PrimaryButton>
<ToolsContainer mobile>
<IconButton
onClick={(e) => {
setPostsPopoverOpen(true);
setPostsAnchorEl(e.currentTarget);
}}
sx={{ borderRadius: "4px" }}
>
<Autorenew />
<Typography sx={{ ml: 2 }}>Moje objave</Typography>
</IconButton>
<IconButton
onClick={(e) => {
setMsgPopoverOpen(true);
setMsgAnchorEl(e.currentTarget);
}}
sx={{ borderRadius: "4px" }}
>
<Badge badgeContent={3} color="primary">
<MailIcon color="action" />
</Badge>
<Typography sx={{ ml: 2 }}>Moje poruke</Typography>
</IconButton>
<IconButton
onClick={(e) => {
setUserPopoverOpen(true);
setUserAnchorEl(e.currentTarget);
}}
sx={{ borderRadius: "4px" }}
>
<AccountCircle />
<Typography sx={{ ml: 2 }}>Moj profil</Typography>
</IconButton>
</ToolsContainer>
</React.Fragment>
) : (
<AuthButtonsDrawerContainer>
<RegisterButton
type="submit"
variant="contained"
height="36px"
fullWidth
buttoncolor={selectedTheme.primaryYellow}
textcolor={selectedTheme.primaryDarkText}
onClick={handleNavigateRegister}
>
{t("register.headerTitle")}
</RegisterButton>
<LoginButton
type="submit"
variant="contained"
height="36px"
fullWidth
buttoncolor={selectedTheme.primaryPurple}
textcolor={selectedTheme.primaryIconBackgroundColor}
onClick={handleNavigateLogin}
>
{t("login.headerTitle")}
</LoginButton>
</AuthButtonsDrawerContainer>
)}
</DrawerContainer> </DrawerContainer>
), ),
[handleToggleDrawer] [handleToggleDrawer]
); );


let listener;
const handleFocusSearch = () => {
listener = (event) => {
if (event.keyCode === 13) {
event.preventDefault();
handleSearch(searchRef.current.value);
}
};
searchRef.current.addEventListener("keyup", listener);
};
const handleBlurSearch = () => {
searchRef.current.removeEventListener("keyup", listener);
};
const handleSearch = (value) => {
console.log(value);
if (value.length === 0) return;
search.searchOffers(value);
};
const toggleFilters = () => {
setOpenFilters((prevState) => !prevState);
};

return ( return (
<AppBar
elevation={0}
position="fixed"
// positionFixed
sx={{ backgroundColor: "white" }}
style={{ display: shouldShow ? "flex" : "none" }}
>
<Toolbar>
<ToolsContainer>
<LogoContainer>
<LogoHorizontal />
</LogoContainer>
{matches && (
<Drawer
open={openDrawer}
toggleOpen={handleToggleDrawer}
content={drawerContent}
<HeaderContainer style={{ display: shouldShow ? "block" : "none" }}>
<AppBar
elevation={0}
position="fixed"
// positionFixed
sx={{ backgroundColor: "white" }}
>
<Toolbar>
<ToolsContainer>
<LogoContainer>
<LogoHorizontal />
</LogoContainer>
{matches && (
<Drawer
open={openDrawer}
toggleOpen={handleToggleDrawer}
content={drawerContent}
/>
)}
<SearchInput
fullWidth
InputProps={{
endAdornment: (
<EndIcon size="36px">
<SearchIcon
onClick={() => handleSearch(searchRef.current.value)}
/>
</EndIcon>
),
}}
placeholder={t("header.searchOffers")}
onFocus={handleFocusSearch}
onBlur={handleBlurSearch}
ref={searchRef}
/> />
)}
<SearchInput
fullWidth
InputProps={{
startAdornment: (
<Icon size="36px">
<SearchIcon />
</Icon>
),
}}
label={t("header.searchOffers")}
onFocus={handleFocusSearch}
onBlur={handleBlurSearch}
ref={searchRef}
/>
<ToolsButtonsContainer mobile={matches}>
{matches ? (
<ToggleDrawerButton>
<IconButton onClick={handleToggleDrawer}>
<MenuOutlinedIcon />
</IconButton>
</ToggleDrawerButton>
{user ? (
<ToolsButtonsContainer mobile={matches}>
{matches ? (
<ToggleDrawerButton>
<IconButton onClick={handleToggleDrawer}>
<MenuOutlinedIcon />
</IconButton>
</ToggleDrawerButton>
) : (
<React.Fragment>
<AddOfferButton
type="submit"
variant="contained"
fullWidth
buttoncolor={selectedTheme.primaryYellow}
textcolor={selectedTheme.primaryDarkText}
onClick={() => {
setUserPopoverOpen(false);
setUserAnchorEl(null);
}}
>
{t("header.addOffer")}
</AddOfferButton>
<IconButton
onClick={(e) => {
setPostsPopoverOpen(true);
setPostsAnchorEl(e.currentTarget);
}}
style={{
background: selectedTheme.primaryIconBackgroundColor,
color: selectedTheme.primaryPurple,
}}
>
<Autorenew />
</IconButton>
<IconButton
onClick={(e) => {
setMsgPopoverOpen(true);
setMsgAnchorEl(e.currentTarget);
}}
style={{
background: selectedTheme.primaryIconBackgroundColor,
color: selectedTheme.primaryPurple,
}}
>
<Badge badgeContent={3} color="primary">
<MailIcon />
</Badge>
</IconButton>
<UserButton
onClick={(e) => {
setUserPopoverOpen(true);
setUserAnchorEl(e.currentTarget);
}}
>
<UserName>{name}</UserName>
<IconButton
style={{
background: selectedTheme.primaryIconBackgroundColor,
color: selectedTheme.primaryPurple,
}}
>
<AccountCircle />
</IconButton>
</UserButton>
</React.Fragment>
)}
</ToolsButtonsContainer>
) : ( ) : (
<React.Fragment>
<AddOfferButton
type="submit"
variant="contained"
fullWidth
buttoncolor={selectedTheme.primaryYellow}
textcolor={selectedTheme.primaryDarkText}
>
{t("header.addOffer")}
</AddOfferButton>
<IconButton
onClick={(e) => {
setPostsPopoverOpen(true);
setPostsAnchorEl(e.currentTarget);
}}
style={{
background: selectedTheme.primaryIconBackgroundColor,
color: selectedTheme.primaryPurple,
}}
>
<Autorenew />
</IconButton>
<IconButton
onClick={(e) => {
setMsgPopoverOpen(true);
setMsgAnchorEl(e.currentTarget);
}}
style={{
background: selectedTheme.primaryIconBackgroundColor,
color: selectedTheme.primaryPurple,
}}
>
<Badge badgeContent={3} color="primary">
<MailIcon />
</Badge>
</IconButton>
<UserButton
onClick={(e) => {
setUserPopoverOpen(true);
setUserAnchorEl(e.currentTarget);
}}
>
<UserName>{name}</UserName>
<IconButton
style={{
background: selectedTheme.primaryIconBackgroundColor,
color: selectedTheme.primaryPurple,
}}
>
<AccountCircle />
</IconButton>
</UserButton>
</React.Fragment>
<AuthButtonsContainer mobile={matches}>
{matches ? (
<ToggleDrawerButton>
<IconButton onClick={handleToggleDrawer}>
<MenuOutlinedIcon />
</IconButton>
</ToggleDrawerButton>
) : (
<React.Fragment>
<LoginButton
type="submit"
variant="contained"
fullWidth
buttoncolor={selectedTheme.primaryPurple}
textcolor={selectedTheme.offerBackgroundColor}
onClick={handleNavigateLogin}
>
{t("login.headerTitle")}
</LoginButton>
<RegisterButton
type="submit"
variant="contained"
fullWidth
buttoncolor={selectedTheme.primaryYellow}
textcolor={selectedTheme.primaryDarkText}
onClick={handleNavigateRegister}
>
{t("register.headerTitle")}
</RegisterButton>
</React.Fragment>
)}
</AuthButtonsContainer>
)} )}
</ToolsButtonsContainer>
</ToolsContainer>
</Toolbar>
<PopoverComponent
anchorEl={postsAnchorEl}
open={postsPopoverOpen}
onClose={() => {
setPostsPopoverOpen(false);
setPostsAnchorEl(null);
}}
content={<MyPosts />}
/>
<PopoverComponent
anchorEl={msgAnchorEl}
open={msgPopoverOpen}
onClose={() => {
setMsgPopoverOpen(false);
setMsgAnchorEl(null);
</ToolsContainer>
</Toolbar>
{user && (
<React.Fragment>
<PopoverComponent
anchorEl={postsAnchorEl}
open={postsPopoverOpen}
onClose={() => {
setPostsPopoverOpen(false);
setPostsAnchorEl(null);
}}
content={<MyPosts />}
/>
<PopoverComponent
anchorEl={msgAnchorEl}
open={msgPopoverOpen}
onClose={() => {
setMsgPopoverOpen(false);
setMsgAnchorEl(null);
}}
content={<MyMessages />}
/>
<PopoverComponent
anchorEl={userAnchorEl}
open={userPopoverOpen}
onClose={() => {
setUserPopoverOpen(false);
setUserAnchorEl(null);
}}
content={<MyProfile />}
/>
</React.Fragment>
)}
</AppBar>
<SearchInputMobile
fullWidth
ref={searchMobileRef}
InputProps={{
endAdornment: (
<React.Fragment>
<FilterContainer number={numberOfFilters}>
<FilterIcon onClick={toggleFilters} />
</FilterContainer>
<EndIcon size="36px">
<SearchIcon
onClick={() => handleSearch(searchMobileRef.current.value)}
/>
</EndIcon>
</React.Fragment>
),
}} }}
content={<MyMessages />}
placeholder={t("header.searchOffers")}
italicPlaceholder
onFocus={handleFocusSearch}
onBlur={handleBlurSearch}
/> />
<PopoverComponent
anchorEl={userAnchorEl}
open={userPopoverOpen}
onClose={() => {
setUserPopoverOpen(false);
setUserAnchorEl(null);
}}
content={<MyProfile />}
<FilterCard
responsive={true}
responsiveOpen={openFilters}
closeResponsive={toggleFilters}
/> />
</AppBar>
</HeaderContainer>
); );
}; };


Header.propTypes = {
isGrid: PropTypes.bool,
};

export default Header; export default Header;

+ 127
- 14
src/components/Header/Header.styled.js Ver fichero

import { PrimaryButton } from "../Buttons/PrimaryButton/PrimaryButton"; import { PrimaryButton } from "../Buttons/PrimaryButton/PrimaryButton";
import { TextField } from "../TextFields/TextField/TextField"; import { TextField } from "../TextFields/TextField/TextField";
import { ReactComponent as Search } from "../../assets/images/svg/magnifying-glass.svg"; import { ReactComponent as Search } from "../../assets/images/svg/magnifying-glass.svg";
import { ReactComponent as Filter } from "../../assets/images/svg/filter.svg";
import selectedTheme from "../../themes"; import selectedTheme from "../../themes";
import { Icon } from "../Icon/Icon";
import IconWithNumber from "../Icon/IconWithNumber/IconWithNumber";


export const SearchInput = styled(TextField)` export const SearchInput = styled(TextField)`
margin-left: 3.8rem;
background-color: #f4f4f4; background-color: #f4f4f4;
width: 45%; width: 45%;
flex: 3;
max-width: 520px; max-width: 520px;
margin-right: 30px; margin-right: 30px;
font-family: "Open Sans"; font-family: "Open Sans";
@media (max-width: 1700px) {
margin-left: 15%;
}
@media (max-width: 1550px) {
margin-left: 15%;
}
@media (max-width: 1320px) {
margin-left: 7%;
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
width: 36%; width: 36%;
} }
@media (max-width: 1000px) { @media (max-width: 1000px) {
width: 54%;
margin-left: 2.8rem;
width: 36%;
margin-left: 5%;
margin-right: 10px; margin-right: 10px;
} }
@media (max-width: 600px) {
width: 60%;
margin-left: 2rem;
@media (max-width: 550px) {
display: none;
width: 0;
} }
`; `;
export const DrawerContainer = styled(Box)` export const DrawerContainer = styled(Box)`
export const ToolsContainer = styled(Box)` export const ToolsContainer = styled(Box)`
display: flex; display: flex;
flex-direction: row; flex-direction: row;

justify-content: ${(props) => (props.mobile ? "center" : "space-between")}; justify-content: ${(props) => (props.mobile ? "center" : "space-between")};
align-items: ${(props) => (props.mobile ? "start" : "center")}; align-items: ${(props) => (props.mobile ? "start" : "center")};
${(props) => !props.mobile && `width: 100%;`} ${(props) => !props.mobile && `width: 100%;`}
& div button { & div button {
${props => props.mobile && `width: auto;`}
${(props) => props.mobile && `width: auto;`}
} }
`; `;
export const LogoContainer = styled(Box)` export const LogoContainer = styled(Box)`
display: flex; display: flex;
flex: 4; flex: 4;
justify-content: space-between; justify-content: space-between;
min-width: ${props => props.mobile ? "40px" : "600px"};
min-width: ${(props) => (props.mobile ? "40px" : "600px")};
max-width: 600px; max-width: 600px;
align-items: center; align-items: center;
flex-wrap: nowrap; flex-wrap: nowrap;
@media (max-width: 1400px) {
min-width: 450px;
}
@media (max-width: 1200px) { @media (max-width: 1200px) {
min-width: 400px; min-width: 400px;
} }
@media (max-width: 950px) {
min-width: 250px;
}
@media (max-width: 800px) {
@media (max-width: 900px) {
flex: 0.35;
min-width: 0px; min-width: 0px;
width: 0px;
width: 60px;
justify-content: right;
} }
`; `;
export const ToggleDrawerButton = styled(Box)` export const ToggleDrawerButton = styled(Box)`
width: 180px; width: 180px;
font-weight: 600; font-weight: 600;
`; `;
export const EndIcon = styled(Icon)``;
export const SearchIcon = styled(Search)` export const SearchIcon = styled(Search)`
position: relative; position: relative;
top: 11px; top: 11px;
left: 4px; left: 4px;
cursor: pointer;
color: ${selectedTheme.primaryPurple}; color: ${selectedTheme.primaryPurple};
& path { & path {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
@media (max-width: 600px) {
height: 14px;
width: 14px;
left: 11px;
}
`; `;
export const UserButton = styled(Box)` export const UserButton = styled(Box)`
display: flex; display: flex;
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
`; `;
export const RegisterButton = styled(PrimaryButton)`
height: 49px;
width: 180px;
font-weight: 600;
@media (max-width: 550px) {
margin-bottom: 20px;
}
`;
export const LoginButton = styled(PrimaryButton)`
height: 49px;
width: 180px;
font-weight: 600;
margin-right: 10px;
`;
export const AuthButtonsContainer = styled(Box)`
display: flex;
justify-content: flex-start;
flex: 1;
min-width: ${(props) => (props.mobile ? "40px" : "200px")};
max-width: 600px;
align-items: flex-start;
flex-wrap: nowrap;
margin-left: 40px;
& div {
margin-left: 20px;
}
@media (max-width: 1300px) {
margin-left: 0;
}
@media (max-width: 1200px) {
min-width: 400px;
}
@media (max-width: 900px) {
min-width: 0px;
width: 0px;
justify-content: right;
}
`;

export const AuthButtonsDrawerContainer = styled(Box)`
position: relative;
left: 10px;
height: 200px;
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-around;
`;
export const SearchInputMobile = styled(SearchInput)`
@media (max-width: 550px) {
display: block;
position: relative;
width: 80%;
top: 70px;
height: 46px;
left: -50px;
font-family: "Open Sans";
& div {
background-color: white;
height: 40px;
overflow: visible;
& input {
font-size: 14px !important;
}
}
}
@media (min-width: 551px) {
display: none;
width: 0;
}
`;
export const FilterContainer = styled(IconWithNumber)`
position: relative;
top: 8px;
left: 95px;
cursor: pointer;
background-color: ${selectedTheme.offerBackgroundColor} !important;
& div {
width: 16px;
height: 16px;
background-color: ${selectedTheme.primaryPurple};
position: absolute;
top: -5px;
right: -5px;
line-height: 15px;
text-align: center;
padding-right: 2px;
}
`;
export const FilterIcon = styled(Filter)`
background-color: ${selectedTheme.offerBackgroundColor};
`;
export const HeaderContainer = styled(Box)``;

+ 2
- 1
src/components/Icon/IconWithNumber/IconWithNumber.js Ver fichero



const IconWithNumber = (props) => { const IconWithNumber = (props) => {
return ( return (
<IconWithNumberContainer>
<IconWithNumberContainer className={props.className}>
{props.children} {props.children}
{props.number > 0 && <Number>{props.number}</Number>} {props.number > 0 && <Number>{props.number}</Number>}
</IconWithNumberContainer> </IconWithNumberContainer>
IconWithNumber.propTypes = { IconWithNumber.propTypes = {
children: PropTypes.node, children: PropTypes.node,
number: PropTypes.number, number: PropTypes.number,
className: PropTypes.string,
} }


export default IconWithNumber export default IconWithNumber

+ 20
- 10
src/components/MarketPlace/Header/Header.js Ver fichero

import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { import {
HeaderAltLocation,
HeaderButton, HeaderButton,
HeaderButtons, HeaderButtons,
HeaderContainer, HeaderContainer,
HeaderOptions, HeaderOptions,
HeaderSelect, HeaderSelect,
IconStyled, IconStyled,
SelectOption,
} from "./Header.styled"; } from "./Header.styled";
import { ReactComponent as GridSquare } from "../../../assets/images/svg/offer-grid-square.svg"; 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 GridLine } from "../../../assets/images/svg/offer-grid-line.svg";
import { ReactComponent as Down } from "../../../assets/images/svg/down-arrow.svg"; import { ReactComponent as Down } from "../../../assets/images/svg/down-arrow.svg";
import selectedTheme from "../../../themes"; import selectedTheme from "../../../themes";
import Option from "../../Select/Option/Option";
import { sortEnum } from "../../../enums/sortEnum"; import { sortEnum } from "../../../enums/sortEnum";
import useFilters from "../../../hooks/useFilters"; import useFilters from "../../../hooks/useFilters";
import useSorting from "../../../hooks/useSorting"; import useSorting from "../../../hooks/useSorting";
import { useTranslation } from "react-i18next";
import { Tooltip } from "@mui/material";


const DownArrow = (props) => ( const DownArrow = (props) => (
<IconStyled {...props}> <IconStyled {...props}>
const Header = (props) => { const Header = (props) => {
const filters = useFilters(); const filters = useFilters();
const sorting = useSorting(); const sorting = useSorting();
const { t } = useTranslation();
const [sortOption, setSortOption] = useState(sortEnum.INITIAL); const [sortOption, setSortOption] = useState(sortEnum.INITIAL);
const [headerString, setHeaderString] = useState("SVE KATEGORIJE"); const [headerString, setHeaderString] = useState("SVE KATEGORIJE");




useEffect(() => { useEffect(() => {
if (filters.isApplied) { if (filters.isApplied) {
let headerStringLocal = "SVE KATEGORIJE";
let headerStringLocal = "Sve kategorije";
if (filters.selectedCategory?.name) { if (filters.selectedCategory?.name) {
headerStringLocal = filters.selectedCategory.name; headerStringLocal = filters.selectedCategory.name;
if (filters.selectedSubcategory?.name) { if (filters.selectedSubcategory?.name) {


return ( return (
<HeaderContainer> <HeaderContainer>
<HeaderLocation>{headerString}</HeaderLocation>
<Tooltip title={headerString}>
{headerString === "Sve kategorije" &&
(sorting.selectedSortOption === sortEnum.INITIAL ||
sorting.selectedSortOption === sortEnum.NEW) ? (
<React.Fragment>
<HeaderLocation initial>{headerString}</HeaderLocation>
<HeaderAltLocation>{t("header.newOffers")}</HeaderAltLocation>
</React.Fragment>
) : (
<HeaderLocation>{headerString}</HeaderLocation>
)}
</Tooltip>
<HeaderOptions> <HeaderOptions>
<HeaderButtons> <HeaderButtons>
<HeaderButton <HeaderButton
<HeaderSelect <HeaderSelect
value={sortOption?.value ? sortOption.value : sortEnum.INITIAL.value} value={sortOption?.value ? sortOption.value : sortEnum.INITIAL.value}
IconComponent={DownArrow} IconComponent={DownArrow}
width="209px"
height="34px"
onChange={handleChangeSelect} onChange={handleChangeSelect}
> >
{Object.keys(sortEnum).map((property) => { {Object.keys(sortEnum).map((property) => {
return ( return (
<Option
<SelectOption
value={sortEnum[property].value} value={sortEnum[property].value}
key={sortEnum[property].value} key={sortEnum[property].value}
style={{
display: sortEnum[property].value === 0 ? "none" : "flex",
}}
> >
{sortEnum[property].mainText} {sortEnum[property].mainText}
</Option>
</SelectOption>
); );
})} })}
</HeaderSelect> </HeaderSelect>

+ 82
- 45
src/components/MarketPlace/Header/Header.styled.js Ver fichero

import { Box, MenuItem } from "@mui/material";
import { Box, MenuItem, Typography } from "@mui/material";
import styled from "styled-components"; import styled from "styled-components";
import selectedTheme from "../../../themes"; import selectedTheme from "../../../themes";
import { IconButton } from "../../Buttons/IconButton/IconButton"; import { IconButton } from "../../Buttons/IconButton/IconButton";
import Option from "../../Select/Option/Option";
import Select from "../../Select/Select"; import Select from "../../Select/Select";


export const HeaderContainer = styled(Box)` export const HeaderContainer = styled(Box)`
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
`
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
`;
export const HeaderLocation = styled(Box)` export const HeaderLocation = styled(Box)`
font-family: "Open Sans";
color: ${selectedTheme.primaryPurple};
font-weight: 700;
line-height: 22px;
font-size: 16px;
flex: 2;
`
export const HeaderButton = styled(IconButton)`
padding: 2px 10px;
@media (max-width: 1500px) {
display: none;
font-family: "Open Sans";
color: ${selectedTheme.primaryPurple};
font-weight: 700;
line-height: 22px;
font-size: 16px;
flex: 2;
max-width: ${props => props.initial ? "fit-content" : "50%"};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:after {
content: ${props => props.initial ? `":"` : `""`};
@media (max-width: 600px) {
content: "";
} }
`
}
@media (max-width: 600px) {
font-size: 14px;
padding-top: 3px;
}
`;
export const HeaderButton = styled(IconButton)`
padding: 2px 10px;
@media (max-width: 1500px) {
display: none;
}
`;
export const HeaderOptions = styled(Box)` export const HeaderOptions = styled(Box)`
display: flex;
flex-direction: row;
flex: 1;
justify-content: end;
`
display: flex;
flex-direction: row;
flex: 1;
justify-content: end;
`;
export const HeaderSelect = styled(Select)` export const HeaderSelect = styled(Select)`
width: 210px;
height: 35px;
font-family: "Open Sans";
margin-top: 3px;
font-weight: 400;
position: relative;
left: -5px;
& div:first-child {
padding-left: 8px;
}
`
width: 210px;
height: 35px;
font-family: "Open Sans";
margin-top: 3px;
font-weight: 400;
position: relative;
left: -5px;
& div:first-child {
padding-left: 8px;
}

@media (max-width: 650px) {
width: 144px;
height: 30px;
font-size: 14px;
}
`;
export const SelectItem = styled(MenuItem)` export const SelectItem = styled(MenuItem)`
font-family: "Open Sans";
`
font-family: "Open Sans";
`;
export const SelectOption = styled(Option)`
@media (max-width: 600px) {
height: 20px !important;
min-height: 35px;
margin: 2px;
}
`;
export const IconStyled = styled(Box)` export const IconStyled = styled(Box)`
position: relative;
top: 0;
right: 10px;
`
position: relative;
top: 0;
right: 10px;
`;
export const HeaderButtons = styled(Box)` export const HeaderButtons = styled(Box)`
flex-direction: row;
display: flex;
justify-content: space-between;
margin-right: 40px;
`
flex-direction: row;
display: flex;
justify-content: space-between;
margin-right: 40px;
`;
export const HeaderAltLocation = styled(Typography)`
font-family: "Open Sans";
font-size: 16px;
color: ${selectedTheme.primaryText};
margin-left: 5px;
@media (max-width: 600px) {
display: none;
}
`

+ 2
- 2
src/components/MarketPlace/MarketPlace.styled.js Ver fichero

export const MarketPlaceContainer = styled(Box)` export const MarketPlaceContainer = styled(Box)`
height: 100%; height: 100%;
margin: 0 70px; margin: 0 70px;
@media (max-width: 600px) {
margin: 0 1.8rem;
@media (max-width: 550px) {
margin: -30px 1.8rem;
} }
`; `;

+ 83
- 46
src/components/MarketPlace/Offers/Offers.js Ver fichero

import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { OffersContainer } from "./Offers.styled"; import { OffersContainer } from "./Offers.styled";
import OfferCard from "../../Cards/OfferCard/OfferCard"; import OfferCard from "../../Cards/OfferCard/OfferCard";
import {
fetchMoreOffers,
fetchOffers,
} from "../../../store/actions/offers/offersActions";
import { fetchOffers } from "../../../store/actions/offers/offersActions";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
selectNoMoreOffers,
selectOffers, selectOffers,
selectPinnedOffers, selectPinnedOffers,
selectTotalOffers,
} from "../../../store/selectors/offersSelectors"; } from "../../../store/selectors/offersSelectors";
import useFilters from "../../../hooks/useFilters";
// import useFilters from "../../../hooks/useFilters";
import Paging from "../../Paging/Paging";
// import { convertQueryString } from "../../../util/helpers/queryHelpers";
import { HOME_PAGE } from "../../../constants/pages";
import { useHistory } from "react-router-dom";
// import qs from "query-string";
// import useSorting from "../../../hooks/useSorting";
import { useQueryString } from "../../../hooks/useQueryString";


const Offers = (props) => { const Offers = (props) => {
const filters = useFilters();
const [page, setPage] = useState(2);
const [initialLoad, setInitialLoad] = useState(true);
// const filters = useFilters();
const [page, setPage] = useState(1);
// const [initialLoad, setInitialLoad] = useState(true);
const pinnedOffers = useSelector(selectPinnedOffers); const pinnedOffers = useSelector(selectPinnedOffers);
const offers = useSelector(selectOffers); const offers = useSelector(selectOffers);
const total = useSelector(selectTotalOffers);
const history = useHistory();
// const sorting = useSorting();
const dispatch = useDispatch(); const dispatch = useDispatch();
const offersRef = useRef(null); const offersRef = useRef(null);
const noMoreOffersStatus = useSelector(selectNoMoreOffers);
let timeout = null;
let listener;
const queryStringHook = useQueryString();


// const queryString = history.location.search.substring(1);
// const queryObject = qs.parse(queryString);
useEffect(() => { useEffect(() => {
listener = () => {
if (
!noMoreOffersStatus &&
offers?.length > 0 &&
window.scrollY + window.innerHeight >
window.document.body.offsetHeight * 0.8
) {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
}, 5000);
dispatch(
fetchMoreOffers({ page: page, queryString: filters.queryString })
);
}
}
};
window.addEventListener("scroll", listener);
return () => window.removeEventListener("scroll", listener);
}, [page, noMoreOffersStatus, filters.queryString, timeout, offers]);

useEffect(() => {
setPage(Math.floor((offers.length + pinnedOffers.length ) / 10) + 1);
}, [offers, pinnedOffers]);
let queryObject = queryStringHook.getQueryObject();
if (queryObject.page && queryObject.page !== 1) {
setPage(parseInt(queryObject.page));
}
}, [history.location.search]);


useEffect(() => { useEffect(() => {
dispatch(fetchOffers({ page: 1 }));
}, []);
// console.log("sorting.loadedQS", sorting.loadedQS);
// console.log("queryString", queryString)
if (queryStringHook.loadedFromURL) {
dispatch(fetchOffers({ queryString: "?" + queryStringHook.queryString }));
history.push({
pathname: HOME_PAGE,
search: queryStringHook.getGlobalQueryString(),
});
window.scrollTo({
top: 0,
behavior: "smooth",
});
const queryObject = new URLSearchParams(queryStringHook.queryString);
if (queryObject.has("page")) {
if (queryObject.get("page") !== page.toString())
setPage(parseInt(queryObject.get("page")));
} else {
setPage(1);
}
} else {
queryStringHook.appendMultipleToQueryString([
{ key: "size", value: "10" },
{ key: "page", value: "1" },
]);
// queryStringHook.appendToQueryString("page", 1);
}
}, [queryStringHook.loadedFromURL, queryStringHook.queryString]);


useEffect(() => { useEffect(() => {
if (filters.queryString) {
if (filters.queryString.length > 1) {
if (initialLoad) {
dispatch(fetchOffers({ page: 1, queryString: filters.queryString }));
setInitialLoad(false);
// if (page !== 1) {
const queryObject = new URLSearchParams(queryStringHook.queryString);
if (queryObject.has("page")) {
if (queryObject.get("page") !== page.toString()) {
queryStringHook.appendToQueryString("page", page);
} }
} else { } else {
setInitialLoad(false);
queryStringHook.appendToQueryString("page", page);
} }
}
}, [filters.queryString, initialLoad]);
console.log("PAGE KONZOLAAAAAAAAAAAAAAAAAAAAAA")
// }
}, [page]);

// useEffect(() => {
// if (filters.queryString) {
// if (filters.queryString.length > 1) {
// if (initialLoad) {
// dispatch(fetchOffers({ page: 1, queryString: filters.queryString }));
// setInitialLoad(false);
// }
// } else {
// setInitialLoad(false);
// }
// }
// }, [filters.queryString, initialLoad]);

const handleDifferentPage = (pageNum) => {
setPage(pageNum);
};


return ( return (
<OffersContainer ref={offersRef}> <OffersContainer ref={offersRef}>
<OfferCard key={item._id} offer={item} halfwidth={props.isGrid} /> <OfferCard key={item._id} offer={item} halfwidth={props.isGrid} />
); );
})} })}
<Paging
totalElements={total}
elementsPerPage={10}
current={page}
changePage={handleDifferentPage}
/>
</OffersContainer> </OffersContainer>
); );
}; };

+ 3
- 0
src/components/MarketPlace/Offers/Offers.styled.js Ver fichero

flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
margin-top: 5px;
position: relative;
padding-bottom: 60px;
`; `;

+ 76
- 0
src/components/Paging/Paging.js Ver fichero

import React from "react";
import PropTypes from "prop-types";
import {
Arrow,
ArrowIcon,
PageNumber,
PagingContainer,
ThreeDots,
} from "./Paging.styled";

const Paging = (props) => {
const pages = props.pages
? props.pages
: props.totalElements
? Math.ceil(props.totalElements / props.elementsPerPage)
: 1;
let moving = 0;
const pagesAsArray = Array.apply(null, Array(5)).map(() => {});
const threeDotsBefore = props.current - 2 > 1;
const threeDotsAfter = props.current + 2 < pages;
return (
<PagingContainer>
<Arrow
onClick={() => props.changePage(props.current - 1)}
disabled={props.current - 1 < 1}
>
<ArrowIcon side="left" />
</Arrow>
{threeDotsBefore && (
<React.Fragment>
<PageNumber onClick={() => props.changePage(1)}>1</PageNumber>
{props.current - 3 !== 1 && <ThreeDots>...</ThreeDots>}
</React.Fragment>
)}
{pagesAsArray.map((item, index) => {
const pageNum = props.current - 2 + moving++;
if (pageNum > pages ) return;
if (pageNum < 1) return;
return (
<PageNumber
current={pageNum === props.current}
key={index}
onClick={() => props.changePage(pageNum)}
>
{pageNum}
</PageNumber>
);
})}
{threeDotsAfter && (
<React.Fragment>
{props.current + 3 !== pages && <ThreeDots>...</ThreeDots>}
<PageNumber onClick={() => props.changePage(pages)}>
{pages}
</PageNumber>
</React.Fragment>
)}
<Arrow
onClick={() => props.changePage(props.current + 1)}
disabled={props.current + 1 > pages}
>
<ArrowIcon side="right" />
</Arrow>
</PagingContainer>
);
};

Paging.propTypes = {
children: PropTypes.any,
totalElements: PropTypes.number,
elementsPerPage: PropTypes.number,
pages: PropTypes.number,
current: PropTypes.number,
changePage: PropTypes.func,
};

export default Paging;

+ 137
- 0
src/components/Paging/Paging.styled.js Ver fichero

import { Box, Button } from "@mui/material";
import styled from "styled-components";
import selectedTheme from "../../themes";
import { ReactComponent as DownArrow } from "../../assets/images/svg/arrow-down.svg";

export const PagingContainer = styled(Box)`
width: calc(100% / 12 * 8.5);
text-align: center;
font-family: "Open Sans";
display: flex;
flex: 1;
justify-content: center;
flex-direction: row;
margin-top: 5px;
margin-bottom: 10px;
position: absolute;
bottom: 0;
padding-left: 0;
padding-right: 0;
margin: auto;
left: 0;
right: 0;
`;
export const ArrowIcon = styled(DownArrow)`
${(props) =>
props.side === "left" &&
`
transform: rotate(180deg);
`}
width: 18px;
height: 18px;
& path {
${(props) =>
props.disabled &&
`
stroke: ${selectedTheme.iconStrokeDisabledColor}
`}
}
`;
export const Arrow = styled(Button)`
border: 1px solid ${selectedTheme.primaryPurple};
border-radius: 100%;
min-width: 36px;
width: 36px;
height: 36px;
display: block;
box-sizing: border-box;
cursor: pointer;
padding-left: 8px;
padding-top: 8px;
margin: auto 10px;
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;
}
`}
@media (max-width: 600px) {
width: 30px;
min-width: 30px;
height: 30px;
padding-top: 5px;
padding-left: 5px;
}
`;
export const PageNumber = styled(Box)`
color: ${(props) => (!props.current ? selectedTheme.primaryPurple : "white")};
font-weight: 600;
font-size: 16px;
line-height: 18px;
font-family: "Open Sans";
height: 40px;
min-width: 40px;
max-width: 40px;
width: 40px !important;
margin: 5px;
padding-top: 10px;
background-color: ${(props) => props.current && selectedTheme.primaryPurple};
border-radius: 100%;
position: relative;
top: 1px;
&:hover {
cursor: pointer;
${(props) =>
!props.current &&
`
color: ${props.current ? selectedTheme.primaryPurple : "white"};
background-color: ${!props.current && selectedTheme.primaryPurple};
`}
}
@media (max-width: 600px) {
height: 30px;
min-width: 30px;
max-width: 30px;
width: 30px !important;
padding-top: 6px;
font-size: 14px;
margin: 1px;
}
`;
export const ThreeDots = styled(Box)`
color: ${(props) => (!props.current ? selectedTheme.primaryPurple : "white")};
font-weight: 600;
font-size: 16px;
line-height: 18px;
font-family: "Open Sans";
height: 40px;
min-width: 40px;
max-width: 40px;
width: 40px !important;
margin: 5px;
padding-top: 10px;
background-color: ${(props) => props.current && selectedTheme.primaryPurple};
border-radius: 100%;
position: relative;
top: 1px;
@media (max-width: 600px) {
height: 10px;
min-width: 10px;
max-width: 10px;
width: 10px !important;
padding-top: 6px;
font-size: 14px;
margin: 1px;
}
`

+ 43
- 21
src/components/Popovers/HeaderPopover/HeaderPopover.js Ver fichero

import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { import {
EyeIcon,
HeaderPopoverContainer, HeaderPopoverContainer,
PopoverButton, PopoverButton,
PopoverButtonsContainer,
PopoverList, PopoverList,
PopoverListItem, PopoverListItem,
PopoverListItemAvatar, PopoverListItemAvatar,
PopoverListItemAvatarContainer, PopoverListItemAvatarContainer,
PopoverListItemProfileAvatar, PopoverListItemProfileAvatar,
PopoverListItemTextContainer, PopoverListItemTextContainer,
PopoverNoItemsText,
PopoverTitle, PopoverTitle,
} from "./HeaderPopover.styled"; } from "./HeaderPopover.styled";
import selectedTheme from "../../../themes";


const HeaderPopover = (props) => { const HeaderPopover = (props) => {
return ( return (
<HeaderPopoverContainer> <HeaderPopoverContainer>
<PopoverTitle p={2}>{props.title}</PopoverTitle> <PopoverTitle p={2}>{props.title}</PopoverTitle>
<PopoverList> <PopoverList>
{props.items.map((item, index) => (
{props.items?.length > 0 ? props.items.map((item, index) => (
<PopoverListItem key={index}> <PopoverListItem key={index}>
<PopoverListItemAvatarContainer> <PopoverListItemAvatarContainer>
{props.isProfile ? (
<PopoverListItemProfileAvatar alt={item.alt} src={item.src} />
) : (
<PopoverListItemAvatar alt={item.alt} src={item.src} />
)}
{props.isProfile ? (
<PopoverListItemProfileAvatar alt={item.alt} src={item.src} />
) : (
<PopoverListItemAvatar alt={item.alt} src={item.src} />
)}
</PopoverListItemAvatarContainer> </PopoverListItemAvatarContainer>
<PopoverListItemTextContainer <PopoverListItemTextContainer
primary={item.title}
secondary={item.text}
>
</PopoverListItemTextContainer>
primary={item.title}
secondary={item.text}
></PopoverListItemTextContainer>
</PopoverListItem> </PopoverListItem>
))}
)) : (
<PopoverNoItemsText>No items at the moment...</PopoverNoItemsText>
)}
</PopoverList> </PopoverList>
<PopoverButton
sx={{
<PopoverButtonsContainer>
<PopoverButton
sx={{
mr: 2, mr: 2,
mb: 2, mb: 2,
}}
variant="text"
endIcon={<EyeIcon color={selectedTheme.iconYellowColor} />}
>
{props.buttonText}
</PopoverButton>
}}
variant="text"
endIcon={props.buttonIcon}
onClick={props.buttonOnClick}
>
{props.buttonText}
</PopoverButton>
{props.secondButtonText && (
<PopoverButton
sx={{
mr: 2,
mb: 2,
}}
variant="text"
endIcon={props.secondButtonIcon}
onClick={props.secondButtonOnClick}
>
{props.secondButtonText}
</PopoverButton>
)}
</PopoverButtonsContainer>
</HeaderPopoverContainer> </HeaderPopoverContainer>
); );
}; };
items: PropTypes.array, items: PropTypes.array,
buttonText: PropTypes.string, buttonText: PropTypes.string,
isProfile: PropTypes.bool, isProfile: PropTypes.bool,
secondButtonText: PropTypes.string,
buttonIcon: PropTypes.any,
secondButtonIcon: PropTypes.any,
buttonOnClick: PropTypes.func,
secondButtonOnClick: PropTypes.func,
}; };


export default HeaderPopover; export default HeaderPopover;

+ 15
- 1
src/components/Popovers/HeaderPopover/HeaderPopover.styled.js Ver fichero

` `
export const PopoverButton = styled(Button)` export const PopoverButton = styled(Button)`
text-decoration: underline; text-decoration: underline;
float: right;
color: ${selectedTheme.primaryPurple}; color: ${selectedTheme.primaryPurple};
font-weight: 500; font-weight: 500;
text-align: right;
height: 20px;
` `
export const PopoverListItemTextContainer = styled(ListItemText)` export const PopoverListItemTextContainer = styled(ListItemText)`
& span { & span {
& path { & path {
stroke: ${selectedTheme.primaryYellow}; stroke: ${selectedTheme.primaryYellow};
} }
`
export const PopoverButtonsContainer = styled(Box)`
flex-direction: column;
display: flex;
align-items: flex-end;
`
export const PopoverNoItemsText = styled(Typography)`
text-align: center;
width: 100%;
font-weight: 600;
padding-top: 5px;
font-size: 13px;
font-family: "Open Sans";
` `

+ 20
- 37
src/components/Popovers/MyMessages/MyMessages.js Ver fichero

import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { fetchChats } from "../../../store/actions/chat/chatActions";
import { fetchHeaderChats } from "../../../store/actions/chat/chatActions";
import { selectLatestChats } from "../../../store/selectors/chatSelectors"; import { selectLatestChats } from "../../../store/selectors/chatSelectors";
import { selectUserId } from "../../../store/selectors/loginSelectors"; import { selectUserId } from "../../../store/selectors/loginSelectors";
import HeaderPopover from "../HeaderPopover/HeaderPopover"; import HeaderPopover from "../HeaderPopover/HeaderPopover";


const dummyData1 = [
{
alt: "Remy Sharp",
src: "/static/images/avatar/1.jpg",
title: "Coca-Cola",
text: "Kompresor je stigao. Samo...",
},
{
alt: "Travis Howard",
src: "/static/images/avatar/2.jpg",
title: "Voda Vrnjci",
text: "Poslao sam vodu. Ukupno i...",
},
];

const convertMessages = (messages) => { const convertMessages = (messages) => {
const allMessages = [
...messages.chatsFromMyOffers,
...messages.initiatedChats,
];
const lastMessageDate = Math.max(
...allMessages.map((item) => new Date(item._modified))
);
const lastMessage = allMessages.find(
(item) =>
JSON.stringify(new Date(item._modified)) ===
JSON.stringify(new Date(lastMessageDate))
);
const lastSecondMessageDate = Math.max([...allMessages.filter(item => item._id !== lastMessage._id).map(item => new Date(item._modified))])
console.log(lastSecondMessageDate);
console.log(allMessages.filter(item => item._id !== lastMessage._id).map(item => new Date(item._modified)))
console.log(lastMessage);
return messages.map((item) => ({
alt: "Tekst",
src: item.interlocutorData.image,
title: item.interlocutorData.name,
text: item?.chat?.messages[0]?.text,
}));
}; };


export const MyMessages = () => { export const MyMessages = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const userId = useSelector(selectUserId); const userId = useSelector(selectUserId);
const chats = useSelector(selectLatestChats); const chats = useSelector(selectLatestChats);
convertMessages(chats);
const [lastChats, setLastChats] = useState([]);

useEffect(() => {
if (userId?.length > 1) {
dispatch(fetchHeaderChats(userId));
}
}, [userId]);
useEffect(() => { useEffect(() => {
dispatch(fetchChats(userId));
}, []);
if (chats?.length > 0) {
setLastChats([...convertMessages(chats)]);
}
}, [chats]);
return ( return (
<HeaderPopover <HeaderPopover
title={t("header.myMessages")} title={t("header.myMessages")}
items={dummyData1}
items={lastChats}
buttonText={t("header.checkEverything")} buttonText={t("header.checkEverything")}
/> />
); );

+ 64
- 19
src/components/Popovers/MyPosts/MyPosts.js Ver fichero

import React from "react";
import React, { useEffect, useState } from "react";
import { PostsImgSuit } from "./MyPosts.styled"; import { PostsImgSuit } from "./MyPosts.styled";


const dummyData2 = [
{
alt: "Remy Sharp",
src: "/static/images/avatar/1.jpg",
title: "Gitara",
text: (<React.Fragment><PostsImgSuit/> Player.rs</React.Fragment>)
},
{
alt: "Remy Sharp",
src: "/static/images/avatar/1.jpg",
title: "Gitara",
text: (<React.Fragment><PostsImgSuit/> Player.rs</React.Fragment>)
}
]
// const dummyData2 = [
// {
// alt: "Remy Sharp",
// src: "/static/images/avatar/1.jpg",
// title: "Gitara",
// text: (<React.Fragment><PostsImgSuit/> Player.rs</React.Fragment>)
// },
// {
// alt: "Remy Sharp",
// src: "/static/images/avatar/1.jpg",
// title: "Gitara",
// text: (<React.Fragment><PostsImgSuit/> Player.rs</React.Fragment>)
// }
// ]
import HeaderPopover from "../HeaderPopover/HeaderPopover"; import HeaderPopover from "../HeaderPopover/HeaderPopover";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { selectMineOffers } from "../../../store/selectors/offersSelectors";
import { fetchMineOffers } from "../../../store/actions/offers/offersActions";
import { selectProfileName } from "../../../store/selectors/profileSelectors";


export const MyPosts = () => { export const MyPosts = () => {
const {t} = useTranslation();
const { t } = useTranslation();
const dispatch = useDispatch();
const mineOffers = useSelector(selectMineOffers);
const name = useSelector(selectProfileName);
const [arrayOfMineOffers, setArrayOfMineOffers] = useState([]);
useEffect(() => {
dispatch(fetchMineOffers());
}, []);
useEffect(() => {
if (mineOffers?.length > 0) {
if (mineOffers.length > 1) {
setArrayOfMineOffers(
[mineOffers[0], mineOffers[1]].map((item) => ({
alt: "Photo",
src: item.images[0],
title: item.name,
text: (
<React.Fragment>
<PostsImgSuit /> {name}
</React.Fragment>
),
}))
);
} else if (mineOffers.length > 0) {
setArrayOfMineOffers(
[mineOffers[0]].map((item) => ({
alt: "Photo",
src: item.images[0],
title: item.name,
text: (
<React.Fragment>
<PostsImgSuit /> {name}
</React.Fragment>
),
}))
);
} else {
setArrayOfMineOffers([])
}
}
});
return ( return (
<HeaderPopover <HeaderPopover
title={t("header.myOffers")}
items={dummyData2}
buttonText={t("header.checkEverything")}/>
title={t("header.myOffers")}
items={arrayOfMineOffers}
buttonText={t("header.checkEverything")}
/>
); );
}; };

+ 50
- 13
src/components/Popovers/MyProfile/MyProfile.js Ver fichero

import React from "react";
import { ProfileImgPIB } from "./MyProfile.styled";
import React, { useEffect, useState } from "react";
import { LogoutIcon, ProfileImgPIB } from "./MyProfile.styled";
import HeaderPopover from "../HeaderPopover/HeaderPopover"; import HeaderPopover from "../HeaderPopover/HeaderPopover";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const dummyData3 = [
{
alt: "Profile",
src: "/static/images/avatar/2.jpg",
title: "Player.rs",
text: <React.Fragment><ProfileImgPIB/> PIB - 1234567890 </React.Fragment>
},
];
import { useDispatch, useSelector } from "react-redux";
import { selectProfile } from "../../../store/selectors/profileSelectors";
import { selectUserId } from "../../../store/selectors/loginSelectors";
import { fetchProfile } from "../../../store/actions/profile/profileActions";
import selectedTheme from "../../../themes";
import { EyeIcon } from "../HeaderPopover/HeaderPopover.styled";
import { logoutUser } from "../../../store/actions/login/loginActions";
import { useHistory } from "react-router-dom";
import { LOGIN_PAGE } from "../../../constants/pages";


export const MyProfile = () => { export const MyProfile = () => {
const {t} = useTranslation();
const { t } = useTranslation();
const profile = useSelector(selectProfile);
const userId = useSelector(selectUserId);
const dispatch = useDispatch();
const history = useHistory();
const [profileAsArray, setProfileAsArray] = useState([]);
useEffect(() => {
if (userId?.length > 1) {
dispatch(fetchProfile(userId));
}
}, [userId]);
useEffect(() => {
if (profile?.statistics) {
setProfileAsArray([
{
alt: "Profile",
src: `${profile.image}`,
title: profile.company.name,
text: (
<React.Fragment>
<ProfileImgPIB />
PIB - {profile.company.PIB}
</React.Fragment>
),
},
]);
}
}, [profile]);
const handleLogoutSuccess = () => {
history.replace(LOGIN_PAGE);
};
const handleLogout = () => {
dispatch(logoutUser(handleLogoutSuccess));
};
return ( return (
<HeaderPopover <HeaderPopover
title={t("header.myProfile")} title={t("header.myProfile")}
items={dummyData3}
items={profileAsArray}
buttonText={t("header.checkProfile")} buttonText={t("header.checkProfile")}
buttonIcon={<EyeIcon color={selectedTheme.iconYellowColor} />}
isProfile isProfile
secondButtonIcon={<LogoutIcon color={selectedTheme.iconYellowColor} />}
secondButtonText={"Odjavite se"}
secondButtonOnClick={handleLogout}
/> />
); );
}; };

+ 3
- 0
src/components/Popovers/MyProfile/MyProfile.styled.js Ver fichero

import { TextField, Avatar } from "@mui/material"; import { TextField, Avatar } from "@mui/material";
import styled from "styled-components"; import styled from "styled-components";
import PIB from '@mui/icons-material/AdminPanelSettingsOutlined'; import PIB from '@mui/icons-material/AdminPanelSettingsOutlined';
import {ReactComponent as Logout} from "../../../assets/images/svg/log-out.svg";


export const ProfileImgPIB = styled(PIB)` export const ProfileImgPIB = styled(PIB)`
width: 1rem; width: 1rem;
@media (max-width: 600px) { @media (max-width: 600px) {
width: 36%; width: 36%;
} }
`
export const LogoutIcon = styled(Logout)`
` `

+ 4
- 0
src/components/Select/Select.styled.js Ver fichero

import { Box, Select } from "@mui/material"; import { Box, Select } from "@mui/material";
import styled from "styled-components"; import styled from "styled-components";
import selectedTheme from "../../themes";


export const SelectStyled = styled(Select)` export const SelectStyled = styled(Select)`
width: ${props => props.width}; width: ${props => props.width};
font-weight: 600; font-weight: 600;
font-family: "Open Sans"; font-family: "Open Sans";
cursor: pointer; cursor: pointer;
& fieldset {
border-color: ${props => props.borderColor ? props.borderColor : selectedTheme.primaryPurple} !important;
}
` `
export const SelectIcon = styled(Box)` export const SelectIcon = styled(Box)`
position: relative; position: relative;

+ 0
- 1
src/components/TextFields/TextField/TextField.js Ver fichero

setIsFieldEmpty(false); setIsFieldEmpty(false);
} }
}, [props.value]); }, [props.value]);

return ( return (
<TextFieldContainer <TextFieldContainer
style={props.containerStyle} style={props.containerStyle}

+ 150
- 95
src/hooks/useFilters.js Ver fichero

import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
// import { useHistory } from "react-router-dom";
import { fetchCategories } from "../store/actions/categories/categoriesActions"; import { fetchCategories } from "../store/actions/categories/categoriesActions";
import { import {
setFilteredCategory, setFilteredCategory,
setFilteredLocations, setFilteredLocations,
setFilteredSubcategory, setFilteredSubcategory,
setIsAppliedStatus, setIsAppliedStatus,
setQueryString,
// setQueryString,
} from "../store/actions/filters/filtersActions"; } from "../store/actions/filters/filtersActions";
import { fetchLocations } from "../store/actions/locations/locationsActions"; import { fetchLocations } from "../store/actions/locations/locationsActions";
import { import {
} from "../store/selectors/categoriesSelectors"; } from "../store/selectors/categoriesSelectors";
import { import {
selectAppliedStatus, selectAppliedStatus,
selectQueryString,
// selectQueryString,
selectSelectedCategory, selectSelectedCategory,
selectSelectedLocations, selectSelectedLocations,
selectSelectedSubcategory, selectSelectedSubcategory,
} from "../store/selectors/filtersSelectors"; } from "../store/selectors/filtersSelectors";
import qs from "query-string";
import { selectLocations } from "../store/selectors/locationsSelectors"; import { selectLocations } from "../store/selectors/locationsSelectors";
import { fetchOffers } from "../store/actions/offers/offersActions";
import { HOME_PAGE } from "../constants/pages";
import { convertQueryString } from "../util/helpers/queryHelpers";
// import { fetchOffers } from "../store/actions/offers/offersActions";
// import { HOME_PAGE } from "../constants/pages";
// import { convertQueryString } from "../util/helpers/queryHelpers";
// import qs from "query-string";
import { useQueryString } from "./useQueryString";
// import qs from "query-string";




const useFilters = () => { const useFilters = () => {
const queryString = useSelector(selectQueryString);
const selectedCategory = useSelector(selectSelectedCategory); const selectedCategory = useSelector(selectSelectedCategory);
const selectedSubcategory = useSelector(selectSelectedSubcategory); const selectedSubcategory = useSelector(selectSelectedSubcategory);
const selectedLocations = useSelector(selectSelectedLocations); const selectedLocations = useSelector(selectSelectedLocations);
const [loadedFromQS, setLoadedFromQS] = useState(false); const [loadedFromQS, setLoadedFromQS] = useState(false);
const [loaded, setLoadedStatus] = useState(false); const [loaded, setLoadedStatus] = useState(false);
const isApplied = useSelector(selectAppliedStatus); const isApplied = useSelector(selectAppliedStatus);
const history = useHistory();
// const history = useHistory();
const categories = useSelector(selectCategories); const categories = useSelector(selectCategories);
const subcategories = useSelector( const subcategories = useSelector(
selectSubcategories(selectedCategory?.name) selectSubcategories(selectedCategory?.name)
); );
const locations = useSelector(selectLocations); const locations = useSelector(selectLocations);
const dispatch = useDispatch(); const dispatch = useDispatch();
const queryStringHook = useQueryString();
useEffect(() => { useEffect(() => {
if (!loaded) { if (!loaded) {
dispatch(fetchCategories()); dispatch(fetchCategories());
setLoadedStatus(true); setLoadedStatus(true);
} }
}, [categories, locations]); }, [categories, locations]);

// useEffect(() => {
// if (loadedFromQS) {
// makeQueryString();
// }
// }, [selectedCategory, selectedLocations, selectedSubcategory, loadedFromQS]);

useEffect(() => { useEffect(() => {
if (!loadedFromQS) {
if (categories && locations) {
const queryString = history.location.search.substring(1);
const queryObject = qs.parse(queryString);
let category;
if (queryObject.category) {
category = categories.find(
(item) => item.name === queryObject.category.toString()
);
setSelectedCategory(category);
}
if (queryObject.subcategory) {
setSelectedSubcategory(
category.subcategories.find(
(item) =>
item.name.toString() === queryObject.subcategory.toString()
)
);
}
if (queryObject.location) {
let locationsToPush = [];
if (Array.isArray(queryObject.location)) {
queryObject.location.forEach((item) => {
locationsToPush.push(locations.find((p) => p.city === item));
});
} else {
const queryObject = new URLSearchParams(queryStringHook.queryString);
if (categories?.length > 0 && locations?.length > 0) {
let category;
if (queryObject.has("category")) {
category = categories.find(
(item) => item.name === queryObject.get("category").toString()
);
setSelectedCategory(category);
}
if (queryObject.has("subcategory")) {
setSelectedSubcategory(
category?.subcategories?.find(
(item) =>
item.name.toString() === queryObject.get("subcategory").toString()
)
);
}
if (queryObject.has("location")) {
let locationsToPush = [];
// if (Array.isArray(queryObject.location)) {
queryObject.getAll("location").forEach((item) => {
locationsToPush.push( locationsToPush.push(
locations.find((p) => p.city === queryObject.location)
locations.find((p) => p.city === item)
); );
}
setSelectedLocations([...locationsToPush]);
}
// For future changes if needed
// if (queryObject.sortBy) {
// if (queryObject.sortBy === sortOptions.OLD.queryString) {
// setSelectedSortOption(sortOptions.OLD);
// }
// if (queryObject.sortBy === sortOptions.NEW.queryString) {
// setSelectedSortOption(sortOptions.NEW);
// }
// if (queryObject.sortBy === sortOptions.POPULAR.queryString) {
// setSelectedSortOption(sortOptions.POPULAR);
// }
});
// } else {
// locationsToPush.push(
// locations.find((p) => p.city === queryObject.location)
// );
// } // }
setLoadedFromQS(true);
dispatch(setIsAppliedStatus(true));
setSelectedLocations([...locationsToPush]);
} }
} }
}, [history.location.search]);

useEffect(() => {
if (loadedFromQS) {
makeQueryString();
}
}, [
selectedCategory,
selectedLocations,
selectedSubcategory,
loadedFromQS,
]);
}, [queryStringHook.queryString, categories, locations]);
// useEffect(() => {
// console.log("if (!loadedFromQS) {");
// console.log("if (!loadedFromQS) {", queryString);
// if (!loadedFromQS) {
// if (categories && locations) {
// const queryString = history.location.search.substring(1);
// const queryObject = qs.parse(queryString);
// console.log(queryObject)
// let category;
// if (queryObject.category) {
// category = categories.find(
// (item) => item.name === queryObject.category.toString()
// );
// setSelectedCategory(category);
// }
// if (queryObject.subcategory) {
// setSelectedSubcategory(
// category?.subcategories?.find(
// (item) =>
// item.name.toString() === queryObject.subcategory.toString()
// )
// );
// }
// if (queryObject.location) {
// let locationsToPush = [];
// if (Array.isArray(queryObject.location)) {
// queryObject.location.forEach((item) => {
// locationsToPush.push(
// locations.find((p) => p.city === item)
// );
// });
// } else {
// locationsToPush.push(
// locations.find((p) => p.city === queryObject.location)
// );
// }
// setSelectedLocations([...locationsToPush]);
// }
// setLoadedFromQS(true);
// dispatch(setIsAppliedStatus(true));
// }
// }
// }, [history.location.search]);


// Apply everything // Apply everything
const applyFilters = () => { const applyFilters = () => {
dispatch(setIsAppliedStatus(true));
dispatch(fetchOffers({ queryString: queryString }));
history.push({
pathname: HOME_PAGE,
search: convertQueryString(queryString),
});
window.scrollTo({
top: 0,
behavior: "smooth",
});
// console.log("dispatch(setIsAppliedStatus(true));");
// console.log("dispatch(setIsAppliedStatus(true));", queryString);
// dispatch(setIsAppliedStatus(true));
// dispatch(fetchOffers({ queryString: queryStringHook.getQueryString() }));
// history.push({
// pathname: HOME_PAGE,
// search: queryStringHook.getGlobalQueryString(),
// });
// window.scrollTo({
// top: 0,
// behavior: "smooth",
// });
makeQueryString();
}; };


// Clear function // Clear function
// Helper function // Helper function
const makeQueryString = () => { const makeQueryString = () => {
let qsArray = []; let qsArray = [];
// if (selectedCategory && selectedCategory?._id !== 0) {
// qsArray.push("category=" + encodeURIComponent(selectedCategory.name));
// queryStringHook.appendToQueryString("category", selectedCategory.name);
qsArray.push({key: "category", value: selectedCategory?.name })
// }
// if (selectedSubcategory && selectedSubcategory?._id !== 0) {
// qsArray.push(
// "subcategory=" + encodeURIComponent(selectedSubcategory.name)
// );
// queryStringHook.appendToQueryString("subcategory", selectedSubcategory.name)
qsArray.push({key: "subcategory", value: selectedSubcategory?.name})
// }
// if (selectedLocations && selectedLocations?.length > 0) {
selectedLocations?.forEach((location) => {
// qsArray.push("location=" + encodeURIComponent(location.city));
// queryStringHook.appendToQueryString("location", location.city);
qsArray.push({key: "location", value: location?.city})
});
qsArray.push({key: "page", value: "1"})
// }
// let qsStringFromArray = "?" + qsArray.join("&");
// dispatch(setQueryString(qsStringFromArray));
queryStringHook.appendMultipleToQueryString(qsArray);
};

//Calculate chosen categories for number above filter icon on mobile responsive version
const calculateFiltersChosen = () => {
let sum = 0;
if (selectedCategory && selectedCategory?._id !== 0) { if (selectedCategory && selectedCategory?._id !== 0) {
qsArray.push("category=" + encodeURIComponent(selectedCategory.name));
sum++;
} }
if (selectedSubcategory && selectedSubcategory?._id !== 0) { if (selectedSubcategory && selectedSubcategory?._id !== 0) {
qsArray.push(
"subcategory=" + encodeURIComponent(selectedSubcategory.name)
);
sum++;
} }
if (selectedLocations && selectedLocations?.length > 0) { if (selectedLocations && selectedLocations?.length > 0) {
selectedLocations.forEach((location) => {
qsArray.push("location=" + encodeURIComponent(location.city));
});
sum += selectedLocations.length;
} }
let qsStringFromArray = "?" + qsArray.join("&");
// if (queryString.length > 1) {
// dispatch(setQueryString(queryString + "&" + qsStringFromArray));
// } else {
// let queryStringEdited = new URLSearchParams(qsStringFromArray);

dispatch(setQueryString(qsStringFromArray));
// }
return sum;
}; };



// Setters // Setters
const setSelectedCategory = (payload) => { const setSelectedCategory = (payload) => {
if (isApplied !== false) { if (isApplied !== false) {
dispatch(setIsAppliedStatus(false)); dispatch(setIsAppliedStatus(false));
} }
dispatch(setFilteredCategory(payload));
if (JSON.stringify(payload) !== JSON.stringify(selectedCategory)) {
dispatch(setFilteredCategory(payload));
}
}; };
const setSelectedSubcategory = (payload) => { const setSelectedSubcategory = (payload) => {
if (isApplied !== false) { if (isApplied !== false) {
dispatch(setIsAppliedStatus(false)); dispatch(setIsAppliedStatus(false));
} }
dispatch(setFilteredSubcategory(payload));
if (JSON.stringify(payload) !== JSON.stringify(selectedSubcategory)) {
dispatch(setFilteredSubcategory(payload));
}
}; };
const setSelectedLocations = (payload) => { const setSelectedLocations = (payload) => {
if (isApplied !== false) { if (isApplied !== false) {
dispatch(setIsAppliedStatus(false)); dispatch(setIsAppliedStatus(false));
} }
dispatch(setFilteredLocations(payload));
if (JSON.stringify(payload) !== JSON.stringify(selectedLocations)) {
dispatch(setFilteredLocations(payload));
}
}; };
return { return {
selectedCategory, selectedCategory,
categories, categories,
subcategories, subcategories,
locations, locations,
queryString,
applyFilters, applyFilters,
clearFilters, clearFilters,
isApplied, isApplied,
makeQueryString
makeQueryString,
calculateFiltersChosen,
loadedFromQS,
setLoadedFromQS,
}; };
}; };



+ 8
- 0
src/hooks/usePaging.js Ver fichero

import { useState } from "react"

export const usePaging = () => {
const [page, setPage] = useState();
return {
page, setPage
}
}

+ 182
- 0
src/hooks/useQueryString.js Ver fichero

import _ from "lodash";
import { useEffect, useState } from "react";
// import _ from "lodash"
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { HOME_PAGE } from "../constants/pages";
import { setQueryString as setQueryStringSaga } from "../store/actions/queryString/queryStringActions";
import { selectQueryString } from "../store/selectors/queryStringSelectors";
// import useFilters from "./useFilters";
// import useSorting from "./useSorting";
// import { sortEnum } from "../enums/sortEnum";
import { convertQueryStringBackend, convertQueryStringFrontend } from "../util/helpers/queryHelpers";

export const useQueryString = () => {
const queryString = useSelector(selectQueryString);
const history = useHistory();
const [globalQueryString, setGlobalQueryString] = useState("");
const [initial, setInitial] = useState(true);
const [loadedFromURL, setLoadedFromURL] = useState(false);
// const [isFetched, setIsFetched] = useState(false);
const dispatch = useDispatch();
// const sorting = useSorting();

useEffect(() => {
const queryStringLocal = history.location.search.substring(1);
setQueryString(convertQueryStringBackend(queryStringLocal));
setGlobalQueryString(queryStringLocal);
console.log("initial bre")
}, []);

useEffect(() => {
console.log(globalQueryString);
console.log(history.location.search.substring(1))
if (globalQueryString === history.location.search.substring(1))
setLoadedFromURL(true);
// qo.delete("location")
}, [globalQueryString]);

useEffect(() => {
if (initial && loadedFromURL) {
if (queryString?.length > 0) {
const fun = _.once(() => {
setGlobalQueryString(convertQueryStringFrontend(queryString));
setInitial(false);
});
fun();
}
} else {
setGlobalQueryString(convertQueryStringFrontend(queryString));
}
}, [queryString, loadedFromURL]);

useEffect(() => {
if (!initial) {
history.push({
pathname: HOME_PAGE,
search: "?" + globalQueryString,
});
}
}, [globalQueryString, initial]);

const getQueryString = () => {
return queryString;
};
const setQueryString = (newQueryString) => {
dispatch(setQueryStringSaga(newQueryString));
};
const getQueryObject = () => {
const urlParams = new URLSearchParams(queryString);
return Object.fromEntries(urlParams);
};
const appendToQueryString = (key, value) => {
if (loadedFromURL) {
let urlParams = new URLSearchParams(queryString);
if (key === "location") {
if (urlParams.has(key)) {
let arrayOfLocations = urlParams.getAll(key);
if (arrayOfLocations.includes(value)) {
arrayOfLocations = arrayOfLocations.filter(
(item) => item?.toString() !== value?.toString()
);
urlParams.delete(key);
arrayOfLocations.forEach((item) => {
urlParams.append(key, item);
});
}
}
} else {
if (urlParams.has(key)) {
urlParams.delete(key);
}
}
if (!value) setQueryString(urlParams.toString());
urlParams.append(key, value);
setQueryString(urlParams.toString());
return urlParams.toString();
}
};
const appendMultipleToQueryString = (array = []) => {
if (loadedFromURL) {
let urlParams = new URLSearchParams(queryString);
if (
array.find((item) => item.key === "category") ||
array.find((item) => item.key === "subcategory")
) {
urlParams.delete("location");
}
array.forEach((item) => {
// if (item.key === "location") {
// if (urlParams.has(item.key)) {
// let arrayOfLocations = urlParams.getAll(item.key);
// if (arrayOfLocations.includes(item.value)) {
// arrayOfLocations = arrayOfLocations.filter(
// (itemInList) =>
// itemInList?.toString() !== item?.value?.toString()
// );
// urlParams.delete(item.key);
// arrayOfLocations.forEach((location) => {
// urlParams.append(item.key, location);
// });
// }
// }
// } else {
if (urlParams.has(item.key) && item.key !== "location") {
urlParams.delete(item.key);
}
// }
console.log("item.key: ", item.key);
console.log("item.value: ", item.value);
if (!item.value) return;
urlParams.append(item.key, item.value);
console.log("querytString: ", urlParams.toString())
});
console.log("postavlja se");
setQueryString(urlParams.toString());
return urlParams.toString();
}
};
const deleteFromQueryString = (key, value = null) => {
let urlParams = new URLSearchParams(queryString);
if (key === "location") {
let arrayOfLocations = urlParams.getAll(key);
arrayOfLocations = arrayOfLocations.filter((item) => item !== value);
urlParams.delete(key);
arrayOfLocations.forEach((item) => {
urlParams.append(key, item);
});
} else if (key === "sortBy") {
urlParams.delete("_des_date");
urlParams.delete("_des_popular");
} else {
urlParams.delete(key);
}
setQueryString(urlParams.toString());
return urlParams.toString();
};
const getInitialQueryString = () => {
let urlParams = new URLSearchParams(queryString);
urlParams = new URLSearchParams(appendToQueryString("size", 10));
urlParams = new URLSearchParams(appendToQueryString("page", 1));
return urlParams;
};

const getGlobalQueryString = () => {
return globalQueryString;
};

return {
queryString,
globalQueryString,
getQueryString,
setQueryString,
getQueryObject,
initial,
loadedFromURL,
appendMultipleToQueryString,
getGlobalQueryString,
appendToQueryString,
getInitialQueryString,
deleteFromQueryString,
};
};

+ 30
- 0
src/hooks/useScreenDimensions.js Ver fichero

import { useEffect, useState } from "react";

const getScreenDimensions = () => {
const height = window.innerHeight;
const width = window.innerWidth;
return {
height,
width
}
}

const useScreenDimensions = () => {
const [screenDimensions, setScreenDimensions] = useState(getScreenDimensions());
const [width, setWidth] = useState(getScreenDimensions().width);
const [height, setHeight] = useState(getScreenDimensions().height);
useEffect(() => {
const resize = () => {
setScreenDimensions(getScreenDimensions());
setWidth(getScreenDimensions().width);
setHeight(getScreenDimensions().height);
}
window.addEventListener('resize', resize);
resize();
return () => window.removeEventListener('resize', resize);
}, [])
return {
screenDimensions, width, height
}
}
export default useScreenDimensions;

+ 12
- 8
src/hooks/useSearch.js Ver fichero

import { useDispatch, useSelector } from "react-redux";
import { fetchOffers } from "../store/actions/offers/offersActions";
import { selectQueryString } from "../store/selectors/filtersSelectors";
// import { useDispatch } from "react-redux";
import { useQueryString } from "./useQueryString";
// import { fetchOffers } from "../store/actions/offers/offersActions";
// import { selectQueryString } from "../store/selectors/queryStringSelectors";
// import { selectQueryString } from "../store/selectors/filtersSelectors";


export const useSearch = () => { export const useSearch = () => {
const dispatch = useDispatch();
const queryString = useSelector(selectQueryString);
// const dispatch = useDispatch();
const queryStringHook = useQueryString();
// const queryString = useSelector(selectQueryString);
const searchOffers = (searchString) => { const searchOffers = (searchString) => {
const queryObject = new URLSearchParams(queryString);
queryObject.set('oname', searchString);
dispatch(fetchOffers({queryString: "?" + queryObject.toString()}));
queryStringHook.appendToQueryString("oname", searchString);
// const queryObject = new URLSearchParams(queryString);
// queryObject.set('oname', searchString);
// dispatch(fetchOffers({queryString: "?" + queryObject.toString()}));
} }


return { return {

+ 42
- 72
src/hooks/useSorting.js Ver fichero

import { useEffect, useState } from "react";
// import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { HOME_PAGE } from "../constants/pages";
// import { useHistory } from "react-router-dom";
// import { HOME_PAGE } from "../constants/pages";
import { sortEnum } from "../enums/sortEnum"; import { sortEnum } from "../enums/sortEnum";
import { setFilteredSortOption} from "../store/actions/filters/filtersActions";
import { fetchOffers } from "../store/actions/offers/offersActions";
import qs from "query-string";
import {
selectQueryString,
selectSelectedSortOption,
} from "../store/selectors/filtersSelectors";
import useFilters from "./useFilters";
import { setFilteredSortOption } from "../store/actions/filters/filtersActions";
// import { fetchOffers } from "../store/actions/offers/offersActions";
// import qs from "query-string";
import { selectSelectedSortOption } from "../store/selectors/filtersSelectors";
// import useFilters from "./useFilters";
// import { convertQueryString } from "../util/helpers/queryHelpers";
import { useQueryString } from "./useQueryString";
import { convertQueryStringFrontend } from "../util/helpers/queryHelpers";


const useSorting = () => { const useSorting = () => {
// Local query string is query string used for fetching data from API
const [localQueryString, setLocalQueryString] = useState("");
// Global query string is query string shown for user (more user-friendly)
const [globalQueryString, setGlobalQueryString] = useState("");

const history = useHistory();
const filters = useFilters();

const queryString = useSelector(selectQueryString);

const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedSortOption = useSelector(selectSelectedSortOption); const selectedSortOption = useSelector(selectSelectedSortOption);
const sortOptions = sortEnum; const sortOptions = sortEnum;
const queryStringHook = useQueryString();


useEffect(() => { useEffect(() => {
if (localQueryString.length > 0) {
if (queryString.length > 1) {
let queryStringEdited = new URLSearchParams(queryString);
queryStringEdited.delete('_des_date');
queryStringEdited.delete('_des_popular');
dispatch(
fetchOffers({ queryString: "?" + queryStringEdited + "&" + localQueryString })
);
history.push({
pathname: HOME_PAGE,
search: "?" + queryStringEdited + "&" + globalQueryString,
});
} else {
dispatch(fetchOffers({queryString: "?" + localQueryString}));
history.push({
pathname: HOME_PAGE,
search: "?" + globalQueryString,
});
}
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
}, [localQueryString]);

useEffect(() => {
const queryStringFromUrl = history.location.search.substring(1);
const queryObjectFromUrl = qs.parse(queryStringFromUrl);
if (queryObjectFromUrl?.sortBy) {
if (queryObjectFromUrl.sortBy === sortOptions.OLD.queryString) {
setSelectedSortOption(sortOptions.OLD)
}
if (queryObjectFromUrl.sortBy === sortOptions.NEW.queryString) {
setSelectedSortOption(sortOptions.NEW)
}
if (queryObjectFromUrl.sortBy === sortOptions.POPULAR.queryString) {
setSelectedSortOption(sortOptions.POPULAR)
if (queryStringHook.loadedFromURL) {
const queryString = queryStringHook.queryString;
let queryObject = new URLSearchParams(
convertQueryStringFrontend(queryString)
);
if (queryObject.has("sortBy"))
if (queryObject.get("sortBy") === "newest") {
setSelectedSortOption(sortEnum.NEW);
} }
} else {
setSelectedSortOption(sortOptions.INITIAL)
if (queryObject.get("sortBy") === "oldest") {
setSelectedSortOption(sortEnum.OLD);
}
if (queryObject.get("sortBy") === "popular") {
setSelectedSortOption(sortEnum.POPULAR);
}
} }
setGlobalQueryString(qs.stringify(queryObjectFromUrl));
filters.makeQueryString();
}, [history.location.search]);
}, [queryStringHook.queryString, queryStringHook.loadedFromURL]);


const setSelectedSortOption = (payload) => { const setSelectedSortOption = (payload) => {
dispatch(setFilteredSortOption(payload)); dispatch(setFilteredSortOption(payload));
console.log("konzola sort: ", payload)
let _des_date = null; let _des_date = null;
let _des_popular = null; let _des_popular = null;
if (payload === sortOptions.NEW) {
setGlobalQueryString(`sortBy=${sortOptions.NEW.queryString}`);
if (payload.value === sortOptions.NEW.value) {
_des_date = true; _des_date = true;
} }
if (payload === sortOptions.OLD) {
setGlobalQueryString(`sortBy=${sortOptions.OLD.queryString}`);
if (payload.value === sortOptions.OLD.value) {
_des_date = false; _des_date = false;
} }
if (payload === sortOptions.POPULAR) {
setGlobalQueryString(`sortBy=${sortOptions.POPULAR.queryString}`);
if (payload.value === sortOptions.POPULAR.value) {
_des_popular = true; _des_popular = true;
} }
if (_des_date !== null) { if (_des_date !== null) {
setLocalQueryString(`_des_date=${_des_date}`);
queryStringHook.appendMultipleToQueryString([
{ key: "_des_date", value: `${_des_date}` },
{ key: "_des_popular" },
]);
} }
if (_des_popular !== null) { if (_des_popular !== null) {
setLocalQueryString(`_des_popular=${_des_popular}`);
queryStringHook.appendMultipleToQueryString([
{ key: "_des_popular", value: `${_des_popular}` },
{ key: "_des_date" },
]);
} }
}; };

return { return {
selectedSortOption, selectedSortOption,
setSelectedSortOption, setSelectedSortOption,
sortOptions, sortOptions,
// loadedQS
}; };
}; };
export default useSorting; export default useSorting;

+ 3
- 0
src/i18n/resources/rs.js Ver fichero

forgotPasswordEmail: "Email", forgotPasswordEmail: "Email",
useDifferentEmail: "Iskoristite drugačiju lozinku.", useDifferentEmail: "Iskoristite drugačiju lozinku.",
wrongCredentials: "Pogrešan mail ili lozinka!", wrongCredentials: "Pogrešan mail ili lozinka!",
headerTitle: "Ulogujte se"
}, },
password: { password: {
weak: "slaba", weak: "slaba",
}, },
register: { register: {
title: "Registruj se", title: "Registruj se",
headerTitle: "Registrujte se",
descriptionMain: "Trampa sa kolegama na dohvat ruke", descriptionMain: "Trampa sa kolegama na dohvat ruke",
descriptionFirst: descriptionFirst:
"Email i Lozinka biće Vam primarni način da se ulogujete u aplikaciju", "Email i Lozinka biće Vam primarni način da se ulogujete u aplikaciju",
myOffers: "Moje objave", myOffers: "Moje objave",
checkEverything: "POGLEDAJ SVE", checkEverything: "POGLEDAJ SVE",
myMessages: "Moje poruke", myMessages: "Moje poruke",
newOffers: "Najnovije ponude"
}, },
}; };

+ 0
- 44
src/pages/HomePage/HomePageMUI.js Ver fichero

import MarketPlace from "../../components/MarketPlace/MarketPlace"; import MarketPlace from "../../components/MarketPlace/MarketPlace";


const HomePage = () => { 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);

// if (queryObject.category) {
// category = Mockupdata[1].find(
// (item) => item.string === queryObject.category.toString()
// ).id;
// }
// 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) => {
// cities.push(Mockupdata[0].find((p) => p.string === item).id);
// });
// } else {
// cities.push(
// Mockupdata[0].find((p) => p.string === queryObject.city).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;
// }
// 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]);

return ( return (
<HomePageContainer> <HomePageContainer>
<MainLayout leftCard={<FilterCard />} content={<MarketPlace />} /> <MainLayout leftCard={<FilterCard />} content={<MarketPlace />} />

+ 2
- 2
src/pages/RegisterPages/Register/Register.styled.js Ver fichero

width: 335px; width: 335px;
padding: 0; padding: 0;
flex: 1; flex: 1;
position: relative;
/* position: relative; */
transition: 1s all; transition: 1s all;
${props => props.currentstep === 3 && `margin-top: 40px`}; ${props => props.currentstep === 3 && `margin-top: 40px`};
@media (max-height: 900px) { @media (max-height: 900px) {
padding: 0; padding: 0;
`; `;
export const Footer = styled(Box)` export const Footer = styled(Box)`
position: absolute;
position: relative;
bottom: 36px; bottom: 36px;
display: flex; display: flex;
width: 100%; width: 100%;

+ 2
- 1
src/request/apiEndpoints.js Ver fichero

getOffers: 'offers', getOffers: 'offers',
addOffer: 'offers', addOffer: 'offers',
categories: 'categories', categories: 'categories',
locations: 'locations'
locations: 'locations',
mineOffers: 'users'
} }
}; };

+ 3
- 0
src/request/chatRequest.js Ver fichero



export const attemptFetchChats = (payload) => { export const attemptFetchChats = (payload) => {
return getRequest(`users/${payload}/chat`); return getRequest(`users/${payload}/chat`);
}
export const attemptFetchHeaderChats = (payload) => {
return getRequest(`users/${payload}/chat/?page=1&size=2`)
} }

+ 5
- 2
src/request/offersRequest.js Ver fichero

import apiEndpoints from "./apiEndpoints" import apiEndpoints from "./apiEndpoints"


export const attemptFetchOffers = (payload) => { export const attemptFetchOffers = (payload) => {
if (payload) return getRequest(apiEndpoints.offers.getOffers + payload + "&size=10&page=1")
return getRequest(apiEndpoints.offers.getOffers + "?size=10&page=1")
if (payload) return getRequest(apiEndpoints.offers.getOffers + payload)
return getRequest(apiEndpoints.offers.getOffers)
} }
export const attemptFetchMoreOffers = (page, payload) => { export const attemptFetchMoreOffers = (page, payload) => {
if (payload) return getRequest(apiEndpoints.offers.getOffers + payload + `&size=10&page=${page}`); if (payload) return getRequest(apiEndpoints.offers.getOffers + payload + `&size=10&page=${page}`);
} }
export const attemptAddOffer = (payload) => { export const attemptAddOffer = (payload) => {
return postRequest(apiEndpoints.offers.addOffer, payload) return postRequest(apiEndpoints.offers.addOffer, payload)
}
export const attemptFetchMineOffers = (payload) => {
return getRequest(`${apiEndpoints.offers.mineOffers}/${payload}/offers`)
} }

+ 2
- 0
src/store/actions/chat/chatActionConstants.js Ver fichero

import { createFetchType } from "../actionHelpers"; import { createFetchType } from "../actionHelpers";


const CHAT_SCOPE = "CHAT_SCOPE"; const CHAT_SCOPE = "CHAT_SCOPE";
const CHAT_HEADER_SCOPE = "CHAT_HEADER_SCOPE";


export const CHAT_FETCH = createFetchType(CHAT_SCOPE); export const CHAT_FETCH = createFetchType(CHAT_SCOPE);
export const CHAT_HEADER_FETCH = createFetchType(CHAT_HEADER_SCOPE);


export const CHAT_SET = "CHAT_SET"; export const CHAT_SET = "CHAT_SET";

+ 5
- 1
src/store/actions/chat/chatActions.js Ver fichero

import { CHAT_FETCH, CHAT_SET } from "./chatActionConstants";
import { CHAT_FETCH, CHAT_HEADER_FETCH, CHAT_SET } from "./chatActionConstants";


export const fetchChats = (payload) => ({ export const fetchChats = (payload) => ({
type: CHAT_FETCH, type: CHAT_FETCH,
payload, payload,
}) })
export const fetchHeaderChats = (payload) => ({
type: CHAT_HEADER_FETCH,
payload,
})
export const setChats = (payload) => ({ export const setChats = (payload) => ({
type: CHAT_SET, type: CHAT_SET,
payload, payload,

+ 2
- 1
src/store/actions/login/loginActions.js Ver fichero

type: AUTHENTICATE_USER, type: AUTHENTICATE_USER,
}); });


export const logoutUser = () => ({
export const logoutUser = (payload) => ({
type: LOGOUT_USER, type: LOGOUT_USER,
payload,
}); });


export const refreshUserToken = (payload) => ({ export const refreshUserToken = (payload) => ({

+ 5
- 1
src/store/actions/offers/offersActionConstants.js Ver fichero

const OFFERS_MORE_SCOPE = "OFFERS_MORE_SCOPE"; const OFFERS_MORE_SCOPE = "OFFERS_MORE_SCOPE";
export const OFFERS_FETCH_MORE = createFetchType(OFFERS_MORE_SCOPE); export const OFFERS_FETCH_MORE = createFetchType(OFFERS_MORE_SCOPE);


const OFFERS_MINE_SCOPE = "OFFERS_MINE_SCOPE";
export const OFFERS_MINE_FETCH = createFetchType(OFFERS_MINE_SCOPE);
export const OFFERS_FETCH = createFetchType(OFFERS_SCOPE); export const OFFERS_FETCH = createFetchType(OFFERS_SCOPE);
export const OFFERS_SUCCESS = createSuccessType(OFFERS_SCOPE); export const OFFERS_SUCCESS = createSuccessType(OFFERS_SCOPE);
export const OFFERS_ERROR = createErrorType(OFFERS_SCOPE); export const OFFERS_ERROR = createErrorType(OFFERS_SCOPE);
export const OFFERS_PINNED_ADD = "OFFERS_PINNED_ADD"; export const OFFERS_PINNED_ADD = "OFFERS_PINNED_ADD";
export const OFFERS_SET = "OFFERS_SET"; export const OFFERS_SET = "OFFERS_SET";
export const OFFERS_ADD = "OFFERS_ADD"; export const OFFERS_ADD = "OFFERS_ADD";
export const OFFERS_NO_MORE = "OFFERS_NO_MORE";
export const OFFERS_NO_MORE = "OFFERS_NO_MORE";
export const OFFERS_SET_TOTAL = "OFFERS_SET_TOTAL";
export const OFFERS_MINE_SET = "OFFERS_MY_ADD";

+ 55
- 30
src/store/actions/offers/offersActions.js Ver fichero

import { OFFERS_ADD, OFFERS_CLEAR, OFFERS_ERROR, OFFERS_FETCH, OFFERS_FETCH_MORE, OFFERS_NO_MORE, OFFERS_PINNED_ADD, OFFERS_PINNED_SET, OFFERS_SET, OFFERS_SUCCESS} from "./offersActionConstants";
import {
OFFERS_ADD,
OFFERS_CLEAR,
OFFERS_ERROR,
OFFERS_FETCH,
OFFERS_FETCH_MORE,
OFFERS_MINE_FETCH,
OFFERS_MINE_SET,
OFFERS_NO_MORE,
OFFERS_PINNED_ADD,
OFFERS_PINNED_SET,
OFFERS_SET,
OFFERS_SET_TOTAL,
OFFERS_SUCCESS,
} from "./offersActionConstants";


export const fetchOffers = (payload) => ({ export const fetchOffers = (payload) => ({
type: OFFERS_FETCH,
payload,
})
type: OFFERS_FETCH,
payload,
});
export const fetchOffersSuccess = (payload) => ({ export const fetchOffersSuccess = (payload) => ({
type: OFFERS_SUCCESS,
payload
})
type: OFFERS_SUCCESS,
payload,
});
export const fetchOffersError = (payload) => ({ export const fetchOffersError = (payload) => ({
type: OFFERS_ERROR,
payload,
})
type: OFFERS_ERROR,
payload,
});
export const clearOffers = () => ({ export const clearOffers = () => ({
type: OFFERS_CLEAR,
})
type: OFFERS_CLEAR,
});
export const setOffers = (payload) => ({ export const setOffers = (payload) => ({
type: OFFERS_SET,
payload,
})
type: OFFERS_SET,
payload,
});
export const setPinnedOffers = (payload) => ({ export const setPinnedOffers = (payload) => ({
type: OFFERS_PINNED_SET,
payload
})
type: OFFERS_PINNED_SET,
payload,
});
export const addPinnedOffers = (payload) => ({ export const addPinnedOffers = (payload) => ({
type: OFFERS_PINNED_ADD,
payload,
})
type: OFFERS_PINNED_ADD,
payload,
});
export const addOffers = (payload) => ({ export const addOffers = (payload) => ({
type: OFFERS_ADD,
payload
})
type: OFFERS_ADD,
payload,
});
export const fetchMoreOffers = (payload) => ({ export const fetchMoreOffers = (payload) => ({
type: OFFERS_FETCH_MORE,
payload
})
type: OFFERS_FETCH_MORE,
payload,
});
export const setNoMoreOffersStatus = (payload) => ({ export const setNoMoreOffersStatus = (payload) => ({
type: OFFERS_NO_MORE,
payload
})
type: OFFERS_NO_MORE,
payload,
});
export const setTotalOffers = (payload) => ({
type: OFFERS_SET_TOTAL,
payload,
});
export const fetchMineOffers = () => ({
type: OFFERS_MINE_FETCH,
});
export const setMineOffers = (payload) => ({
type: OFFERS_MINE_SET,
payload,
});

+ 2
- 0
src/store/actions/queryString/queryStringActionConstants.js Ver fichero

export const QUERY_STRING_SET = "QUERY_STRING_SET";
export const QUERY_STRING_SET_REDUX = "QUERY_STRING_SET_REDUX";

+ 10
- 0
src/store/actions/queryString/queryStringActions.js Ver fichero

import { QUERY_STRING_SET, QUERY_STRING_SET_REDUX } from "./queryStringActionConstants"

export const setQueryStringRedux = (payload) => ({
type: QUERY_STRING_SET_REDUX,
payload,
})
export const setQueryString = (payload) => ({
type: QUERY_STRING_SET,
payload,
})

+ 0
- 11
src/store/reducers/filters/filtersReducer.js Ver fichero

SET_FILTERS, SET_FILTERS,
SET_IS_APPLIED, SET_IS_APPLIED,
SET_LOCATIONS, SET_LOCATIONS,
SET_QUERY_STRING,
SET_SORT_OPTION, SET_SORT_OPTION,
SET_SUBCATEGORY, SET_SUBCATEGORY,
} from "../../actions/filters/filtersActionConstants"; } from "../../actions/filters/filtersActionConstants";
[SET_LOCATIONS]: setFilteredLocations, [SET_LOCATIONS]: setFilteredLocations,
[SET_SORT_OPTION]: setFilteredSortOption, [SET_SORT_OPTION]: setFilteredSortOption,
[SET_IS_APPLIED]: setIsAppliedStatus, [SET_IS_APPLIED]: setIsAppliedStatus,
[SET_QUERY_STRING]: setQueryString,
}, },
initialState initialState
); );
isApplied: payload, isApplied: payload,
} }
} }
}
function setQueryString(state, {payload}) {
return {
...state,
filters: {
...state.filters,
queryString: payload,
}
}
} }

+ 3
- 1
src/store/reducers/index.js Ver fichero

import locationsReducer from "./locations/locationsReducer"; import locationsReducer from "./locations/locationsReducer";
import profileReducer from "./profile/profileReducer"; import profileReducer from "./profile/profileReducer";
import chatReducer from "./chat/chatReducer"; import chatReducer from "./chat/chatReducer";
import queryStringReducer from "./queryString/queryStringReducer";


const loginPersistConfig = { const loginPersistConfig = {
key: "login", key: "login",
categories: persistReducer(categoriesPersistConfig, categoriesReducer), categories: persistReducer(categoriesPersistConfig, categoriesReducer),
locations: persistReducer(locationsPersistConfig, locationsReducer), locations: persistReducer(locationsPersistConfig, locationsReducer),
profile: persistReducer(profilePersistConfig, profileReducer), profile: persistReducer(profilePersistConfig, profileReducer),
chat: persistReducer(chatPersistConfig, chatReducer)
chat: persistReducer(chatPersistConfig, chatReducer),
queryString: queryStringReducer,
}); });

+ 18
- 0
src/store/reducers/offers/offersReducer.js Ver fichero

OFFERS_ADD, OFFERS_ADD,
OFFERS_CLEAR, OFFERS_CLEAR,
OFFERS_ERROR, OFFERS_ERROR,
OFFERS_MINE_SET,
OFFERS_NO_MORE, OFFERS_NO_MORE,
OFFERS_PINNED_ADD, OFFERS_PINNED_ADD,
OFFERS_PINNED_SET, OFFERS_PINNED_SET,
OFFERS_SET, OFFERS_SET,
OFFERS_SET_TOTAL,
} from "../../actions/offers/offersActionConstants"; } from "../../actions/offers/offersActionConstants";
import createReducer from "../../utils/createReducer"; import createReducer from "../../utils/createReducer";


const initialState = { const initialState = {
offers: [], offers: [],
pinnedOffers: [], pinnedOffers: [],
mineOffers: [],
total: 0,
error: "", error: "",
newOffer: "", newOffer: "",
noMoreOffers: false, noMoreOffers: false,
[OFFERS_NO_MORE]: setNoMoreOffersStatus, [OFFERS_NO_MORE]: setNoMoreOffersStatus,
[OFFERS_PINNED_ADD]: addPinnedOffers, [OFFERS_PINNED_ADD]: addPinnedOffers,
[OFFERS_PINNED_SET]: setPinnedOffers, [OFFERS_PINNED_SET]: setPinnedOffers,
[OFFERS_SET_TOTAL]: setTotalOffers,
[OFFERS_MINE_SET]: setMineOffers
}, },
initialState initialState
); );
noMoreOffers: action.payload noMoreOffers: action.payload
} }
} }
function setTotalOffers(state, action) {
return {
...state,
total: action.payload
}
}
function setMineOffers(state, action) {
return {
...state,
mineOffers: action.payload
}
}

+ 20
- 0
src/store/reducers/queryString/queryStringReducer.js Ver fichero

import { QUERY_STRING_SET_REDUX } from "../../actions/queryString/queryStringActionConstants";
import createReducer from "../../utils/createReducer";

const initialState = {
queryString: "",
};

export default createReducer(
{
[QUERY_STRING_SET_REDUX]: setQueryStringRedux,
},
initialState
);

function setQueryStringRedux(state, action) {
return {
...state,
queryString: action.payload,
};
}

+ 13
- 3
src/store/saga/chatSaga.js Ver fichero

import { all, takeLatest, call, put } from "@redux-saga/core/effects"; import { all, takeLatest, call, put } from "@redux-saga/core/effects";
import { attemptFetchChats } from "../../request/chatRequest";
import { CHAT_FETCH } from "../actions/chat/chatActionConstants";
import { attemptFetchChats, attemptFetchHeaderChats } from "../../request/chatRequest";
import { CHAT_FETCH, CHAT_HEADER_FETCH } from "../actions/chat/chatActionConstants";
import { setChats } from "../actions/chat/chatActions"; import { setChats } from "../actions/chat/chatActions";


function* fetchChats(payload) { function* fetchChats(payload) {
} }
} }


function* fetchHeaderChats(payload) {
try {
const data = yield call(attemptFetchHeaderChats, payload.payload);
yield put(setChats(data.data));
} catch (e) {
console.log(e);
}
}

export default function* chatSaga() { export default function* chatSaga() {
yield all([ yield all([
takeLatest(CHAT_FETCH, fetchChats)
takeLatest(CHAT_FETCH, fetchChats),
takeLatest(CHAT_HEADER_FETCH, fetchHeaderChats)
]); ]);
} }

+ 3
- 1
src/store/saga/index.js Ver fichero

import loginSaga from './loginSaga'; import loginSaga from './loginSaga';
import offersSaga from './offersSaga'; import offersSaga from './offersSaga';
import profileSaga from './profileSaga'; import profileSaga from './profileSaga';
import queryStringSaga from './queryStringSaga';
import registerSaga from './registerSaga'; import registerSaga from './registerSaga';


export default function* rootSaga() { export default function* rootSaga() {
categoriesSaga(), categoriesSaga(),
locationsSaga(), locationsSaga(),
profileSaga(), profileSaga(),
chatSaga()
chatSaga(),
queryStringSaga(),
]); ]);
} }

+ 7
- 3
src/store/saga/loginSaga.js Ver fichero

} }
} }


function* logout() {
function* logout(payload) {
try { try {
const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN);
const user = jwt.decode(JwtToken);
const user = yield call(jwt.decode, JwtToken);
console.log(payload);
if (user) { if (user) {
yield call(logoutUserRequest, { token: JwtToken, userId: user._id });
const requestBody = {token: JwtToken}
yield call(logoutUserRequest, requestBody);
yield call(payload.payload)
} }
} catch (error) { } catch (error) {
console.log(error); // eslint-disable-line console.log(error); // eslint-disable-line
} finally { } finally {
console.log('ovde');
yield call(authScopeClearHelper); yield call(authScopeClearHelper);
yield call(removeHeaderToken); yield call(removeHeaderToken);
yield put(resetLoginState()); yield put(resetLoginState());

+ 57
- 23
src/store/saga/offersSaga.js Ver fichero

import { all, takeLatest, call, put } from "@redux-saga/core/effects";
import { all, takeLatest, call, put, select } from "@redux-saga/core/effects";
import { import {
attemptFetchMineOffers,
attemptFetchMoreOffers, attemptFetchMoreOffers,
attemptFetchOffers, attemptFetchOffers,
} from "../../request/offersRequest"; } from "../../request/offersRequest";
import { setQueryString } from "../actions/filters/filtersActions";
import { convertQueryStringBackend } from "../../util/helpers/queryHelpers";
// import { setQueryString } from "../actions/filters/filtersActions";
import { import {
OFFERS_FETCH, OFFERS_FETCH,
OFFERS_FETCH_MORE, OFFERS_FETCH_MORE,
OFFERS_MINE_FETCH,
} from "../actions/offers/offersActionConstants"; } from "../actions/offers/offersActionConstants";
import { import {
addOffers, addOffers,
addPinnedOffers, addPinnedOffers,
clearOffers, clearOffers,
setMineOffers,
setNoMoreOffersStatus, setNoMoreOffersStatus,
setOffers, setOffers,
setPinnedOffers, setPinnedOffers,
setTotalOffers,
} from "../actions/offers/offersActions"; } from "../actions/offers/offersActions";
import { selectUserId } from "../selectors/loginSelectors";
import {
selectOffers,
selectPinnedOffers,
selectTotalOffers,
} from "../selectors/offersSelectors";


function* fetchOffers(payload) { function* fetchOffers(payload) {
try { try {
yield put(clearOffers()); yield put(clearOffers());
yield put(setNoMoreOffersStatus(false));
const data = yield call(attemptFetchOffers, payload.payload.queryString);
if (
data.data.items.pinnedOffers.length +
data.data.items.regularOffers.length <
10 ||
data.data.items.pinnedOffers.length +
data.data.items.regularOffers.length >
data.data.total
) {
yield put(setNoMoreOffersStatus(true));
}
if (payload.payload.queryString) {
yield put(setQueryString(payload.payload.queryString));
}

// adding page and size to query string
// let newQueryString = new URLSearchParams(payload.payload.queryString);
// if (!newQueryString.has('page')) {
// newQueryString.append('page', 1);
// if (newQueryString.has('size')) newQueryString.delete('size');
// newQueryString.append('size', 10);
// }
const newQueryString = new URLSearchParams(
convertQueryStringBackend(payload.payload.queryString)
);
console.log("fetch offers: ", newQueryString.toString());
const data = yield call(
attemptFetchOffers,
"?" + newQueryString.toString()
);
console.log("if (payload.payload.queryString) {");
// if (payload.payload.queryString) {
// yield put(setQueryString(payload.payload.queryString));
// }
yield put(setTotalOffers(data.data.total));
yield put(setOffers(data.data.items.regularOffers)); yield put(setOffers(data.data.items.regularOffers));
yield put(setPinnedOffers(data.data.items.pinnedOffers)); yield put(setPinnedOffers(data.data.items.pinnedOffers));
} catch (e) { } catch (e) {
payload.payload?.page, payload.payload?.page,
payload.payload?.queryString payload.payload?.queryString
); );
if (payload.payload.queryString) {
yield put(setQueryString(payload.payload.queryString));
// if (payload.payload.queryString) {
// yield put(setQueryString(payload.payload.queryString));
// }
const oldOffers = yield select(selectOffers);
const oldPinnedOffers = yield select(selectPinnedOffers);
const totalNumberOfOffers = yield select(selectTotalOffers);
if (oldOffers.length + oldPinnedOffers.length < totalNumberOfOffers) {
yield put(addOffers(data.data.items.regularOffers));
yield put(addPinnedOffers(data.data.items.pinnedOffers));
} }
console.log(data.data.items);
yield put(addOffers(data.data.items.regularOffers));
yield put(addPinnedOffers(data.data.items.pinnedOffers));
if ( if (
data.data.items.pinnedOffers + data.data.items.regularOffers < 10 ||
data.data.items.pinnedOffers.length +
data.data.items.regularOffers.length <
10 ||
data.data.items.pinnedOffers.length + data.data.items.pinnedOffers.length +
data.data.items.regularOffers.length > data.data.items.regularOffers.length >
data.data.total data.data.total
} }
} }


function* fetchMineOffers() {
try {
const userId = yield select(selectUserId);
const data = yield call(attemptFetchMineOffers, userId);
yield put(setMineOffers(data.data));
} catch (e) {
console.log(e);
}
}

export default function* offersSaga() { export default function* offersSaga() {
yield all([ yield all([
takeLatest(OFFERS_FETCH, fetchOffers), takeLatest(OFFERS_FETCH, fetchOffers),
takeLatest(OFFERS_FETCH_MORE, fetchMoreOffers), takeLatest(OFFERS_FETCH_MORE, fetchMoreOffers),
takeLatest(OFFERS_MINE_FETCH, fetchMineOffers),
]); ]);
} }

+ 30
- 0
src/store/saga/queryStringSaga.js Ver fichero

import { all, takeLatest, put } from "@redux-saga/core/effects";
import { convertQueryStringBackend } from "../../util/helpers/queryHelpers";
// import { combineQueryStrings } from "../../util/helpers/queryHelpers";
import { QUERY_STRING_SET } from "../actions/queryString/queryStringActionConstants";
import { setQueryStringRedux } from "../actions/queryString/queryStringActions";
// import { selectQueryString } from "../selectors/queryStringSelectors";

function* setQueryString(payload) {
try {
console.log("trenutni queryString: ", payload.payload);
console.log(payload);
// const currentQS = yield select(selectQueryString);
// let newQueryString = payload.payload
// if (currentQS?.length > 0) {
// newQueryString = yield call(
// combineQueryStrings,
// currentQS,
// payload.payload,
// );
// }
const newQueryString = convertQueryStringBackend(payload.payload)
yield put(setQueryStringRedux(newQueryString));
} catch (e) {
console.log(e);
}
}

export default function* queryStringSaga() {
yield all([takeLatest(QUERY_STRING_SET, setQueryString)]);
}

+ 0
- 4
src/store/selectors/filtersSelectors.js Ver fichero

filtersSelector, filtersSelector,
(state) => state.filters.isApplied (state) => state.filters.isApplied
) )
export const selectQueryString = createSelector(
filtersSelector,
(state) => state.filters.queryString
)

+ 8
- 0
src/store/selectors/offersSelectors.js Ver fichero

offersSelector, offersSelector,
(state) => state.pinnedOffers (state) => state.pinnedOffers
) )
export const selectTotalOffers = createSelector(
offersSelector,
(state) => state.total
)
export const selectMineOffers = createSelector(
offersSelector,
(state) => state.mineOffers
)

+ 8
- 0
src/store/selectors/queryStringSelectors.js Ver fichero

import { createSelector } from "reselect";

const queryStringSelector = (state) => state.queryString;

export const selectQueryString = createSelector(
queryStringSelector,
(state => state.queryString)
)

+ 0
- 2
src/util/helpers/authScopeHelpers.js Ver fichero



export function authScopeStringGetHelper(key) { export function authScopeStringGetHelper(key) {
if (sessionStorage.getItem(SESSION_STORAGE_SCOPE)) { if (sessionStorage.getItem(SESSION_STORAGE_SCOPE)) {
console.log(sessionStorage.getItem(key))
return sessionStorage.getItem(key); return sessionStorage.getItem(key);
} }
console.log(localStorage.getItem(key))


return localStorage.getItem(key); return localStorage.getItem(key);
} }

+ 119
- 15
src/util/helpers/queryHelpers.js Ver fichero

import { sortEnum } from "../../enums/sortEnum"; import { sortEnum } from "../../enums/sortEnum";
// import qs from "query-string";


export const convertQueryString = (queryURL) => {
const queryObject = new URLSearchParams(queryURL);
const queryObjectToReturn = new URLSearchParams(queryURL);
if (queryObject.has('_des_date')) {
queryObjectToReturn.delete('_des_date');
if (queryObject.get('_des_date') === 'true') {
queryObjectToReturn.append('sortBy', sortEnum.NEW.queryString);
} else {
queryObjectToReturn.append('sortBy', sortEnum.OLD.queryString);
}
export const convertQueryStringFrontend = (queryURL) => {
const queryObject = new URLSearchParams(queryURL);
const queryObjectToReturn = new URLSearchParams(queryURL);
if (queryObject.has("_des_date")) {
queryObjectToReturn.delete("_des_date");
if (queryObject.get("_des_date") === "true") {
queryObjectToReturn.append("sortBy", sortEnum.NEW.queryString);
} else {
queryObjectToReturn.append("sortBy", sortEnum.OLD.queryString);
} }
if (queryObject.has('_des_popular')) {
queryObjectToReturn.delete('_des_popular');
queryObjectToReturn.append('sortBy', sortEnum.POPULAR.queryString);
}
if (queryObject.has("oname")) {
queryObjectToReturn.delete("oname");
queryObjectToReturn.append("search", queryObject.get("oname"));
}
if (queryObject.has("_des_popular")) {
queryObjectToReturn.delete("_des_popular");
queryObjectToReturn.append("sortBy", sortEnum.POPULAR.queryString);
}
if (queryObject.has("size")) {
queryObjectToReturn.delete("size");
}
if (queryObject.has("page")) {
if (queryObject.get("page") === "1") {
queryObjectToReturn.delete("page");
return queryObjectToReturn.toString();
} else {
queryObjectToReturn.delete("page");
return (
queryObjectToReturn.toString() + "&page=" + queryObject.get("page")
);
} }
return queryObjectToReturn.toString();
}
}
return queryObjectToReturn.toString();
};

export const combineQueryStrings = (firstQuery, secondQuery) => {
const firstQueryObject = new URLSearchParams(firstQuery);
const secondQueryObject = new URLSearchParams(secondQuery);
const thirdQueryObject = new URLSearchParams(secondQueryObject.toString());
let arrayOfProperties = Object.getOwnPropertyNames(
Object.fromEntries(firstQueryObject)
);
arrayOfProperties.forEach((property) => {
console.log("firstQueryObject[property]: ", firstQueryObject.toString());
console.log("secondQueryObject[property]", secondQueryObject.toString());
if (!secondQueryObject.has(property)) {
// console.log("ovde je doslo query: ", property);
// thirdQueryObject.append(property, secondQueryObject.get(property));
thirdQueryObject.append(property, firstQueryObject.get(property));
} else {
// console.log("ovde ispod: ", property);
}
});
console.log("thirdQueryObject[property]: ", thirdQueryObject.toString());

return thirdQueryObject.toString();
};

export const convertQueryStringBackend = (queryURL) => {
const queryObject = new URLSearchParams(queryURL);
const newQueryObject = new URLSearchParams();
if (queryObject.has("category")) {
newQueryObject.append(
"category",
queryObject.getAll("category")[queryObject.getAll("category").length - 1]
);
}
if (queryObject.has("subcategory")) {
newQueryObject.append(
"subcategory",
queryObject.getAll("subcategory")[
queryObject.getAll("subcategory").length - 1
]
);
}
if (queryObject.has("search")) {
newQueryObject.append(
"oname",
queryObject.getAll("search")[queryObject.getAll("search").length - 1]
);
}
if (queryObject.has("oname")) {
newQueryObject.append(
"oname",
queryObject.getAll("oname")[queryObject.getAll("oname").length - 1]
);
}
if (queryObject.has("location")) {
const arrayOfLocations = queryObject.getAll("location");
arrayOfLocations.forEach((item) => {
newQueryObject.append("location", item);
});
}
if (queryObject.has("sortBy")) {
newQueryObject.delete("sortBy");
if (queryObject.get("sortBy") === "newest") {
newQueryObject.append("_des_date", "true");
}
if (queryObject.get("sortBy") === "oldest") {
newQueryObject.append("_des_date", "false");
}
if (queryObject.get("sortBy") === "popular") {
newQueryObject.append("_des_popular", "true");
}
}
if (queryObject.has("_des_date")) {
newQueryObject.append("_des_date", queryObject.get("_des_date"));
}
if (queryObject.has("_des_popular")) {
newQueryObject.append("_des_popular", queryObject.get("_des_popular"));
}
newQueryObject.append("size", "10");
if (!queryObject.has("page")) {
newQueryObject.append("page", "1");
} else {
newQueryObject.append("page", queryObject.get("page"));
}
return newQueryObject.toString();
};

Cargando…
Cancelar
Guardar