| "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", |
| "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", |
| <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> | ||||
| ); | ); |
| -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; | |||||
| } | } | ||||
| * { | * { |
| 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} | ||||
| /> | /> | ||||
| ); | ); |
| 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} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; |
| 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} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; |
| 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 = { |
| @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; |
| 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 ( |
| 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={{ |
| 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 = { |
| 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> |
| 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}> |
| 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} /> | ||||
| )} | )} |
| 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)``; |
| 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; |
| 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, |
| 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; |
| 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; |
| 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; | |||||
| } | |||||
| ` |
| 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 = { |
| 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; | : 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(() => {}); | ||||
| 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"; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| /* 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, | |||||
| }; | |||||
| }; |
| 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 { |
| 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; |
| 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; |
| 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} | |||||
| /> | /> | ||||
| } | } | ||||
| /> | /> |
| 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> | ||||
| ); | ); |
| 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", |
| 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"); |
| 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 | |||||
| }) | }) |
| 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); | ||||
| 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 | |||||
| } | |||||
| } | |||||
| } |
| import createReducer from "../../utils/createReducer"; | import createReducer from "../../utils/createReducer"; | ||||
| const initialState = { | const initialState = { | ||||
| queryString: "", | |||||
| queryString: "" | |||||
| }; | }; | ||||
| export default createReducer( | export default createReducer( |
| 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, |
| 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); |
| 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, | |||||
| ) |
| 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; | |||||
| } | |||||