Browse Source

Finished feature 549

feature/587
Djordje Mitrovic 3 years ago
parent
commit
182302cf35
53 changed files with 1327 additions and 1085 deletions
  1. 5
    0
      package-lock.json
  2. 1
    0
      package.json
  3. 2
    2
      src/App.js
  4. 1
    0
      src/assets/styles/_base.scss
  5. 7
    7
      src/components/Cards/FilterCard/Choser/CategoryChoser/CategoryChoser.js
  6. 3
    7
      src/components/Cards/FilterCard/Choser/LocationChoser/LocationChoser.js
  7. 21
    21
      src/components/Cards/FilterCard/Choser/SubcategoryChoser/SubcategoryChoser.js
  8. 8
    8
      src/components/Cards/FilterCard/FilterCard.js
  9. 1
    1
      src/components/Cards/FilterCard/FilterCard.styled.js
  10. 1
    0
      src/components/Cards/FilterCard/FilterDropdown/Checkbox/FilterCheckboxDropdown.js
  11. 2
    3
      src/components/Cards/FilterCard/FilterDropdown/Radio/FilterRadioDropdown.js
  12. 8
    6
      src/components/Cards/FilterCard/FilterFooter/FilterFooter.js
  13. 1
    1
      src/components/Cards/FilterCard/FilterHeader/FilterHeader.js
  14. 1
    1
      src/components/ChatColumn/ChatColumn.js
  15. 46
    49
      src/components/Header/Header.js
  16. 0
    23
      src/components/Header/Header.styled.js
  17. 17
    13
      src/components/Icon/IconWithNumber/IconWithNumber.js
  18. 91
    127
      src/components/MarketPlace/Header/Header.js
  19. 19
    2
      src/components/MarketPlace/MarketPlace.js
  20. 4
    3
      src/components/MarketPlace/Offers/HeaderMyOffers.js/HeadersMyOffers.js
  21. 8
    0
      src/components/MarketPlace/Offers/HeaderMyOffers.js/HeadersMyOffers.styled.js
  22. 28
    12
      src/components/MarketPlace/Offers/Offers.js
  23. 27
    0
      src/components/MarketPlace/Offers/Offers.styled.js
  24. 1
    0
      src/components/Paging/Paging.js
  25. 3
    0
      src/constants/queryStringConstants.js
  26. 0
    182
      src/hooks/useFilters.js
  27. 0
    253
      src/hooks/useOffers.js
  28. 66
    0
      src/hooks/useOffers/useCategoryFilter.js
  29. 50
    0
      src/hooks/useOffers/useFilters.js
  30. 54
    0
      src/hooks/useOffers/useLocationsFilter.js
  31. 101
    0
      src/hooks/useOffers/useMyOffers.js
  32. 134
    0
      src/hooks/useOffers/useOffers.js
  33. 38
    0
      src/hooks/useOffers/usePaging.js
  34. 60
    0
      src/hooks/useOffers/useQueryString.js
  35. 46
    0
      src/hooks/useOffers/useSearch.js
  36. 65
    0
      src/hooks/useOffers/useSorting.js
  37. 55
    0
      src/hooks/useOffers/useSubcategoryFilter.js
  38. 0
    174
      src/hooks/useQueryString.js
  39. 11
    11
      src/hooks/useSearch.js
  40. 36
    0
      src/hooks/useSkeleton.js
  41. 0
    86
      src/hooks/useSorting.js
  42. 19
    18
      src/pages/HomePage/HomePageMUI.js
  43. 38
    3
      src/pages/MyOffers/MyOffers.js
  44. 1
    1
      src/request/index.js
  45. 2
    0
      src/store/actions/filters/filtersActionConstants.js
  46. 9
    1
      src/store/actions/filters/filtersActions.js
  47. 18
    13
      src/store/index.js
  48. 24
    0
      src/store/reducers/filters/filtersReducer.js
  49. 1
    1
      src/store/reducers/queryString/queryStringReducer.js
  50. 2
    2
      src/store/saga/offersSaga.js
  51. 2
    2
      src/store/saga/queryStringSaga.js
  52. 8
    0
      src/store/selectors/filtersSelectors.js
  53. 181
    52
      src/util/helpers/queryHelpers.js

+ 5
- 0
package-lock.json View File

"react-transition-group": "^4.3.0" "react-transition-group": "^4.3.0"
} }
}, },
"react-singleton-hook": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-singleton-hook/-/react-singleton-hook-3.4.0.tgz",
"integrity": "sha512-eQEpyacGAaRejmWUizUdNNQFn5AO0iaKRSl1jxgC0FQadVY/I1WFuPrYiutglPzO9s8yEbIh95UXVJQel4d7HQ=="
},
"react-toastify": { "react-toastify": {
"version": "9.0.3", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.3.tgz", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.3.tgz",

+ 1
- 0
package.json View File

"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-select": "^4.3.1", "react-select": "^4.3.1",
"react-singleton-hook": "^3.4.0",
"react-toastify": "^9.0.3", "react-toastify": "^9.0.3",
"redux": "^4.1.0", "redux": "^4.1.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",

+ 2
- 2
src/App.js View File

<Header /> <Header />
<GlobalStyle /> <GlobalStyle />
<ToastContainer /> <ToastContainer />
{/* <div>
{/* <div>
<p>Connected: {"" + isConnected}</p> <p>Connected: {"" + isConnected}</p>
<br /> <br />
<p>Last pong: {lastPong || "-"}</p> <p>Last pong: {lastPong || "-"}</p>
<br /> <br />
<button onClick={sendPing}>Send ping</button> <button onClick={sendPing}>Send ping</button>
</div> */} </div> */}
<AppRoutes />
<AppRoutes />
</StyledEngineProvider> </StyledEngineProvider>
</Router> </Router>
); );

+ 1
- 0
src/assets/styles/_base.scss View File

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


* { * {

+ 7
- 7
src/components/Cards/FilterCard/Choser/CategoryChoser/CategoryChoser.js View File

const filters = props.filters; const filters = props.filters;
const { t } = useTranslation(); const { t } = useTranslation();
const handleSelectCategory = (category) => { const handleSelectCategory = (category) => {
filters.setSelectedCategory(category);
filters.clearSelectedSubcategory();
filters.category.setSelectedCategory(category);
filters.subcategory.setSelectedSubcategory({});
}; };
return ( return (
<FilterRadioDropdown <FilterRadioDropdown
data={[...filters?.categories]}
data={[...filters?.category.allCategories]}
icon={ icon={
filters.selectedCategory?.name ? (
filters.category.selectedCategoryLocally?.name ? (
<CategoryChosenIcon /> <CategoryChosenIcon />
) : ( ) : (
<CategoryIcon /> <CategoryIcon />
) )
} }
title={ title={
filters.selectedCategory?.name
? filters.selectedCategory?.name
filters.category.selectedCategoryLocally?.name
? filters.category.selectedCategoryLocally?.name
: t("filters.categories.title") : t("filters.categories.title")
} }
searchPlaceholder={t("filters.categories.placeholder")} searchPlaceholder={t("filters.categories.placeholder")}
setSelected={handleSelectCategory} setSelected={handleSelectCategory}
selected={filters.selectedCategory}
selected={filters.category.selectedCategoryLocally}
firstOption={firstCategoryOption} firstOption={firstCategoryOption}
/> />
); );

+ 3
- 7
src/components/Cards/FilterCard/Choser/LocationChoser/LocationChoser.js View File

return ( return (
<FilterCheckboxDropdown <FilterCheckboxDropdown
searchPlaceholder={t("filters.location.placeholder")} searchPlaceholder={t("filters.location.placeholder")}
data={[...filters.locations]}
filters={
filters?.selectedLocations?.length > 0
? [...filters.selectedLocations]
: []
}
data={[...filters.locations.allLocations]}
filters={[...filters.locations.selectedLocationsLocally]}
icon={<LocationIcon />} icon={<LocationIcon />}
title={t("filters.location.title")} title={t("filters.location.title")}
setItemsSelected={filters.setSelectedLocations}
setItemsSelected={filters.locations.setSelectedLocations}
/> />
); );
}; };

+ 21
- 21
src/components/Cards/FilterCard/Choser/SubcategoryChoser/SubcategoryChoser.js View File

import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { SubcategoryIcon } from "./SubcategoryChoser.styled"; import { SubcategoryIcon } from "./SubcategoryChoser.styled";
import FilterRadioDropdown from "../../FilterDropdown/Radio/FilterRadioDropdown"; import FilterRadioDropdown from "../../FilterDropdown/Radio/FilterRadioDropdown";
import _ from "lodash";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";


const firstSubcategoryOption = {
label: "SVE PODKATEGORIJE",
value: { _id: 0 },
};
// const firstSubcategoryOption = {
// label: "SVE PODKATEGORIJE",
// value: { _id: 0 },
// };


const SubcategoryChoser = (props) => { const SubcategoryChoser = (props) => {
const filters = props.filters; const filters = props.filters;
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);

const setInitialOpen = useMemo(() => {
return _.once(() => {
setIsOpened(true);
});
}, []);
const subcategories = useMemo(() => {
return filters.category.getSubcategories(
filters.category.selectedCategoryLocally?.name
);
}, [filters.category.selectedCategoryLocally]);


useEffect(() => { useEffect(() => {
if (!filters.selectedCategory || filters.selectedCategory?._id === 0) {
if (!filters.category.selectedCategoryLocally || filters.category.selectedCategoryLocally?._id === 0) {
setIsOpened(false); setIsOpened(false);
setIsDisabled(true); setIsDisabled(true);
} else { } else {
setIsDisabled(false); setIsDisabled(false);
setInitialOpen();
setIsOpened(true);
} }
}, [filters.selectedCategory]);
}, [filters.category.selectedCategoryLocally]);


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

console.log(filters);

return ( return (
<FilterRadioDropdown <FilterRadioDropdown
data={filters.subcategories ? [...filters.subcategories] : []}
data={subcategories}
icon={<SubcategoryIcon />} icon={<SubcategoryIcon />}
title={ title={
filters.selectedSubcategory?.name
? filters.selectedSubcategory?.name
filters.subcategory.selectedSubcategory?.name
? filters.subcategory.selectedSubcategory?.name
: t("filters.subcategories.title") : t("filters.subcategories.title")
} }
searchPlaceholder={t("filters.subcategories.placeholder")} searchPlaceholder={t("filters.subcategories.placeholder")}
setSelected={filters.setSelectedSubcategory}
selected={filters.selectedSubcategory}
setSelected={filters.subcategory.setSelectedSubcategory}
selected={filters.subcategory.selectedSubcategoryLocally}
open={isOpened} open={isOpened}
disabled={isDisabled} disabled={isDisabled}
handleOpen={handleOpen} handleOpen={handleOpen}
firstOption={firstSubcategoryOption}
firstOption={filters.subcategory.initialOption}
/> />
); );
}; };

+ 8
- 8
src/components/Cards/FilterCard/FilterCard.js View File

import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { ContentContainer, FilterCardContainer } from "./FilterCard.styled"; import { ContentContainer, FilterCardContainer } from "./FilterCard.styled";
import useFilters from "../../../hooks/useFilters";
import HeaderBack from "../../ItemDetails/Header/Header"; import HeaderBack from "../../ItemDetails/Header/Header";
import FilterHeader from "./FilterHeader/FilterHeader"; import FilterHeader from "./FilterHeader/FilterHeader";
import FilterFooter from "./FilterFooter/FilterFooter"; import FilterFooter from "./FilterFooter/FilterFooter";
import SkeletonFilterCard from "./Skeleton/SkeletonFilterCard"; import SkeletonFilterCard from "./Skeleton/SkeletonFilterCard";


const FilterCard = (props) => { const FilterCard = (props) => {
const filters = useFilters(props.myOffers);
const offers = props.offers;
const filters = offers.filters;
return ( return (
<FilterCardContainer <FilterCardContainer
responsiveOpen={props.responsiveOpen}
responsive={props.responsive}
filtersOpened={props.filtersOpened}
myOffers={props.myOffers} myOffers={props.myOffers}
skeleton={props.skeleton} skeleton={props.skeleton}
> >
</ContentContainer> </ContentContainer>


<FilterFooter <FilterFooter
closeResponsive={props.closeResponsive}
responsiveOpen={props.responsiveOpen}
filters={filters}
toggleFilters={props.toggleFilters}
filters={offers}
/> />
</FilterCardContainer> </FilterCardContainer>
); );


FilterCard.propTypes = { FilterCard.propTypes = {
children: PropTypes.node, children: PropTypes.node,
filters: PropTypes.any,
offers: PropTypes.any,
responsive: PropTypes.bool, responsive: PropTypes.bool,
responsiveOpen: PropTypes.bool, responsiveOpen: PropTypes.bool,
closeResponsive: PropTypes.func, closeResponsive: PropTypes.func,
myOffers: PropTypes.bool, myOffers: PropTypes.bool,
skeleton: PropTypes.bool, skeleton: PropTypes.bool,
animationStage: PropTypes.number, animationStage: PropTypes.number,
filtersOpened: PropTypes.bool,
toggleFilters: PropTypes.func,
}; };


FilterCard.defaultProps = { FilterCard.defaultProps = {

+ 1
- 1
src/components/Cards/FilterCard/FilterCard.styled.js View File

@media (max-width: 900px) { @media (max-width: 900px) {
margin-left: -400px; margin-left: -400px;
${(props) => ${(props) =>
props.responsiveOpen
props.filtersOpened
? ` ? `
display: "flex"; display: "flex";
margin-left: 0; margin-left: 0;

+ 1
- 0
src/components/Cards/FilterCard/FilterDropdown/Checkbox/FilterCheckboxDropdown.js View File

searchPlaceholder={props.searchPlaceholder} searchPlaceholder={props.searchPlaceholder}
isOpened={isOpened} isOpened={isOpened}
setIsOpened={setIsOpened} setIsOpened={setIsOpened}
setItemsSelected={props.setItemsSelected}
> >
{dataToShow.map((item) => { {dataToShow.map((item) => {
return ( return (

+ 2
- 3
src/components/Cards/FilterCard/FilterDropdown/Radio/FilterRadioDropdown.js View File

setIsOpened((prevState) => !prevState); setIsOpened((prevState) => !prevState);
if (props.handleOpen) props.handleOpen(); if (props.handleOpen) props.handleOpen();
}; };

return ( return (
<DropdownList <DropdownList
title={props.title} title={props.title}
textcolor={ textcolor={
!props.selected || props.selected?._id === 0
!props.selected || props.selected?._id === 0 || !props.selected?._id
? selectedTheme.primaryText ? selectedTheme.primaryText
: selectedTheme.primaryPurple : selectedTheme.primaryPurple
} }
toggleIconClosed={<DropdownDown />} toggleIconClosed={<DropdownDown />}
toggleIconOpened={<DropdownUp />} toggleIconOpened={<DropdownUp />}
fullWidth fullWidth
open={ props?.open !== undefined ? props.open : isOpened}
open={props?.open !== undefined ? props.open : isOpened}
disabled={props.disabled} disabled={props.disabled}
setIsOpened={handleOpen} setIsOpened={handleOpen}
toggleIconStyles={{ toggleIconStyles={{

+ 8
- 6
src/components/Cards/FilterCard/FilterFooter/FilterFooter.js View File

import selectedTheme from "../../../../themes"; import selectedTheme from "../../../../themes";
import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton"; import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useScreenDimensions from "../../../../hooks/useScreenDimensions";


const FilterFooter = (props) => { const FilterFooter = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const filters = props.filters; const filters = props.filters;
const screenDimensions = useScreenDimensions();
const handleFilters = () => { const handleFilters = () => {
filters.applyFilters();
if (props.closeResponsive) props.closeResponsive();
filters.apply();
if (props.toggleFilters) props.toggleFilters();
}; };
return ( return (
<FilterFooterContainer responsiveOpen={props.responsiveOpen}>
{props.responsiveOpen && (
<FilterFooterContainer responsiveOpen={screenDimensions.width < 600}>
{screenDimensions.width < 600 && (
<PrimaryButton <PrimaryButton
variant="outlined" variant="outlined"
fullWidth fullWidth
onClick={props.closeResponsive}
onClick={props.toggleFilters}
textcolor={selectedTheme.primaryPurple} textcolor={selectedTheme.primaryPurple}
font="Open Sans" font="Open Sans"
style={{ style={{


(FilterFooter.propTypes = { (FilterFooter.propTypes = {
responsiveOpen: PropTypes.bool, responsiveOpen: PropTypes.bool,
closeResponsive: PropTypes.func,
toggleFilters: PropTypes.func,
filters: PropTypes.any, filters: PropTypes.any,
}), }),
(FilterFooter.defaultProps = { (FilterFooter.defaultProps = {

+ 1
- 1
src/components/Cards/FilterCard/FilterHeader/FilterHeader.js View File

const filters = props.filters; const filters = props.filters;
const { t } = useTranslation(); const { t } = useTranslation();
const clearFilters = () => { const clearFilters = () => {
filters.clearFilters();
filters.clear();
}; };
return ( return (
<FilterHeaderContainer> <FilterHeaderContainer>

+ 1
- 1
src/components/ChatColumn/ChatColumn.js View File

TitleSortContainer, TitleSortContainer,
} from "./ChatColumn.styled"; } from "./ChatColumn.styled";
import { sortEnum } from "../../enums/sortEnum"; import { sortEnum } from "../../enums/sortEnum";
import useSorting from "../../hooks/useSorting";
import { ReactComponent as Down } from "../../assets/images/svg/down-arrow.svg"; import { ReactComponent as Down } from "../../assets/images/svg/down-arrow.svg";
import { IconStyled } from "../Icon/Icon.styled"; import { IconStyled } from "../Icon/Icon.styled";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { selectLatestChats } from "../../store/selectors/chatSelectors"; import { selectLatestChats } from "../../store/selectors/chatSelectors";
import { fetchChats } from "../../store/actions/chat/chatActions"; import { fetchChats } from "../../store/actions/chat/chatActions";
import useSorting from "../../hooks/useOffers/useSorting";


export const DownArrow = (props) => { export const DownArrow = (props) => {
<IconStyled {...props}> <IconStyled {...props}>

+ 46
- 49
src/components/Header/Header.js View File

AddOfferButton, AddOfferButton,
AuthButtonsContainer, AuthButtonsContainer,
EndIcon, EndIcon,
FilterContainer,
FilterIcon,
// FilterContainer,
// FilterIcon,
HeaderContainer, HeaderContainer,
LoginButton, LoginButton,
LogoContainer, LogoContainer,
import { IconButton } from "../Buttons/IconButton/IconButton"; import { IconButton } from "../Buttons/IconButton/IconButton";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { selectUserId } from "../../store/selectors/loginSelectors"; import { selectUserId } from "../../store/selectors/loginSelectors";
import { useSearch } from "../../hooks/useSearch";
import { selectProfileName } from "../../store/selectors/profileSelectors"; import { selectProfileName } from "../../store/selectors/profileSelectors";
import { useHistory, useRouteMatch } from "react-router-dom"; import { useHistory, useRouteMatch } from "react-router-dom";
import { import {
BASE_PAGE,
FORGOT_PASSWORD_MAIL_SENT, FORGOT_PASSWORD_MAIL_SENT,
FORGOT_PASSWORD_PAGE, FORGOT_PASSWORD_PAGE,
HOME_PAGE, HOME_PAGE,
REGISTER_SUCCESSFUL_PAGE, REGISTER_SUCCESSFUL_PAGE,
RESET_PASSWORD_PAGE, RESET_PASSWORD_PAGE,
} from "../../constants/pages"; } from "../../constants/pages";
import useFilters from "../../hooks/useFilters";
import FilterCard from "../Cards/FilterCard/FilterCard";
import { useQueryString } from "../../hooks/useQueryString";
import { convertQueryStringFrontend } from "../../util/helpers/queryHelpers";
// import { convertQueryStringForFrontend } from "../../util/helpers/queryHelpers";
import { fetchMineProfile } from "../../store/actions/profile/profileActions"; import { fetchMineProfile } from "../../store/actions/profile/profileActions";
import CreateOffer from "../Cards/CreateOfferCard/CreateOffer"; import CreateOffer from "../Cards/CreateOfferCard/CreateOffer";
import { Drawer as HeaderDrawer } from "./Drawer/Drawer"; import { Drawer as HeaderDrawer } from "./Drawer/Drawer";
import useSearch from "../../hooks/useOffers/useSearch";
// import useQueryString from "../../hooks/useOffers/useQueryString";


const Header = () => { const Header = () => {
const [openFilters, setOpenFilters] = useState(false);
// const setOpenFilters = useState(false)[1];
const [showSearchBar, setShowSearchBar] = useState(true); const [showSearchBar, setShowSearchBar] = useState(true);
const [numberOfFilters, setNumberOfFilters] = useState(0);
const [showCreateOfferModal, setShowCreateOfferModal] = useState(false); const [showCreateOfferModal, setShowCreateOfferModal] = useState(false);
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(selectUserId); const user = useSelector(selectUserId);
const search = useSearch();
const search = useSearch(() => {});
const dispatch = useDispatch(); const dispatch = useDispatch();
const name = useSelector(selectProfileName); const name = useSelector(selectProfileName);
const history = useHistory(); const history = useHistory();
const routeMatch = useRouteMatch(); const routeMatch = useRouteMatch();
const filters = useFilters();
const searchMobileRef = useRef(null); const searchMobileRef = useRef(null);
const queryStringHook = useQueryString();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);


useEffect(() => { useEffect(() => {
setUserAnchorEl(null); setUserAnchorEl(null);
}; };
}, []); }, []);
useEffect(() => {
searchRef.current.value = search.searchString ?? "";
}, [search.searchString]);
useEffect(() => { useEffect(() => {
if (history.location.pathname !== "/home") { if (history.location.pathname !== "/home") {
setShowSearchBar(false); setShowSearchBar(false);
setShowSearchBar(true); setShowSearchBar(true);
} }
}, [history.location.pathname]); }, [history.location.pathname]);
useEffect(() => {
setNumberOfFilters(filters.calculateFiltersChosen());
}, [
filters.selectedCategory,
filters.selectedLocations,
filters.selectedSubcategory,
]);


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


const closeCreateOfferModal = () => { const closeCreateOfferModal = () => {
setShowCreateOfferModal(false); setShowCreateOfferModal(false);
location.pathname === REGISTER_SUCCESSFUL_PAGE || location.pathname === REGISTER_SUCCESSFUL_PAGE ||
location.pathname === FORGOT_PASSWORD_PAGE || location.pathname === FORGOT_PASSWORD_PAGE ||
location.pathname === FORGOT_PASSWORD_MAIL_SENT || location.pathname === FORGOT_PASSWORD_MAIL_SENT ||
location.pathname === RESET_PASSWORD_PAGE ||
location.pathname === "/"
location.pathname === RESET_PASSWORD_PAGE
) { ) {
shouldShowHeader = false; shouldShowHeader = false;
} }
searchRef.current.removeEventListener("keyup", listener); searchRef.current.removeEventListener("keyup", listener);
}; };
const handleSearch = (value) => { const handleSearch = (value) => {
search.searchOffers(value);
};
const toggleFilters = () => {
setOpenFilters((prevState) => !prevState);
if (
history.location.pathname !== HOME_PAGE &&
history.location.pathname !== BASE_PAGE
) {
const newQueryString = new URLSearchParams({ search: value });
history.push({
pathname: HOME_PAGE,
search: newQueryString.toString(),
});
} else {
search.searchOffers(value);
}
}; };
// const toggleFilters = () => {
// setOpenFilters((prevState) => !prevState);
// };


const handleToggleDrawer = () => { const handleToggleDrawer = () => {
setOpenDrawer(!openDrawer); setOpenDrawer(!openDrawer);
fullWidth fullWidth
shouldShow={showSearchBar} shouldShow={showSearchBar}
ref={searchMobileRef} ref={searchMobileRef}
placeholder={t("header.searchOffers")}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<React.Fragment>
<FilterContainer number={numberOfFilters}>
<FilterIcon onClick={toggleFilters} />
</FilterContainer>
<EndIcon size="36px">
<SearchIcon
onClick={() => handleSearch(searchMobileRef.current.value)}
/>
</EndIcon>
</React.Fragment>
<EndIcon size="36px">
<SearchIcon
onClick={() => handleSearch(searchMobileRef.current.value)}
/>
</EndIcon>
), ),
}} }}
placeholder={t("header.searchOffers")}
italicPlaceholder italicPlaceholder
onFocus={handleFocusSearch} onFocus={handleFocusSearch}
onBlur={handleBlurSearch} onBlur={handleBlurSearch}
/> />
<FilterCard
{/* <FilterCard
responsive={true} responsive={true}
responsiveOpen={openFilters} responsiveOpen={openFilters}
closeResponsive={toggleFilters} closeResponsive={toggleFilters}
/>
/> */}
{showCreateOfferModal && ( {showCreateOfferModal && (
<CreateOffer closeCreateOfferModal={closeCreateOfferModal} /> <CreateOffer closeCreateOfferModal={closeCreateOfferModal} />
)} )}

+ 0
- 23
src/components/Header/Header.styled.js View File

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 { Icon } from "../Icon/Icon";
import IconWithNumber from "../Icon/IconWithNumber/IconWithNumber";


export const SearchInput = styled(TextField)` export const SearchInput = styled(TextField)`
background-color: #f4f4f4; background-color: #f4f4f4;
width: 0; 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)``; export const HeaderContainer = styled(Box)``;

+ 17
- 13
src/components/Icon/IconWithNumber/IconWithNumber.js View File

import React from 'react'
import PropTypes from 'prop-types'
import { IconWithNumberContainer, Number } from './IconWithNumber.styled'
import React from "react";
import PropTypes from "prop-types";
import { IconWithNumberContainer, Number } from "./IconWithNumber.styled";


const IconWithNumber = (props) => { const IconWithNumber = (props) => {
return ( return (
<IconWithNumberContainer className={props.className}>
{props.children}
{props.number > 0 && <Number>{props.number}</Number>}
<IconWithNumberContainer
className={props.className}
onClick={props.onClick}
>
{props.children}
{props.number > 0 && <Number>{props.number}</Number>}
</IconWithNumberContainer> </IconWithNumberContainer>
)
}
);
};


IconWithNumber.propTypes = { IconWithNumber.propTypes = {
children: PropTypes.node,
number: PropTypes.number,
className: PropTypes.string,
}
children: PropTypes.node,
number: PropTypes.number,
className: PropTypes.string,
onClick: PropTypes.func,
};


export default IconWithNumber
export default IconWithNumber;

+ 91
- 127
src/components/MarketPlace/Header/Header.js View File

import React, { useEffect, useState } from "react";
import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { import {
HeaderAltLocation, HeaderAltLocation,
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 { sortEnum } from "../../../enums/sortEnum"; import { sortEnum } from "../../../enums/sortEnum";
import useFilters from "../../../hooks/useFilters";
import useSorting from "../../../hooks/useSorting";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tooltip } from "@mui/material"; import { Tooltip } from "@mui/material";
import {
ALL_CATEGORIES,
COMMA,
SPREAD,
} from "../../../constants/marketplaceHeaderTitle";
import SkeletonHeader from "./SkeletonHeader/SkeletonHeader"; import SkeletonHeader from "./SkeletonHeader/SkeletonHeader";
import { useSelector } from "react-redux";
import { selectHeaderString } from "../../../store/selectors/filtersSelectors";


const DownArrow = (props) => ( const DownArrow = (props) => (
<IconStyled {...props}> <IconStyled {...props}>
); );


const Header = (props) => { const Header = (props) => {
const filters = useFilters();
const sorting = useSorting();
const { t } = useTranslation(); const { t } = useTranslation();
const [sortOption, setSortOption] = useState(sortEnum.INITIAL);
const [headerString, setHeaderString] = useState(ALL_CATEGORIES);

//Changing shown sort option on select menu
useEffect(() => {
setSortOption(sorting.selectedSortOption);
}, [sorting.selectedSortOption]);

const sorting = props.sorting;
const headerString = useSelector(selectHeaderString);
// Changing header string on refresh or on load // Changing header string on refresh or on load
useEffect(() => {
let headerStringLocal = ALL_CATEGORIES;
if (filters.isApplied) {
// Adding category to header string
if (filters.selectedCategory?.name) {
headerStringLocal = filters.selectedCategory.name;
// Adding subcategories to header string
if (filters.selectedSubcategory?.name) {
headerStringLocal += `${SPREAD}${filters.selectedSubcategory.name}`;
}
}
// Adding locations to header string
if (filters.selectedLocations && filters.selectedLocations?.length > 0) {
headerStringLocal += SPREAD;

filters.selectedLocations.forEach((location, index) => {
// Checking if item is last
if (index + 1 === filters.selectedLocations.length) {
headerStringLocal += location.city;
} else {
headerStringLocal += location.city + COMMA;
}
});
}
}
setHeaderString(headerStringLocal);
}, [
filters.isApplied,
filters.selectedCategory,
filters.selectedSubcategory,
filters.selectedLocations,
]);


const handleChangeSelect = (event) => { const handleChangeSelect = (event) => {
let chosenOption;
for (const sortOption in sortEnum) {
if (sortEnum[sortOption].value === event.target.value) {
chosenOption = sortEnum[sortOption];
sorting.changeSorting(chosenOption);
}
}
// let chosenOption;
sorting.changeSorting(event.target.value);
// for (const sortOption in sortEnum) {
// if (sortEnum[sortOption].value === event.target.value) {
// chosenOption = sortEnum[sortOption];
// sorting.changeSorting(chosenOption);
// }
// }
}; };


return ( return (
<> <>
<SkeletonHeader skeleton={props.skeleton} animationStage={props.animationStage} />
<HeaderContainer skeleton={props.skeleton}>
{/* Setting appropriate header title if page is market place or my offers */}
<Tooltip title={headerString}>
{!props.myOffers ? (
headerString === "Sve kategorije" &&
(sorting.selectedSortOption === sortEnum.INITIAL ||
sorting.selectedSortOption === sortEnum.NEW) ? (
<React.Fragment>
<HeaderLocation initial>{headerString}</HeaderLocation>
<HeaderAltLocation>{t("header.newOffers")}</HeaderAltLocation>
</React.Fragment>
<SkeletonHeader
skeleton={props.skeleton}
animationStage={props.animationStage}
/>
<HeaderContainer skeleton={props.skeleton}>
{/* Setting appropriate header title if page is market place or my offers */}
<Tooltip title={headerString}>
{!props.myOffers ? (
headerString === "Sve kategorije" &&
(sorting.selectedSortOption === sortEnum.INITIAL ||
sorting.selectedSortOption === sortEnum.NEW) ? (
<React.Fragment>
<HeaderLocation initial>{headerString}</HeaderLocation>
<HeaderAltLocation>{t("header.newOffers")}</HeaderAltLocation>
</React.Fragment>
) : (
<HeaderLocation>{headerString}</HeaderLocation>
)
) : ( ) : (
<HeaderLocation>{headerString}</HeaderLocation>
)
) : (
<MySwapsTitle>
<RefreshIcon /> {t("header.myOffers")}
</MySwapsTitle>
)}
</Tooltip>
{/* ^^^^^^ */}
<MySwapsTitle>
<RefreshIcon /> {t("header.myOffers")}
</MySwapsTitle>
)}
</Tooltip>
{/* ^^^^^^ */}


<HeaderOptions>
<HeaderButtons>
{/* Setting display of offer cards to full width */}
<HeaderButton
iconColor={
props.isGrid
? selectedTheme.iconStrokeColor
: selectedTheme.primaryPurple
}
onClick={() => props.setIsGrid(false)}
>
<GridLine />
</HeaderButton>
{/* ^^^^^^ */}
<HeaderOptions>
<HeaderButtons>
{/* Setting display of offer cards to full width */}
<HeaderButton
iconColor={
props.isGrid
? selectedTheme.iconStrokeColor
: selectedTheme.primaryPurple
}
onClick={() => props.setIsGrid(false)}
>
<GridLine />
</HeaderButton>
{/* ^^^^^^ */}

{/* Setting display of offer cards to half width (Grid) */}
<HeaderButton
iconColor={
props.isGrid
? selectedTheme.primaryPurple
: selectedTheme.iconStrokeColor
}
onClick={() => props.setIsGrid(true)}
>
<GridSquare />
</HeaderButton>
{/* ^^^^^^ */}
</HeaderButtons>


{/* Setting display of offer cards to half width (Grid) */}
<HeaderButton
iconColor={
props.isGrid
? selectedTheme.primaryPurple
: selectedTheme.iconStrokeColor
{/* Select option to choose sorting */}
<HeaderSelect
value={
sorting.selectedSortOption?.value
? sorting.selectedSortOption
: "default"
} }
onClick={() => props.setIsGrid(true)}
IconComponent={DownArrow}
onChange={handleChangeSelect}
> >
<GridSquare />
</HeaderButton>
<SelectOption style={{ display: "none" }} value="default">
Sortiraj po
</SelectOption>
{Object.keys(sortEnum).map((property) => {
if (sortEnum[property].value === 0) return;
return (
<SelectOption
value={sortEnum[property]}
key={sortEnum[property].value}
>
{sortEnum[property].mainText}
</SelectOption>
);
})}
</HeaderSelect>
{/* ^^^^^^ */} {/* ^^^^^^ */}
</HeaderButtons>

{/* Select option to choose sorting */}
<HeaderSelect
value={sortOption?.value ? sortOption.value : "default"}
IconComponent={DownArrow}
onChange={handleChangeSelect}
>
<SelectOption style={{ display: "none" }} value="default">
Sortiraj po
</SelectOption>
{Object.keys(sortEnum).map((property) => {
if(sortEnum[property].value === 0) return;
return (
<SelectOption
value={sortEnum[property].value}
key={sortEnum[property].value}
>
{sortEnum[property].mainText}
</SelectOption>
);
})}
</HeaderSelect>
{/* ^^^^^^ */}
</HeaderOptions>
</HeaderContainer>
</HeaderOptions>
</HeaderContainer>
</> </>
); );
}; };
myOffers: PropTypes.bool, myOffers: PropTypes.bool,
skeleton: PropTypes.bool, skeleton: PropTypes.bool,
animationStage: PropTypes.number, animationStage: PropTypes.number,
sorting: PropTypes.any,
}; };
Header.defaultProps = { Header.defaultProps = {
isGrid: false, isGrid: false,

+ 19
- 2
src/components/MarketPlace/MarketPlace.js View File



const MarketPlace = (props) => { const MarketPlace = (props) => {
const [isGrid, setIsGrid] = useState(false); const [isGrid, setIsGrid] = useState(false);
const offers = props.offers;


return ( return (
<MarketPlaceContainer> <MarketPlaceContainer>
<Header isGrid={isGrid} setIsGrid={setIsGrid} myOffers={props.myOffers} skeleton={props.skeleton} animationStage={props.animationStage}/>
<Offers isGrid={isGrid} myOffers={props.myOffers} animationStage={props.animationStage} skeleton={props.skeleton} />
<Header
isGrid={isGrid}
setIsGrid={setIsGrid}
myOffers={props.myOffers}
sorting={props.offers.sorting}
skeleton={props.skeleton}
animationStage={props.animationStage}
/>
<Offers
isGrid={isGrid}
myOffers={props.myOffers}
animationStage={props.animationStage}
skeleton={props.skeleton}
offers={offers}
toggleFilters={props.toggleFilters}
/>
</MarketPlaceContainer> </MarketPlaceContainer>
); );
}; };
myOffers: PropTypes.bool, myOffers: PropTypes.bool,
animationStage: PropTypes.number, animationStage: PropTypes.number,
skeleton: PropTypes.bool, skeleton: PropTypes.bool,
offers: PropTypes.any,
toggleFilters: PropTypes.func
}; };


export default MarketPlace; export default MarketPlace;

+ 4
- 3
src/components/MarketPlace/Offers/HeaderMyOffers.js/HeadersMyOffers.js View File

import React, { useCallback, useRef } from "react"; import React, { useCallback, useRef } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { TextField } from "../../../TextFields/TextField/TextField";
import { EndIcon, SearchIcon } from "./HeadersMyOffers.styled";
import { EndIcon, SearchIcon, SearchInput } from "./HeadersMyOffers.styled";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";


const HeadersMyOffers = (props) => { const HeadersMyOffers = (props) => {
}; };
const handleSearch = () => { const handleSearch = () => {
props.searchMyOffers(searchRef.current.value); props.searchMyOffers(searchRef.current.value);
props.handleSearch();
}; };
return ( return (
<TextField
<SearchInput
fullWidth fullWidth
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
HeadersMyOffers.propTypes = { HeadersMyOffers.propTypes = {
children: PropTypes.node, children: PropTypes.node,
searchMyOffers: PropTypes.func, searchMyOffers: PropTypes.func,
handleSearch: PropTypes.func,
}; };


export default HeadersMyOffers; export default HeadersMyOffers;

+ 8
- 0
src/components/MarketPlace/Offers/HeaderMyOffers.js/HeadersMyOffers.styled.js View File

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




export const HeadersMyOffersContainer = styled(Box)``; export const HeadersMyOffersContainer = styled(Box)``;
left: 11px; left: 11px;
} }
`; `;
export const SearchInput = styled(TextField)`
width: 90%;
height: 36px;
& div {
height: 40px;
}
`

+ 28
- 12
src/components/MarketPlace/Offers/Offers.js View File

import React, { useRef } from "react"; import React, { useRef } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { OffersContainer } from "./Offers.styled";
import { FilterContainer, FilterIcon, OffersContainer } from "./Offers.styled";
import OfferCard from "../../Cards/OfferCard/OfferCard"; import OfferCard from "../../Cards/OfferCard/OfferCard";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import Paging from "../../Paging/Paging"; import Paging from "../../Paging/Paging";
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 { startChat } from "../../../util/helpers/chatHelper"; import { startChat } from "../../../util/helpers/chatHelper";
import useOffers from "../../../hooks/useOffers";
import OffersNotFound from "./OffersNotFound"; import OffersNotFound from "./OffersNotFound";
import HeadersMyOffers from "./HeaderMyOffers.js/HeadersMyOffers"; import HeadersMyOffers from "./HeaderMyOffers.js/HeadersMyOffers";
import SkeletonOfferCard from "../../Cards/OfferCard/SkeletonOfferCard/SkeletonOfferCard"; import SkeletonOfferCard from "../../Cards/OfferCard/SkeletonOfferCard/SkeletonOfferCard";
const chats = useSelector(selectLatestChats); const chats = useSelector(selectLatestChats);
const offersRef = useRef(null); const offersRef = useRef(null);
const userId = useSelector(selectUserId); const userId = useSelector(selectUserId);
const offers = useOffers(props.myOffers);
const arrayForMapping = Array.apply(null, Array(4)).map(
() => {}
);
const offers = props.offers;
const arrayForMapping = Array.apply(null, Array(4)).map(() => {});


const messageOneUser = (offer) => { const messageOneUser = (offer) => {
startChat(chats, offer, userId); startChat(chats, offer, userId);
}; };
const toggleFilters = () => {
props.toggleFilters();
};


return ( return (
<> <>
<FilterContainer
onClick={toggleFilters}
number={offers.filters.numOfFiltersChosen}
myOffers={props.myOffers}
>
<FilterIcon />
</FilterContainer>
{!props.skeleton ? ( {!props.skeleton ? (
<> <>
{props.myOffers && ( {props.myOffers && (
<HeadersMyOffers searchMyOffers={offers.searchMyOffers} />
<HeadersMyOffers
searchMyOffers={offers.search.searchOffers}
handleSearch={offers.apply}
/>
)} )}
{offers.allOffersToShow.length === 0 ? ( {offers.allOffersToShow.length === 0 ? (
<OffersNotFound /> <OffersNotFound />
<Paging <Paging
totalElements={offers.totalOffers} totalElements={offers.totalOffers}
elementsPerPage={10} elementsPerPage={10}
current={offers.page}
changePage={offers.handleDifferentPage}
current={parseInt(offers.paging.currentPage)}
changePage={offers.paging.changePage}
/> />
</OffersContainer> </OffersContainer>
)} )}
</> </>
) : ( ) : (
<> <>
{arrayForMapping.map((item, index)=> (
<SkeletonOfferCard key={index} skeleton animationStage={props.animationStage} />
))}
{arrayForMapping.map((item, index) => (
<SkeletonOfferCard
key={index}
skeleton
animationStage={props.animationStage}
/>
))}
</> </>
)} )}
</> </>
myOffers: PropTypes.bool, myOffers: PropTypes.bool,
skeleton: PropTypes.bool, skeleton: PropTypes.bool,
animationStage: PropTypes.number, animationStage: PropTypes.number,
offers: PropTypes.any,
toggleFilters: PropTypes.func,
}; };


Offers.defaultProps = { Offers.defaultProps = {

+ 27
- 0
src/components/MarketPlace/Offers/Offers.styled.js View File

import { Box } from "@mui/material"; import { Box } from "@mui/material";
import styled from "styled-components"; import styled from "styled-components";
import selectedTheme from "../../../themes";
import IconWithNumber from "../../Icon/IconWithNumber/IconWithNumber";
import { ReactComponent as Filter } from "../../../assets/images/svg/filter.svg";



export const OffersContainer = styled(Box)` export const OffersContainer = styled(Box)`
display: flex; display: flex;
position: relative; position: relative;
padding-bottom: 60px; padding-bottom: 60px;
`; `;
export const FilterContainer = styled(IconWithNumber)`
position: absolute;
top: ${props => props.myOffers ? "126px" : "93px"};
right: 18px;
cursor: pointer;
background-color: ${selectedTheme.offerBackgroundColor} !important;
& div {
width: 16px;
height: 16px;
background-color: ${selectedTheme.primaryPurple};
position: absolute;
top: -5px;
right: -5px;
line-height: 15px;
text-align: center;
}
@media (min-width: 600px) {
display: none;
}
`;
export const FilterIcon = styled(Filter)`
background-color: ${selectedTheme.offerBackgroundColor};
`;

+ 1
- 0
src/components/Paging/Paging.js View File

: 1; : 1;


let moving = 0; let moving = 0;
console.log(props.current)
// Making array of pages which contains 2 pages before and after current page // Making array of pages which contains 2 pages before and after current page
const pagesAsArray = Array.apply(null, Array(5)).map(() => {}); const pagesAsArray = Array.apply(null, Array(5)).map(() => {});



+ 3
- 0
src/constants/queryStringConstants.js View File

export const KEY_SORT_DATE = "_des_date"; export const KEY_SORT_DATE = "_des_date";
export const KEY_SORT_POPULAR = "_des_popular"; export const KEY_SORT_POPULAR = "_des_popular";
export const KEY_LOCATION = "location" export const KEY_LOCATION = "location"
export const KEY_NAME = "name";
export const KEY_SEARCH = "search"
export const VALUE_SORTBY_NEW = "newest"; export const VALUE_SORTBY_NEW = "newest";
export const VALUE_SORTBY_OLD = "oldest"; export const VALUE_SORTBY_OLD = "oldest";
export const VALUE_SORTBY_POPULAR = "popular"; export const VALUE_SORTBY_POPULAR = "popular";
export const initialSize = "10";

+ 0
- 182
src/hooks/useFilters.js View File

import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { fetchCategories } from "../store/actions/categories/categoriesActions";
import {
setFilteredCategory,
setFilteredLocations,
setFilteredSubcategory,
// setIsAppliedStatus,
} from "../store/actions/filters/filtersActions";
import { fetchLocations } from "../store/actions/locations/locationsActions";
import {
selectCategories,
selectSubcategories,
} from "../store/selectors/categoriesSelectors";
import {
selectAppliedStatus,
selectSelectedCategory,
selectSelectedLocations,
selectSelectedSubcategory,
} from "../store/selectors/filtersSelectors";
import { selectLocations } from "../store/selectors/locationsSelectors";
import { useQueryString } from "./useQueryString";

const useFilters = (myOffers) => {
const selectedCategory = useSelector(selectSelectedCategory);
const selectedSubcategory = useSelector(selectSelectedSubcategory);
const selectedLocations = useSelector(selectSelectedLocations);
const [loadedFromQS, setLoadedFromQS] = useState(false);
const isApplied = useSelector(selectAppliedStatus);
const categories = useSelector(selectCategories);
const subcategories = useSelector(
selectSubcategories(selectedCategory?.name)
);
const locations = useSelector(selectLocations);
const dispatch = useDispatch();
const queryStringHook = useQueryString();

const fetchCategoriesAndLocations = useCallback(
_.once(() => {
dispatch(fetchCategories());
dispatch(fetchLocations());
}),
[]
);

useEffect(() => {
fetchCategoriesAndLocations();
}, []);

useEffect(() => {
const queryObject = new URLSearchParams(queryStringHook.queryString);
if (categories?.length > 0 && locations?.length > 0) {
let category;
if (queryObject.has("category")) {
category = categories.find(
(item) => item.name === queryObject.get("category").toString()
);
setSelectedCategory(category);
} else {
if (!myOffers) {
setSelectedCategory();
}
}
if (queryObject.has("subcategory")) {
setSelectedSubcategory(
category?.subcategories?.find(
(item) =>
item.name.toString() === queryObject.get("subcategory").toString()
)
);
} else {
if (!myOffers) {
setSelectedSubcategory();
}
}
}
if (queryObject.has("location")) {
let locationsToPush = [];
queryObject.getAll("location").forEach((item) => {
locationsToPush.push(locations.find((p) => p.city === item));
});
setSelectedLocations([...locationsToPush]);
} else {
if (!myOffers) {
setSelectedLocations([]);
}
}
// dispatch(setIsAppliedStatus(true));
}, [queryStringHook.queryString, categories, locations]);

// Apply everything
const applyFilters = () => {
makeQueryString();
};

// Clear function
const clearFilters = () => {
setSelectedLocations([]);
setSelectedSubcategory();
setSelectedCategory();
applyFilters();
};

// Helper function
const makeQueryString = () => {
let qsArray = [];
qsArray.push({ key: "category", value: selectedCategory?.name });
qsArray.push({ key: "subcategory", value: selectedSubcategory?.name });
selectedLocations?.forEach((location) => {
qsArray.push({ key: "location", value: location?.city });
});
qsArray.push({ key: "page", value: "1" });
queryStringHook.appendMultipleToQueryString(qsArray);
};

//Calculate chosen categories for number above filter icon on mobile responsive version
const calculateFiltersChosen = () => {
let sum = 0;
if (selectedCategory && selectedCategory?._id !== 0) {
sum++;
}
if (selectedSubcategory && selectedSubcategory?._id !== 0) {
sum++;
}
if (selectedLocations && selectedLocations?.length > 0) {
sum += selectedLocations.length;
}
return sum;
};

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

export default useFilters;

+ 0
- 253
src/hooks/useOffers.js View File

import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { HOME_PAGE } from "../constants/pages";
import { KEY_PAGE, KEY_SIZE } from "../constants/queryStringConstants";
import { sortEnum } from "../enums/sortEnum";
import { fetchChats } from "../store/actions/chat/chatActions";
import {
fetchMineOffers,
fetchOffers,
} from "../store/actions/offers/offersActions";
import {
selectAppliedStatus,
selectSelectedCategory,
selectSelectedLocations,
selectSelectedSortOption,
selectSelectedSubcategory,
} from "../store/selectors/filtersSelectors";
import {
selectMineOffers,
selectOffers,
selectPinnedOffers,
selectTotalOffers,
} from "../store/selectors/offersSelectors";
import { useQueryString } from "./useQueryString";

const useOffers = (myOffers) => {
const history = useHistory();
const pinnedOffers = useSelector(selectPinnedOffers);
const offers = useSelector(selectOffers);
const mineOffers = useSelector(selectMineOffers);
const dispatch = useDispatch();
const queryStringHook = useQueryString();
const selectedCategory = useSelector(selectSelectedCategory);
const selectedSubcategory = useSelector(selectSelectedSubcategory);
const selectedLocations = useSelector(selectSelectedLocations);
const selectedSortOption = useSelector(selectSelectedSortOption);
const isApplied = useSelector(selectAppliedStatus);
const total = useSelector(selectTotalOffers);
const [page, setPage] = useState(1);
const [searchQuery, setSearchQuery] = useState("");
const [myOffersLength, setMyOffersLength] = useState(0);

//Fetching chats
useEffect(() => {
dispatch(fetchChats());
}, []);

//Setting appropriate page based on query string
useEffect(() => {
let queryObject = new URLSearchParams(queryStringHook.queryString);
if (queryObject.has(KEY_PAGE) && queryObject.get(KEY_PAGE) !== 1) {
setPage(parseInt(queryObject.get(KEY_PAGE)));
}
}, [history.location.search]);

// Checking if page is opened by clicking on logo on header, and fetching offers
// with empty query string if previous statement is true
useEffect(() => {
if (history?.location?.state?.logo || history?.location?.state?.refetch) {
dispatch(fetchOffers({ queryString: "" }));
queryStringHook.setQueryString("");
setPage(1);
history.location.state = undefined;
}
}, [history.location.state]);

// Initialy loading offers with filters from query string
useEffect(() => {
if (queryStringHook.loadedFromURL) {
refetch();
} else {
queryStringHook.appendMultipleToQueryString([
{ key: KEY_SIZE, value: "10" },
{ key: KEY_PAGE, value: "1" },
]);
}
}, [queryStringHook.loadedFromURL, queryStringHook.queryString]);

// Changing offers when page changes
useEffect(() => {
const queryObject = new URLSearchParams(queryStringHook.queryString);
if (queryObject.has(KEY_PAGE)) {
if (queryObject.get(KEY_PAGE) !== page.toString()) {
queryStringHook.appendToQueryString(KEY_PAGE, page);
} else {
refetch();
}
} else {
queryStringHook.appendToQueryString(KEY_PAGE, page);
}
}, [page]);

// All pinned to show when market place is opened
const pinnedOffersToShow = useMemo(() => {
if (myOffers) {
return mineOffers.filter((item) => item.pinned);
}
return pinnedOffers;
}, [pinnedOffers, mineOffers, page, myOffers]);

// Normal offers to show when market place is opened
const offersToShow = useMemo(() => {
if (myOffers) {
return mineOffers.filter((item) => item.pinned === false);
}
return offers;
}, [offers, mineOffers, page, myOffers]);

// Offers to show when market place is opened and when my offers are opened
const allOffersToShow = useMemo(() => {
let newOffers = [...pinnedOffersToShow, ...offersToShow];
if (myOffers) {
// Filtering my offers based on category
if (selectedCategory && selectedCategory?._id !== 0) {
newOffers = newOffers.filter(
(item) => item.category.name === selectedCategory.name
);
}
// Filtering my offers based on subcategory
if (selectedSubcategory && selectedSubcategory?._id !== 0) {
newOffers = newOffers.filter(
(item) => item.subcategory === selectedSubcategory.name
);
}
// Filtering my offers based on locations
if (selectedLocations && selectedLocations?.length > 0) {
newOffers = newOffers.filter((item) => {
let isInOneOfLocations = false;
selectedLocations?.forEach((location) => {
if (item.location.city === location.city) {
isInOneOfLocations = true;
}
});
return isInOneOfLocations;
});
}
// Sorting my offers based on chosen sorting option
// Old offers are arrays used for sorting
let oldOffers = [...offersToShow];
let oldPinnedOffers = [...pinnedOffersToShow];
if (
selectedSortOption &&
selectedSortOption.value === sortEnum.NEW.value
) {
newOffers = [
...oldPinnedOffers.sort(
(itemA, itemB) =>
new Date(itemB._created) - new Date(itemA._created)
),
...oldOffers.sort(
(itemA, itemB) =>
new Date(itemB._created) - new Date(itemA._created)
),
];
}
if (
selectedSortOption &&
selectedSortOption.value === sortEnum.OLD.value
) {
newOffers = newOffers.sort(
(itemA, itemB) => new Date(itemA._created) - new Date(itemB._created)
);
newOffers = [
...oldPinnedOffers.sort(
(itemA, itemB) =>
new Date(itemA._created) - new Date(itemB._created)
),
...oldOffers.sort(
(itemA, itemB) =>
new Date(itemA._created) - new Date(itemB._created)
),
];
}
if (
selectedSortOption &&
selectedSortOption.value === sortEnum.POPULAR.value
) {
newOffers = [
...oldPinnedOffers.sort(
(itemA, itemB) => itemB.views.count - itemA.views.count
),
...oldOffers.sort(
(itemA, itemB) => itemB.views.count - itemA.views.count
),
];
}
newOffers = newOffers.filter((item) =>
item?.name?.toLowerCase().includes(searchQuery.toLowerCase(), 0)
);
setMyOffersLength(newOffers?.length);
newOffers = newOffers.slice((page - 1) * 10, page * 10);
}
return newOffers;
}, [
pinnedOffersToShow,
offersToShow,
myOffers,
page,
searchQuery,
isApplied,
]);

// Total number of all offers that can be shown
const totalOffers = useMemo(() => {
if (myOffers) {
return myOffersLength;
}
return total;
}, [total, myOffersLength]);

const searchMyOffers = (searchValue) => {
setSearchQuery(searchValue);
};

// Changing page
const handleDifferentPage = (pageNum) => {
setPage(pageNum);
};

// Refetching offers based on query string
const refetch = () => {
if (!myOffers) {
dispatch(fetchOffers({ queryString: "?" + queryStringHook.queryString }));
history.replace({
pathname: HOME_PAGE,
search: queryStringHook.getGlobalQueryString(),
});
} else {
dispatch(fetchMineOffers());
}
window.scrollTo({
top: 0,
behavior: "smooth",
});
const queryObject = new URLSearchParams(queryStringHook.queryString);
if (queryObject.has(KEY_PAGE)) {
if (queryObject.get(KEY_PAGE) !== page.toString())
setPage(parseInt(queryObject.get(KEY_PAGE)));
} else {
setPage(1);
}
};

return {
handleDifferentPage,
totalOffers,
allOffersToShow,
page,
searchMyOffers,
};
};
export default useOffers;

+ 66
- 0
src/hooks/useOffers/useCategoryFilter.js View File

import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setFilteredCategory } from "../../store/actions/filters/filtersActions";
import { selectCategories } from "../../store/selectors/categoriesSelectors";
import { selectSelectedCategory } from "../../store/selectors/filtersSelectors";

const useCategoryFilter = () => {
const selectedCategory = useSelector(selectSelectedCategory);
const allCategories = useSelector(selectCategories);
const dispatch = useDispatch();
const [selectedCategoryLocally, setSelectedCategoryLocally] = useState({});
const initialOption = useMemo(() => {
return {
_id: 0,
};
}, []);

useEffect(() => {
setSelectedCategoryLocally(selectedCategory);
}, [selectedCategory]);

// Set selected category locally in state
// If second argument is true, then selected category is also updated in redux
const setSelectedCategory = (category, immediateApply = false) => {
setSelectedCategoryLocally(category);
if (immediateApply) {
dispatch(setFilteredCategory(category));
}
};

// Find category object by providing its name
const findCategory = (categoryName) => {
return allCategories.find((category) => category.name === categoryName);
};

// Get all subcategories by providing its category name
const getSubcategories = (categoryName) => {
let category = findCategory(categoryName);
return category?.subcategories ? category.subcategories : [];
};


// Update selected category in redux
const apply = () => {
dispatch(setFilteredCategory(selectedCategoryLocally));
};

// Clear category chosen
const clear = () => {
setSelectedCategoryLocally(initialOption);
dispatch(setFilteredCategory(initialOption));
};

return {
selectedCategory,
selectedCategoryLocally,
setSelectedCategory,
getSubcategories,
findCategory,
allCategories,
apply,
clear,
};
};

export default useCategoryFilter;

+ 50
- 0
src/hooks/useOffers/useFilters.js View File

import { useEffect, useMemo } from "react";
import useCategoryFilter from "./useCategoryFilter";
import useLocationsFilter from "./useLocationsFilter";
import useSubcategoryFilter from "./useSubcategoryFilter";

const useFilters = (clearAll = false) => {
const category = useCategoryFilter();
const subcategory = useSubcategoryFilter();
const locations = useLocationsFilter();

useEffect(() => {
if (clearAll) {
clear();
}
}, []);

const numOfFiltersChosen = useMemo(() => {
let sumOfFiltersChosen = 0;
if (category.selectedCategoryLocally?._id) sumOfFiltersChosen++;
if (subcategory.selectedSubcategoryLocally?._id) sumOfFiltersChosen++;
sumOfFiltersChosen += locations.selectedLocationsLocally.length;
return sumOfFiltersChosen;
}, [
category.selectedCategoryLocally,
subcategory.selectedSubcategoryLocally,
locations.selectedLocationsLocally,
]);

const apply = () => {
category.apply();
subcategory.apply();
locations.apply();
};

const clear = () => {
category.clear();
subcategory.clear();
locations.clear();
};

return {
category,
subcategory,
locations,
numOfFiltersChosen,
apply,
clear,
};
};
export default useFilters;

+ 54
- 0
src/hooks/useOffers/useLocationsFilter.js View File

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setFilteredLocations } from "../../store/actions/filters/filtersActions";
import { selectSelectedLocations } from "../../store/selectors/filtersSelectors";
import { selectLocations } from "../../store/selectors/locationsSelectors";

const useLocationsFilter = () => {
const selectedLocations = useSelector(selectSelectedLocations);
const dispatch = useDispatch();
const allLocations = useSelector(selectLocations);
const [selectedLocationsLocally, setSelectedLocationsLocally] = useState([]);

useEffect(() => {
setSelectedLocationsLocally(selectedLocations);
}, [selectedLocations]);

// Set selected locations globally
const setSelectedLocations = (locations, immediateApply = false) => {
setSelectedLocationsLocally(locations);
if (immediateApply) {
dispatch(setFilteredLocations(locations));
}
};

// Find locations from array made from query string, and set locations globally
const setSelectedLocationsFromArray = (locations) => {
let locationsToPush = [];
locations.forEach((locationName) => {
locationsToPush.push(allLocations.find((p) => p.city === locationName));
});
setSelectedLocations([...locationsToPush])
};

const apply = () => {
dispatch(setFilteredLocations(selectedLocationsLocally));
};

const clear = () => {
setSelectedLocationsLocally([]);
dispatch(setFilteredLocations([]));
};

return {
selectedLocations,
selectedLocationsLocally,
setSelectedLocations,
setSelectedLocationsFromArray,
allLocations,
apply,
clear,
};
};

export default useLocationsFilter;

+ 101
- 0
src/hooks/useOffers/useMyOffers.js View File

import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { sortEnum } from "../../enums/sortEnum";
import { fetchMineOffers } from "../../store/actions/offers/offersActions";
import { selectMineOffers } from "../../store/selectors/offersSelectors";
import useFilters from "./useFilters";
import usePaging from "./usePaging";
import useSearch from "./useSearch";
import useSorting from "./useSorting";

const useMyOffers = () => {
const filters = useFilters(true);
const sorting = useSorting();
const mineOffers = useSelector(selectMineOffers);
const search = useSearch();
const dispatch = useDispatch();
const paging = usePaging();
const [appliedFilters, setAppliedFilters] = useState(false);
const [totalOffers, setTotalOffers] = useState(0);

useEffect(() => {
dispatch(fetchMineOffers());
}, []);

const apply = () => {
paging.changePage(1);
setAppliedFilters(false);
};

// Filter, search and sort all mine offers
const allOffersToShow = useMemo(() => {
let mineOffersFiltered = [...mineOffers];
// Filter mine offers by category
if (filters.category.selectedCategoryLocally?.name)
mineOffersFiltered = mineOffersFiltered.filter(
(offer) =>
offer?.category?.name ===
filters.category.selectedCategoryLocally.name
);
// Filter mine offers by subcategory
if (filters.subcategory.selectedSubcategoryLocally?.name) {
mineOffersFiltered = mineOffersFiltered.filter(
(offer) =>
offer?.subcategory ===
filters.subcategory.selectedSubcategoryLocally?.name
);
}
// Filter mine offers by locations
if (filters.locations.selectedLocationsLocally?.length > 0) {
mineOffersFiltered = mineOffersFiltered.filter((offer) =>
filters.locations.selectedLocationsLocally.find(
(location) => location?.city === offer?.location?.city
)
);
}
// Sort mine offers
if (sorting.selectedSortOptionLocally.value !== sortEnum.INITIAL.value) {
if (sorting.selectedSortOptionLocally.value === sortEnum.OLD.value) {
mineOffersFiltered.sort(
(a, b) => new Date(a._created) - new Date(b._created)
);
}
if (sorting.selectedSortOptionLocally.value === sortEnum.NEW.value) {
mineOffersFiltered.sort(
(a, b) => new Date(b._created) - new Date(a._created)
);
}
if (sorting.selectedSortOptionLocally.value === sortEnum.POPULAR.value) {
mineOffersFiltered.sort((a, b) => b.views.count - a.views.count);
}
}
mineOffersFiltered = mineOffersFiltered.filter((offer) =>
offer?.name?.toLowerCase()?.includes(search.searchStringLocally)
);
setTotalOffers(mineOffersFiltered?.length);
mineOffersFiltered = mineOffersFiltered.slice(
(paging.currentPage - 1) * 10,
paging.currentPage * 10
);
if (!appliedFilters) {
setAppliedFilters(true);
}
return [...mineOffersFiltered];
}, [
appliedFilters,
sorting.selectedSortOptionLocally,
mineOffers,
paging.currentPage,
]);

return {
filters,
paging,
sorting,
search,
allOffersToShow,
totalOffers,
apply,
};
};
export default useMyOffers;

+ 134
- 0
src/hooks/useOffers/useOffers.js View File

import { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
KEY_CATEGORY,
KEY_LOCATION,
KEY_PAGE,
KEY_SEARCH,
KEY_SORTBY,
KEY_SUBCATEGORY,
} from "../../constants/queryStringConstants";
import { fetchCategories } from "../../store/actions/categories/categoriesActions";
import { fetchLocations } from "../../store/actions/locations/locationsActions";
import {
selectOffers,
selectTotalOffers,
} from "../../store/selectors/offersSelectors";
import useFilters from "./useFilters";
import useQueryString from "./useQueryString";
import { setQueryString } from "../../store/actions/queryString/queryStringActions";
import {
convertQueryStringForBackend,
makeHeaderStringHelper,
makeQueryStringHelper,
} from "../../util/helpers/queryHelpers";
import useSorting from "./useSorting";
import useSearch from "./useSearch";
import {
setHeaderString,
setSearchString,
} from "../../store/actions/filters/filtersActions";
import usePaging from "./usePaging";

const useOffers = () => {
const dispatch = useDispatch();
const filters = useFilters();
const queryStringHook = useQueryString();
const offers = useSelector(selectOffers);
const totalOffers = useSelector(selectTotalOffers);

// Always fetch categories and locations,
// becouse count of total offers change over time
useEffect(() => {
dispatch(fetchCategories());
dispatch(fetchLocations());
return () => clear();
}, []);

// On every change of query string, new header string should be created
// Header string is shown on Home page above offers
useEffect(() => {
const headerStringLocal = makeHeaderStringHelper(filters);
dispatch(setHeaderString(headerStringLocal));
}, [queryStringHook.queryString]);

// Initially set category, location and subcategory based on query string
useEffect(() => {
if (queryStringHook.isInitiallyLoaded) {
const queryObject = queryStringHook.queryObject;
if (KEY_CATEGORY in queryObject) {
const category = filters.category.findCategory(
queryObject[KEY_CATEGORY]
);
filters.category.setSelectedCategory(category);
if (KEY_SUBCATEGORY in queryObject) {
const subcategory = filters.category
.getSubcategories(category?.name)
.find(
(subcategory) => subcategory.name === queryObject[KEY_SUBCATEGORY]
);
filters.subcategory.setSelectedSubcategory(subcategory);
}
}
if (KEY_LOCATION in queryObject) {
filters.locations.setSelectedLocationsFromArray(
queryObject[KEY_LOCATION]
);
}
if (KEY_SORTBY in queryObject) {
sorting.changeSortingFromName(queryObject[KEY_SORTBY]);
}
if (KEY_PAGE in queryObject) {
if (queryObject[KEY_PAGE] !== 1)
paging.changePage(queryObject[KEY_PAGE]);
}
dispatch(setSearchString(queryObject[KEY_SEARCH]));
}
}, [queryStringHook.isInitiallyLoaded]);

const allOffersToShow = useMemo(() => {
return offers;
}, [offers]);

const apply = () => {
filters.apply();
const newQueryString = makeQueryStringHelper(
filters,
paging,
search,
sorting
);
dispatch(setQueryString(convertQueryStringForBackend(newQueryString)));
};

// Those hooks are below becouse function apply cannot be put on props before initialization
const sorting = useSorting(apply);
const paging = usePaging(apply);
const search = useSearch(apply);

// On every change of search string, offers should be immediately searched
useEffect(() => {
if (queryStringHook.isInitiallyLoaded) {
search.searchOffers(search.searchString);
}
}, [search.searchString]);

const clear = () => {
filters.clear();
sorting.clear();
paging.changePage(1);
};

return {
filters,
sorting,
paging,
queryStringHook,
allOffersToShow,
totalOffers,
apply,
clear,
};
};

export default useOffers;

+ 38
- 0
src/hooks/useOffers/usePaging.js View File

import { useEffect, useState } from "react";

const usePaging = (applyAllFilters) => {
const [currentPage, setCurrentPage] = useState(1);
const [isInitallyLoaded, setIsInitiallyLoaded] = useState(false);

// If state currentPage is changed, new request to backend should be sent,
// except on initial load
useEffect(() => {
if (isInitallyLoaded && applyAllFilters) {
applyAllFilters();
}
window.scrollTo({
top: 0,
behavior: "smooth",
});
}, [currentPage]);

const changePage = (pageNumber) => {
setCurrentPage(pageNumber);
setIsInitiallyLoaded(true);
};

const goToNextPage = () => {
setCurrentPage((prevPage) => prevPage + 1);
};
const goToPrevPage = () => {
setCurrentPage((prevPage) => prevPage - 1);
};

return {
currentPage,
changePage,
goToNextPage,
goToPrevPage,
};
};
export default usePaging;

+ 60
- 0
src/hooks/useOffers/useQueryString.js View File

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { fetchOffers } from "../../store/actions/offers/offersActions";
import { setQueryString } from "../../store/actions/queryString/queryStringActions";
import { selectQueryString } from "../../store/selectors/queryStringSelectors";
import {
convertQueryStringForBackend,
convertQueryStringForFrontend,
getQueryObjectHelper,
} from "../../util/helpers/queryHelpers";

const useQueryString = () => {
const queryString = useSelector(selectQueryString);
const history = useHistory();
const dispatch = useDispatch();
const [isInitiallyLoaded, setIsInitallyLoaded] = useState(false);
const [queryObject, setQueryObject] = useState({});

// Initially read filters, sorting and paging from querystring
useEffect(() => {
if ((!isInitiallyLoaded || history.location?.state?.logo) && !history.location?.state?.from) {
const queryStringFromUrl = history.location?.search;
setQueryObject(getQueryObjectHelper(queryStringFromUrl));
dispatch(setQueryString(queryStringFromUrl));
}
history.location.state = {}
}, [history.location]);

// Set initially loaded to true on initial load
useEffect(() => {
if (
convertQueryStringForFrontend(queryString) ===
convertQueryStringForFrontend(history.location.search) &&
!isInitiallyLoaded
) {
setIsInitallyLoaded(true);
}
}, [queryString]);

// Updating offers on query string change
useEffect(() => {
if (isInitiallyLoaded) {
dispatch(
fetchOffers({ queryString: convertQueryStringForBackend(queryString) })
);
setQueryObject(getQueryObjectHelper(queryString));
history.replace({
search: convertQueryStringForFrontend(queryString),
});
}
}, [queryString, isInitiallyLoaded]);

return {
queryString,
queryObject,
isInitiallyLoaded,
};
};
export default useQueryString;

+ 46
- 0
src/hooks/useOffers/useSearch.js View File

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setSearchString } from "../../store/actions/filters/filtersActions";
import { selectSearchString } from "../../store/selectors/filtersSelectors";

const useSearch = (applyAllFilters) => {
const [searchStringLocally, setSearchStringLocally] = useState("");
const [isInitallyLoaded, setIsInitiallyLoaded] = useState(false);
const dispatch = useDispatch();
const searchString = useSelector(selectSearchString);

// On every global change of search string, new request to backend should be sent
useEffect(() => {
if (searchStringLocally !== searchString && applyAllFilters) {
setSearchStringLocally(searchString);
}
if (isInitallyLoaded) {
if (applyAllFilters) applyAllFilters();
}
}, [searchString]);

// On every local change of search string, global state of search string should be also updated
useEffect(() => {
if (isInitallyLoaded && applyAllFilters) {
dispatch(setSearchString(searchStringLocally));
}
}, [searchStringLocally]);

const searchOffers = (searchValue) => {
setIsInitiallyLoaded(true);
setSearchStringLocally(searchValue);
};

const clear = () => {
setSearchStringLocally("");
};

return {
searchOffers,
setSearchStringLocally,
searchStringLocally,
searchString,
clear,
};
};
export default useSearch;

+ 65
- 0
src/hooks/useOffers/useSorting.js View File

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
VALUE_SORTBY_NEW,
VALUE_SORTBY_OLD,
VALUE_SORTBY_POPULAR,
} from "../../constants/queryStringConstants";
import { sortEnum } from "../../enums/sortEnum";
import { setFilteredSortOption } from "../../store/actions/filters/filtersActions";
import { selectSelectedSortOption } from "../../store/selectors/filtersSelectors";

const useSorting = (applyAllFilters) => {
const selectedSortOption = useSelector(selectSelectedSortOption);
const [selectedSortOptionLocally, setSelectedSortOptionLocally] = useState(
sortEnum.INITIAL
);
const [isInitiallyLoaded, setIsInitallyLoaded] = useState(false);
const dispatch = useDispatch();

// On every change of sorting option, new request to backend should be sent
useEffect(() => {
if (isInitiallyLoaded) {
if (applyAllFilters) applyAllFilters();
}
}, [isInitiallyLoaded, selectedSortOption]);

const changeSorting = (newSortOption) => {
dispatch(setFilteredSortOption(newSortOption));
setSelectedSortOptionLocally(newSortOption);
if (!isInitiallyLoaded) {
setIsInitallyLoaded(true);
}
};

// Change sorting by name of sorting option that is shown on frontned
const changeSortingFromName = (sortingName) => {
if (sortingName === VALUE_SORTBY_NEW) {
changeSorting(sortEnum.NEW, true);
}
if (sortingName === VALUE_SORTBY_OLD) {
changeSorting(sortEnum.OLD, true);
}
if (sortingName === VALUE_SORTBY_POPULAR) {
changeSorting(sortEnum.POPULAR, true);
}
};

const apply = () => {
// For future changes
};

const clear = () => {
dispatch(setFilteredSortOption(sortEnum.INITIAL));
};

return {
selectedSortOption,
selectedSortOptionLocally,
changeSorting,
changeSortingFromName,
apply,
clear,
};
};
export default useSorting;

+ 55
- 0
src/hooks/useOffers/useSubcategoryFilter.js View File

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setFilteredSubcategory } from "../../store/actions/filters/filtersActions";
import { selectSelectedSubcategory } from "../../store/selectors/filtersSelectors";

const useSubcategoryFilter = () => {
const selectedSubcategory = useSelector(selectSelectedSubcategory);
const dispatch = useDispatch();
const initialOption = {
label: "SVE PODKATEGORIJE",
_id: 0,
};
const [selectedSubcategoryLocally, setSelectedSubcategoryLocally] =
useState(initialOption);

useEffect(() => {
if (selectedSubcategory)
if ("_id" in selectedSubcategory) {
setSelectedSubcategoryLocally(selectedSubcategory);
}
}, [selectedSubcategory]);

useEffect(() => {
if (Object.keys(selectedSubcategoryLocally)?.length === 0) {
setSelectedSubcategoryLocally(initialOption);
}
}, [initialOption]);

const setSelectedSubcategory = (subcategory, immediateApply = false) => {
setSelectedSubcategoryLocally(subcategory);
if (immediateApply) {
dispatch(setFilteredSubcategory(subcategory));
}
};

const apply = () => {
dispatch(setFilteredSubcategory(selectedSubcategoryLocally));
};

const clear = () => {
setSelectedSubcategoryLocally(initialOption);
dispatch(setFilteredSubcategory(initialOption));
};

return {
selectedSubcategory,
selectedSubcategoryLocally,
setSelectedSubcategory,
initialOption,
apply,
clear,
};
};

export default useSubcategoryFilter;

+ 0
- 174
src/hooks/useQueryString.js View File

/* eslint-disable */
import _ from "lodash";
import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import PropTypes from "prop-types";
import { useHistory } from "react-router-dom";
import { HOME_PAGE } from "../constants/pages";
import { setQueryString as setQueryStringSaga } from "../store/actions/queryString/queryStringActions";
import { selectQueryString } from "../store/selectors/queryStringSelectors";
import {
convertQueryStringBackend,
convertQueryStringFrontend,
} from "../util/helpers/queryHelpers";
import { KEY_CATEGORY, KEY_LOCATION, KEY_PAGE, KEY_SIZE, KEY_SORTBY, KEY_SORT_DATE, KEY_SORT_POPULAR, KEY_SUBCATEGORY } from "../constants/queryStringConstants";

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

// Initially loading query string and putting it into redux
useEffect(() => {
// Substring(1) used becouse query string in initial state is ?key=value
// and in redux query string is stored as only key=value
const queryStringLocal = history.location.search.substring(1);
setQueryString(convertQueryStringBackend(queryStringLocal));
setGlobalQueryString(queryStringLocal);
}, []);

// Setting loadedFromUrl to true when query string is loaded and filters/sorting/search
// has been changed so global query string matches current query string
useEffect(() => {
if (globalQueryString === history.location.search.substring(1))
setLoadedFromURL(true);
}, [globalQueryString]);

// Making function to initially set global query string when all filters and sorting
// has been set
const fun = useMemo(() => {
return _.once(() => {
setGlobalQueryString(convertQueryStringFrontend(queryString));
setInitial(false);
});
}, [queryString]);

// Setting global query string when all filters and sorting has been set
useEffect(() => {
if (initial && loadedFromURL) {
if (queryString?.length > 0) {
fun();
}
} else {
setGlobalQueryString(convertQueryStringFrontend(queryString));
}
}, [queryString, loadedFromURL]);

// When global query string is changed, updating query string that user sees
useEffect(() => {
if (!initial && history.location.pathname === HOME_PAGE) {
history.replace({
pathname: HOME_PAGE,
search: "?" + globalQueryString,
});
}
}, [globalQueryString, initial]);

const getQueryString = () => {
return queryString;
};
const setQueryString = (newQueryString) => {
dispatch(setQueryStringSaga(newQueryString));
};
const getQueryObject = () => {
const urlParams = new URLSearchParams(queryString);
return Object.fromEntries(urlParams);
};
// Adding key-value pairs to query string, deletes its previous duplicates, except
// when working with locations, then just appends it, or doesnt append if query string
// already contains provided location
const appendToQueryString = (key, value) => {
if (loadedFromURL) {
let urlParams = new URLSearchParams(queryString);
if (key === KEY_LOCATION) {
if (urlParams.has(key)) {
let arrayOfLocations = urlParams.getAll(key);
if (arrayOfLocations.includes(value)) {
arrayOfLocations = arrayOfLocations.filter(
(item) => item?.toString() !== value?.toString()
);
urlParams.delete(key);
arrayOfLocations.forEach((item) => {
urlParams.append(key, item);
});
}
}
} else {
if (urlParams.has(key)) {
urlParams.delete(key);
}
}
if (!value) setQueryString(urlParams.toString());
urlParams.append(key, value);
setQueryString(urlParams.toString());
return urlParams.toString();
}
};
// Same as appendToQueryString, just adds multiple key-value pairs at once
const appendMultipleToQueryString = (array = []) => {
if (loadedFromURL) {
let urlParams = new URLSearchParams(queryString);
if (
array.find((item) => item.key === KEY_CATEGORY) ||
array.find((item) => item.key === KEY_SUBCATEGORY)
) {
urlParams.delete(KEY_LOCATION);
}
array.forEach((item) => {
if (urlParams.has(item.key) && item.key !== KEY_LOCATION) {
urlParams.delete(item.key);
}
if (!item.value) return;
urlParams.append(item.key, item.value);
});
setQueryString(urlParams.toString());
return urlParams.toString();
}
};
const deleteFromQueryString = (key, value = null) => {
let urlParams = new URLSearchParams(queryString);
if (key === KEY_LOCATION) {
let arrayOfLocations = urlParams.getAll(key);
arrayOfLocations = arrayOfLocations.filter((item) => item !== value);
urlParams.delete(key);
arrayOfLocations.forEach((item) => {
urlParams.append(key, item);
});
} else if (key === KEY_SORTBY) {
urlParams.delete(KEY_SORT_DATE);
urlParams.delete(KEY_SORT_POPULAR);
} else {
urlParams.delete(key);
}
setQueryString(urlParams.toString());
return urlParams.toString();
};
const getInitialQueryString = () => {
let urlParams = new URLSearchParams(queryString);
urlParams = new URLSearchParams(appendToQueryString(KEY_SIZE, 10));
urlParams = new URLSearchParams(appendToQueryString(KEY_PAGE, 1));
return urlParams;
};

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

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

+ 11
- 11
src/hooks/useSearch.js View File

import { useQueryString } from "./useQueryString";
// import useQueryString from "./useOffers/useQueryString";


export const useSearch = () => { export const useSearch = () => {
const queryStringHook = useQueryString();
const searchOffers = (searchString) => {
if (searchString?.length !== 0) {
queryStringHook.appendToQueryString("oname", searchString);
} else {
const newQueryString = new URLSearchParams(queryStringHook.queryString);
if (newQueryString.has("oname")) {
queryStringHook.deleteFromQueryString("oname");
}
}
// const queryStringHook = useQueryString();
const searchOffers = () => {
// if (searchString?.length !== 0) {
// queryStringHook.appendToQueryString("oname", searchString);
// } else {
// const newQueryString = new URLSearchParams(queryStringHook.queryString);
// if (newQueryString.has("oname")) {
// queryStringHook.deleteFromQueryString("oname");
// }
// }
}; };


return { return {

+ 36
- 0
src/hooks/useSkeleton.js View File

import { useCallback, useEffect, useMemo, useState } from "react";

const useSkeleton = (skeletonOptions) => {
const [transitionStage, setTransitionStage] = useState(1);

// After how long skeleton changes screen
const timeoutInterval = useMemo(() => {
return skeletonOptions?.timeoutInterval || 900;
});

// isLoadingIndicator is boolean that indicates should skeleton screen be shown
const isLoadingIndicator = useMemo(() => {
return skeletonOptions?.isLoadingIndicator;
}, [skeletonOptions]);

// Timeout function to change transition stage
const timeout = useCallback(() => {
setTransitionStage((prevTransitionStage) => {
if (prevTransitionStage === 2) return 1;
return prevTransitionStage + 1;
});
}, [transitionStage]);

useEffect(() => {
let newTimeout;
if (isLoadingIndicator) {
newTimeout = setTimeout(timeout, timeoutInterval);
}
return () => clearTimeout(newTimeout);
}, [timeout, isLoadingIndicator]);

return {
transitionStage,
};
};
export default useSkeleton;

+ 0
- 86
src/hooks/useSorting.js View File

import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { sortEnum } from "../enums/sortEnum";
import { setFilteredSortOption } from "../store/actions/filters/filtersActions";
import { selectSelectedSortOption } from "../store/selectors/filtersSelectors";
import { useQueryString } from "./useQueryString";
import { convertQueryStringFrontend } from "../util/helpers/queryHelpers";
import {
KEY_PAGE,
KEY_SORTBY,
KEY_SORT_DATE,
KEY_SORT_POPULAR,
VALUE_SORTBY_NEW,
VALUE_SORTBY_OLD,
VALUE_SORTBY_POPULAR,
} from "../constants/queryStringConstants";

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

// Setting sort option on initially load or refresh page
useEffect(() => {
if (queryStringHook.loadedFromURL) {
const queryString = queryStringHook.queryString;
let queryObject = new URLSearchParams(
convertQueryStringFrontend(queryString)
);
if (queryObject.has(KEY_SORTBY)) {
if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_NEW) {
setSelectedSortOption(sortEnum.NEW);
}
if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_OLD) {
setSelectedSortOption(sortEnum.OLD);
}
if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_POPULAR) {
setSelectedSortOption(sortEnum.POPULAR);
}
} else {
setSelectedSortOption(sortOptions.INITIAL);
}
}
}, [queryStringHook.queryString, queryStringHook.loadedFromURL]);

const setSelectedSortOption = (payload, shouldGoFirstPage = false) => {
dispatch(setFilteredSortOption(payload));
let _des_date = null;
let _des_popular = null;
if (payload.value === sortOptions.NEW.value) {
_des_date = true;
}
if (payload.value === sortOptions.OLD.value) {
_des_date = false;
}
if (payload.value === sortOptions.POPULAR.value) {
_des_popular = true;
}
let queryArray = [];
if (_des_date !== null) {
queryArray.push({ key: KEY_SORT_DATE, value: `${_des_date}` });
queryArray.push({ key: KEY_SORT_POPULAR });
}
if (_des_popular !== null) {
queryArray.push({ key: KEY_SORT_POPULAR, value: `${_des_popular}` });
queryArray.push({ key: KEY_SORT_DATE });
}
if (shouldGoFirstPage) {
queryArray.push({ key: KEY_PAGE, value: "1" });
}
queryStringHook.appendMultipleToQueryString(queryArray);
};

const changeSorting = (payload) => {
setSelectedSortOption(payload, true);
};

return {
selectedSortOption,
setSelectedSortOption,
sortOptions,
changeSorting,
};
};
export default useSorting;

+ 19
- 18
src/pages/HomePage/HomePageMUI.js View File

import React, { useCallback, useEffect, useState } from "react";
import React, { useState } from "react";
import { HomePageContainer } from "./HomePage.styled"; import { HomePageContainer } from "./HomePage.styled";
import FilterCard from "../../components/Cards/FilterCard/FilterCard"; import FilterCard from "../../components/Cards/FilterCard/FilterCard";
import MainLayout from "../../layouts/MainLayout/MainLayout"; import MainLayout from "../../layouts/MainLayout/MainLayout";
import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors"; import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { OFFERS_SCOPE } from "../../store/actions/offers/offersActionConstants"; import { OFFERS_SCOPE } from "../../store/actions/offers/offersActionConstants";
import useOffers from "../../hooks/useOffers/useOffers";
import useSkeleton from "../../hooks/useSkeleton";


const HomePage = () => { const HomePage = () => {
const [animationStage, setAnimationStage] = useState(1);
const isLoadingOffers = useSelector( const isLoadingOffers = useSelector(
selectIsLoadingByActionType(OFFERS_SCOPE) selectIsLoadingByActionType(OFFERS_SCOPE)
); );
const [filtersOpened, setFiltersOpened] = useState(false);
const offers = useOffers();
const { transitionStage } = useSkeleton({
timeoutInterval: 900,
isLoadingIndicator: isLoadingOffers,
});
const toggleFilters = () => {
setFiltersOpened((prevFiltersOpened) => !prevFiltersOpened);
};


const timeout = useCallback(() => {
setAnimationStage((prevAnimationStage) => {
if (prevAnimationStage === 2) return 1;
return prevAnimationStage + 1;
});
}, [animationStage]);

useEffect(() => {
let newTimeout;
if (isLoadingOffers) {
newTimeout = setTimeout(timeout, 900);
}
return () => clearTimeout(newTimeout);
}, [timeout, isLoadingOffers]);
return ( return (
<HomePageContainer> <HomePageContainer>
<MainLayout <MainLayout
leftCard={ leftCard={
<FilterCard <FilterCard
offers={offers}
filtersOpened={filtersOpened}
skeleton={isLoadingOffers} skeleton={isLoadingOffers}
animationStage={animationStage}
animationStage={transitionStage}
toggleFilters={toggleFilters}
/> />
} }
content={ content={
<MarketPlace <MarketPlace
offers={offers}
skeleton={isLoadingOffers} skeleton={isLoadingOffers}
animationStage={animationStage}
animationStage={transitionStage}
toggleFilters={toggleFilters}
/> />
} }
/> />

+ 38
- 3
src/pages/MyOffers/MyOffers.js View File

import React from "react";
import React, { useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { MyOffersContainer } from "./MyOffers.styled"; import { MyOffersContainer } from "./MyOffers.styled";
import MainLayout from "../../layouts/MainLayout/MainLayout"; import MainLayout from "../../layouts/MainLayout/MainLayout";
import FilterCard from "../../components/Cards/FilterCard/FilterCard"; import FilterCard from "../../components/Cards/FilterCard/FilterCard";
import MarketPlace from "../../components/MarketPlace/MarketPlace"; import MarketPlace from "../../components/MarketPlace/MarketPlace";
import useMyOffers from "../../hooks/useOffers/useMyOffers";
import useSkeleton from "../../hooks/useSkeleton";
import { useSelector } from "react-redux";
import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors";
import { OFFERS_MINE_SCOPE } from "../../store/actions/offers/offersActionConstants";


const MyOffers = () => { const MyOffers = () => {
const offers = useMyOffers();
const isLoadingMineOffers = useSelector(
selectIsLoadingByActionType(OFFERS_MINE_SCOPE)
);
const [filtersOpened, setFiltersOpened] = useState(false);

const { transitionStage } = useSkeleton({
timeoutInterval: 900,
isLoadingIndicator: isLoadingMineOffers,
});
const toggleFilters = () => {
setFiltersOpened((prevFiltersOpened) => !prevFiltersOpened);
};
return ( return (
<MyOffersContainer> <MyOffersContainer>
<MainLayout <MainLayout
leftCard={<FilterCard myOffers />}
content={<MarketPlace myOffers={true} />}
leftCard={
<FilterCard
myOffers
offers={offers}
animationStage={transitionStage}
filtersOpened={filtersOpened}
toggleFilters={toggleFilters}
skeleton={isLoadingMineOffers}
/>
}
content={
<MarketPlace
myOffers={true}
offers={offers}
animationStage={transitionStage}
skeleton={isLoadingMineOffers}
toggleFilters={toggleFilters}
/>
}
/> />
</MyOffersContainer> </MyOffersContainer>
); );

+ 1
- 1
src/request/index.js View File

const request = axios.create({ const request = axios.create({
// baseURL: "http://192.168.88.150:3001/", // baseURL: "http://192.168.88.150:3001/",
// baseURL: "http://192.168.88.175:3005/", // baseURL: "http://192.168.88.175:3005/",
baseURL: "https://trampa-api.dilig.net/",
baseURL: "https://trampa-api-test.dilig.net/",


headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

+ 2
- 0
src/store/actions/filters/filtersActionConstants.js View File

export const SET_SORT_OPTION = createSetType("FILTERS_SET_SORT_OPTION"); export const SET_SORT_OPTION = createSetType("FILTERS_SET_SORT_OPTION");
export const SET_IS_APPLIED = createSetType("FILTERS_SET_IS_APPLIED"); export const SET_IS_APPLIED = createSetType("FILTERS_SET_IS_APPLIED");
export const SET_QUERY_STRING = createSetType("FILTERS_SET_QUERY_STRING"); export const SET_QUERY_STRING = createSetType("FILTERS_SET_QUERY_STRING");
export const SET_HEADER_STRING = createSetType("FILTERS_SET_HEADER_STRING");
export const SET_SEARCH_STRING = createSetType("FILTERS_SET_SEARCH");

+ 9
- 1
src/store/actions/filters/filtersActions.js View File

import { CLEAR_FILTERS, SET_CATEGORY, SET_FILTERS, SET_IS_APPLIED, SET_LOCATIONS, SET_QUERY_STRING, SET_SORT_OPTION, SET_SUBCATEGORY } from "./filtersActionConstants";
import { CLEAR_FILTERS, SET_CATEGORY, SET_FILTERS, SET_HEADER_STRING, SET_IS_APPLIED, SET_LOCATIONS, SET_QUERY_STRING, SET_SEARCH_STRING, SET_SORT_OPTION, SET_SUBCATEGORY } from "./filtersActionConstants";


export const setFilters = (payload) => ({ export const setFilters = (payload) => ({
type: SET_FILTERS, type: SET_FILTERS,
export const setQueryString = (payload) => ({ export const setQueryString = (payload) => ({
type: SET_QUERY_STRING, type: SET_QUERY_STRING,
payload, payload,
})
export const setHeaderString = (payload) => ({
type: SET_HEADER_STRING,
payload
})
export const setSearchString = (payload) => ({
type: SET_SEARCH_STRING,
payload
}) })

+ 18
- 13
src/store/index.js View File

import { applyMiddleware, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './saga';
import loadingMiddleware from './middleware/loadingMiddleware';
import requestStatusMiddleware from './middleware/requestStatusMiddleware';
import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware';
import persistStore from 'redux-persist/es/persistStore';
import accessTokensMiddleware from './middleware/accessTokensMiddleware';
import { applyMiddleware, compose, createStore } from "redux";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./reducers";
import rootSaga from "./saga";
import loadingMiddleware from "./middleware/loadingMiddleware";
import requestStatusMiddleware from "./middleware/requestStatusMiddleware";
import internalServerErrorMiddleware from "./middleware/internalServerErrorMiddleware";
import persistStore from "redux-persist/es/persistStore";
import accessTokensMiddleware from "./middleware/accessTokensMiddleware";



const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const composeEnhancers =
(window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
trace: true,
traceLimit: 25,
})) ||
compose;
const sagaMiddleware = createSagaMiddleware(); const sagaMiddleware = createSagaMiddleware();
export const store = createStore( export const store = createStore(
rootReducer, rootReducer,
requestStatusMiddleware, requestStatusMiddleware,
internalServerErrorMiddleware, internalServerErrorMiddleware,
accessTokensMiddleware accessTokensMiddleware
),
),
)
)
); );
export const persistor = persistStore(store); export const persistor = persistStore(store);



+ 24
- 0
src/store/reducers/filters/filtersReducer.js View File

CLEAR_FILTERS, CLEAR_FILTERS,
SET_CATEGORY, SET_CATEGORY,
SET_FILTERS, SET_FILTERS,
SET_HEADER_STRING,
SET_IS_APPLIED, SET_IS_APPLIED,
SET_LOCATIONS, SET_LOCATIONS,
SET_SEARCH_STRING,
SET_SORT_OPTION, SET_SORT_OPTION,
SET_SUBCATEGORY, SET_SUBCATEGORY,
} from "../../actions/filters/filtersActionConstants"; } from "../../actions/filters/filtersActionConstants";
sortOption: null, sortOption: null,
isApplied: false, isApplied: false,
queryString: "", queryString: "",
headerString: "",
searchString: ""
}, },
}; };


[SET_LOCATIONS]: setFilteredLocations, [SET_LOCATIONS]: setFilteredLocations,
[SET_SORT_OPTION]: setFilteredSortOption, [SET_SORT_OPTION]: setFilteredSortOption,
[SET_IS_APPLIED]: setIsAppliedStatus, [SET_IS_APPLIED]: setIsAppliedStatus,
[SET_HEADER_STRING]: setHeaderString,
[SET_SEARCH_STRING]: setSearchString,
}, },
initialState initialState
); );
} }
} }
} }
function setHeaderString(state, {payload}) {
return {
...state,
filters: {
...state.filters,
headerString: payload
}
}
}
function setSearchString(state, {payload}) {
return {
...state,
filters: {
...state.filters,
searchString: payload
}
}
}

+ 1
- 1
src/store/reducers/queryString/queryStringReducer.js View File

import createReducer from "../../utils/createReducer"; import createReducer from "../../utils/createReducer";


const initialState = { const initialState = {
queryString: "",
queryString: ""
}; };


export default createReducer( export default createReducer(

+ 2
- 2
src/store/saga/offersSaga.js View File

attemptFetchProfileOffers, attemptFetchProfileOffers,
attemptFetchMoreOffers, attemptFetchMoreOffers,
} from "../../request/offersRequest"; } from "../../request/offersRequest";
import { convertQueryStringBackend } from "../../util/helpers/queryHelpers";
import { convertQueryStringForBackend } from "../../util/helpers/queryHelpers";
// import { setQueryString } from "../actions/filters/filtersActions"; // import { setQueryString } from "../actions/filters/filtersActions";
import { import {
OFFERS_FETCH_MORE, OFFERS_FETCH_MORE,
try { try {
yield put(clearOffers()); yield put(clearOffers());
const newQueryString = new URLSearchParams( const newQueryString = new URLSearchParams(
convertQueryStringBackend(payload.payload.queryString)
convertQueryStringForBackend(payload.payload.queryString)
); );
const data = yield call( const data = yield call(
attemptFetchOffers, attemptFetchOffers,

+ 2
- 2
src/store/saga/queryStringSaga.js View File

import { all, takeLatest, put } from "@redux-saga/core/effects"; import { all, takeLatest, put } from "@redux-saga/core/effects";
import { convertQueryStringBackend } from "../../util/helpers/queryHelpers";
import { convertQueryStringForBackend } from "../../util/helpers/queryHelpers";
// import { combineQueryStrings } from "../../util/helpers/queryHelpers"; // import { combineQueryStrings } from "../../util/helpers/queryHelpers";
import { QUERY_STRING_SET } from "../actions/queryString/queryStringActionConstants"; import { QUERY_STRING_SET } from "../actions/queryString/queryStringActionConstants";
import { setQueryStringRedux } from "../actions/queryString/queryStringActions"; import { setQueryStringRedux } from "../actions/queryString/queryStringActions";
// payload.payload, // payload.payload,
// ); // );
// } // }
const newQueryString = convertQueryStringBackend(payload.payload);
const newQueryString = convertQueryStringForBackend(payload.payload);
yield put(setQueryStringRedux(newQueryString)); yield put(setQueryStringRedux(newQueryString));
} catch (e) { } catch (e) {
console.log(e); console.log(e);

+ 8
- 0
src/store/selectors/filtersSelectors.js View File

filtersSelector, filtersSelector,
(state) => state.filters.isApplied (state) => state.filters.isApplied
) )
export const selectHeaderString = createSelector(
filtersSelector,
(state) => state.filters.headerString
)
export const selectSearchString = createSelector(
filtersSelector,
(state) => state.filters.searchString,
)

+ 181
- 52
src/util/helpers/queryHelpers.js View File

import { ALL_CATEGORIES, COMMA, SPREAD } from "../../constants/marketplaceHeaderTitle";
import {
initialSize,
KEY_CATEGORY,
KEY_LOCATION,
KEY_NAME,
KEY_PAGE,
KEY_SEARCH,
KEY_SIZE,
KEY_SORTBY,
KEY_SORT_DATE,
KEY_SORT_POPULAR,
KEY_SUBCATEGORY,
VALUE_SORTBY_NEW,
VALUE_SORTBY_OLD,
VALUE_SORTBY_POPULAR,
} from "../../constants/queryStringConstants";
import { sortEnum } from "../../enums/sortEnum"; import { sortEnum } from "../../enums/sortEnum";
// import qs from "query-string"; // import qs from "query-string";


export const convertQueryStringFrontend = (queryURL) => {
export const convertQueryStringForFrontend = (queryURL) => {
const queryObject = new URLSearchParams(queryURL); const queryObject = new URLSearchParams(queryURL);
const queryObjectToReturn = 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);
if (queryObject.has(KEY_SORT_DATE)) {
queryObjectToReturn.delete(KEY_SORT_DATE);
if (queryObject.get(KEY_SORT_DATE) === "true") {
queryObjectToReturn.append(KEY_SORTBY, sortEnum.NEW.queryString);
} else { } else {
queryObjectToReturn.append("sortBy", sortEnum.OLD.queryString);
queryObjectToReturn.append(KEY_SORTBY, sortEnum.OLD.queryString);
} }
} }
if (queryObject.has("oname")) {
queryObjectToReturn.delete("oname");
queryObjectToReturn.append("search", queryObject.get("oname"));
if (queryObject.has(KEY_NAME)) {
queryObjectToReturn.delete(KEY_NAME);
queryObjectToReturn.append(KEY_SEARCH, queryObject.get(KEY_NAME));
} }
if (queryObject.has("_des_popular")) {
queryObjectToReturn.delete("_des_popular");
queryObjectToReturn.append("sortBy", sortEnum.POPULAR.queryString);
if (queryObject.has(KEY_SORT_POPULAR)) {
queryObjectToReturn.delete(KEY_SORT_POPULAR);
queryObjectToReturn.append(KEY_SORTBY, sortEnum.POPULAR.queryString);
} }
if (queryObject.has("size")) {
queryObjectToReturn.delete("size");
if (queryObject.has(KEY_SIZE)) {
queryObjectToReturn.delete(KEY_SIZE);
} }
if (queryObject.has("page")) {
if (queryObject.get("page") === "1") {
queryObjectToReturn.delete("page");
if (queryObject.has(KEY_PAGE)) {
if (queryObject.get(KEY_PAGE) === "1") {
queryObjectToReturn.delete(KEY_PAGE);
queryObjectToReturn.delete(KEY_SIZE);
return queryObjectToReturn.toString(); return queryObjectToReturn.toString();
} else { } else {
queryObjectToReturn.delete("page");
queryObjectToReturn.delete(KEY_PAGE);
return ( return (
queryObjectToReturn.toString() + "&page=" + queryObject.get("page")
queryObjectToReturn.toString() + "&page=" + queryObject.get(KEY_PAGE)
); );
} }
} }
return thirdQueryObject.toString(); return thirdQueryObject.toString();
}; };


export const convertQueryStringBackend = (queryURL) => {
export const convertQueryStringForBackend = (queryURL) => {
const queryObject = new URLSearchParams(queryURL); const queryObject = new URLSearchParams(queryURL);
const newQueryObject = new URLSearchParams(); const newQueryObject = new URLSearchParams();
if (queryObject.has("category")) {
if (queryObject.has(KEY_CATEGORY)) {
newQueryObject.append( newQueryObject.append(
"category",
queryObject.getAll("category")[queryObject.getAll("category").length - 1]
KEY_CATEGORY,
queryObject.getAll(KEY_CATEGORY)[
queryObject.getAll(KEY_CATEGORY).length - 1
]
); );
} }
if (queryObject.has("subcategory")) {
if (queryObject.has(KEY_SUBCATEGORY)) {
newQueryObject.append( newQueryObject.append(
"subcategory",
queryObject.getAll("subcategory")[
queryObject.getAll("subcategory").length - 1
KEY_SUBCATEGORY,
queryObject.getAll(KEY_SUBCATEGORY)[
queryObject.getAll(KEY_SUBCATEGORY).length - 1
] ]
); );
} }
if (queryObject.has("search")) {
if (queryObject.has(KEY_SEARCH) && queryObject.get(KEY_SEARCH)?.length > 0) {
newQueryObject.append( newQueryObject.append(
"oname",
queryObject.getAll("search")[queryObject.getAll("search").length - 1]
KEY_NAME,
queryObject.getAll(KEY_SEARCH)[queryObject.getAll(KEY_SEARCH).length - 1]
); );
} }
if (queryObject.has("oname")) {
if (queryObject.has(KEY_NAME)) {
newQueryObject.append( newQueryObject.append(
"oname",
queryObject.getAll("oname")[queryObject.getAll("oname").length - 1]
KEY_NAME,
queryObject.getAll(KEY_NAME)[queryObject.getAll(KEY_NAME).length - 1]
); );
} }
if (queryObject.has("location")) {
const arrayOfLocations = queryObject.getAll("location");
if (queryObject.has(KEY_LOCATION)) {
const arrayOfLocations = queryObject.getAll(KEY_LOCATION);
arrayOfLocations.forEach((item) => { arrayOfLocations.forEach((item) => {
newQueryObject.append("location", item);
newQueryObject.append(KEY_LOCATION, item);
}); });
} }
if (queryObject.has("sortBy")) {
newQueryObject.delete("sortBy");
if (queryObject.get("sortBy") === "newest") {
newQueryObject.append("_des_date", "true");
if (queryObject.has(KEY_SORTBY)) {
newQueryObject.delete(KEY_SORTBY);
if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_NEW) {
newQueryObject.append(KEY_SORT_DATE, "true");
} }
if (queryObject.get("sortBy") === "oldest") {
newQueryObject.append("_des_date", "false");
if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_OLD) {
newQueryObject.append(KEY_SORT_DATE, "false");
} }
if (queryObject.get("sortBy") === "popular") {
newQueryObject.append("_des_popular", "true");
if (queryObject.get(KEY_SORTBY) === VALUE_SORTBY_POPULAR) {
newQueryObject.append(KEY_SORT_POPULAR, "true");
} }
} }
if (queryObject.has("_des_date")) {
newQueryObject.append("_des_date", queryObject.get("_des_date"));
if (queryObject.has(KEY_SORT_DATE)) {
newQueryObject.append(KEY_SORT_DATE, queryObject.get(KEY_SORT_DATE));
} }
if (queryObject.has("_des_popular")) {
newQueryObject.append("_des_popular", queryObject.get("_des_popular"));
if (queryObject.has(KEY_SORT_POPULAR)) {
newQueryObject.append(KEY_SORT_POPULAR, queryObject.get(KEY_SORT_POPULAR));
} }
newQueryObject.append("size", "10");
if (!queryObject.has("page")) {
newQueryObject.append("page", "1");
newQueryObject.append(KEY_SIZE, initialSize);
if (!queryObject.has(KEY_PAGE)) {
newQueryObject.append(KEY_PAGE, "1");
} else { } else {
newQueryObject.append("page", queryObject.get("page"));
newQueryObject.append(KEY_PAGE, queryObject.get(KEY_PAGE));
} }
return newQueryObject.toString(); return newQueryObject.toString();
}; };

export const getQueryObjectHelper = (queryString) => {
let newObject = {};
const queryObject = new URLSearchParams(queryString);
if (queryObject.has(KEY_CATEGORY)) {
newObject[KEY_CATEGORY] = queryObject.get(KEY_CATEGORY);
}
if (queryObject.has(KEY_SUBCATEGORY)) {
newObject[KEY_SUBCATEGORY] = queryObject.get(KEY_SUBCATEGORY);

}
if (queryObject.has(KEY_SEARCH)) {
newObject[KEY_SEARCH] = queryObject.get(KEY_SEARCH);
}
if (queryObject.has(KEY_NAME)) {
newObject[KEY_NAME] = queryObject.get(KEY_NAME);

}
if (queryObject.has(KEY_LOCATION)) {
const arrayOfLocations = queryObject.getAll(KEY_LOCATION);
newObject[KEY_LOCATION] = [];
arrayOfLocations.forEach((item) => {
newObject[KEY_LOCATION].push(item);
});
}
if (queryObject.has(KEY_SORTBY)) {
newObject[KEY_SORTBY] = queryObject.get(KEY_SORTBY);
}
if (queryObject.has(KEY_SORT_DATE)) {
newObject[KEY_SORT_DATE] = queryObject.get(KEY_SORT_DATE);
}
if (queryObject.has(KEY_SORT_POPULAR)) {
newObject[KEY_SORT_POPULAR] = queryObject.get(KEY_SORT_POPULAR);
}
if (queryObject.has(KEY_PAGE)) {
newObject[KEY_PAGE] = queryObject.get(KEY_PAGE);
}
if (queryObject.has(KEY_SIZE)) {
newObject[KEY_SIZE] = queryObject.get(KEY_SIZE);
}
return newObject;
};

export const makeHeaderStringHelper = (filters) => {
let headerStringLocal = ALL_CATEGORIES;
// Adding category to header string
if (filters.category.selectedCategory?.name) {
headerStringLocal = filters.category.selectedCategory?.name;
// Adding subcategories to header string
if (filters.subcategory.selectedSubcategory?.name) {
headerStringLocal += `${SPREAD}${filters.subcategory.selectedSubcategory.name}`;
}
}
// Adding locations to header string
if (
filters.locations.selectedLocations &&
filters.locations.selectedLocations?.length > 0
) {
headerStringLocal += SPREAD;

filters.locations.selectedLocations.forEach((location, index) => {
// Checking if item is last
if (index + 1 === filters.locations.selectedLocations.length) {
headerStringLocal += location.city;
} else {
headerStringLocal += location.city + COMMA;
}
});
}
return headerStringLocal;
}
export const makeQueryStringHelper = (filters, paging, search, sorting) => {
const newQueryString = new URLSearchParams();
if (filters.category.selectedCategoryLocally?.name) {
newQueryString.append(
KEY_CATEGORY,
filters.category.selectedCategoryLocally.name
);
}
if (filters.subcategory.selectedSubcategoryLocally?.name) {
newQueryString.append(
KEY_SUBCATEGORY,
filters.subcategory.selectedSubcategoryLocally.name
);
}
if (filters.locations.selectedLocationsLocally?.length > 0) {
filters.locations.selectedLocationsLocally.forEach((location) =>
newQueryString.append(KEY_LOCATION, location?.city)
);
}
if (sorting.selectedSortOption?.value) {
if (sorting.selectedSortOption?.value === sortEnum.NEW.value) {
newQueryString.append(KEY_SORTBY, VALUE_SORTBY_NEW);
}
if (sorting.selectedSortOption?.value === sortEnum.OLD.value) {
newQueryString.append(KEY_SORTBY, VALUE_SORTBY_OLD);
}
if (sorting.selectedSortOption?.value === sortEnum.POPULAR.value) {
newQueryString.append(KEY_SORTBY, VALUE_SORTBY_POPULAR);
}
}
if (paging.currentPage !== 1) {
newQueryString.append(KEY_PAGE, paging.currentPage);
}
newQueryString.append(KEY_SEARCH, search.searchString ?? "");
return newQueryString;
}



Loading…
Cancel
Save