| @@ -120,6 +120,7 @@ export const OfferTitle = styled(Typography)` | |||
| line-height: 22px; | |||
| margin-top: 5px; | |||
| font-size: 18px; | |||
| max-width: none; | |||
| margin-bottom: 0px !important; | |||
| `} | |||
| @@ -24,7 +24,7 @@ const Paging = (props) => { | |||
| const threeDotsBefore = props.current - 2 > 1; | |||
| const threeDotsAfter = props.current + 2 < pages; | |||
| return ( | |||
| <PagingContainer> | |||
| <PagingContainer className={props.className}> | |||
| {/* Left arrow */} | |||
| <Arrow | |||
| onClick={() => props.changePage(props.current - 1)} | |||
| @@ -88,6 +88,7 @@ Paging.propTypes = { | |||
| current: PropTypes.number, | |||
| changePage: PropTypes.func, | |||
| customPaging: PropTypes.bool, | |||
| className: PropTypes.string, | |||
| }; | |||
| Paging.defaultProps = { | |||
| elementsPerPage: 10, | |||
| @@ -7,7 +7,6 @@ import { useDispatch, useSelector } from "react-redux"; | |||
| import { selectUserId } from "../../store/selectors/loginSelectors"; | |||
| import { useRouteMatch } from "react-router-dom"; | |||
| import { fetchProfile } from "../../store/actions/profile/profileActions"; | |||
| import { fetchProfileOffers } from "../../store/actions/offers/offersActions"; | |||
| import Header from "./Header/Header"; | |||
| const Profile = (props) => { | |||
| @@ -18,7 +17,6 @@ const Profile = (props) => { | |||
| useEffect(() => { | |||
| if (idProfile?.length > 0) { | |||
| dispatch(fetchProfile(idProfile)); | |||
| dispatch(fetchProfileOffers(idProfile)); | |||
| } | |||
| }, [idProfile]); | |||
| const isMyProfile = useMemo(() => { | |||
| @@ -28,7 +26,11 @@ const Profile = (props) => { | |||
| <ProfileContainer className={props.className}> | |||
| <Header isAdmin={props.isAdmin} /> | |||
| <ProfileCard isAdmin={props.isAdmin} isMyProfile={isMyProfile} /> | |||
| <ProfileOffers isAdmin={props.isAdmin} isMyProfile={isMyProfile} /> | |||
| <ProfileOffers | |||
| isAdmin={props.isAdmin} | |||
| isMyProfile={isMyProfile} | |||
| idProfile={idProfile} | |||
| /> | |||
| </ProfileContainer> | |||
| ); | |||
| }; | |||
| @@ -4,13 +4,17 @@ import { | |||
| OffersContainer, | |||
| OffersScroller, | |||
| ProfileOffersContainer, | |||
| ProfilePaging, | |||
| SkeletonContainer, | |||
| } from "./ProfileOffers.styled"; | |||
| import { useState } from "react"; | |||
| import { useEffect } from "react"; | |||
| import { useSelector } from "react-redux"; | |||
| // import { useEffect } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import OfferCard from "../../Cards/OfferCard/OfferCard"; | |||
| import { selectProfileOffers } from "../../../store/selectors/offersSelectors"; | |||
| import { | |||
| selectProfileOffers, | |||
| selectTotalOffers, | |||
| } from "../../../store/selectors/offersSelectors"; | |||
| import { selectLatestChats } from "../../../store/selectors/chatSelectors"; | |||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | |||
| import NoProfileOffers from "./NoProfileOffers.js/NoProfileOffers"; | |||
| @@ -23,9 +27,16 @@ import SelectSortField from "./SelectSortField/SelectSortField"; | |||
| import HeaderTitle from "./HeaderTitle/HeaderTitle"; | |||
| import SearchBar from "./SearchBar/SearchBar"; | |||
| import { messageUserHelper } from "../../../util/helpers/messageHelper"; | |||
| import { sortEnum } from "../../../enums/sortEnum"; | |||
| import usePaging from "../../../hooks/useOffers/usePaging"; | |||
| import { useEffect } from "react"; | |||
| import { | |||
| fetchProfileOffers, | |||
| setProfileOffers, | |||
| } from "../../../store/actions/offers/offersActions"; | |||
| import { useRef } from "react"; | |||
| const ProfileOffers = (props) => { | |||
| const [offersToShow, setOffersToShow] = useState([]); | |||
| const isLoadingMineOffers = useSelector( | |||
| selectIsLoadingByActionType(OFFERS_PROFILE_SCOPE) | |||
| ); | |||
| @@ -34,25 +45,64 @@ const ProfileOffers = (props) => { | |||
| const { isMobile } = useIsMobile(); | |||
| const userId = useSelector(selectUserId); | |||
| const arrayForMapping = Array.apply(null, Array(4)).map(() => {}); | |||
| const [searchValue, setSearchValue] = useState(""); | |||
| const [sortOption, setSortOption] = useState(sortEnum.INITIAL); | |||
| const [append, setAppend] = useState(true); | |||
| const paging = usePaging(null, true); | |||
| const total = useSelector(selectTotalOffers); | |||
| const dispatch = useDispatch(); | |||
| const searchRef = useRef(null); | |||
| useEffect(() => { | |||
| dispatch( | |||
| fetchProfileOffers({ | |||
| idProfile: props.idProfile, | |||
| searchValue: searchValue, | |||
| sortOption: sortOption, | |||
| append: isMobile && append, | |||
| page: paging.currentPage, | |||
| }) | |||
| ); | |||
| setAppend(true); | |||
| }, [searchValue, sortOption, paging.currentPage]); | |||
| useEffect(() => { | |||
| if ( | |||
| isLoadingMineOffers === false && | |||
| searchRef.current && | |||
| searchRef.current?.searchValue !== searchValue | |||
| ) { | |||
| searchRef.current.changeSearchValue(searchValue); | |||
| } | |||
| }, [isLoadingMineOffers]); | |||
| const messageUser = (offer) => { | |||
| messageUserHelper(chats, offer, userId); | |||
| }; | |||
| useEffect(() => { | |||
| if (profileOffers) setOffersToShow(profileOffers); | |||
| }, [profileOffers]); | |||
| const handleInfiniteScroll = () => { | |||
| if (total !== profileOffers?.length) { | |||
| paging.goToNextPage(); | |||
| } | |||
| }; | |||
| const handleSearch = (value) => { | |||
| let newOffersToShow = profileOffers.filter((item) => | |||
| item.name.toLowerCase().includes(value.toLowerCase()) | |||
| ); | |||
| setOffersToShow([...newOffersToShow]); | |||
| const handleChangeSortOption = (newValue) => { | |||
| dispatch(setProfileOffers([])); | |||
| setAppend(true); | |||
| paging.changePage(1); | |||
| setSortOption(newValue); | |||
| }; | |||
| const handleSearch = (newValue) => { | |||
| dispatch(setProfileOffers([])); | |||
| paging.changePage(1); | |||
| setSearchValue(newValue); | |||
| }; | |||
| return ( | |||
| <ProfileOffersContainer isAdmin={props.isAdmin}> | |||
| {isLoadingMineOffers || isLoadingMineOffers === undefined ? ( | |||
| {(isLoadingMineOffers || isLoadingMineOffers === undefined) && | |||
| profileOffers?.length === 0 ? ( | |||
| <> | |||
| <ProfileOffersHeaderSkeleton /> | |||
| {isMobile ? ( | |||
| @@ -72,46 +122,63 @@ const ProfileOffers = (props) => { | |||
| ) : ( | |||
| <OffersContainer> | |||
| <SelectSortField | |||
| offersToShow={offersToShow} | |||
| setOffersToShow={setOffersToShow} | |||
| offersToShow={profileOffers} | |||
| setSortOption={handleChangeSortOption} | |||
| /> | |||
| <HeaderTitle isMyProfile={props.isMyProfile && !props.isAdmin} /> | |||
| <SearchBar handleSearch={handleSearch} /> | |||
| {!isMobile ? ( | |||
| offersToShow.length !== 0 ? ( | |||
| offersToShow.map((item) => ( | |||
| <OfferCard | |||
| isAdmin={props.isAdmin} | |||
| isMyOffer={props.isMyProfile || props.isAdmin} | |||
| offer={item} | |||
| key={JSON.stringify(item)} | |||
| messageUser={messageUser} | |||
| /> | |||
| )) | |||
| ) : ( | |||
| <NoProfileOffers /> | |||
| ) | |||
| ) : ( | |||
| <OffersScroller hideArrows noOffers> | |||
| {offersToShow.length !== 0 ? ( | |||
| offersToShow.map((item) => ( | |||
| <SearchBar | |||
| handleSearch={handleSearch} | |||
| value={searchValue} | |||
| ref={searchRef} | |||
| /> | |||
| {!isMobile && | |||
| (profileOffers.length !== 0 ? ( | |||
| <> | |||
| {profileOffers.map((item) => ( | |||
| <OfferCard | |||
| isAdmin={props.isAdmin} | |||
| vertical | |||
| isMyOffer={props.isMyProfile || props.isAdmin} | |||
| offer={item} | |||
| key={JSON.stringify(item)} | |||
| pinned={item.pinned} | |||
| messageUser={messageUser} | |||
| /> | |||
| )) | |||
| ) : ( | |||
| <NoProfileOffers /> | |||
| )} | |||
| </OffersScroller> | |||
| )} | |||
| ))} | |||
| </> | |||
| ) : ( | |||
| <NoProfileOffers /> | |||
| ))} | |||
| </OffersContainer> | |||
| )} | |||
| {isMobile ? ( | |||
| <OffersScroller | |||
| hideArrows | |||
| noOffers | |||
| infiniteScroll={handleInfiniteScroll} | |||
| > | |||
| {profileOffers.length !== 0 ? ( | |||
| profileOffers.map((item) => ( | |||
| <OfferCard | |||
| isAdmin={props.isAdmin} | |||
| vertical | |||
| isMyOffer={props.isMyProfile || props.isAdmin} | |||
| offer={item} | |||
| key={JSON.stringify(item)} | |||
| pinned={item.pinned} | |||
| messageUser={messageUser} | |||
| /> | |||
| )) | |||
| ) : ( | |||
| <NoProfileOffers /> | |||
| )} | |||
| </OffersScroller> | |||
| ) : ( | |||
| <ProfilePaging | |||
| current={paging.currentPage} | |||
| changePage={paging.changePage} | |||
| totalElements={total} | |||
| /> | |||
| )} | |||
| </ProfileOffersContainer> | |||
| ); | |||
| }; | |||
| @@ -120,6 +187,7 @@ ProfileOffers.propTypes = { | |||
| children: PropTypes.node, | |||
| isMyProfile: PropTypes.bool, | |||
| isAdmin: PropTypes.bool, | |||
| idProfile: PropTypes.string, | |||
| }; | |||
| export default ProfileOffers; | |||
| @@ -1,5 +1,6 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import Paging from "../../Paging/Paging"; | |||
| import HorizontalScroller from "../../Scroller/HorizontalScroller"; | |||
| export const ProfileOffersContainer = styled(Box)` | |||
| @@ -35,3 +36,7 @@ export const SkeletonContainer = styled(Box)` | |||
| max-width: calc(100vw - 36px); | |||
| overflow: hidden; | |||
| `; | |||
| export const ProfilePaging = styled(Paging)` | |||
| position: static; | |||
| margin-bottom: 9px; | |||
| ` | |||
| @@ -4,10 +4,21 @@ import { IconContainer, SearchIcon, SearchInput } from "./SearchBar.styled"; | |||
| import { useCallback } from "react"; | |||
| import { useRef } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { forwardRef } from "react"; | |||
| import { useImperativeHandle } from "react"; | |||
| import { useState } from "react"; | |||
| const SearchBar = (props) => { | |||
| const SearchBar = forwardRef((props, ref) => { | |||
| const searchRef = useRef(null); | |||
| const [value, setSearchValue] = useState(""); | |||
| const { t } = useTranslation(); | |||
| useImperativeHandle(ref, () => ({ | |||
| changeSearchValue: (newValue) => { | |||
| setSearchValue(newValue); | |||
| }, | |||
| searchValue: value, | |||
| })); | |||
| let listener = useCallback( | |||
| (event) => { | |||
| if (event.keyCode === 13) { | |||
| @@ -29,6 +40,8 @@ const SearchBar = (props) => { | |||
| <SearchInput | |||
| fullWidth | |||
| ref={searchRef} | |||
| value={value} | |||
| onChange={(event) => setSearchValue(event.target.value)} | |||
| onFocus={handleFocusSearch} | |||
| onBlur={handleBlurSearch} | |||
| placeholder={t("header.searchOffers")} | |||
| @@ -44,10 +57,13 @@ const SearchBar = (props) => { | |||
| }} | |||
| /> | |||
| ); | |||
| }; | |||
| }); | |||
| SearchBar.displayName = "SearchBar"; | |||
| SearchBar.propTypes = { | |||
| handleSearch: PropTypes.func, | |||
| value: PropTypes.string, | |||
| }; | |||
| export default SearchBar; | |||
| @@ -13,6 +13,7 @@ export const SearchInput = styled(TextField)` | |||
| & div fieldset { | |||
| border-color: ${selectedTheme.colors.primaryPurple} !important; | |||
| } | |||
| margin-bottom: 24px; | |||
| @media (max-width: 600px) { | |||
| top: 5px; | |||
| height: 46px; | |||
| @@ -7,34 +7,17 @@ import { | |||
| } from "./SelectSortField.styled"; | |||
| import { sortEnum } from "../../../../enums/sortEnum"; | |||
| import { useState } from "react"; | |||
| import { useEffect } from "react"; | |||
| const SelectSortField = (props) => { | |||
| const [sortOption, setSortOption] = useState(sortEnum.INITIAL); | |||
| useEffect(() => { | |||
| let newOffersToShow = [...props.offersToShow]; | |||
| if (sortOption.value === sortEnum.OLD.value) { | |||
| newOffersToShow.sort( | |||
| (a, b) => new Date(a._created) - new Date(b._created) | |||
| ); | |||
| } | |||
| if (sortOption.value === sortEnum.NEW.value) { | |||
| newOffersToShow.sort( | |||
| (a, b) => new Date(b._created) - new Date(a._created) | |||
| ); | |||
| } | |||
| if (sortOption.value === sortEnum.POPULAR.value) { | |||
| newOffersToShow.sort((a, b) => b.views.count - a.views.count); | |||
| } | |||
| props.setOffersToShow([...newOffersToShow]); | |||
| }, [sortOption]); | |||
| const handleChangeSelect = (event) => { | |||
| let chosenOption; | |||
| for (const sortOption in sortEnum) { | |||
| if (sortEnum[sortOption].value === event.target.value) { | |||
| chosenOption = sortEnum[sortOption]; | |||
| setSortOption(chosenOption); | |||
| props.setSortOption(chosenOption); | |||
| } | |||
| } | |||
| }; | |||
| @@ -68,7 +51,7 @@ const SelectSortField = (props) => { | |||
| SelectSortField.propTypes = { | |||
| offersToShow: PropTypes.array, | |||
| setOffersToShow: PropTypes.func, | |||
| setSortOption: PropTypes.func, | |||
| }; | |||
| export default SelectSortField; | |||
| @@ -6,6 +6,7 @@ import { | |||
| } from "./HorizontalScroller.styled"; | |||
| import { ArrowButton } from "../Buttons/ArrowButton/ArrowButton"; | |||
| import useIsMobile from "../../hooks/useIsMobile"; | |||
| import { debounceHelper } from "../../util/helpers/debounceHelper"; | |||
| const HorizontalScroller = (props) => { | |||
| const scrollRef = useRef(null); | |||
| @@ -13,6 +14,7 @@ const HorizontalScroller = (props) => { | |||
| const [isDisabledLeftButton, setIsDisabledLeftButton] = useState(true); | |||
| const [isDisabledRightButton, setIsDisabledRightButton] = useState(false); | |||
| const handleScroll = (event) => { | |||
| if (!event.external) { | |||
| if (scrollRef.current.scrollLeft === 0) { | |||
| @@ -29,6 +31,14 @@ const HorizontalScroller = (props) => { | |||
| if (isDisabledRightButton !== false) setIsDisabledRightButton(false); | |||
| } | |||
| } | |||
| if (props.infiniteScroll) { | |||
| if ( | |||
| scrollRef.current.scrollWidth - scrollRef.current.scrollLeft < | |||
| scrollRef.current.clientWidth + 170 | |||
| ) { | |||
| debounceHelper(() => props.infiniteScroll(), 500); | |||
| } | |||
| } | |||
| }; | |||
| const handleRight = () => { | |||
| @@ -108,6 +118,7 @@ HorizontalScroller.propTypes = { | |||
| listStyle: PropTypes.any, | |||
| hideArrows: PropTypes.bool, | |||
| isCarousel: PropTypes.bool, | |||
| infiniteScroll: PropTypes.bool, | |||
| }; | |||
| export default HorizontalScroller; | |||
| @@ -1,6 +1,6 @@ | |||
| import { useEffect, useState } from "react"; | |||
| const usePaging = (applyAllFilters) => { | |||
| const usePaging = (applyAllFilters, disableScroll) => { | |||
| const [currentPage, setCurrentPage] = useState(1); | |||
| const [isInitallyLoaded, setIsInitiallyLoaded] = useState(false); | |||
| @@ -10,10 +10,12 @@ const usePaging = (applyAllFilters) => { | |||
| if (isInitallyLoaded && applyAllFilters) { | |||
| applyAllFilters(); | |||
| } | |||
| window.scrollTo({ | |||
| top: 0, | |||
| behavior: "smooth", | |||
| }); | |||
| if (!disableScroll) { | |||
| window.scrollTo({ | |||
| top: 0, | |||
| behavior: "smooth", | |||
| }); | |||
| } | |||
| }, [currentPage]); | |||
| const changePage = (pageNumber) => { | |||
| @@ -175,6 +175,7 @@ export default { | |||
| categories: "categories", | |||
| locations: "locations", | |||
| mineOffers: "users", | |||
| profileOffers: "users/{userId}/offers", | |||
| removeOffer: "/users/{userId}/offers/{offerId}", | |||
| removeOfferAsAdmin: "/admin/offers/{offerId}", | |||
| pinOffer: "admin/offers/{id}/pin", | |||
| @@ -35,8 +35,12 @@ export const attemptAddOffer = (payload, data) => { | |||
| data | |||
| ); | |||
| }; | |||
| export const attemptFetchProfileOffers = (payload) => { | |||
| return getRequest(`${apiEndpoints.offers.mineOffers}/${payload}/offers`); | |||
| export const attemptFetchProfileOffers = (userId, queryString = "") => { | |||
| return getRequest( | |||
| replaceInUrl(apiEndpoints.offers.profileOffers, { | |||
| userId: userId, | |||
| }) + `?${queryString}` | |||
| ); | |||
| }; | |||
| export const attemptRemoveOffer = (payload, offerId) => { | |||
| return deleteRequest( | |||
| @@ -60,6 +60,7 @@ export const OFFERS_ADD = createSetType("OFFERS_ADD"); | |||
| export const OFFERS_NO_MORE = createSetType("OFFERS_NO_MORE"); | |||
| export const OFFERS_SET_TOTAL = createSetType("OFFERS_SET_TOTAL"); | |||
| export const OFFERS_PROFILE_SET = createSetType("OFFERS_PROFILE_SET"); | |||
| export const OFFERS_PROFILE_ADD = createSetType("OFFERS_PROFILE_ADD"); | |||
| export const OFFERS_MINE_SET = createSetType("OFFERS_MY_ADD"); | |||
| export const OFFER_INDEX_SET = createSetType("OFFER_INDEX_SET"); | |||
| export const OFFER_INDEX_CLEAR = createClearType("OFFER_INDEX_CLEAR"); | |||
| @@ -22,6 +22,7 @@ import { | |||
| OFFERS_NO_MORE, | |||
| OFFERS_PINNED_ADD, | |||
| OFFERS_PINNED_SET, | |||
| OFFERS_PROFILE_ADD, | |||
| OFFERS_PROFILE_ERROR, | |||
| OFFERS_PROFILE_FETCH, | |||
| OFFERS_PROFILE_SET, | |||
| @@ -243,6 +244,10 @@ export const setProfileOffers = (payload) => ({ | |||
| type: OFFERS_PROFILE_SET, | |||
| payload, | |||
| }); | |||
| export const addProfileOffers = (payload) => ({ | |||
| type: OFFERS_PROFILE_ADD, | |||
| payload, | |||
| }); | |||
| export const setOffer = (payload) => ({ | |||
| type: OFFER_SET, | |||
| payload, | |||
| @@ -21,6 +21,7 @@ import { | |||
| OFFER_FEATURED_PAGE_SET, | |||
| OFFER_SELECTED_CLEAR, | |||
| OFFERS_HEADER_SET, | |||
| OFFERS_PROFILE_ADD, | |||
| } from "../../actions/offers/offersActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| @@ -57,6 +58,7 @@ export default createReducer( | |||
| [OFFERS_MINE_SET]: setMineOffers, | |||
| [OFFERS_HEADER_SET]: setHeaderOffers, | |||
| [OFFERS_PROFILE_SET]: setProfileOffers, | |||
| [OFFERS_PROFILE_ADD]: addProfileOffers, | |||
| [OFFERS_FEATURED_CLEAR]: clearFeaturedOffers, | |||
| [OFFERS_FEATURED_SET]: setFeaturedOffers, | |||
| [OFFER_INDEX_SET]: setOfferIndex, | |||
| @@ -196,3 +198,9 @@ function setHeaderOffers(state, { payload }) { | |||
| mineHeaderOffers: payload, | |||
| }; | |||
| } | |||
| function addProfileOffers(state, { payload }) { | |||
| return { | |||
| ...state, | |||
| profileOffers: [...state.profileOffers, ...payload], | |||
| }; | |||
| } | |||
| @@ -46,6 +46,7 @@ import { | |||
| fetchMineHeaderOffersError, | |||
| pinOfferSuccess, | |||
| pinOfferError, | |||
| addProfileOffers, | |||
| // fetchAllOffersSuccess, | |||
| // fetchAllOffersError, | |||
| // setFeaturedOfferPage, | |||
| @@ -82,6 +83,12 @@ import { | |||
| // import { NOT_FOUND_PAGE } from "../../constants/pages"; | |||
| import { makeErrorToastMessage } from "../utils/makeToastMessage"; | |||
| import i18next from "i18next"; | |||
| import { | |||
| KEY_NAME, | |||
| KEY_PAGE, | |||
| KEY_SIZE, | |||
| KEY_SORTBY, | |||
| } from "../../constants/queryStringConstants"; | |||
| function* fetchOffers(payload) { | |||
| try { | |||
| @@ -225,11 +232,27 @@ function* fetchMineHeaderOffers() { | |||
| function* fetchProfileOffers(payload) { | |||
| try { | |||
| const userId = payload.payload; | |||
| console.log(payload.payload); | |||
| const userId = payload.payload.idProfile; | |||
| if (!userId || userId?.length === 0) | |||
| throw new Error("User id is not defined!"); | |||
| const data = yield call(attemptFetchProfileOffers, userId); | |||
| yield put(setProfileOffers(data.data.offers)); | |||
| const queryString = new URLSearchParams(); | |||
| queryString.set(KEY_SIZE, 10); | |||
| queryString.set(KEY_PAGE, payload.payload.page); | |||
| if (payload.payload.searchValue?.length > 0) | |||
| queryString.set(KEY_NAME, payload.payload.searchValue); | |||
| queryString.set(KEY_SORTBY, payload.payload.sortOption.queryString); | |||
| const data = yield call( | |||
| attemptFetchProfileOffers, | |||
| userId, | |||
| convertQueryStringForBackend(queryString) | |||
| ); | |||
| if (payload.payload.append) { | |||
| yield put(addProfileOffers(data.data.offers)); | |||
| } else { | |||
| yield put(setProfileOffers(data.data.offers)); | |||
| } | |||
| yield put(setTotalOffers(data.data.total)); | |||
| yield put(fetchProfileOffersSuccess()); | |||
| } catch (e) { | |||
| console.dir(e); | |||