From 41d152c3bb1fa48578fa78f522cfd8e8b7b6b665 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Fri, 11 Feb 2022 22:50:05 +0000 Subject: [PATCH 01/24] Add: loading offers when the user reaches the last one --- .../SearchResultsWidget/LoadingOfferItem.js | 23 ++++++ .../OfferItemsContainer.js | 76 +++++++++++-------- 2 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js new file mode 100644 index 00000000..57a7b081 --- /dev/null +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js @@ -0,0 +1,23 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import OfferItem from "../Offer/OfferItem"; +import { Divider, List } from "@material-ui/core"; + +const LoadingOfferIcon = ({ dividerOnTop }) => ( + + {dividerOnTop && } + + + + + + + +); + +LoadingOfferIcon.propTypes = { + dividerOnTop: PropTypes.bool, +}; + +export default LoadingOfferIcon; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index eece57d2..a22a6f90 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -1,19 +1,14 @@ -import React from "react"; +import React, { useRef, useCallback, useState } from "react"; import PropTypes from "prop-types"; import Offer from "../Offer/Offer"; import OfferItem from "../Offer/OfferItem"; -import { - Button, - Divider, - List, - ListItem, - makeStyles, -} from "@material-ui/core"; +import { Button, Divider, List, ListItem, makeStyles } from "@material-ui/core"; import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import { Tune } from "@material-ui/icons"; import clsx from "clsx"; +import LoadingOfferIcon from "./LoadingOfferItem"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ root: { @@ -52,27 +47,44 @@ ToggleFiltersButton.propTypes = { enabled: PropTypes.bool, }; -const OfferItemsContainer = ({ offers, loading, selectedOfferIdx, setSelectedOfferIdx, showSearchFilters, toggleShowSearchFilters }) => { +const OfferItemsContainer = ({ + offers, + loading, + selectedOfferIdx, + setSelectedOfferIdx, + showSearchFilters, + toggleShowSearchFilters, +}) => { const classes = useSearchResultsWidgetStyles(); - if (loading) return ( -
- - - - - - - - -
- ); + const [offset, setOffset] = useState(0); + const [hasMore, setHasMore] = useState(true); + const [infiniteScrollLoading, setInfiniteScrollLoading] = useState(false); + const observer = useRef(); + const lastOfferElementRef = useCallback((node) => { + if (loading) return; + if (infiniteScrollLoading) return; + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasMore) { + setOffset((previousOffset) => previousOffset + 5); + setInfiniteScrollLoading(true); + } + }); + if (node) observer.current.observe(node); + }, [hasMore, infiniteScrollLoading, loading]); + + console.log(`Offset: ${offset}`); + + if (loading) + return ( +
+ +
+ ); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); @@ -80,7 +92,10 @@ const OfferItemsContainer = ({ offers, loading, selectedOfferIdx, setSelectedOff }; return ( -
+
toggleShowSearchFilters()} /> {offers.map((offer, i) => ( - +
{i !== 0 && } - +
))} + {infiniteScrollLoading && }
); From 98e6112e0120a84e8c33d67aa4b72776f6a82926 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sat, 12 Feb 2022 19:40:12 +0000 Subject: [PATCH 02/24] Add: useLoadMoreOffers hook --- .../OfferItemsContainer.js | 36 +++++---- .../SearchResultsDesktop.js | 2 +- .../SearchResultsMobile.js | 2 +- src/hooks/useLoadMoreOffers.js | 78 +++++++++++++++++++ 4 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 src/hooks/useLoadMoreOffers.js diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index a22a6f90..a06e9fd0 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -1,6 +1,5 @@ import React, { useRef, useCallback, useState } from "react"; import PropTypes from "prop-types"; -import Offer from "../Offer/Offer"; import OfferItem from "../Offer/OfferItem"; import { Button, Divider, List, ListItem, makeStyles } from "@material-ui/core"; @@ -9,6 +8,7 @@ import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import { Tune } from "@material-ui/icons"; import clsx from "clsx"; import LoadingOfferIcon from "./LoadingOfferItem"; +import useLoadMoreOffers from "../../../../hooks/useLoadMoreOffers"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ root: { @@ -48,7 +48,7 @@ ToggleFiltersButton.propTypes = { }; const OfferItemsContainer = ({ - offers, + // offers, loading, selectedOfferIdx, setSelectedOfferIdx, @@ -58,23 +58,34 @@ const OfferItemsContainer = ({ const classes = useSearchResultsWidgetStyles(); const [offset, setOffset] = useState(0); - const [hasMore, setHasMore] = useState(true); - const [infiniteScrollLoading, setInfiniteScrollLoading] = useState(false); + + const { + offers, + hasMore, + loading: infiniteScrollLoading, + error: infiniteScrollError, + } = useLoadMoreOffers({ offset }); + const observer = useRef(); const lastOfferElementRef = useCallback((node) => { - if (loading) return; - if (infiniteScrollLoading) return; + if (loading || infiniteScrollLoading) return; + if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { - setOffset((previousOffset) => previousOffset + 5); - setInfiniteScrollLoading(true); + setOffset((previousOffset) => previousOffset + 3); } }); if (node) observer.current.observe(node); }, [hasMore, infiniteScrollLoading, loading]); - console.log(`Offset: ${offset}`); + const handleOfferSelection = (...args) => { + toggleShowSearchFilters(false); + setSelectedOfferIdx(...args); + }; + + // BUG: the new offers are not being added + console.log(`Length: ${offers?.length}`); if (loading) return ( @@ -86,11 +97,6 @@ const OfferItemsContainer = ({
); - const handleOfferSelection = (...args) => { - toggleShowSearchFilters(false); - setSelectedOfferIdx(...args); - }; - return (
: : { + + const dispatch = useDispatch(); + const offerSearch = useSelector((state) => state.offerSearch); + const filters = { ...offerSearch, offset, limit: 3 }; + const oldOffers = useSelector((state) => state.offerSearch.offers); + const initialOffersLoading = useSelector((state) => state.offerSearch.loading); + + const [hasMore, setHasMore] = useState(true); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [offers, setOffers] = useState(oldOffers); + + // TODO: Find why I need this + if (offers.length === 0 && oldOffers.length > 0) { + setOffers(oldOffers); + } + + const [counter, setCounter] = useState(0); + + useEffect(() => { + + // TODO: solve this + if (initialOffersLoading || loading || oldOffers?.length === 0 || offset === 0 || counter > 5) return; + + setCounter((counter) => counter + 1); + + try { + const query = parseFiltersToURL(filters); + const res = fetch(`${API_HOSTNAME}/offers?${query}`, { + method: "GET", + credentials: "include", + }); + if (!res.ok) { + setError({ + cause: ErrorTypes.BAD_RESPONSE, + error: res.status, + }); + setLoading(false); + + return; + } + const offersData = res.json(); + const newOffers = [ + ...oldOffers, + ...offersData.map((offerData) => new Offer(offerData)), + ]; + + setHasMore(offersData.length > 0); + dispatch(setSearchOffers(newOffers)); + setOffers(newOffers); + setLoading(false); + + } catch (error) { + setError({ + cause: ErrorTypes.NETWORK_FAILURE, + error, + }); + setLoading(false); + } + }, [dispatch, filters, initialOffersLoading, oldOffers, offset, loading, counter]); + + + return { offers, hasMore, loading, error }; +}; From 4eeae5f368be68bf78881c850e7eb4c32dd9d7f9 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sat, 12 Feb 2022 22:46:38 +0000 Subject: [PATCH 03/24] Add: useLoadMoreOffers hook returning new offers when scrolling --- .../SearchResultsWidget/LoadingOfferItem.js | 6 +- .../OfferItemsContainer.js | 27 ++++-- src/hooks/useLoadMoreOffers.js | 88 ++++++++++++------- 3 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js index 57a7b081..2d4a8c37 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/LoadingOfferItem.js @@ -4,7 +4,7 @@ import PropTypes from "prop-types"; import OfferItem from "../Offer/OfferItem"; import { Divider, List } from "@material-ui/core"; -const LoadingOfferIcon = ({ dividerOnTop }) => ( +const LoadingOfferItem = ({ dividerOnTop }) => ( {dividerOnTop && } @@ -16,8 +16,8 @@ const LoadingOfferIcon = ({ dividerOnTop }) => ( ); -LoadingOfferIcon.propTypes = { +LoadingOfferItem.propTypes = { dividerOnTop: PropTypes.bool, }; -export default LoadingOfferIcon; +export default LoadingOfferItem; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index a06e9fd0..598c0632 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -7,8 +7,10 @@ import { Button, Divider, List, ListItem, makeStyles } from "@material-ui/core"; import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import { Tune } from "@material-ui/icons"; import clsx from "clsx"; -import LoadingOfferIcon from "./LoadingOfferItem"; +import LoadingOfferItem from "./LoadingOfferItem"; import useLoadMoreOffers from "../../../../hooks/useLoadMoreOffers"; +import { connect } from "react-redux"; +import { addSnackbar } from "../../../../actions/notificationActions"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ root: { @@ -49,6 +51,7 @@ ToggleFiltersButton.propTypes = { const OfferItemsContainer = ({ // offers, + addSnackbar, loading, selectedOfferIdx, setSelectedOfferIdx, @@ -70,6 +73,13 @@ const OfferItemsContainer = ({ const lastOfferElementRef = useCallback((node) => { if (loading || infiniteScrollLoading) return; + if (infiniteScrollError) { + addSnackbar({ + message: "An error occurred while fetching new offers", + key: `${Date.now()}-fetch-new-offers`, + }); + } + if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { @@ -77,23 +87,20 @@ const OfferItemsContainer = ({ } }); if (node) observer.current.observe(node); - }, [hasMore, infiniteScrollLoading, loading]); + }, [addSnackbar, hasMore, infiniteScrollError, infiniteScrollLoading, loading]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); setSelectedOfferIdx(...args); }; - // BUG: the new offers are not being added - console.log(`Length: ${offers?.length}`); - if (loading) return (
- +
); @@ -120,7 +127,7 @@ const OfferItemsContainer = ({ />
))} - {infiniteScrollLoading && } + {infiniteScrollLoading && } ); @@ -135,4 +142,8 @@ OfferItemsContainer.propTypes = { toggleShowSearchFilters: PropTypes.func.isRequired, }; -export default OfferItemsContainer; +const mapDispatchToProps = (dispatch) => ({ + addSnackbar: (notification) => dispatch(addSnackbar(notification)), +}); + +export default connect(null, mapDispatchToProps)(OfferItemsContainer); diff --git a/src/hooks/useLoadMoreOffers.js b/src/hooks/useLoadMoreOffers.js index d98a9549..32d2c672 100644 --- a/src/hooks/useLoadMoreOffers.js +++ b/src/hooks/useLoadMoreOffers.js @@ -14,8 +14,21 @@ const { API_HOSTNAME } = config; export default ({ offset }) => { const dispatch = useDispatch(); - const offerSearch = useSelector((state) => state.offerSearch); - const filters = { ...offerSearch, offset, limit: 3 }; + const offerSearch = useSelector(({ offerSearch }) => ({ + value: offerSearch.searchValue, + jobType: offerSearch.jobType, + jobMinDuration: offerSearch.jobDuration[0], + jobMaxDuration: offerSearch.jobDuration[1], + fields: offerSearch.fields, + technologies: offerSearch.technologies, + })); + + const filters = { + offset, + limit: 3, + ...offerSearch, + }; + const oldOffers = useSelector((state) => state.offerSearch.offers); const initialOffersLoading = useSelector((state) => state.offerSearch.loading); @@ -33,45 +46,56 @@ export default ({ offset }) => { useEffect(() => { - // TODO: solve this - if (initialOffersLoading || loading || oldOffers?.length === 0 || offset === 0 || counter > 5) return; + const fetchOffers = async () => { + // TODO: solve this + if (initialOffersLoading || loading || oldOffers?.length === 0 || offset === 0 || counter > 5) return; - setCounter((counter) => counter + 1); + setCounter((counter) => counter + 1); - try { - const query = parseFiltersToURL(filters); - const res = fetch(`${API_HOSTNAME}/offers?${query}`, { - method: "GET", - credentials: "include", - }); - if (!res.ok) { - setError({ - cause: ErrorTypes.BAD_RESPONSE, - error: res.status, + try { + const query = parseFiltersToURL(filters); + const res = await fetch(`${API_HOSTNAME}/offers?${query}`, { + method: "GET", + credentials: "include", }); + if (!res.ok) { + setError({ + cause: ErrorTypes.BAD_RESPONSE, + error: res.status, + }); + setLoading(false); + + return; + } + const offersData = await res.json(); + const newOffers = [ + ...oldOffers, + ...offersData.map((offerData) => new Offer(offerData)), + ]; + + setHasMore(offersData.length > 0); + dispatch(setSearchOffers(newOffers)); + setOffers(newOffers); setLoading(false); + setError(null); - return; + } catch (error) { + setError({ + cause: ErrorTypes.NETWORK_FAILURE, + error, + }); + setLoading(false); } - const offersData = res.json(); - const newOffers = [ - ...oldOffers, - ...offersData.map((offerData) => new Offer(offerData)), - ]; - - setHasMore(offersData.length > 0); - dispatch(setSearchOffers(newOffers)); - setOffers(newOffers); - setLoading(false); - - } catch (error) { + }; + + fetchOffers().catch((error) => { setError({ - cause: ErrorTypes.NETWORK_FAILURE, + cause: ErrorTypes.UNEXPECTED, error, }); - setLoading(false); - } - }, [dispatch, filters, initialOffersLoading, oldOffers, offset, loading, counter]); + }); + + }, [dispatch, filters, initialOffersLoading, oldOffers, offset, loading, counter, error]); return { offers, hasMore, loading, error }; From dcacd6126f94223a899d71715e48fed20c30db80 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sat, 12 Feb 2022 23:52:12 +0000 Subject: [PATCH 04/24] Add: useLoadMoreOffers hook main functionality done --- .../OfferItemsContainer.js | 13 ++++++---- src/hooks/useLoadMoreOffers.js | 24 +++++++++---------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 598c0632..26d15916 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -61,13 +61,14 @@ const OfferItemsContainer = ({ const classes = useSearchResultsWidgetStyles(); const [offset, setOffset] = useState(0); + const [fetchMoreOffers, setFetchMoreOffers] = useState(false); const { offers, hasMore, loading: infiniteScrollLoading, error: infiniteScrollError, - } = useLoadMoreOffers({ offset }); + } = useLoadMoreOffers({ offset, fetchMoreOffers }); const observer = useRef(); const lastOfferElementRef = useCallback((node) => { @@ -76,14 +77,17 @@ const OfferItemsContainer = ({ if (infiniteScrollError) { addSnackbar({ message: "An error occurred while fetching new offers", - key: `${Date.now()}-fetch-new-offers`, + key: "fetch-new-offers", }); } if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMore) { - setOffset((previousOffset) => previousOffset + 3); + if (entries[0].isIntersecting && hasMore && !loading && !infiniteScrollLoading) { + setOffset((previousOffset) => previousOffset + 5); + setFetchMoreOffers(true); + } else { + setFetchMoreOffers(false); } }); if (node) observer.current.observe(node); @@ -135,6 +139,7 @@ const OfferItemsContainer = ({ OfferItemsContainer.propTypes = { // offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), + addSnackbar: PropTypes.func, loading: PropTypes.bool, selectedOfferIdx: PropTypes.number, setSelectedOfferIdx: PropTypes.func.isRequired, diff --git a/src/hooks/useLoadMoreOffers.js b/src/hooks/useLoadMoreOffers.js index 32d2c672..ace9922e 100644 --- a/src/hooks/useLoadMoreOffers.js +++ b/src/hooks/useLoadMoreOffers.js @@ -11,7 +11,7 @@ import ErrorTypes from "../utils/ErrorTypes"; const { API_HOSTNAME } = config; -export default ({ offset }) => { +export default ({ offset, fetchMoreOffers }) => { const dispatch = useDispatch(); const offerSearch = useSelector(({ offerSearch }) => ({ @@ -25,7 +25,7 @@ export default ({ offset }) => { const filters = { offset, - limit: 3, + limit: 5, ...offerSearch, }; @@ -42,15 +42,11 @@ export default ({ offset }) => { setOffers(oldOffers); } - const [counter, setCounter] = useState(0); - useEffect(() => { const fetchOffers = async () => { - // TODO: solve this - if (initialOffersLoading || loading || oldOffers?.length === 0 || offset === 0 || counter > 5) return; - setCounter((counter) => counter + 1); + if (!fetchMoreOffers || error) return; try { const query = parseFiltersToURL(filters); @@ -68,10 +64,14 @@ export default ({ offset }) => { return; } const offersData = await res.json(); - const newOffers = [ - ...oldOffers, - ...offersData.map((offerData) => new Offer(offerData)), - ]; + + const oldOfferIds = [...oldOffers.map((oldOffer) => oldOffer._id)]; + const newOffers = [...oldOffers]; + + offersData.forEach((offerData) => { + if (!oldOfferIds.includes(offerData._id)) + newOffers.push(new Offer(offerData)); + }); setHasMore(offersData.length > 0); dispatch(setSearchOffers(newOffers)); @@ -95,7 +95,7 @@ export default ({ offset }) => { }); }); - }, [dispatch, filters, initialOffersLoading, oldOffers, offset, loading, counter, error]); + }, [dispatch, filters, initialOffersLoading, oldOffers, offset, loading, error, fetchMoreOffers]); return { offers, hasMore, loading, error }; From 0fd301042bfe98de01a8016dd4eac7c99166cdc0 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sun, 13 Feb 2022 09:37:37 +0000 Subject: [PATCH 05/24] Fix: new search after seing the results is now working --- .../SearchResultsWidget/OfferItemsContainer.js | 4 ++++ src/hooks/useLoadMoreOffers.js | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 26d15916..dbe0dc5c 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -79,8 +79,12 @@ const OfferItemsContainer = ({ message: "An error occurred while fetching new offers", key: "fetch-new-offers", }); + return; } + // TODO: + // Problem: when we change the search query, the infinite scrolling does not work sometimes + if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore && !loading && !infiniteScrollLoading) { diff --git a/src/hooks/useLoadMoreOffers.js b/src/hooks/useLoadMoreOffers.js index ace9922e..f4ef1a7b 100644 --- a/src/hooks/useLoadMoreOffers.js +++ b/src/hooks/useLoadMoreOffers.js @@ -37,10 +37,9 @@ export default ({ offset, fetchMoreOffers }) => { const [error, setError] = useState(null); const [offers, setOffers] = useState(oldOffers); - // TODO: Find why I need this - if (offers.length === 0 && oldOffers.length > 0) { + useEffect(() => { setOffers(oldOffers); - } + }, [offerSearch, oldOffers]); useEffect(() => { From c67e63dc961ef569c674adc61f61531896d3d88b Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sun, 13 Feb 2022 11:03:57 +0000 Subject: [PATCH 06/24] Add: submitNumber property to the redux state, to reset infinite scroll variables --- src/actions/searchOffersActions.js | 6 +++++ .../HomePage/SearchArea/SearchArea.js | 19 ++++++++++---- .../OfferItemsContainer.js | 26 +++++++++++-------- .../SearchResultsDesktop.js | 5 +--- .../SearchResultsMobile.js | 5 +--- src/hooks/useLoadMoreOffers.js | 11 ++++++-- src/reducers/searchOffersReducer.js | 6 +++++ 7 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/actions/searchOffersActions.js b/src/actions/searchOffersActions.js index 10e839ae..041557af 100644 --- a/src/actions/searchOffersActions.js +++ b/src/actions/searchOffersActions.js @@ -1,6 +1,7 @@ import { INITIAL_JOB_TYPE, INITIAL_JOB_DURATION, INITIAL_ERROR } from "../reducers/searchOffersReducer"; export const OfferSearchTypes = Object.freeze({ + SET_SUBMIT_NUMBER: "SET_SUBMIT_NUMBER", SET_SEARCH_VALUE: "SET_SEARCH_VALUE", SET_JOB_DURATION: "SET_JOB_DURATION", SET_JOB_TYPE: "SET_JOB_TYPE", @@ -16,6 +17,11 @@ export const OfferSearchTypes = Object.freeze({ ADMIN_ENABLE_OFFER: "ADMIN_ENABLE_OFFER", }); +export const setSubmitNumber = (submitNumber) => ({ + type: OfferSearchTypes.SET_SUBMIT_NUMBER, + submitNumber, +}); + export const setLoadingOffers = (loading) => ({ type: OfferSearchTypes.SET_OFFERS_LOADING, loading, diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 90cc93e5..1d553555 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; @@ -11,6 +11,7 @@ import { setFields, setShowJobDurationSlider, setTechs, + setSubmitNumber, } from "../../../actions/searchOffersActions"; import { INITIAL_JOB_TYPE, INITIAL_JOB_DURATION } from "../../../reducers/searchOffersReducer"; @@ -32,6 +33,7 @@ export const AdvancedSearchController = ({ enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, searchOffers, onMobileClose, + submitNumber, setSubmitNumber, }) => { const advancedSearchProps = useAdvancedSearch({ @@ -52,17 +54,20 @@ export const AdvancedSearchController = ({ const submitForm = useCallback((e) => { if (e) e.preventDefault(); - searchOffers({ - value: searchValue, + searchOffers({ value: searchValue, jobMinDuration: showJobDurationSlider && jobMinDuration, jobMaxDuration: showJobDurationSlider && jobMaxDuration, jobType, fields, technologies, }); + setSubmitNumber(submitNumber + 1); if (onSubmit) onSubmit(); - }, [fields, jobMaxDuration, jobMinDuration, jobType, onSubmit, searchOffers, searchValue, showJobDurationSlider, technologies]); + }, [ + fields, jobMaxDuration, jobMinDuration, jobType, onSubmit, searchOffers, searchValue, + setSubmitNumber, showJobDurationSlider, submitNumber, technologies, + ]); return { ...advancedSearchProps, @@ -79,7 +84,7 @@ export const AdvancedSearchController = ({ }; }; -export const SearchArea = ({ onSubmit, searchOffers, searchValue, +export const SearchArea = ({ onSubmit, searchOffers, searchValue, submitNumber, setSubmitNumber, jobMinDuration = INITIAL_JOB_DURATION, jobMaxDuration = INITIAL_JOB_DURATION + 1, jobType = INITIAL_JOB_TYPE, fields, technologies, showJobDurationSlider, setShowJobDurationSlider, advanced: enableAdvancedSearchDefault = false, setSearchValue, setJobDuration, setJobType, setFields, setTechs, resetAdvancedSearchFields, onMobileClose }) => { @@ -98,6 +103,7 @@ export const SearchArea = ({ onSubmit, searchOffers, searchValue, enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, searchOffers, onMobileClose, + submitNumber, setSubmitNumber, }, AdvancedSearchControllerContext ); @@ -146,6 +152,7 @@ SearchArea.propTypes = { jobMinDuration: PropTypes.number, jobMaxDuration: PropTypes.number, jobType: PropTypes.string, + setSubmitNumber: PropTypes.func, setSearchValue: PropTypes.func.isRequired, setJobDuration: PropTypes.func.isRequired, setJobType: PropTypes.func.isRequired, @@ -161,6 +168,7 @@ SearchArea.propTypes = { }; export const mapStateToProps = ({ offerSearch }) => ({ + submitNumber: offerSearch.submitNumber, searchValue: offerSearch.searchValue, jobType: offerSearch.jobType, jobMinDuration: offerSearch.jobDuration[0], @@ -172,6 +180,7 @@ export const mapStateToProps = ({ offerSearch }) => ({ export const mapDispatchToProps = (dispatch) => ({ searchOffers: (filters) => dispatch(searchOffers(filters)), + setSubmitNumber: (submitNumber) => dispatch((setSubmitNumber(submitNumber))), setSearchValue: (value) => dispatch(setSearchValue(value)), setJobDuration: (_, value) => dispatch(setJobDuration(...value)), setJobType: (e) => dispatch(setJobType(e.target.value)), diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index dbe0dc5c..2a1d76ce 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -1,4 +1,4 @@ -import React, { useRef, useCallback, useState } from "react"; +import React, { useRef, useCallback, useState, useEffect } from "react"; import PropTypes from "prop-types"; import OfferItem from "../Offer/OfferItem"; @@ -50,7 +50,6 @@ ToggleFiltersButton.propTypes = { }; const OfferItemsContainer = ({ - // offers, addSnackbar, loading, selectedOfferIdx, @@ -62,17 +61,26 @@ const OfferItemsContainer = ({ const [offset, setOffset] = useState(0); const [fetchMoreOffers, setFetchMoreOffers] = useState(false); + const [lastOfferNode, setLastOfferNode] = useState(null); const { offers, hasMore, loading: infiniteScrollLoading, error: infiniteScrollError, - } = useLoadMoreOffers({ offset, fetchMoreOffers }); + } = useLoadMoreOffers({ offset, setOffset, fetchMoreOffers }); const observer = useRef(); const lastOfferElementRef = useCallback((node) => { - if (loading || infiniteScrollLoading) return; + if (node) setLastOfferNode(node); + }, []); + + useEffect(() => { + + if (loading || infiniteScrollLoading) { + setFetchMoreOffers(false); + return; + } if (infiniteScrollError) { addSnackbar({ @@ -82,20 +90,17 @@ const OfferItemsContainer = ({ return; } - // TODO: - // Problem: when we change the search query, the infinite scrolling does not work sometimes - if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMore && !loading && !infiniteScrollLoading) { + if (entries[0].isIntersecting && hasMore) { setOffset((previousOffset) => previousOffset + 5); setFetchMoreOffers(true); } else { setFetchMoreOffers(false); } }); - if (node) observer.current.observe(node); - }, [addSnackbar, hasMore, infiniteScrollError, infiniteScrollLoading, loading]); + if (lastOfferNode) observer.current.observe(lastOfferNode); + }, [addSnackbar, hasMore, infiniteScrollError, infiniteScrollLoading, lastOfferNode, loading, offers]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); @@ -142,7 +147,6 @@ const OfferItemsContainer = ({ }; OfferItemsContainer.propTypes = { - // offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), addSnackbar: PropTypes.func, loading: PropTypes.bool, selectedOfferIdx: PropTypes.number, diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js index c219e180..94313f7f 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js @@ -10,7 +10,7 @@ import { WorkOff } from "@material-ui/icons"; import clsx from "clsx"; import { SearchResultsControllerContext } from "./SearchResultsWidget"; -const OffersList = ({ noOffers, classes, offers, selectedOfferIdx, offersLoading, setSelectedOfferIdx, +const OffersList = ({ noOffers, classes, selectedOfferIdx, offersLoading, setSelectedOfferIdx, showSearchFilters, toggleShowSearchFilters, }) => ( @@ -23,7 +23,6 @@ const OffersList = ({ noOffers, classes, offers, selectedOfferIdx, offersLoading : { ( +const OffersList = ({ noOffers, classes, offersLoading, showOfferDetails, showSearchFilters, toggleShowSearchFilters }) => ( {noOffers ? @@ -22,7 +22,6 @@ const OffersList = ({ noOffers, classes, offers, offersLoading, showOfferDetails : { { +export default ({ offset, setOffset, fetchMoreOffers }) => { const dispatch = useDispatch(); const offerSearch = useSelector(({ offerSearch }) => ({ @@ -31,6 +31,7 @@ export default ({ offset, fetchMoreOffers }) => { const oldOffers = useSelector((state) => state.offerSearch.offers); const initialOffersLoading = useSelector((state) => state.offerSearch.loading); + const submitNumber = useSelector((state) => state.offerSearch.submitNumber); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); @@ -39,7 +40,13 @@ export default ({ offset, fetchMoreOffers }) => { useEffect(() => { setOffers(oldOffers); - }, [offerSearch, oldOffers]); + }, [oldOffers]); + + useEffect(() => { + setOffset(0); + setError(null); + setLoading(false); + }, [setOffset, submitNumber]); useEffect(() => { diff --git a/src/reducers/searchOffersReducer.js b/src/reducers/searchOffersReducer.js index cc246a3a..6f2b8356 100644 --- a/src/reducers/searchOffersReducer.js +++ b/src/reducers/searchOffersReducer.js @@ -9,6 +9,7 @@ export const JOB_MIN_DURATION = 1; export const JOB_MAX_DURATION = 12; const initialState = { + submitNumber: 0, searchValue: "", jobType: INITIAL_JOB_TYPE, filterJobDuration: false, @@ -23,6 +24,11 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { + case OfferSearchTypes.SET_SUBMIT_NUMBER: + return { + ...state, + submitNumber: action.submitNumber, + }; case OfferSearchTypes.SET_OFFERS_SEARCH_RESULT: return { ...state, From 6f7f93e19aa54042d11eda1bbb2f7a4877bd2d62 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sun, 13 Feb 2022 19:17:42 +0000 Subject: [PATCH 07/24] Add: search offers infinite scrolling working correctly --- .../HomePage/SearchArea/SearchArea.js | 2 +- .../OfferItemsContainer.js | 6 ++++- src/hooks/useLoadMoreOffers.js | 26 ++++++++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 1d553555..cde929ff 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 2a1d76ce..22122070 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -75,6 +75,10 @@ const OfferItemsContainer = ({ if (node) setLastOfferNode(node); }, []); + useEffect(() => { + setOffset(offers?.length); + }, [offers]); + useEffect(() => { if (loading || infiniteScrollLoading) { @@ -100,7 +104,7 @@ const OfferItemsContainer = ({ } }); if (lastOfferNode) observer.current.observe(lastOfferNode); - }, [addSnackbar, hasMore, infiniteScrollError, infiniteScrollLoading, lastOfferNode, loading, offers]); + }, [addSnackbar, hasMore, infiniteScrollError, infiniteScrollLoading, lastOfferNode, loading]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); diff --git a/src/hooks/useLoadMoreOffers.js b/src/hooks/useLoadMoreOffers.js index 30a47803..4baf8ce3 100644 --- a/src/hooks/useLoadMoreOffers.js +++ b/src/hooks/useLoadMoreOffers.js @@ -37,6 +37,7 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [offers, setOffers] = useState(oldOffers); + const [fetchedOffsets, setFetchedOffsets] = useState([]); useEffect(() => { setOffers(oldOffers); @@ -44,17 +45,18 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { useEffect(() => { setOffset(0); + setHasMore(true); setError(null); setLoading(false); + setFetchedOffsets([]); }, [setOffset, submitNumber]); useEffect(() => { const fetchOffers = async () => { - if (!fetchMoreOffers || error) return; - try { + setFetchedOffsets((offsets) => [...offsets, filters.offset]); const query = parseFiltersToURL(filters); const res = await fetch(`${API_HOSTNAME}/offers?${query}`, { method: "GET", @@ -66,7 +68,6 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { error: res.status, }); setLoading(false); - return; } const offersData = await res.json(); @@ -91,18 +92,23 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { error, }); setLoading(false); + return; } }; - fetchOffers().catch((error) => { - setError({ - cause: ErrorTypes.UNEXPECTED, - error, + if (fetchMoreOffers && !fetchedOffsets.includes(filters.offset) && !initialOffersLoading && !loading) { + fetchOffers().catch((error) => { + setError({ + cause: ErrorTypes.UNEXPECTED, + error, + }); }); - }); - - }, [dispatch, filters, initialOffersLoading, oldOffers, offset, loading, error, fetchMoreOffers]); + } + }, [ + dispatch, fetchMoreOffers, filters, oldOffers, initialOffersLoading, offset, + fetchedOffsets, submitNumber, hasMore, loading, offers, + ]); return { offers, hasMore, loading, error }; }; From f4ffa45f44960419d15fdc6145556868dbd2cd11 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Mon, 14 Feb 2022 18:31:35 +0000 Subject: [PATCH 08/24] Remove: unnecessary variables --- src/actions/searchOffersActions.js | 6 --- .../HomePage/SearchArea/SearchArea.js | 11 +---- .../OfferItemsContainer.js | 9 ++-- .../SearchResultsWidget/SearchResultsUtils.js | 4 ++ .../SearchResultsWidget}/useLoadMoreOffers.js | 48 ++++++++----------- src/reducers/searchOffersReducer.js | 6 --- 6 files changed, 30 insertions(+), 54 deletions(-) create mode 100644 src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js rename src/{hooks => components/HomePage/SearchResultsArea/SearchResultsWidget}/useLoadMoreOffers.js (70%) diff --git a/src/actions/searchOffersActions.js b/src/actions/searchOffersActions.js index 041557af..10e839ae 100644 --- a/src/actions/searchOffersActions.js +++ b/src/actions/searchOffersActions.js @@ -1,7 +1,6 @@ import { INITIAL_JOB_TYPE, INITIAL_JOB_DURATION, INITIAL_ERROR } from "../reducers/searchOffersReducer"; export const OfferSearchTypes = Object.freeze({ - SET_SUBMIT_NUMBER: "SET_SUBMIT_NUMBER", SET_SEARCH_VALUE: "SET_SEARCH_VALUE", SET_JOB_DURATION: "SET_JOB_DURATION", SET_JOB_TYPE: "SET_JOB_TYPE", @@ -17,11 +16,6 @@ export const OfferSearchTypes = Object.freeze({ ADMIN_ENABLE_OFFER: "ADMIN_ENABLE_OFFER", }); -export const setSubmitNumber = (submitNumber) => ({ - type: OfferSearchTypes.SET_SUBMIT_NUMBER, - submitNumber, -}); - export const setLoadingOffers = (loading) => ({ type: OfferSearchTypes.SET_OFFERS_LOADING, loading, diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index cde929ff..78f57b78 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -11,7 +11,6 @@ import { setFields, setShowJobDurationSlider, setTechs, - setSubmitNumber, } from "../../../actions/searchOffersActions"; import { INITIAL_JOB_TYPE, INITIAL_JOB_DURATION } from "../../../reducers/searchOffersReducer"; @@ -33,7 +32,6 @@ export const AdvancedSearchController = ({ enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, searchOffers, onMobileClose, - submitNumber, setSubmitNumber, }) => { const advancedSearchProps = useAdvancedSearch({ @@ -61,12 +59,11 @@ export const AdvancedSearchController = ({ fields, technologies, }); - setSubmitNumber(submitNumber + 1); if (onSubmit) onSubmit(); }, [ fields, jobMaxDuration, jobMinDuration, jobType, onSubmit, searchOffers, searchValue, - setSubmitNumber, showJobDurationSlider, submitNumber, technologies, + showJobDurationSlider, technologies, ]); return { @@ -84,7 +81,7 @@ export const AdvancedSearchController = ({ }; }; -export const SearchArea = ({ onSubmit, searchOffers, searchValue, submitNumber, setSubmitNumber, +export const SearchArea = ({ onSubmit, searchOffers, searchValue, jobMinDuration = INITIAL_JOB_DURATION, jobMaxDuration = INITIAL_JOB_DURATION + 1, jobType = INITIAL_JOB_TYPE, fields, technologies, showJobDurationSlider, setShowJobDurationSlider, advanced: enableAdvancedSearchDefault = false, setSearchValue, setJobDuration, setJobType, setFields, setTechs, resetAdvancedSearchFields, onMobileClose }) => { @@ -103,7 +100,6 @@ export const SearchArea = ({ onSubmit, searchOffers, searchValue, submitNumber, enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, searchOffers, onMobileClose, - submitNumber, setSubmitNumber, }, AdvancedSearchControllerContext ); @@ -152,7 +148,6 @@ SearchArea.propTypes = { jobMinDuration: PropTypes.number, jobMaxDuration: PropTypes.number, jobType: PropTypes.string, - setSubmitNumber: PropTypes.func, setSearchValue: PropTypes.func.isRequired, setJobDuration: PropTypes.func.isRequired, setJobType: PropTypes.func.isRequired, @@ -168,7 +163,6 @@ SearchArea.propTypes = { }; export const mapStateToProps = ({ offerSearch }) => ({ - submitNumber: offerSearch.submitNumber, searchValue: offerSearch.searchValue, jobType: offerSearch.jobType, jobMinDuration: offerSearch.jobDuration[0], @@ -180,7 +174,6 @@ export const mapStateToProps = ({ offerSearch }) => ({ export const mapDispatchToProps = (dispatch) => ({ searchOffers: (filters) => dispatch(searchOffers(filters)), - setSubmitNumber: (submitNumber) => dispatch((setSubmitNumber(submitNumber))), setSearchValue: (value) => dispatch(setSearchValue(value)), setJobDuration: (_, value) => dispatch(setJobDuration(...value)), setJobType: (e) => dispatch(setJobType(e.target.value)), diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 22122070..097dea9b 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -8,9 +8,10 @@ import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import { Tune } from "@material-ui/icons"; import clsx from "clsx"; import LoadingOfferItem from "./LoadingOfferItem"; -import useLoadMoreOffers from "../../../../hooks/useLoadMoreOffers"; +import useLoadMoreOffers from "./useLoadMoreOffers"; import { connect } from "react-redux"; import { addSnackbar } from "../../../../actions/notificationActions"; +import { SearchResultsConstants } from "./SearchResultsUtils"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ root: { @@ -75,10 +76,6 @@ const OfferItemsContainer = ({ if (node) setLastOfferNode(node); }, []); - useEffect(() => { - setOffset(offers?.length); - }, [offers]); - useEffect(() => { if (loading || infiniteScrollLoading) { @@ -97,7 +94,7 @@ const OfferItemsContainer = ({ if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { - setOffset((previousOffset) => previousOffset + 5); + setOffset((previousOffset) => previousOffset + SearchResultsConstants.fetchNewOffersLimit); setFetchMoreOffers(true); } else { setFetchMoreOffers(false); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js new file mode 100644 index 00000000..c6e14036 --- /dev/null +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js @@ -0,0 +1,4 @@ +export const SearchResultsConstants = { + initialLimit: 5, + fetchNewOffersLimit: 5, +}; diff --git a/src/hooks/useLoadMoreOffers.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js similarity index 70% rename from src/hooks/useLoadMoreOffers.js rename to src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js index 4baf8ce3..7cc3f1a7 100644 --- a/src/hooks/useLoadMoreOffers.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js @@ -1,13 +1,12 @@ import { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { - setSearchOffers, -} from "../actions/searchOffersActions"; -import Offer from "../components/HomePage/SearchResultsArea/Offer/Offer"; -import { parseFiltersToURL } from "../utils"; -import config from "../config"; -import ErrorTypes from "../utils/ErrorTypes"; +import { setSearchOffers } from "../../../../actions/searchOffersActions"; +import Offer from "../Offer/Offer"; +import { parseFiltersToURL } from "../../../../utils"; +import config from "../../../../config"; +import ErrorTypes from "../../../../utils/ErrorTypes"; +import { SearchResultsConstants } from "./SearchResultsUtils"; const { API_HOSTNAME } = config; @@ -25,31 +24,27 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { const filters = { offset, - limit: 5, + limit: SearchResultsConstants.fetchNewOffersLimit, ...offerSearch, }; - const oldOffers = useSelector((state) => state.offerSearch.offers); + const offers = useSelector((state) => state.offerSearch.offers); const initialOffersLoading = useSelector((state) => state.offerSearch.loading); - const submitNumber = useSelector((state) => state.offerSearch.submitNumber); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [offers, setOffers] = useState(oldOffers); const [fetchedOffsets, setFetchedOffsets] = useState([]); useEffect(() => { - setOffers(oldOffers); - }, [oldOffers]); - - useEffect(() => { - setOffset(0); - setHasMore(true); - setError(null); - setLoading(false); - setFetchedOffsets([]); - }, [setOffset, submitNumber]); + if (initialOffersLoading) { + setOffset(0); + setHasMore(true); + setError(null); + setLoading(false); + setFetchedOffsets([]); + } + }, [setOffset, initialOffersLoading]); useEffect(() => { @@ -72,17 +67,16 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { } const offersData = await res.json(); - const oldOfferIds = [...oldOffers.map((oldOffer) => oldOffer._id)]; - const newOffers = [...oldOffers]; + const offerIds = [...offers.map((offer) => offer._id)]; + const newOffers = [...offers]; offersData.forEach((offerData) => { - if (!oldOfferIds.includes(offerData._id)) + if (!offerIds.includes(offerData._id)) newOffers.push(new Offer(offerData)); }); setHasMore(offersData.length > 0); dispatch(setSearchOffers(newOffers)); - setOffers(newOffers); setLoading(false); setError(null); @@ -106,8 +100,8 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { } }, [ - dispatch, fetchMoreOffers, filters, oldOffers, initialOffersLoading, offset, - fetchedOffsets, submitNumber, hasMore, loading, offers, + dispatch, fetchMoreOffers, filters, initialOffersLoading, + fetchedOffsets, loading, offers, ]); return { offers, hasMore, loading, error }; diff --git a/src/reducers/searchOffersReducer.js b/src/reducers/searchOffersReducer.js index 6f2b8356..cc246a3a 100644 --- a/src/reducers/searchOffersReducer.js +++ b/src/reducers/searchOffersReducer.js @@ -9,7 +9,6 @@ export const JOB_MIN_DURATION = 1; export const JOB_MAX_DURATION = 12; const initialState = { - submitNumber: 0, searchValue: "", jobType: INITIAL_JOB_TYPE, filterJobDuration: false, @@ -24,11 +23,6 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { - case OfferSearchTypes.SET_SUBMIT_NUMBER: - return { - ...state, - submitNumber: action.submitNumber, - }; case OfferSearchTypes.SET_OFFERS_SEARCH_RESULT: return { ...state, From 9895fbd8955213e2f80aac289d8b11fd7cca5581 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Mon, 14 Feb 2022 18:51:14 +0000 Subject: [PATCH 09/24] Fix: warning in mobile view regarding PropTypes --- .../AdvancedSearch/AdvancedSearchMobile.js | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.js b/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.js index 38690e49..4e448da0 100644 --- a/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.js +++ b/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.js @@ -176,21 +176,19 @@ const AdvancedSearchMobile = () => { }; AdvancedSearchMobile.propTypes = { - open: PropTypes.bool.isRequired, - close: PropTypes.func.isRequired, - searchValue: PropTypes.string.isRequired, - submitForm: PropTypes.func.isRequired, - setSearchValue: PropTypes.func.isRequired, - resetAdvancedSearch: PropTypes.func.isRequired, - FieldsSelectorProps: PropTypes.object.isRequired, - TechsSelectorProps: PropTypes.object.isRequired, - JobTypeSelectorProps: PropTypes.object.isRequired, - JobDurationSwitchProps: PropTypes.object.isRequired, - ResetButtonProps: PropTypes.object.isRequired, - JobDurationSliderText: PropTypes.string.isRequired, - JobDurationCollapseProps: PropTypes.object.isRequired, - JobDurationSwitchLabel: PropTypes.string.isRequired, - JobDurationSliderProps: PropTypes.object.isRequired, + searchValue: PropTypes.string, + submitForm: PropTypes.func, + setSearchValue: PropTypes.func, + resetAdvancedSearch: PropTypes.func, + FieldsSelectorProps: PropTypes.object, + TechsSelectorProps: PropTypes.object, + JobTypeSelectorProps: PropTypes.object, + JobDurationSwitchProps: PropTypes.object, + ResetButtonProps: PropTypes.object, + JobDurationSliderText: PropTypes.string, + JobDurationCollapseProps: PropTypes.object, + JobDurationSwitchLabel: PropTypes.string, + JobDurationSliderProps: PropTypes.object, onMobileClose: PropTypes.func, }; From 217fa40da8a0e66db5277b328556556d16b7066a Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Mon, 14 Feb 2022 19:14:44 +0000 Subject: [PATCH 10/24] Fix: infinite scroll loading display --- .../SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js index 7cc3f1a7..9a73822c 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js @@ -52,6 +52,7 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { try { setFetchedOffsets((offsets) => [...offsets, filters.offset]); + setLoading(true); const query = parseFiltersToURL(filters); const res = await fetch(`${API_HOSTNAME}/offers?${query}`, { method: "GET", From 91fbe5e19656f03410343de839f891ddf9db3ea0 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Tue, 22 Feb 2022 19:15:23 +0000 Subject: [PATCH 11/24] Refactor: renamed variables --- .../OfferItemsContainer.js | 29 +++++++++---------- .../SearchResultsWidget/useLoadMoreOffers.js | 20 ++++++------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 097dea9b..9c19f39a 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -1,15 +1,14 @@ import React, { useRef, useCallback, useState, useEffect } from "react"; import PropTypes from "prop-types"; -import OfferItem from "../Offer/OfferItem"; - +import { connect } from "react-redux"; +import clsx from "clsx"; import { Button, Divider, List, ListItem, makeStyles } from "@material-ui/core"; +import { Tune } from "@material-ui/icons"; +import OfferItem from "../Offer/OfferItem"; import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; -import { Tune } from "@material-ui/icons"; -import clsx from "clsx"; import LoadingOfferItem from "./LoadingOfferItem"; import useLoadMoreOffers from "./useLoadMoreOffers"; -import { connect } from "react-redux"; import { addSnackbar } from "../../../../actions/notificationActions"; import { SearchResultsConstants } from "./SearchResultsUtils"; @@ -60,16 +59,16 @@ const OfferItemsContainer = ({ }) => { const classes = useSearchResultsWidgetStyles(); - const [offset, setOffset] = useState(0); - const [fetchMoreOffers, setFetchMoreOffers] = useState(false); + const [offerOffset, setOfferOffset] = useState(0); + const [shouldFetchMoreOffers, setShouldFetchMoreOffers] = useState(false); const [lastOfferNode, setLastOfferNode] = useState(null); const { offers, - hasMore, + hasMoreOffers, loading: infiniteScrollLoading, error: infiniteScrollError, - } = useLoadMoreOffers({ offset, setOffset, fetchMoreOffers }); + } = useLoadMoreOffers({ offerOffset, setOfferOffset, shouldFetchMoreOffers }); const observer = useRef(); const lastOfferElementRef = useCallback((node) => { @@ -79,7 +78,7 @@ const OfferItemsContainer = ({ useEffect(() => { if (loading || infiniteScrollLoading) { - setFetchMoreOffers(false); + setShouldFetchMoreOffers(false); return; } @@ -93,15 +92,15 @@ const OfferItemsContainer = ({ if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMore) { - setOffset((previousOffset) => previousOffset + SearchResultsConstants.fetchNewOffersLimit); - setFetchMoreOffers(true); + if (entries[0].isIntersecting && hasMoreOffers) { + setOfferOffset((previousOffset) => previousOffset + SearchResultsConstants.fetchNewOffersLimit); + setShouldFetchMoreOffers(true); } else { - setFetchMoreOffers(false); + setShouldFetchMoreOffers(false); } }); if (lastOfferNode) observer.current.observe(lastOfferNode); - }, [addSnackbar, hasMore, infiniteScrollError, infiniteScrollLoading, lastOfferNode, loading]); + }, [addSnackbar, hasMoreOffers, infiniteScrollError, infiniteScrollLoading, lastOfferNode, loading]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js index 9a73822c..ce51ab7b 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js @@ -10,7 +10,7 @@ import { SearchResultsConstants } from "./SearchResultsUtils"; const { API_HOSTNAME } = config; -export default ({ offset, setOffset, fetchMoreOffers }) => { +export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { const dispatch = useDispatch(); const offerSearch = useSelector(({ offerSearch }) => ({ @@ -23,7 +23,7 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { })); const filters = { - offset, + offset: offerOffset, limit: SearchResultsConstants.fetchNewOffersLimit, ...offerSearch, }; @@ -31,20 +31,20 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { const offers = useSelector((state) => state.offerSearch.offers); const initialOffersLoading = useSelector((state) => state.offerSearch.loading); - const [hasMore, setHasMore] = useState(true); + const [hasMoreOffers, setHasMoreOffers] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [fetchedOffsets, setFetchedOffsets] = useState([]); useEffect(() => { if (initialOffersLoading) { - setOffset(0); - setHasMore(true); + setOfferOffset(0); + setHasMoreOffers(true); setError(null); setLoading(false); setFetchedOffsets([]); } - }, [setOffset, initialOffersLoading]); + }, [setOfferOffset, initialOffersLoading]); useEffect(() => { @@ -76,7 +76,7 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { newOffers.push(new Offer(offerData)); }); - setHasMore(offersData.length > 0); + setHasMoreOffers(offersData.length > 0); dispatch(setSearchOffers(newOffers)); setLoading(false); setError(null); @@ -91,7 +91,7 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { } }; - if (fetchMoreOffers && !fetchedOffsets.includes(filters.offset) && !initialOffersLoading && !loading) { + if (shouldFetchMoreOffers && !fetchedOffsets.includes(filters.offset) && !initialOffersLoading && !loading) { fetchOffers().catch((error) => { setError({ cause: ErrorTypes.UNEXPECTED, @@ -101,9 +101,9 @@ export default ({ offset, setOffset, fetchMoreOffers }) => { } }, [ - dispatch, fetchMoreOffers, filters, initialOffersLoading, + dispatch, shouldFetchMoreOffers, filters, initialOffersLoading, fetchedOffsets, loading, offers, ]); - return { offers, hasMore, loading, error }; + return { offers, hasMoreOffers, loading, error }; }; From 46bf5efeb0f5affe55cd30a26eea6f6a1ce9407a Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Tue, 22 Feb 2022 19:34:07 +0000 Subject: [PATCH 12/24] Refactor: moved infinite scroll to SearchResultsController --- .../OfferItemsContainer.js | 28 +++++++++++------- .../SearchResultsDesktop.js | 29 +++++++++++++++++-- .../SearchResultsMobile.js | 28 +++++++++++++++++- .../SearchResultsWidget.js | 17 +++++++++-- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 9c19f39a..0ea927d7 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -8,9 +8,9 @@ import { Tune } from "@material-ui/icons"; import OfferItem from "../Offer/OfferItem"; import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import LoadingOfferItem from "./LoadingOfferItem"; -import useLoadMoreOffers from "./useLoadMoreOffers"; import { addSnackbar } from "../../../../actions/notificationActions"; import { SearchResultsConstants } from "./SearchResultsUtils"; +import Offer from "../Offer/Offer"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ root: { @@ -56,20 +56,17 @@ const OfferItemsContainer = ({ setSelectedOfferIdx, showSearchFilters, toggleShowSearchFilters, + offers, + setOfferOffset, + setShouldFetchMoreOffers, + hasMoreOffers, + infiniteScrollLoading, + infiniteScrollError, }) => { const classes = useSearchResultsWidgetStyles(); - const [offerOffset, setOfferOffset] = useState(0); - const [shouldFetchMoreOffers, setShouldFetchMoreOffers] = useState(false); const [lastOfferNode, setLastOfferNode] = useState(null); - const { - offers, - hasMoreOffers, - loading: infiniteScrollLoading, - error: infiniteScrollError, - } = useLoadMoreOffers({ offerOffset, setOfferOffset, shouldFetchMoreOffers }); - const observer = useRef(); const lastOfferElementRef = useCallback((node) => { if (node) setLastOfferNode(node); @@ -100,7 +97,10 @@ const OfferItemsContainer = ({ } }); if (lastOfferNode) observer.current.observe(lastOfferNode); - }, [addSnackbar, hasMoreOffers, infiniteScrollError, infiniteScrollLoading, lastOfferNode, loading]); + }, [ + addSnackbar, hasMoreOffers, infiniteScrollError, infiniteScrollLoading, + lastOfferNode, loading, setOfferOffset, setShouldFetchMoreOffers, + ]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); @@ -153,6 +153,12 @@ OfferItemsContainer.propTypes = { setSelectedOfferIdx: PropTypes.func.isRequired, showSearchFilters: PropTypes.bool, toggleShowSearchFilters: PropTypes.func.isRequired, + offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), + setOfferOffset: PropTypes.func, + setShouldFetchMoreOffers: PropTypes.func, + hasMoreOffers: PropTypes.bool, + infiniteScrollLoading: PropTypes.bool, + infiniteScrollError: PropTypes.bool, }; const mapDispatchToProps = (dispatch) => ({ diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js index 94313f7f..8c57743d 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js @@ -10,8 +10,10 @@ import { WorkOff } from "@material-ui/icons"; import clsx from "clsx"; import { SearchResultsControllerContext } from "./SearchResultsWidget"; -const OffersList = ({ noOffers, classes, selectedOfferIdx, offersLoading, setSelectedOfferIdx, - showSearchFilters, toggleShowSearchFilters, +const OffersList = ({ + noOffers, classes, selectedOfferIdx, offersLoading, setSelectedOfferIdx, + showSearchFilters, toggleShowSearchFilters, offers, setOfferOffset, + setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, infiniteScrollError, }) => ( @@ -29,6 +31,12 @@ const OffersList = ({ noOffers, classes, selectedOfferIdx, offersLoading, setSel noOffers={noOffers} showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} + offers={offers} + setOfferOffset={setOfferOffset} + setShouldFetchMoreOffers={setShouldFetchMoreOffers} + hasMoreOffers={hasMoreOffers} + infiniteScrollLoading={infiniteScrollLoading} + infiniteScrollError={infiniteScrollError} /> } { handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, + setOfferOffset, + setShouldFetchMoreOffers, + hasMoreOffers, + infiniteScrollLoading, + infiniteScrollError, } = useContext(SearchResultsControllerContext); const classes = useSearchResultsWidgetStyles(); @@ -151,6 +170,12 @@ const SearchResultsDesktop = () => { setSelectedOfferIdx={setSelectedOfferIdx} showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} + offers={offers} + setOfferOffset={setOfferOffset} + setShouldFetchMoreOffers={setShouldFetchMoreOffers} + hasMoreOffers={hasMoreOffers} + infiniteScrollLoading={infiniteScrollLoading} + infiniteScrollError={infiniteScrollError} /> {showSearchFilters ? ( +const OffersList = ({ + noOffers, classes, offersLoading, showOfferDetails, showSearchFilters, toggleShowSearchFilters, offers, + setOfferOffset, setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, infiniteScrollError, +}) => ( {noOffers ? @@ -27,6 +30,12 @@ const OffersList = ({ noOffers, classes, offersLoading, showOfferDetails, showSe noOffers={noOffers} showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} + offers={offers} + setOfferOffset={setOfferOffset} + setShouldFetchMoreOffers={setShouldFetchMoreOffers} + hasMoreOffers={hasMoreOffers} + infiniteScrollLoading={infiniteScrollLoading} + infiniteScrollError={infiniteScrollError} /> } @@ -46,6 +55,12 @@ OffersList.propTypes = { showOfferDetails: PropTypes.func.isRequired, showSearchFilters: PropTypes.bool.isRequired, toggleShowSearchFilters: PropTypes.func.isRequired, + offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), + setOfferOffset: PropTypes.func, + setShouldFetchMoreOffers: PropTypes.func, + hasMoreOffers: PropTypes.bool, + infiniteScrollLoading: PropTypes.bool, + infiniteScrollError: PropTypes.bool, }; export const OfferViewer = ({ @@ -116,6 +131,11 @@ const SearchResultsMobile = () => { handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, + shouldFetchMoreOffers, + setShouldFetchMoreOffers, + hasMoreOffers, + infiniteScrollLoading, + infiniteScrollError, } = useContext(SearchResultsControllerContext); const showOfferDetails = (offerIdx) => { @@ -143,6 +163,12 @@ const SearchResultsMobile = () => { showOfferDetails={showOfferDetails} showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} + offers={offers} + shouldFetchMoreOffers={shouldFetchMoreOffers} + setShouldFetchMoreOffers={setShouldFetchMoreOffers} + hasMoreOffers={hasMoreOffers} + infiniteScrollLoading={infiniteScrollLoading} + infiniteScrollError={infiniteScrollError} /> {showSearchFilters ? { const [selectedOfferIdx, setSelectedOfferIdx] = useState(null); + const [offerOffset, setOfferOffset] = useState(0); + const [shouldFetchMoreOffers, setShouldFetchMoreOffers] = useState(false); // Reset the selected offer on every "loading", so that it does not show up after finished loading useEffect(() => { if (offersLoading) setSelectedOfferIdx(null); }, [offersLoading]); + const { + offers, + hasMoreOffers, + loading: infiniteScrollLoading, + error: infiniteScrollError, + } = useLoadMoreOffers({ offerOffset, setOfferOffset, shouldFetchMoreOffers }); + const handleDisableOffer = useCallback(({ offer, adminReason, onSuccess, onError }) => { disableOfferService(offer._id, adminReason).then(() => { disableOffer(selectedOfferIdx, adminReason); @@ -112,6 +121,11 @@ const SearchResultsController = ({ handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, + setOfferOffset, + setShouldFetchMoreOffers, + hasMoreOffers, + infiniteScrollLoading, + infiniteScrollError, }, }, }; @@ -173,7 +187,6 @@ SearchResultsWidget.propTypes = { }; const mapStateToProps = (state) => ({ - offers: state.offerSearch.offers, offersLoading: state.offerSearch.loading, offersSearchError: state.offerSearch.error, }); From d20f73de0c857e63fd09f3c11a6d765134663fd5 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Thu, 24 Mar 2022 18:56:30 +0000 Subject: [PATCH 13/24] Update: offer search offset logic to queryToken --- src/actions/searchOffersActions.js | 6 ++++++ .../OfferItemsContainer.js | 6 +----- .../SearchResultsDesktop.js | 8 ++----- .../SearchResultsMobile.js | 4 +--- .../SearchResultsWidget.js | 4 +--- .../SearchResultsWidget/useLoadMoreOffers.js | 21 +++++++++---------- src/reducers/searchOffersReducer.js | 5 +++++ src/services/offerService.js | 13 ++++++++++-- 8 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/actions/searchOffersActions.js b/src/actions/searchOffersActions.js index 10e839ae..ae03fcad 100644 --- a/src/actions/searchOffersActions.js +++ b/src/actions/searchOffersActions.js @@ -7,6 +7,7 @@ export const OfferSearchTypes = Object.freeze({ SET_JOB_FIELDS: "SET_JOB_FIELDS", SET_JOB_TECHS: "SET_JOB_TECHS", SET_OFFERS_SEARCH_RESULT: "SET_OFFERS_SEARCH_RESULT", + SET_SEARCH_QUERY_TOKEN: "SET_SEARCH_QUERY_TOKEN", SET_OFFERS_LOADING: "SET_OFFERS_LOADING", SET_SEARCH_OFFERS_ERROR: "SET_SEARCH_OFFERS_ERROR", SET_JOB_DURATION_TOGGLE: "SET_JOB_DURATION_TOGGLE", @@ -26,6 +27,11 @@ export const setSearchOffers = (offers) => ({ offers, }); +export const setSearchQueryToken = (queryToken) => ({ + type: OfferSearchTypes.SET_SEARCH_QUERY_TOKEN, + queryToken, +}); + export const setOffersFetchError = (error) => ({ type: OfferSearchTypes.SET_SEARCH_OFFERS_ERROR, error, diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 0ea927d7..d51a8d3f 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -9,7 +9,6 @@ import OfferItem from "../Offer/OfferItem"; import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import LoadingOfferItem from "./LoadingOfferItem"; import { addSnackbar } from "../../../../actions/notificationActions"; -import { SearchResultsConstants } from "./SearchResultsUtils"; import Offer from "../Offer/Offer"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ @@ -57,7 +56,6 @@ const OfferItemsContainer = ({ showSearchFilters, toggleShowSearchFilters, offers, - setOfferOffset, setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, @@ -90,7 +88,6 @@ const OfferItemsContainer = ({ if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMoreOffers) { - setOfferOffset((previousOffset) => previousOffset + SearchResultsConstants.fetchNewOffersLimit); setShouldFetchMoreOffers(true); } else { setShouldFetchMoreOffers(false); @@ -99,7 +96,7 @@ const OfferItemsContainer = ({ if (lastOfferNode) observer.current.observe(lastOfferNode); }, [ addSnackbar, hasMoreOffers, infiniteScrollError, infiniteScrollLoading, - lastOfferNode, loading, setOfferOffset, setShouldFetchMoreOffers, + lastOfferNode, loading, setShouldFetchMoreOffers, ]); const handleOfferSelection = (...args) => { @@ -154,7 +151,6 @@ OfferItemsContainer.propTypes = { showSearchFilters: PropTypes.bool, toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), - setOfferOffset: PropTypes.func, setShouldFetchMoreOffers: PropTypes.func, hasMoreOffers: PropTypes.bool, infiniteScrollLoading: PropTypes.bool, diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js index 8c57743d..d3dff348 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js @@ -12,8 +12,8 @@ import { SearchResultsControllerContext } from "./SearchResultsWidget"; const OffersList = ({ noOffers, classes, selectedOfferIdx, offersLoading, setSelectedOfferIdx, - showSearchFilters, toggleShowSearchFilters, offers, setOfferOffset, - setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, infiniteScrollError, + showSearchFilters, toggleShowSearchFilters, offers, setShouldFetchMoreOffers, + hasMoreOffers, infiniteScrollLoading, infiniteScrollError, }) => ( @@ -32,7 +32,6 @@ const OffersList = ({ showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} offers={offers} - setOfferOffset={setOfferOffset} setShouldFetchMoreOffers={setShouldFetchMoreOffers} hasMoreOffers={hasMoreOffers} infiniteScrollLoading={infiniteScrollLoading} @@ -64,7 +63,6 @@ OffersList.propTypes = { showSearchFilters: PropTypes.bool.isRequired, toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), - setOfferOffset: PropTypes.func, setShouldFetchMoreOffers: PropTypes.func, hasMoreOffers: PropTypes.bool, infiniteScrollLoading: PropTypes.bool, @@ -136,7 +134,6 @@ const SearchResultsDesktop = () => { handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, - setOfferOffset, setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, @@ -171,7 +168,6 @@ const SearchResultsDesktop = () => { showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} offers={offers} - setOfferOffset={setOfferOffset} setShouldFetchMoreOffers={setShouldFetchMoreOffers} hasMoreOffers={hasMoreOffers} infiniteScrollLoading={infiniteScrollLoading} diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js index 0af34f8f..ee4919f7 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js @@ -12,7 +12,7 @@ import { SearchResultsControllerContext } from "./SearchResultsWidget"; const OffersList = ({ noOffers, classes, offersLoading, showOfferDetails, showSearchFilters, toggleShowSearchFilters, offers, - setOfferOffset, setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, infiniteScrollError, + setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, infiniteScrollError, }) => ( @@ -31,7 +31,6 @@ const OffersList = ({ showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} offers={offers} - setOfferOffset={setOfferOffset} setShouldFetchMoreOffers={setShouldFetchMoreOffers} hasMoreOffers={hasMoreOffers} infiniteScrollLoading={infiniteScrollLoading} @@ -56,7 +55,6 @@ OffersList.propTypes = { showSearchFilters: PropTypes.bool.isRequired, toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), - setOfferOffset: PropTypes.func, setShouldFetchMoreOffers: PropTypes.func, hasMoreOffers: PropTypes.bool, infiniteScrollLoading: PropTypes.bool, diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.js index d1c5343f..1b58a0dc 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.js @@ -36,7 +36,6 @@ const SearchResultsController = ({ }) => { const [selectedOfferIdx, setSelectedOfferIdx] = useState(null); - const [offerOffset, setOfferOffset] = useState(0); const [shouldFetchMoreOffers, setShouldFetchMoreOffers] = useState(false); // Reset the selected offer on every "loading", so that it does not show up after finished loading @@ -49,7 +48,7 @@ const SearchResultsController = ({ hasMoreOffers, loading: infiniteScrollLoading, error: infiniteScrollError, - } = useLoadMoreOffers({ offerOffset, setOfferOffset, shouldFetchMoreOffers }); + } = useLoadMoreOffers({ shouldFetchMoreOffers }); const handleDisableOffer = useCallback(({ offer, adminReason, onSuccess, onError }) => { disableOfferService(offer._id, adminReason).then(() => { @@ -121,7 +120,6 @@ const SearchResultsController = ({ handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, - setOfferOffset, setShouldFetchMoreOffers, hasMoreOffers, infiniteScrollLoading, diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js index ce51ab7b..858f4132 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { setSearchOffers } from "../../../../actions/searchOffersActions"; +import { setSearchOffers, setSearchQueryToken } from "../../../../actions/searchOffersActions"; import Offer from "../Offer/Offer"; import { parseFiltersToURL } from "../../../../utils"; import config from "../../../../config"; @@ -10,10 +10,11 @@ import { SearchResultsConstants } from "./SearchResultsUtils"; const { API_HOSTNAME } = config; -export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { +export default ({ shouldFetchMoreOffers }) => { const dispatch = useDispatch(); const offerSearch = useSelector(({ offerSearch }) => ({ + queryToken: offerSearch.queryToken, value: offerSearch.searchValue, jobType: offerSearch.jobType, jobMinDuration: offerSearch.jobDuration[0], @@ -23,7 +24,6 @@ export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { })); const filters = { - offset: offerOffset, limit: SearchResultsConstants.fetchNewOffersLimit, ...offerSearch, }; @@ -34,24 +34,20 @@ export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { const [hasMoreOffers, setHasMoreOffers] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [fetchedOffsets, setFetchedOffsets] = useState([]); useEffect(() => { if (initialOffersLoading) { - setOfferOffset(0); setHasMoreOffers(true); setError(null); setLoading(false); - setFetchedOffsets([]); } - }, [setOfferOffset, initialOffersLoading]); + }, [initialOffersLoading]); useEffect(() => { const fetchOffers = async () => { try { - setFetchedOffsets((offsets) => [...offsets, filters.offset]); setLoading(true); const query = parseFiltersToURL(filters); const res = await fetch(`${API_HOSTNAME}/offers?${query}`, { @@ -66,7 +62,9 @@ export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { setLoading(false); return; } - const offersData = await res.json(); + const data = await res.json(); + const offersData = data.results; + const queryToken = data.queryToken; const offerIds = [...offers.map((offer) => offer._id)]; const newOffers = [...offers]; @@ -78,6 +76,7 @@ export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { setHasMoreOffers(offersData.length > 0); dispatch(setSearchOffers(newOffers)); + dispatch(setSearchQueryToken(queryToken)); setLoading(false); setError(null); @@ -91,7 +90,7 @@ export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { } }; - if (shouldFetchMoreOffers && !fetchedOffsets.includes(filters.offset) && !initialOffersLoading && !loading) { + if (shouldFetchMoreOffers && !initialOffersLoading && !loading) { fetchOffers().catch((error) => { setError({ cause: ErrorTypes.UNEXPECTED, @@ -102,7 +101,7 @@ export default ({ offerOffset, setOfferOffset, shouldFetchMoreOffers }) => { }, [ dispatch, shouldFetchMoreOffers, filters, initialOffersLoading, - fetchedOffsets, loading, offers, + loading, offers, ]); return { offers, hasMoreOffers, loading, error }; diff --git a/src/reducers/searchOffersReducer.js b/src/reducers/searchOffersReducer.js index cc246a3a..2dc6c98b 100644 --- a/src/reducers/searchOffersReducer.js +++ b/src/reducers/searchOffersReducer.js @@ -28,6 +28,11 @@ export default (state = initialState, action) => { ...state, offers: action.offers, }; + case OfferSearchTypes.SET_SEARCH_QUERY_TOKEN: + return { + ...state, + queryToken: action.queryToken, + }; case OfferSearchTypes.SET_OFFERS_LOADING: return { ...state, diff --git a/src/services/offerService.js b/src/services/offerService.js index 802a8128..9c0e2da0 100644 --- a/src/services/offerService.js +++ b/src/services/offerService.js @@ -1,4 +1,10 @@ -import { setLoadingOffers, setSearchOffers, setOffersFetchError, resetOffersFetchError } from "../actions/searchOffersActions"; +import { + setLoadingOffers, + setSearchOffers, + setSearchQueryToken, + setOffersFetchError, + resetOffersFetchError, +} from "../actions/searchOffersActions"; import Offer from "../components/HomePage/SearchResultsArea/Offer/Offer"; import config from "../config"; import { parseFiltersToURL, buildCancelableRequest } from "../utils"; @@ -46,7 +52,10 @@ export const searchOffers = (filters) => buildCancelableRequest( return; } - dispatch(setSearchOffers(json.map((offerData) => new Offer(offerData)))); + const offers = json.results; + const queryToken = json.queryToken; + dispatch(setSearchOffers(offers.map((offerData) => new Offer(offerData)))); + dispatch(setSearchQueryToken(queryToken)); dispatch(setLoadingOffers(false)); sendSearchReport(filters, `/offers?${query}`); From 8b167348883a283c9e47c3f774e0c328cf43d40d Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Thu, 24 Mar 2022 19:30:31 +0000 Subject: [PATCH 14/24] Fix: warning in OfferItem Skeleton circle attribute --- src/components/HomePage/SearchResultsArea/Offer/OfferItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HomePage/SearchResultsArea/Offer/OfferItem.js b/src/components/HomePage/SearchResultsArea/Offer/OfferItem.js index 142cf37a..2a83c5b4 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/OfferItem.js +++ b/src/components/HomePage/SearchResultsArea/Offer/OfferItem.js @@ -67,7 +67,7 @@ const OfferItem = ({ offer, offerIdx, selectedOfferIdx, setSelectedOfferIdx, loa {loading ? - + : Date: Thu, 24 Mar 2022 20:42:42 +0000 Subject: [PATCH 15/24] Fix: offer search tests --- .../OfferItemsContainer.js | 6 +--- .../OfferItemsContainer.spec.js | 28 +++++++++++++++---- .../SearchResultsDesktop.js | 2 +- .../SearchResultsMobile.spec.js | 19 +++++++++++-- .../SearchResultsWidget.spec.js | 13 ++++++++- .../SearchResultsWidget/useLoadMoreOffers.js | 16 ++--------- 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index d51a8d3f..eb9d834e 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -87,11 +87,7 @@ const OfferItemsContainer = ({ if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMoreOffers) { - setShouldFetchMoreOffers(true); - } else { - setShouldFetchMoreOffers(false); - } + setShouldFetchMoreOffers(entries[0].isIntersecting && hasMoreOffers); }); if (lastOfferNode) observer.current.observe(lastOfferNode); }, [ diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js index 947d904f..36cd8afb 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js @@ -1,18 +1,35 @@ -import { getByRole, getByText, render, screen } from "@testing-library/react"; +import { createTheme } from "@material-ui/core"; +import { getByRole, getByText, screen } from "@testing-library/react"; import React from "react"; +import { renderWithStoreAndTheme } from "../../../../test-utils"; import Offer from "../Offer/Offer"; import OfferItemsContainer from "./OfferItemsContainer"; describe("OfferItemsContainer", () => { + const theme = createTheme(); + const initialState = {}; + + beforeEach(() => { + // IntersectionObserver isn't available in test environment + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; + }); + describe("render", () => { it("should show loading state when loading", () => { - render( + renderWithStoreAndTheme( {}} toggleShowSearchFilters={() => {}} - /> + setShouldFetchMoreOffers={() => {}} + />, { initialState, theme } ); expect(screen.getAllByTestId("offer-item-loading")).toHaveLength(3); }); @@ -43,13 +60,14 @@ describe("OfferItemsContainer", () => { }), ]; - render( + renderWithStoreAndTheme( {}} toggleShowSearchFilters={() => {}} - />); + />, { initialState, theme } + ); const items = await screen.findAllByTestId("offer-item"); expect(items).toHaveLength(2); expect(getByText(items[0], offers[0].title)).toBeInTheDocument(); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js index d3dff348..19e32988 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js @@ -105,7 +105,7 @@ const OfferWidgetSection = ({ ); OfferWidgetSection.propTypes = { - noOffers: PropTypes.bool.isRequired, + noOffers: PropTypes.bool, classes: PropTypes.shape({ searchOfferErrorContainer: PropTypes.string.isRequired, reviseCriteriaErrorMessage: PropTypes.string.isRequired, diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js index f0443523..888a0ec6 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js @@ -2,7 +2,7 @@ import React from "react"; import SearchResultsMobile from "./SearchResultsMobile"; import Offer from "../Offer/Offer"; import { createTheme } from "@material-ui/core/styles"; -import { render, renderWithStoreAndTheme, screen } from "../../../../test-utils"; +import { renderWithStoreAndTheme, screen } from "../../../../test-utils"; import { createMatchMedia } from "../../../../utils/media-queries"; import { waitForElementToBeRemoved } from "@testing-library/dom"; import { Simulate } from "react-dom/test-utils"; @@ -39,14 +39,27 @@ describe("SearchResultsMobile", () => { }), ]; + beforeEach(() => { + // IntersectionObserver isn't available in test environment + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; + }); + describe("render", () => { it("Should render offers if present", () => { + const initialState = {}; + const context = { offers }; - render( + renderWithStoreAndTheme( - + , { initialState, theme } ); expect(screen.getByRole("button", { name: "Adjust Filters" })).toBeInTheDocument(); expect(screen.getByText("position1")).toBeInTheDocument(); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js index 842856fb..51de96e2 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js @@ -38,6 +38,17 @@ describe("SearchResults", () => { }, }; + beforeEach(() => { + // IntersectionObserver isn't available in test environment + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; + }); + it("should display OfferItemsContainer", () => { renderWithStoreAndTheme( @@ -139,7 +150,7 @@ describe("SearchResults", () => { it("should search with updated filters and hide filters on fetch", async () => { - fetch.mockResponse(JSON.stringify(initialState.offerSearch.offers)); + fetch.mockResponse(JSON.stringify({ results: initialState.offerSearch.offers, queryToken: "123" })); renderWithStoreAndTheme( diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js index 858f4132..23e41f07 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js @@ -29,20 +29,11 @@ export default ({ shouldFetchMoreOffers }) => { }; const offers = useSelector((state) => state.offerSearch.offers); - const initialOffersLoading = useSelector((state) => state.offerSearch.loading); const [hasMoreOffers, setHasMoreOffers] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - if (initialOffersLoading) { - setHasMoreOffers(true); - setError(null); - setLoading(false); - } - }, [initialOffersLoading]); - useEffect(() => { const fetchOffers = async () => { @@ -90,7 +81,7 @@ export default ({ shouldFetchMoreOffers }) => { } }; - if (shouldFetchMoreOffers && !initialOffersLoading && !loading) { + if (shouldFetchMoreOffers) { fetchOffers().catch((error) => { setError({ cause: ErrorTypes.UNEXPECTED, @@ -99,10 +90,7 @@ export default ({ shouldFetchMoreOffers }) => { }); } - }, [ - dispatch, shouldFetchMoreOffers, filters, initialOffersLoading, - loading, offers, - ]); + }, [dispatch, shouldFetchMoreOffers, filters, loading, offers, hasMoreOffers]); return { offers, hasMoreOffers, loading, error }; }; From 9c2282f1073f98205f6107f09867e8cf11096d92 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sat, 26 Mar 2022 14:38:14 +0000 Subject: [PATCH 16/24] Add: requested changes --- .../HomePage/SearchArea/SearchArea.js | 2 ++ .../SearchResultsWidget/SearchResultsUtils.js | 4 +-- .../SearchResultsWidget/useLoadMoreOffers.js | 4 +-- .../useLoadMoreOffers.spec.js | 36 +++++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.spec.js diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 78f57b78..398c29b8 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -3,6 +3,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import { searchOffers } from "../../../services/offerService"; +import { SearchResultsConstants } from "../SearchResultsArea/SearchResultsWidget/SearchResultsUtils"; import { setSearchValue, setJobDuration, @@ -58,6 +59,7 @@ export const AdvancedSearchController = ({ jobType, fields, technologies, + limit: SearchResultsConstants.INITIAL_LIMIT, }); if (onSubmit) onSubmit(); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js index c6e14036..64949b6e 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js @@ -1,4 +1,4 @@ export const SearchResultsConstants = { - initialLimit: 5, - fetchNewOffersLimit: 5, + INITIAL_LIMIT: 15, + FETCH_NEW_OFFERS_LIMIT: 10, }; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js index 23e41f07..4b00cf7d 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js @@ -24,7 +24,7 @@ export default ({ shouldFetchMoreOffers }) => { })); const filters = { - limit: SearchResultsConstants.fetchNewOffersLimit, + limit: SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT, ...offerSearch, }; @@ -90,7 +90,7 @@ export default ({ shouldFetchMoreOffers }) => { }); } - }, [dispatch, shouldFetchMoreOffers, filters, loading, offers, hasMoreOffers]); + }, [dispatch, shouldFetchMoreOffers, filters, offers]); return { offers, hasMoreOffers, loading, error }; }; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.spec.js new file mode 100644 index 00000000..1ea22c7d --- /dev/null +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.spec.js @@ -0,0 +1,36 @@ +import React from "react"; +import { useSelector, useDispatch } from "react-redux"; + +import { render } from "../../../../test-utils"; +import useLoadMoreOffers from "./useLoadMoreOffers"; + +jest.mock("react-redux", () => {}); + +// TODO: complete this + +describe("useLoadMoreOffers hook", () => { + + const HookWrapper = ({ notifyHookResult }) => { + const result = useLoadMoreOffers({ shouldFetchMoreOffers: false }); + notifyHookResult(result); + return null; + }; + + it("should return offer data", () => { + + useSelector.mockImplementation(() => {}); + useDispatch.mockImplementation(() => {}); + + const notifyHookResult = jest.fn(); + render( + + ); + + expect(notifyHookResult).toHaveBeenCalledWith(expect.objectContaining({ + offers: [], + hasMoreOffers: false, + loading: false, + error: null, + })); + }); +}); From ac53e0fe2b88852d82295fa37e7737ea9d39a63a Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sat, 26 Mar 2022 18:57:36 +0000 Subject: [PATCH 17/24] Refactor: offer search --- src/actions/searchOffersActions.js | 3 +- .../HomePage/SearchArea/SearchArea.js | 33 +++---- .../OfferItemsContainer.js | 49 +++------- .../SearchResultsDesktop.js | 27 ++---- .../SearchResultsMobile.js | 31 ++---- .../SearchResultsWidget.js | 42 +++++--- .../SearchResultsWidget/useLoadMoreOffers.js | 96 ------------------- .../SearchResultsWidget/useOffersSearcher.js | 78 +++++++++++++++ ...fers.spec.js => useOffersSearcher.spec.js} | 0 src/reducers/searchOffersReducer.js | 8 +- src/services/offerService.js | 72 +++++++------- 11 files changed, 198 insertions(+), 241 deletions(-) delete mode 100644 src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js create mode 100644 src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js rename src/components/HomePage/SearchResultsArea/SearchResultsWidget/{useLoadMoreOffers.spec.js => useOffersSearcher.spec.js} (100%) diff --git a/src/actions/searchOffersActions.js b/src/actions/searchOffersActions.js index ae03fcad..751cdb0f 100644 --- a/src/actions/searchOffersActions.js +++ b/src/actions/searchOffersActions.js @@ -22,9 +22,10 @@ export const setLoadingOffers = (loading) => ({ loading, }); -export const setSearchOffers = (offers) => ({ +export const setSearchOffers = (offers, accumulate) => ({ type: OfferSearchTypes.SET_OFFERS_SEARCH_RESULT, offers, + accumulate, }); export const setSearchQueryToken = (queryToken) => ({ diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 398c29b8..be2a2c36 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -2,7 +2,6 @@ import React, { useCallback } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; -import { searchOffers } from "../../../services/offerService"; import { SearchResultsConstants } from "../SearchResultsArea/SearchResultsWidget/SearchResultsUtils"; import { setSearchValue, @@ -26,13 +25,14 @@ import AdvancedOptionsToggle from "./AdvancedOptionsToggle"; import AdvancedSearchMobile from "./AdvancedSearch/AdvancedSearchMobile"; import AdvancedSearchDesktop from "./AdvancedSearch/AdvancedSearchDesktop"; import useComponentController from "../../../hooks/useComponentController"; +import useOffersSearcher from "../SearchResultsArea/SearchResultsWidget/useOffersSearcher"; export const AdvancedSearchControllerContext = React.createContext({}); export const AdvancedSearchController = ({ enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, - resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, searchOffers, onMobileClose, + resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, onMobileClose, }) => { const advancedSearchProps = useAdvancedSearch({ @@ -51,22 +51,21 @@ export const AdvancedSearchController = ({ resetAdvancedSearchFields, }); + const { search: searchOffers } = useOffersSearcher({ + value: searchValue, + jobMinDuration: showJobDurationSlider && jobMinDuration, + jobMaxDuration: showJobDurationSlider && jobMaxDuration, + jobType, + fields, + technologies, + }); + const submitForm = useCallback((e) => { if (e) e.preventDefault(); - searchOffers({ value: searchValue, - jobMinDuration: showJobDurationSlider && jobMinDuration, - jobMaxDuration: showJobDurationSlider && jobMaxDuration, - jobType, - fields, - technologies, - limit: SearchResultsConstants.INITIAL_LIMIT, - }); + searchOffers(SearchResultsConstants.INITIAL_LIMIT); if (onSubmit) onSubmit(); - }, [ - fields, jobMaxDuration, jobMinDuration, jobType, onSubmit, searchOffers, searchValue, - showJobDurationSlider, technologies, - ]); + }, [onSubmit, searchOffers]); return { ...advancedSearchProps, @@ -83,7 +82,7 @@ export const AdvancedSearchController = ({ }; }; -export const SearchArea = ({ onSubmit, searchOffers, searchValue, +export const SearchArea = ({ onSubmit, searchValue, jobMinDuration = INITIAL_JOB_DURATION, jobMaxDuration = INITIAL_JOB_DURATION + 1, jobType = INITIAL_JOB_TYPE, fields, technologies, showJobDurationSlider, setShowJobDurationSlider, advanced: enableAdvancedSearchDefault = false, setSearchValue, setJobDuration, setJobType, setFields, setTechs, resetAdvancedSearchFields, onMobileClose }) => { @@ -101,7 +100,7 @@ export const SearchArea = ({ onSubmit, searchOffers, searchValue, { enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, - resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, searchOffers, onMobileClose, + resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, onMobileClose, }, AdvancedSearchControllerContext ); @@ -145,7 +144,6 @@ export const SearchArea = ({ onSubmit, searchOffers, searchValue, SearchArea.propTypes = { onSubmit: PropTypes.func, - searchOffers: PropTypes.func.isRequired, searchValue: PropTypes.string.isRequired, jobMinDuration: PropTypes.number, jobMaxDuration: PropTypes.number, @@ -175,7 +173,6 @@ export const mapStateToProps = ({ offerSearch }) => ({ }); export const mapDispatchToProps = (dispatch) => ({ - searchOffers: (filters) => dispatch(searchOffers(filters)), setSearchValue: (value) => dispatch(setSearchValue(value)), setJobDuration: (_, value) => dispatch(setJobDuration(...value)), setJobType: (e) => dispatch(setJobType(e.target.value)), diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index eb9d834e..3e222b36 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -1,6 +1,5 @@ import React, { useRef, useCallback, useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { connect } from "react-redux"; import clsx from "clsx"; import { Button, Divider, List, ListItem, makeStyles } from "@material-ui/core"; import { Tune } from "@material-ui/icons"; @@ -8,7 +7,6 @@ import { Tune } from "@material-ui/icons"; import OfferItem from "../Offer/OfferItem"; import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import LoadingOfferItem from "./LoadingOfferItem"; -import { addSnackbar } from "../../../../actions/notificationActions"; import Offer from "../Offer/Offer"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ @@ -49,17 +47,14 @@ ToggleFiltersButton.propTypes = { }; const OfferItemsContainer = ({ - addSnackbar, - loading, + initialOffersLoading, selectedOfferIdx, setSelectedOfferIdx, showSearchFilters, toggleShowSearchFilters, offers, - setShouldFetchMoreOffers, - hasMoreOffers, - infiniteScrollLoading, - infiniteScrollError, + moreOffersLoading, + loadMoreOffers, }) => { const classes = useSearchResultsWidgetStyles(); @@ -72,35 +67,23 @@ const OfferItemsContainer = ({ useEffect(() => { - if (loading || infiniteScrollLoading) { - setShouldFetchMoreOffers(false); - return; - } - - if (infiniteScrollError) { - addSnackbar({ - message: "An error occurred while fetching new offers", - key: "fetch-new-offers", - }); + if (initialOffersLoading || moreOffersLoading) { return; } if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { - setShouldFetchMoreOffers(entries[0].isIntersecting && hasMoreOffers); + if (entries[0].isIntersecting) loadMoreOffers(); }); if (lastOfferNode) observer.current.observe(lastOfferNode); - }, [ - addSnackbar, hasMoreOffers, infiniteScrollError, infiniteScrollLoading, - lastOfferNode, loading, setShouldFetchMoreOffers, - ]); + }, [initialOffersLoading, lastOfferNode, loadMoreOffers, moreOffersLoading]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); setSelectedOfferIdx(...args); }; - if (loading) + if (initialOffersLoading) return (
))} - {infiniteScrollLoading && } + {moreOffersLoading && } ); }; OfferItemsContainer.propTypes = { - addSnackbar: PropTypes.func, - loading: PropTypes.bool, + initialOffersLoading: PropTypes.bool, + moreOffersLoading: PropTypes.bool, selectedOfferIdx: PropTypes.number, setSelectedOfferIdx: PropTypes.func.isRequired, showSearchFilters: PropTypes.bool, toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), - setShouldFetchMoreOffers: PropTypes.func, - hasMoreOffers: PropTypes.bool, - infiniteScrollLoading: PropTypes.bool, - infiniteScrollError: PropTypes.bool, + loadMoreOffers: PropTypes.func, }; -const mapDispatchToProps = (dispatch) => ({ - addSnackbar: (notification) => dispatch(addSnackbar(notification)), -}); -export default connect(null, mapDispatchToProps)(OfferItemsContainer); +export default OfferItemsContainer; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js index 19e32988..411250e0 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js @@ -12,8 +12,7 @@ import { SearchResultsControllerContext } from "./SearchResultsWidget"; const OffersList = ({ noOffers, classes, selectedOfferIdx, offersLoading, setSelectedOfferIdx, - showSearchFilters, toggleShowSearchFilters, offers, setShouldFetchMoreOffers, - hasMoreOffers, infiniteScrollLoading, infiniteScrollError, + showSearchFilters, toggleShowSearchFilters, offers, moreOffersLoading, loadMoreOffers, }) => ( @@ -26,16 +25,14 @@ const OffersList = ({ : } { handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, - setShouldFetchMoreOffers, - hasMoreOffers, - infiniteScrollLoading, - infiniteScrollError, + moreOffersLoading, + loadMoreOffers, } = useContext(SearchResultsControllerContext); const classes = useSearchResultsWidgetStyles(); @@ -168,10 +159,8 @@ const SearchResultsDesktop = () => { showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} offers={offers} - setShouldFetchMoreOffers={setShouldFetchMoreOffers} - hasMoreOffers={hasMoreOffers} - infiniteScrollLoading={infiniteScrollLoading} - infiniteScrollError={infiniteScrollError} + moreOffersLoading={moreOffersLoading} + loadMoreOffers={loadMoreOffers} /> {showSearchFilters ? ( @@ -25,16 +25,14 @@ const OffersList = ({ : } @@ -55,10 +53,7 @@ OffersList.propTypes = { showSearchFilters: PropTypes.bool.isRequired, toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), - setShouldFetchMoreOffers: PropTypes.func, - hasMoreOffers: PropTypes.bool, - infiniteScrollLoading: PropTypes.bool, - infiniteScrollError: PropTypes.bool, + moreOffersLoading: PropTypes.bool, }; export const OfferViewer = ({ @@ -129,11 +124,8 @@ const SearchResultsMobile = () => { handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, - shouldFetchMoreOffers, - setShouldFetchMoreOffers, - hasMoreOffers, - infiniteScrollLoading, - infiniteScrollError, + moreOffersLoading, + loadMoreOffers, } = useContext(SearchResultsControllerContext); const showOfferDetails = (offerIdx) => { @@ -162,11 +154,8 @@ const SearchResultsMobile = () => { showSearchFilters={showSearchFilters} toggleShowSearchFilters={toggleShowSearchFilters} offers={offers} - shouldFetchMoreOffers={shouldFetchMoreOffers} - setShouldFetchMoreOffers={setShouldFetchMoreOffers} - hasMoreOffers={hasMoreOffers} - infiniteScrollLoading={infiniteScrollLoading} - infiniteScrollError={infiniteScrollError} + moreOffersLoading={moreOffersLoading} + loadMoreOffers={loadMoreOffers} /> {showSearchFilters ? { const [selectedOfferIdx, setSelectedOfferIdx] = useState(null); - const [shouldFetchMoreOffers, setShouldFetchMoreOffers] = useState(false); // Reset the selected offer on every "loading", so that it does not show up after finished loading useEffect(() => { if (offersLoading) setSelectedOfferIdx(null); }, [offersLoading]); - const { - offers, - hasMoreOffers, - loading: infiniteScrollLoading, - error: infiniteScrollError, - } = useLoadMoreOffers({ shouldFetchMoreOffers }); + const { loadMoreOffers, moreOffersLoading } = useOffersSearcher(searchFilters); const handleDisableOffer = useCallback(({ offer, adminReason, onSuccess, onError }) => { disableOfferService(offer._id, adminReason).then(() => { @@ -120,10 +118,8 @@ const SearchResultsController = ({ handleAdminEnableOffer, showSearchFilters, toggleShowSearchFilters, - setShouldFetchMoreOffers, - hasMoreOffers, - infiniteScrollLoading, - infiniteScrollError, + moreOffersLoading, + loadMoreOffers: () => loadMoreOffers(searchQueryToken, SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT), }, }, }; @@ -137,6 +133,8 @@ export const SearchResultsWidget = React.forwardRef(({ disableOffer, companyEnableOffer, adminEnableOffer, + searchFilters, + searchQueryToken, }, ref) => { const classes = useSearchResultsWidgetStyles(); @@ -150,6 +148,8 @@ export const SearchResultsWidget = React.forwardRef(({ disableOffer, companyEnableOffer, adminEnableOffer, + searchFilters, + searchQueryToken, }, SearchResultsControllerContext); return ( @@ -182,11 +182,23 @@ SearchResultsWidget.propTypes = { disableOffer: PropTypes.func, companyEnableOffer: PropTypes.func, adminEnableOffer: PropTypes.func, + searchFilters: PropTypes.object, + searchQueryToken: PropTypes.string, }; -const mapStateToProps = (state) => ({ - offersLoading: state.offerSearch.loading, - offersSearchError: state.offerSearch.error, +const mapStateToProps = ({ offerSearch }) => ({ + offers: offerSearch.offers, + offersLoading: offerSearch.loading, + offersSearchError: offerSearch.error, + searchFilters: { + value: offerSearch.value, + jobMinDuration: offerSearch.jobMinDuration, + jobMaxDuration: offerSearch.jobMaxDuration, + jobType: offerSearch.jobType, + fields: offerSearch.fields, + technologies: offerSearch.technologies, + }, + searchQueryToken: offerSearch.queryToken, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js deleted file mode 100644 index 4b00cf7d..00000000 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.js +++ /dev/null @@ -1,96 +0,0 @@ -import { useEffect, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; - -import { setSearchOffers, setSearchQueryToken } from "../../../../actions/searchOffersActions"; -import Offer from "../Offer/Offer"; -import { parseFiltersToURL } from "../../../../utils"; -import config from "../../../../config"; -import ErrorTypes from "../../../../utils/ErrorTypes"; -import { SearchResultsConstants } from "./SearchResultsUtils"; - -const { API_HOSTNAME } = config; - -export default ({ shouldFetchMoreOffers }) => { - - const dispatch = useDispatch(); - const offerSearch = useSelector(({ offerSearch }) => ({ - queryToken: offerSearch.queryToken, - value: offerSearch.searchValue, - jobType: offerSearch.jobType, - jobMinDuration: offerSearch.jobDuration[0], - jobMaxDuration: offerSearch.jobDuration[1], - fields: offerSearch.fields, - technologies: offerSearch.technologies, - })); - - const filters = { - limit: SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT, - ...offerSearch, - }; - - const offers = useSelector((state) => state.offerSearch.offers); - - const [hasMoreOffers, setHasMoreOffers] = useState(true); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - - const fetchOffers = async () => { - - try { - setLoading(true); - const query = parseFiltersToURL(filters); - const res = await fetch(`${API_HOSTNAME}/offers?${query}`, { - method: "GET", - credentials: "include", - }); - if (!res.ok) { - setError({ - cause: ErrorTypes.BAD_RESPONSE, - error: res.status, - }); - setLoading(false); - return; - } - const data = await res.json(); - const offersData = data.results; - const queryToken = data.queryToken; - - const offerIds = [...offers.map((offer) => offer._id)]; - const newOffers = [...offers]; - - offersData.forEach((offerData) => { - if (!offerIds.includes(offerData._id)) - newOffers.push(new Offer(offerData)); - }); - - setHasMoreOffers(offersData.length > 0); - dispatch(setSearchOffers(newOffers)); - dispatch(setSearchQueryToken(queryToken)); - setLoading(false); - setError(null); - - } catch (error) { - setError({ - cause: ErrorTypes.NETWORK_FAILURE, - error, - }); - setLoading(false); - return; - } - }; - - if (shouldFetchMoreOffers) { - fetchOffers().catch((error) => { - setError({ - cause: ErrorTypes.UNEXPECTED, - error, - }); - }); - } - - }, [dispatch, shouldFetchMoreOffers, filters, offers]); - - return { offers, hasMoreOffers, loading, error }; -}; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js new file mode 100644 index 00000000..de561cb8 --- /dev/null +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js @@ -0,0 +1,78 @@ +import { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { + resetOffersFetchError, + setLoadingOffers, + setOffersFetchError, + setSearchOffers, + setSearchQueryToken, +} from "../../../../actions/searchOffersActions"; +import { + searchOffers, + BadResponseException, + NetworkFailureException, +} from "../../../../services/offerService"; + +export default (filters) => { + + const dispatch = useDispatch(); + const [hasMoreOffers, setHasMoreOffers] = useState(true); + const [moreOffersFetchError, setMoreOffersFetchError] = useState(null); + const [moreOffersLoading, setMoreOffersLoading] = useState(false); + + // isInitialRequest = true on the first time the search request is made + // the following request will have isInitialRequest = false + const loadOffers = useCallback((isInitialRequest) => async (queryToken, limit) => { + + if (!hasMoreOffers) return; + + if (isInitialRequest) { + dispatch(resetOffersFetchError()); + dispatch(setLoadingOffers(true)); + } else { + setMoreOffersFetchError(null); + setMoreOffersLoading(true); + } + + try { + const { queryToken: updatedQueryToken, results } = await searchOffers({ queryToken, limit, ...filters }); + dispatch(setSearchQueryToken((updatedQueryToken))); + dispatch(setSearchOffers(results, !isInitialRequest)); + + if (results.length === 0) setHasMoreOffers(false); + + if (isInitialRequest) { + dispatch(setLoadingOffers(false)); + } else { + setMoreOffersLoading(false); + } + + } catch (e) { + if (e instanceof BadResponseException) { + if (isInitialRequest) { + dispatch(setOffersFetchError(e.error)); + dispatch(setLoadingOffers(false)); + } else { + setMoreOffersFetchError(e.error); + setMoreOffersLoading(false); + } + } else if (e instanceof NetworkFailureException) { + if (isInitialRequest) { + dispatch(setOffersFetchError(e.error)); + dispatch(setLoadingOffers(false)); + } else { + setMoreOffersFetchError(e.error); + setMoreOffersLoading(false); + } + } + } + + }, [dispatch, filters, hasMoreOffers]); + + return { + search: (...args) => loadOffers(true)(null, ...args), + loadMoreOffers: loadOffers(false), + moreOffersLoading, + moreOffersFetchError, + }; +}; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js similarity index 100% rename from src/components/HomePage/SearchResultsArea/SearchResultsWidget/useLoadMoreOffers.spec.js rename to src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js diff --git a/src/reducers/searchOffersReducer.js b/src/reducers/searchOffersReducer.js index 2dc6c98b..eb6fba7c 100644 --- a/src/reducers/searchOffersReducer.js +++ b/src/reducers/searchOffersReducer.js @@ -24,10 +24,16 @@ export default (state = initialState, action) => { switch (action.type) { case OfferSearchTypes.SET_OFFERS_SEARCH_RESULT: + { + let offers = action.offers; + if (action.accumulate) { + offers = [...state.offers, offers]; + } return { ...state, - offers: action.offers, + offers, }; + } case OfferSearchTypes.SET_SEARCH_QUERY_TOKEN: return { ...state, diff --git a/src/services/offerService.js b/src/services/offerService.js index 9c0e2da0..bbfd3b07 100644 --- a/src/services/offerService.js +++ b/src/services/offerService.js @@ -1,10 +1,3 @@ -import { - setLoadingOffers, - setSearchOffers, - setSearchQueryToken, - setOffersFetchError, - resetOffersFetchError, -} from "../actions/searchOffersActions"; import Offer from "../components/HomePage/SearchResultsArea/Offer/Offer"; import config from "../config"; import { parseFiltersToURL, buildCancelableRequest } from "../utils"; @@ -21,14 +14,25 @@ const OFFER_HIDE_METRIC_ID = "offer/hide"; const OFFER_DISABLE_METRIC_ID = "offer/disable"; const OFFER_ENABLE_METRIC_ID = "offer/enable"; +export class BadResponseException extends Error { + constructor(error) { + super("Bad Response"); + this.error = error; + } +} -export const searchOffers = (filters) => buildCancelableRequest( - measureTime(TIMED_ACTIONS.OFFER_SEARCH, async (dispatch, { signal }) => { - dispatch(resetOffersFetchError()); - dispatch(setLoadingOffers(true)); +export class NetworkFailureException extends Error { + constructor(error) { + super("Network Failure"); + this.error = error; + } +} +export const searchOffers = buildCancelableRequest( + measureTime(TIMED_ACTIONS.OFFER_SEARCH, async ({ queryToken, ...filters }, { signal }) => { + let isErrorRegistered = false; try { - const query = parseFiltersToURL(filters); + const query = queryToken ? `queryToken=${queryToken}` : parseFiltersToURL(filters); const res = await fetch(`${API_HOSTNAME}/offers?${query}`, { method: "GET", credentials: "include", @@ -37,42 +41,42 @@ export const searchOffers = (filters) => buildCancelableRequest( const json = await res.json(); if (!res.ok) { - dispatch(setOffersFetchError({ - cause: ErrorTypes.BAD_RESPONSE, - error: res.status, - })); - dispatch(setLoadingOffers(false)); - + isErrorRegistered = true; createErrorEvent( OFFER_SEARCH_METRIC_ID, ErrorTypes.BAD_RESPONSE, json.errors, res.status ); - return; + throw new BadResponseException({ + cause: ErrorTypes.BAD_RESPONSE, + error: res.status, + }); } const offers = json.results; const queryToken = json.queryToken; - dispatch(setSearchOffers(offers.map((offerData) => new Offer(offerData)))); - dispatch(setSearchQueryToken(queryToken)); - dispatch(setLoadingOffers(false)); - + sendSearchReport(filters, `/offers?${query}`); createEvent(EVENT_TYPES.SUCCESS(OFFER_SEARCH_METRIC_ID, query)); - } catch (error) { - dispatch(setOffersFetchError({ - cause: ErrorTypes.NETWORK_FAILURE, - error, - })); - dispatch(setLoadingOffers(false)); + return { + updatedQueryToken, + results: offers.map((offerData) => new Offer(offerData)), + }; - createErrorEvent( - OFFER_SEARCH_METRIC_ID, - ErrorTypes.BAD_RESPONSE, - Array.isArray(error) ? error : [{ msg: Constants.UNEXPECTED_ERROR_MESSAGE }], - ); + } catch (error) { + if (!isErrorRegistered) { + createErrorEvent( + OFFER_SEARCH_METRIC_ID, + ErrorTypes.BAD_RESPONSE, + Array.isArray(error) ? error : [{ msg: Constants.UNEXPECTED_ERROR_MESSAGE }], + ); + throw new NetworkFailureException({ + cause: ErrorTypes.NETWORK_FAILURE, + error, + }); + } else throw error; } }) ); From 61624ab39e62e0a6720969830137b3e145a17620 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sat, 26 Mar 2022 19:37:47 +0000 Subject: [PATCH 18/24] Fix: load more offers in search bug --- .../SearchResultsWidget/OfferItemsContainer.js | 6 ++++-- .../SearchResultsWidget/SearchResultsDesktop.js | 5 ++++- .../SearchResultsWidget/SearchResultsMobile.js | 5 ++++- .../SearchResultsWidget/SearchResultsWidget.js | 1 + .../SearchResultsWidget/useOffersSearcher.js | 4 ++-- src/reducers/searchOffersReducer.js | 2 +- src/services/offerService.js | 4 ++-- 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 3e222b36..2e572cd2 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -8,6 +8,7 @@ import OfferItem from "../Offer/OfferItem"; import useSearchResultsWidgetStyles from "./searchResultsWidgetStyles"; import LoadingOfferItem from "./LoadingOfferItem"; import Offer from "../Offer/Offer"; +import { SearchResultsConstants } from "./SearchResultsUtils"; const useAdvancedSearchButtonStyles = makeStyles((theme) => ({ root: { @@ -55,6 +56,7 @@ const OfferItemsContainer = ({ offers, moreOffersLoading, loadMoreOffers, + searchQueryToken, }) => { const classes = useSearchResultsWidgetStyles(); @@ -73,10 +75,10 @@ const OfferItemsContainer = ({ if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting) loadMoreOffers(); + if (entries[0].isIntersecting) loadMoreOffers(searchQueryToken, SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT); }); if (lastOfferNode) observer.current.observe(lastOfferNode); - }, [initialOffersLoading, lastOfferNode, loadMoreOffers, moreOffersLoading]); + }, [initialOffersLoading, lastOfferNode, loadMoreOffers, moreOffersLoading, searchQueryToken]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js index 411250e0..28da5307 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js @@ -12,7 +12,7 @@ import { SearchResultsControllerContext } from "./SearchResultsWidget"; const OffersList = ({ noOffers, classes, selectedOfferIdx, offersLoading, setSelectedOfferIdx, - showSearchFilters, toggleShowSearchFilters, offers, moreOffersLoading, loadMoreOffers, + showSearchFilters, toggleShowSearchFilters, offers, moreOffersLoading, loadMoreOffers, searchQueryToken, }) => ( @@ -33,6 +33,7 @@ const OffersList = ({ offers={offers} moreOffersLoading={moreOffersLoading} loadMoreOffers={loadMoreOffers} + searchQueryToken={searchQueryToken} /> } { toggleShowSearchFilters, moreOffersLoading, loadMoreOffers, + searchQueryToken, } = useContext(SearchResultsControllerContext); const classes = useSearchResultsWidgetStyles(); @@ -161,6 +163,7 @@ const SearchResultsDesktop = () => { offers={offers} moreOffersLoading={moreOffersLoading} loadMoreOffers={loadMoreOffers} + searchQueryToken={searchQueryToken} /> {showSearchFilters ? ( @@ -33,6 +33,7 @@ const OffersList = ({ offers={offers} moreOffersLoading={moreOffersLoading} loadMoreOffers={loadMoreOffers} + searchQueryToken={searchQueryToken} /> } @@ -126,6 +127,7 @@ const SearchResultsMobile = () => { toggleShowSearchFilters, moreOffersLoading, loadMoreOffers, + searchQueryToken, } = useContext(SearchResultsControllerContext); const showOfferDetails = (offerIdx) => { @@ -156,6 +158,7 @@ const SearchResultsMobile = () => { offers={offers} moreOffersLoading={moreOffersLoading} loadMoreOffers={loadMoreOffers} + searchQueryToken={searchQueryToken} /> {showSearchFilters ? loadMoreOffers(searchQueryToken, SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT), }, }, diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js index de561cb8..a5c59dd9 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js @@ -35,7 +35,7 @@ export default (filters) => { } try { - const { queryToken: updatedQueryToken, results } = await searchOffers({ queryToken, limit, ...filters }); + const { updatedQueryToken, results } = await searchOffers({ queryToken, limit, ...filters }); dispatch(setSearchQueryToken((updatedQueryToken))); dispatch(setSearchOffers(results, !isInitialRequest)); @@ -71,7 +71,7 @@ export default (filters) => { return { search: (...args) => loadOffers(true)(null, ...args), - loadMoreOffers: loadOffers(false), + loadMoreOffers: (queryToken, ...args) => loadOffers(false)(queryToken, ...args), moreOffersLoading, moreOffersFetchError, }; diff --git a/src/reducers/searchOffersReducer.js b/src/reducers/searchOffersReducer.js index eb6fba7c..c07b18f1 100644 --- a/src/reducers/searchOffersReducer.js +++ b/src/reducers/searchOffersReducer.js @@ -27,7 +27,7 @@ export default (state = initialState, action) => { { let offers = action.offers; if (action.accumulate) { - offers = [...state.offers, offers]; + offers = [...state.offers, ...offers]; } return { ...state, diff --git a/src/services/offerService.js b/src/services/offerService.js index bbfd3b07..58d0e764 100644 --- a/src/services/offerService.js +++ b/src/services/offerService.js @@ -29,10 +29,10 @@ export class NetworkFailureException extends Error { } export const searchOffers = buildCancelableRequest( - measureTime(TIMED_ACTIONS.OFFER_SEARCH, async ({ queryToken, ...filters }, { signal }) => { + measureTime(TIMED_ACTIONS.OFFER_SEARCH, async ({ queryToken, limit, ...filters }, { signal }) => { let isErrorRegistered = false; try { - const query = queryToken ? `queryToken=${queryToken}` : parseFiltersToURL(filters); + const query = parseFiltersToURL(queryToken ? { queryToken, limit } : { ...filters, limit }); const res = await fetch(`${API_HOSTNAME}/offers?${query}`, { method: "GET", credentials: "include", From 9df8c3f67881deb73cd6d48afc0e974e270b7995 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Sat, 26 Mar 2022 21:06:17 +0000 Subject: [PATCH 19/24] Refactor: infinite scrolling is now using scroll percentage as a trigger --- .../OfferItemsContainer.js | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 2e572cd2..eebfa567 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -1,4 +1,4 @@ -import React, { useRef, useCallback, useState, useEffect } from "react"; +import React, { useCallback, useState, useEffect } from "react"; import PropTypes from "prop-types"; import clsx from "clsx"; import { Button, Divider, List, ListItem, makeStyles } from "@material-ui/core"; @@ -60,25 +60,38 @@ const OfferItemsContainer = ({ }) => { const classes = useSearchResultsWidgetStyles(); - const [lastOfferNode, setLastOfferNode] = useState(null); + const [offerResultsWrapperNode, setOfferResultsWrapperNode] = useState(null); + const [scrollPercentage, setScrollPercentage] = useState(0); - const observer = useRef(); - const lastOfferElementRef = useCallback((node) => { - if (node) setLastOfferNode(node); + // BUG: there is no refetching of new offers when the initial_limit is not enough + + const refetchTriggerRef = useCallback((node) => { + if (node) setOfferResultsWrapperNode(node.parentElement); }, []); + const onScroll = useCallback(() => { + if (offerResultsWrapperNode) + setScrollPercentage( + 100 * offerResultsWrapperNode.scrollTop + / (offerResultsWrapperNode.scrollHeight - offerResultsWrapperNode.clientHeight) + ); + }, [offerResultsWrapperNode]); + + useEffect(() => { + if (!offerResultsWrapperNode) return; + offerResultsWrapperNode.addEventListener("scroll", onScroll); + // eslint-disable-next-line consistent-return + return () => offerResultsWrapperNode.removeEventListener("scroll", onScroll); + }, [offerResultsWrapperNode, onScroll]); + useEffect(() => { if (initialOffersLoading || moreOffersLoading) { return; } - if (observer.current) observer.current.disconnect(); - observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting) loadMoreOffers(searchQueryToken, SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT); - }); - if (lastOfferNode) observer.current.observe(lastOfferNode); - }, [initialOffersLoading, lastOfferNode, loadMoreOffers, moreOffersLoading, searchQueryToken]); + if (scrollPercentage > 80) loadMoreOffers(searchQueryToken, SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT); + }, [initialOffersLoading, loadMoreOffers, moreOffersLoading, scrollPercentage, searchQueryToken]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); @@ -99,6 +112,7 @@ const OfferItemsContainer = ({
toggleShowSearchFilters()} /> - {offers.map((offer, i) => ( -
- {i !== 0 && } - -
- ))} +
+ {offers.map((offer, i) => ( +
+ {i !== 0 && } + +
+ ))} +
{moreOffersLoading && }
@@ -133,6 +149,7 @@ OfferItemsContainer.propTypes = { toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), loadMoreOffers: PropTypes.func, + searchQueryToken: PropTypes.string, }; From 432758855d19bc58e4c9fc893d8de96e8d7b9aa1 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Mon, 28 Mar 2022 09:30:34 +0100 Subject: [PATCH 20/24] Fix: load more offers when there is no scroll --- .../OfferItemsContainer.js | 28 +++++++++++++++++-- .../SearchResultsWidget/SearchResultsUtils.js | 4 +-- .../SearchResultsWidget/useOffersSearcher.js | 7 +++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index eebfa567..c24f1101 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -62,6 +62,12 @@ const OfferItemsContainer = ({ const [offerResultsWrapperNode, setOfferResultsWrapperNode] = useState(null); const [scrollPercentage, setScrollPercentage] = useState(0); + const [hasScroll, setHasScroll] = useState(undefined); + + const isVerticalScrollable = useCallback((node) => { + const overflowY = window.getComputedStyle(node)["overflow-y"]; + return (overflowY === "scroll" || overflowY === "auto") && node.scrollHeight > node.clientHeight; + }, []); // BUG: there is no refetching of new offers when the initial_limit is not enough @@ -70,28 +76,44 @@ const OfferItemsContainer = ({ }, []); const onScroll = useCallback(() => { - if (offerResultsWrapperNode) + if (offerResultsWrapperNode) { setScrollPercentage( 100 * offerResultsWrapperNode.scrollTop / (offerResultsWrapperNode.scrollHeight - offerResultsWrapperNode.clientHeight) ); + + } }, [offerResultsWrapperNode]); useEffect(() => { if (!offerResultsWrapperNode) return; + offerResultsWrapperNode.addEventListener("scroll", onScroll); // eslint-disable-next-line consistent-return return () => offerResultsWrapperNode.removeEventListener("scroll", onScroll); }, [offerResultsWrapperNode, onScroll]); + useEffect(() => { + if (!offerResultsWrapperNode) return; + + setHasScroll(isVerticalScrollable(offerResultsWrapperNode)); + }, [isVerticalScrollable, offerResultsWrapperNode, offers, initialOffersLoading, scrollPercentage, moreOffersLoading]); + + useEffect(() => { + if (initialOffersLoading) { + setHasScroll(undefined); + setScrollPercentage(0); + } + }, [initialOffersLoading]); + useEffect(() => { if (initialOffersLoading || moreOffersLoading) { return; } - if (scrollPercentage > 80) loadMoreOffers(searchQueryToken, SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT); - }, [initialOffersLoading, loadMoreOffers, moreOffersLoading, scrollPercentage, searchQueryToken]); + if (scrollPercentage > 80 || hasScroll === false) loadMoreOffers(searchQueryToken, SearchResultsConstants.FETCH_NEW_OFFERS_LIMIT); + }, [hasScroll, initialOffersLoading, loadMoreOffers, moreOffersLoading, scrollPercentage, searchQueryToken]); const handleOfferSelection = (...args) => { toggleShowSearchFilters(false); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js index 64949b6e..df5eda46 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js @@ -1,4 +1,4 @@ export const SearchResultsConstants = { - INITIAL_LIMIT: 15, - FETCH_NEW_OFFERS_LIMIT: 10, + INITIAL_LIMIT: 2, + FETCH_NEW_OFFERS_LIMIT: 1, }; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js index a5c59dd9..3002b762 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js @@ -16,6 +16,8 @@ import { export default (filters) => { const dispatch = useDispatch(); + + // TODO: Move this to redux!!! const [hasMoreOffers, setHasMoreOffers] = useState(true); const [moreOffersFetchError, setMoreOffersFetchError] = useState(null); const [moreOffersLoading, setMoreOffersLoading] = useState(false); @@ -24,12 +26,13 @@ export default (filters) => { // the following request will have isInitialRequest = false const loadOffers = useCallback((isInitialRequest) => async (queryToken, limit) => { - if (!hasMoreOffers) return; - if (isInitialRequest) { dispatch(resetOffersFetchError()); dispatch(setLoadingOffers(true)); + setHasMoreOffers(true); } else { + if (!hasMoreOffers) return; + setMoreOffersFetchError(null); setMoreOffersLoading(true); } From 3d62263a92aab5045f565252e585cda71602c4b7 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Mon, 28 Mar 2022 18:46:42 +0100 Subject: [PATCH 21/24] Fix: bug regarding the fact that the search and loadMoreOffers function do not share state --- .../OfferItemsContainer.js | 2 -- .../OfferItemsContainer.spec.js | 25 ++++--------------- .../SearchResultsDesktop.js | 3 +++ .../SearchResultsMobile.js | 2 ++ .../SearchResultsMobile.spec.js | 19 +++----------- .../SearchResultsWidget/SearchResultsUtils.js | 4 +-- .../SearchResultsWidget.spec.js | 11 -------- .../SearchResultsWidget/useOffersSearcher.js | 20 ++++++++++++--- 8 files changed, 32 insertions(+), 54 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index c24f1101..9c3ed7bc 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -69,8 +69,6 @@ const OfferItemsContainer = ({ return (overflowY === "scroll" || overflowY === "auto") && node.scrollHeight > node.clientHeight; }, []); - // BUG: there is no refetching of new offers when the initial_limit is not enough - const refetchTriggerRef = useCallback((node) => { if (node) setOfferResultsWrapperNode(node.parentElement); }, []); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js index 36cd8afb..63efeb27 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.spec.js @@ -1,35 +1,20 @@ -import { createTheme } from "@material-ui/core"; import { getByRole, getByText, screen } from "@testing-library/react"; import React from "react"; -import { renderWithStoreAndTheme } from "../../../../test-utils"; +import { render } from "../../../../test-utils"; import Offer from "../Offer/Offer"; import OfferItemsContainer from "./OfferItemsContainer"; describe("OfferItemsContainer", () => { - const theme = createTheme(); - const initialState = {}; - - beforeEach(() => { - // IntersectionObserver isn't available in test environment - const mockIntersectionObserver = jest.fn(); - mockIntersectionObserver.mockReturnValue({ - observe: () => null, - unobserve: () => null, - disconnect: () => null, - }); - window.IntersectionObserver = mockIntersectionObserver; - }); - describe("render", () => { it("should show loading state when loading", () => { - renderWithStoreAndTheme( + render( {}} toggleShowSearchFilters={() => {}} setShouldFetchMoreOffers={() => {}} - />, { initialState, theme } + /> ); expect(screen.getAllByTestId("offer-item-loading")).toHaveLength(3); }); @@ -60,13 +45,13 @@ describe("OfferItemsContainer", () => { }), ]; - renderWithStoreAndTheme( + render( {}} toggleShowSearchFilters={() => {}} - />, { initialState, theme } + /> ); const items = await screen.findAllByTestId("offer-item"); expect(items).toHaveLength(2); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js index 28da5307..9058ecd1 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsDesktop.js @@ -61,6 +61,9 @@ OffersList.propTypes = { showSearchFilters: PropTypes.bool.isRequired, toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), + moreOffersLoading: PropTypes.bool, + loadMoreOffers: PropTypes.func, + searchQueryToken: PropTypes.string, }; const OfferWidgetSection = ({ diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js index 97044921..3b293e71 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.js @@ -55,6 +55,8 @@ OffersList.propTypes = { toggleShowSearchFilters: PropTypes.func.isRequired, offers: PropTypes.arrayOf(PropTypes.instanceOf(Offer)), moreOffersLoading: PropTypes.bool, + loadMoreOffers: PropTypes.func, + searchQueryToken: PropTypes.string, }; export const OfferViewer = ({ diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js index 888a0ec6..f0443523 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js @@ -2,7 +2,7 @@ import React from "react"; import SearchResultsMobile from "./SearchResultsMobile"; import Offer from "../Offer/Offer"; import { createTheme } from "@material-ui/core/styles"; -import { renderWithStoreAndTheme, screen } from "../../../../test-utils"; +import { render, renderWithStoreAndTheme, screen } from "../../../../test-utils"; import { createMatchMedia } from "../../../../utils/media-queries"; import { waitForElementToBeRemoved } from "@testing-library/dom"; import { Simulate } from "react-dom/test-utils"; @@ -39,27 +39,14 @@ describe("SearchResultsMobile", () => { }), ]; - beforeEach(() => { - // IntersectionObserver isn't available in test environment - const mockIntersectionObserver = jest.fn(); - mockIntersectionObserver.mockReturnValue({ - observe: () => null, - unobserve: () => null, - disconnect: () => null, - }); - window.IntersectionObserver = mockIntersectionObserver; - }); - describe("render", () => { it("Should render offers if present", () => { - const initialState = {}; - const context = { offers }; - renderWithStoreAndTheme( + render( - , { initialState, theme } +
); expect(screen.getByRole("button", { name: "Adjust Filters" })).toBeInTheDocument(); expect(screen.getByText("position1")).toBeInTheDocument(); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js index df5eda46..64949b6e 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsUtils.js @@ -1,4 +1,4 @@ export const SearchResultsConstants = { - INITIAL_LIMIT: 2, - FETCH_NEW_OFFERS_LIMIT: 1, + INITIAL_LIMIT: 15, + FETCH_NEW_OFFERS_LIMIT: 10, }; diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js index 51de96e2..33d41bc8 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js @@ -38,17 +38,6 @@ describe("SearchResults", () => { }, }; - beforeEach(() => { - // IntersectionObserver isn't available in test environment - const mockIntersectionObserver = jest.fn(); - mockIntersectionObserver.mockReturnValue({ - observe: () => null, - unobserve: () => null, - disconnect: () => null, - }); - window.IntersectionObserver = mockIntersectionObserver; - }); - it("should display OfferItemsContainer", () => { renderWithStoreAndTheme( diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js index 3002b762..7a06d1cd 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js @@ -1,5 +1,5 @@ -import { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; +import { useCallback, useState, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { resetOffersFetchError, setLoadingOffers, @@ -16,12 +16,26 @@ import { export default (filters) => { const dispatch = useDispatch(); + const searchQueryToken = useSelector((state) => state.offerSearch.queryToken); - // TODO: Move this to redux!!! const [hasMoreOffers, setHasMoreOffers] = useState(true); const [moreOffersFetchError, setMoreOffersFetchError] = useState(null); const [moreOffersLoading, setMoreOffersLoading] = useState(false); + // The "search" and "loadMoreOffers" functions do not share the same state; + // When we run "setHasMoreOffers(false)" on the "loadMoreOffers" function, + // the "search" function does not know that the "hasMoreOffers" variable has changed; + // In the same way, when we run "setHasMoreOffers(true) on the "search" function, + // the "loadMoreOffers" function does not know that the "hasMoreOffers" variable has changed; + // Then, when the "loadMoreOffers" function is executed after a previous execution where the + // "hasMoreOffers" variable became "false", the state of this variable is still false, which + // prevents the fetching of new offers. + // Knowing that, I needed a way to set the "hasMoreOffers" variable to "true" when the previous fact + // happens: setting the "hasMoreOffers" to true when the "queryToken" (which is stored in redux) changes + useEffect(() => { + setHasMoreOffers(true); + }, [searchQueryToken]); + // isInitialRequest = true on the first time the search request is made // the following request will have isInitialRequest = false const loadOffers = useCallback((isInitialRequest) => async (queryToken, limit) => { From 127781d2e820bd25820b9e6a8484c2040ac4ac92 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Fri, 8 Apr 2022 20:14:28 +0100 Subject: [PATCH 22/24] Fix: search offers tests --- .../AdvancedSearchDesktop.spec.js | 57 +++++---- .../AdvancedSearchMobile.spec.js | 67 ++++++---- .../HomePage/SearchArea/SearchArea.js | 8 +- .../HomePage/SearchArea/SearchArea.spec.js | 118 +++--------------- .../OfferItemsContainer.js | 2 +- .../OfferItemsContainer.spec.js | 7 +- .../SearchResultsMobile.spec.js | 3 +- .../SearchResultsWidget.spec.js | 28 ++++- .../SearchResultsWidget/useOffersSearcher.js | 3 +- .../useOffersSearcher.spec.js | 42 +++---- src/reducers/searchOffersReducer.spec.js | 4 +- src/services/offerService.js | 4 +- 12 files changed, 159 insertions(+), 184 deletions(-) diff --git a/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchDesktop.spec.js b/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchDesktop.spec.js index a86cdfee..25b067f1 100644 --- a/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchDesktop.spec.js +++ b/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchDesktop.spec.js @@ -2,13 +2,14 @@ import React from "react"; import AdvancedSearchDesktop from "./AdvancedSearchDesktop"; import JobOptions from "../../../utils/offers/JobOptions"; -import { render, screen } from "../../../../test-utils"; +import { renderWithStoreAndTheme, screen } from "../../../../test-utils"; import { AdvancedSearchControllerContext, AdvancedSearchController } from "../SearchArea"; import { fireEvent } from "@testing-library/dom"; import useComponentController from "../../../../hooks/useComponentController"; import FieldOptions from "../../../utils/offers/FieldOptions"; import TechOptions from "../../../utils/offers/TechOptions"; import { INITIAL_JOB_DURATION, INITIAL_JOB_TYPE } from "../../../../reducers/searchOffersReducer"; +import { createTheme } from "@material-ui/core/styles"; const AdvancedSearchWrapper = ({ children, enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration = INITIAL_JOB_DURATION, @@ -36,13 +37,18 @@ const AdvancedSearchWrapper = ({ }; describe("AdvancedSearchDesktop", () => { + + const theme = createTheme(); + const initialState = {}; + describe("render", () => { it("should render a job selector with all job types", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); fireEvent.mouseDown(screen.getByLabelText("Job Type")); @@ -60,7 +66,7 @@ describe("AdvancedSearchDesktop", () => { it("should toggle job duration slider (on)", () => { const setShowJobDurationSliderMock = jest.fn(); - render( + renderWithStoreAndTheme( { setShowJobDurationSlider={setShowJobDurationSliderMock} > - + , + { initialState, theme } ); expect(screen.getByText("Job Duration: 1 - 2 months")).not.toBeVisible(); @@ -81,7 +88,7 @@ describe("AdvancedSearchDesktop", () => { it("should toggle job duration slider (off)", () => { const setShowJobDurationSliderMock = jest.fn(); - render( + renderWithStoreAndTheme( { setShowJobDurationSlider={setShowJobDurationSliderMock} > - + , + { initialState, theme } ); @@ -101,10 +109,11 @@ describe("AdvancedSearchDesktop", () => { it("should render a fields selector with all field types", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); fireEvent.mouseDown(screen.getByLabelText("Fields", { selector: "input" })); @@ -126,10 +135,11 @@ describe("AdvancedSearchDesktop", () => { it("should render a technologies selector with all technology types", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); fireEvent.mouseDown(screen.getByLabelText("Technologies", { selector: "input" })); @@ -151,12 +161,13 @@ describe("AdvancedSearchDesktop", () => { it("should disable reset button if no advanced field is set", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getByRole("button", { name: "Reset Advanced Fields" })).toBeDisabled(); @@ -164,13 +175,14 @@ describe("AdvancedSearchDesktop", () => { it("should enable reset button if some advanced field is set", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getByRole("button", { name: "Reset Advanced Fields" })).not.toBeDisabled(); @@ -184,14 +196,15 @@ describe("AdvancedSearchDesktop", () => { const setFieldsMock = jest.fn(); - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getAllByTestId("chip-option", {})).toHaveLength(1); @@ -213,14 +226,15 @@ describe("AdvancedSearchDesktop", () => { const setTechsMock = jest.fn(); - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getAllByTestId("chip-option", {})).toHaveLength(1); @@ -241,7 +255,7 @@ describe("AdvancedSearchDesktop", () => { it("should call resetAdvancedSearch when Reset button is clicked", () => { const resetFn = jest.fn(); - render( + renderWithStoreAndTheme( {}} @@ -253,7 +267,8 @@ describe("AdvancedSearchDesktop", () => { technologies={[Object.keys(TechOptions)[0]]} // Must have something set to be able to click reset > - + , + { initialState, theme } ); fireEvent.click(screen.getByRole("button", { name: "Reset Advanced Fields" })); diff --git a/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.spec.js b/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.spec.js index aad5c619..69cbf5b0 100644 --- a/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.spec.js +++ b/src/components/HomePage/SearchArea/AdvancedSearch/AdvancedSearchMobile.spec.js @@ -8,7 +8,8 @@ import { INITIAL_JOB_DURATION, INITIAL_JOB_TYPE } from "../../../../reducers/sea import { fireEvent } from "@testing-library/dom"; import FieldOptions from "../../../utils/offers/FieldOptions"; import TechOptions from "../../../utils/offers/TechOptions"; -import { render, screen } from "../../../../test-utils"; +import { renderWithStoreAndTheme, screen } from "../../../../test-utils"; +import { createTheme } from "@material-ui/core/styles"; const AdvancedSearchWrapper = ({ children, enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration = INITIAL_JOB_DURATION, @@ -38,13 +39,18 @@ const AdvancedSearchWrapper = ({ }; describe("AdvancedSearchMobile", () => { + + const theme = createTheme(); + const initialState = {}; + describe("render", () => { it("should render a dialog title with a button to close", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getByText("Advanced Search")).toBeInTheDocument(); @@ -53,10 +59,11 @@ describe("AdvancedSearchMobile", () => { }); it("should render a SearchBar", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getByLabelText("Search")).toBeInTheDocument(); @@ -64,10 +71,11 @@ describe("AdvancedSearchMobile", () => { it("should render a job selector with all job types", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); fireEvent.mouseDown(screen.getByLabelText("Job Type")); @@ -85,7 +93,7 @@ describe("AdvancedSearchMobile", () => { it("should toggle job duration slider (on)", () => { const setShowJobDurationSliderMock = jest.fn(); - render( + renderWithStoreAndTheme( { setShowJobDurationSlider={setShowJobDurationSliderMock} > - + , + { initialState, theme } ); expect(screen.getByText("Job Duration: 1 - 2 months")).not.toBeVisible(); @@ -106,7 +115,7 @@ describe("AdvancedSearchMobile", () => { it("should toggle job duration slider (off)", () => { const setShowJobDurationSliderMock = jest.fn(); - render( + renderWithStoreAndTheme( { setShowJobDurationSlider={setShowJobDurationSliderMock} > - + , + { initialState, theme } ); @@ -126,10 +136,11 @@ describe("AdvancedSearchMobile", () => { it("should render a fields selector with all field types", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); fireEvent.mouseDown(screen.getByLabelText("Fields", { selector: "input" })); @@ -151,10 +162,11 @@ describe("AdvancedSearchMobile", () => { it("should render a technologies selector with all technology types", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); fireEvent.mouseDown(screen.getByLabelText("Technologies", { selector: "input" })); @@ -176,12 +188,13 @@ describe("AdvancedSearchMobile", () => { it("should disable reset button if no advanced field is set", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getByRole("button", { name: "Reset" })).toBeDisabled(); @@ -189,13 +202,14 @@ describe("AdvancedSearchMobile", () => { it("should enable reset button if some advanced field is set", () => { - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getByRole("button", { name: "Reset" })).not.toBeDisabled(); @@ -208,14 +222,15 @@ describe("AdvancedSearchMobile", () => { const setFieldsMock = jest.fn(); - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getAllByTestId("chip-option", {})).toHaveLength(1); @@ -237,14 +252,15 @@ describe("AdvancedSearchMobile", () => { const setTechsMock = jest.fn(); - render( + renderWithStoreAndTheme( - + , + { initialState, theme } ); expect(screen.getAllByTestId("chip-option", {})).toHaveLength(1); @@ -265,7 +281,7 @@ describe("AdvancedSearchMobile", () => { it("should call resetAdvancedSearch when Reset button is clicked", () => { const resetFn = jest.fn(); - render( + renderWithStoreAndTheme( {}} @@ -278,7 +294,8 @@ describe("AdvancedSearchMobile", () => { technologies={[Object.keys(TechOptions)[0]]} // Must have something set to be able to click reset > - + , + { initialState, theme } ); fireEvent.click(screen.getByRole("button", { name: "Reset" })); diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index be2a2c36..b57c355c 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -110,12 +110,14 @@ export const SearchArea = ({ onSubmit, searchValue,
{!useDesktop() ? - + : - + } { let onSubmit; const theme = createTheme(); + const initialState = {}; beforeEach(() => { onSubmit = jest.fn(); }); describe("render", () => { - it("should render a paper", () => { - expect( - mountWithTheme( - , - theme - ).find(Paper).exists() - ).toBe(true); - }); - - it("should render a form", () => { - expect(mountWithTheme( - , - theme - ).find("form").first().prop("id")).toEqual("search_form"); - }); - - it("should render a SearchBar", () => { - const searchBar = mountWithTheme( + it("should render a Paper, a Form, a Search Bar, a Search Button and Advanced Options Button", () => { + renderWithStoreAndTheme( , - theme - ).find(SearchBar).first(); - expect(searchBar.exists()).toBe(true); - }); - - it("should render an Advanced Search Area", () => { - const wrapper = mountWithTheme( - , - theme + { initialState, theme } ); - expect(wrapper.find(AdvancedSearchDesktop).exists() || wrapper.find(AdvancedSearchMobile).exists()).toBe(true); - }); - it("should render a SearchButton", () => { - const searchArea = mountWithTheme( - , - theme - ); - const button = searchArea.find(SubmitSearchButton).first(); - expect(button.exists()).toBe(true); - }); - - it("should render an Advanced Options Button with the correct icon", () => { - const searchValue = "test"; - const setSearchValue = () => {}; - const submitSearchForm = () => {}; - - const wrapper = mountWithTheme( - , - theme - ); - expect(wrapper.find(AdvancedOptionsToggle).exists()).toBe(true); + expect(screen.getByTestId("search-area-paper")).toBeInTheDocument(); + expect(screen.getByTestId("search_form")).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: "Search" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Search" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Toggle Advanced Search" })).toBeInTheDocument(); }); }); describe("interaction", () => { - it("should call onSubmit callback on form submit", () => { - const addSnackbar = () => {}; - const searchOffersMock = jest.fn(); - - // Simulate request success - fetch.mockResponse(JSON.stringify({ mockData: true })); - - const form = mountWithTheme( - , - theme - ).find("form#search_form").first(); - - form.simulate("submit", { - preventDefault: () => {}, - }); - expect(onSubmit).toHaveBeenCalledTimes(1); - expect(searchOffersMock).toHaveBeenCalledTimes(1); - }); - - it("should call searchOffers and onSubmit callback on search button click", () => { + it("should call onSubmit callback on search button click", async () => { const searchValue = "test"; const setSearchValue = () => {}; - const searchOffers = jest.fn(); const onSubmit = jest.fn(); const addSnackbar = () => {}; // Simulate request success fetch.mockResponse(JSON.stringify({ mockData: true })); - const wrapper = mountWithTheme( + renderWithStoreAndTheme( , - theme + { initialState, theme } ); - wrapper.find(Fab).simulate("click", { preventDefault: () => {} }); - expect(searchOffers).toHaveBeenCalledTimes(1); + const searchButton = screen.getByRole("button", { name: "Search" }); + + await act(async () => { + await fireEvent.click(searchButton); + }); + expect(onSubmit).toHaveBeenCalledTimes(1); }); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js index 9c3ed7bc..51d9b46a 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/OfferItemsContainer.js @@ -141,7 +141,7 @@ const OfferItemsContainer = ({ onClick={() => toggleShowSearchFilters()} />
- {offers.map((offer, i) => ( + {offers?.map((offer, i) => (
{i !== 0 && } { it("should show loading state when loading", () => { render( {}} toggleShowSearchFilters={() => {}} - setShouldFetchMoreOffers={() => {}} + loadMoreOffers={() => {}} /> ); expect(screen.getAllByTestId("offer-item-loading")).toHaveLength(3); @@ -48,9 +48,10 @@ describe("OfferItemsContainer", () => { render( {}} toggleShowSearchFilters={() => {}} + loadMoreOffers={() => {}} /> ); const items = await screen.findAllByTestId("offer-item"); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js index f0443523..e0ef41f3 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsMobile.spec.js @@ -42,7 +42,7 @@ describe("SearchResultsMobile", () => { describe("render", () => { it("Should render offers if present", () => { - const context = { offers }; + const context = { offers, loadMoreOffers: () => {} }; render( @@ -89,6 +89,7 @@ describe("SearchResultsMobile", () => { setSelectedOfferIdx: setSelectedOfferIdxMock, selectedOfferIdx: 0, toggleShowSearchFilters: () => {}, + loadMoreOffers: () => {}, }; renderWithStoreAndTheme( diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js index 33d41bc8..1d8c75e1 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js @@ -1,9 +1,11 @@ import React from "react"; import SearchResultsWidget, { SearchResultsControllerContext } from "./SearchResultsWidget"; -import { renderWithStoreAndTheme, screen } from "../../../../test-utils"; +import { renderWithStoreAndTheme, screen, fireEvent, act } from "../../../../test-utils"; import { createTheme } from "@material-ui/core/styles"; import Offer from "../Offer/Offer"; -import { fireEvent } from "@testing-library/dom"; +import { searchOffers } from "../../../../services/offerService"; + +jest.mock("../../../../services/offerService"); describe("SearchResults", () => { const theme = createTheme(); @@ -38,6 +40,8 @@ describe("SearchResults", () => { }, }; + afterEach(() => jest.clearAllMocks()); + it("should display OfferItemsContainer", () => { renderWithStoreAndTheme( @@ -139,7 +143,15 @@ describe("SearchResults", () => { it("should search with updated filters and hide filters on fetch", async () => { - fetch.mockResponse(JSON.stringify({ results: initialState.offerSearch.offers, queryToken: "123" })); + searchOffers.mockImplementation(({ queryToken }) => { + let offers = []; + if (queryToken === null) + offers = initialState.offerSearch.offers; + return { + updatedQueryToken: "123", + results: offers, + }; + }); renderWithStoreAndTheme( @@ -157,16 +169,20 @@ describe("SearchResults", () => { } ); - fireEvent.click(screen.getByRole("button", { name: "Adjust Filters" })); + await act(async () => { + await fireEvent.click(screen.getByRole("button", { name: "Adjust Filters" })); + }); expect(screen.getAllByTestId("offer-item")).toHaveLength(1); - fireEvent.submit(screen.getByRole("form")); + + await act(async () => { + await fireEvent.submit(screen.getByLabelText("Search Area")); + }); // must wait response from server, otherwise it will be 'loading', hence the await + find expect(await screen.findAllByTestId("offer-item")).toHaveLength(2); expect(screen.getByRole("button", { name: "Adjust Filters" })).toBeInTheDocument(); expect(screen.queryByLabelText("Search", { selector: "input" })).not.toBeInTheDocument(); - }); }); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js index 7a06d1cd..b45a951f 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.js @@ -53,7 +53,8 @@ export default (filters) => { try { const { updatedQueryToken, results } = await searchOffers({ queryToken, limit, ...filters }); - dispatch(setSearchQueryToken((updatedQueryToken))); + + dispatch(setSearchQueryToken(updatedQueryToken)); dispatch(setSearchOffers(results, !isInitialRequest)); if (results.length === 0) setHasMoreOffers(false); diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js index 1ea22c7d..d926e177 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js @@ -1,8 +1,8 @@ -import React from "react"; -import { useSelector, useDispatch } from "react-redux"; +// import React from "react"; +// import { useSelector, useDispatch } from "react-redux"; -import { render } from "../../../../test-utils"; -import useLoadMoreOffers from "./useLoadMoreOffers"; +// import { render } from "../../../../test-utils"; +// import useLoadMoreOffers from "./useLoadMoreOffers"; jest.mock("react-redux", () => {}); @@ -10,27 +10,27 @@ jest.mock("react-redux", () => {}); describe("useLoadMoreOffers hook", () => { - const HookWrapper = ({ notifyHookResult }) => { - const result = useLoadMoreOffers({ shouldFetchMoreOffers: false }); - notifyHookResult(result); - return null; - }; + // const HookWrapper = ({ notifyHookResult }) => { + // const result = useLoadMoreOffers({ shouldFetchMoreOffers: false }); + // notifyHookResult(result); + // return null; + // }; it("should return offer data", () => { - useSelector.mockImplementation(() => {}); - useDispatch.mockImplementation(() => {}); + // useSelector.mockImplementation(() => {}); + // useDispatch.mockImplementation(() => {}); - const notifyHookResult = jest.fn(); - render( - - ); + // const notifyHookResult = jest.fn(); + // render( + // + // ); - expect(notifyHookResult).toHaveBeenCalledWith(expect.objectContaining({ - offers: [], - hasMoreOffers: false, - loading: false, - error: null, - })); + // expect(notifyHookResult).toHaveBeenCalledWith(expect.objectContaining({ + // offers: [], + // hasMoreOffers: false, + // loading: false, + // error: null, + // })); }); }); diff --git a/src/reducers/searchOffersReducer.spec.js b/src/reducers/searchOffersReducer.spec.js index a2621afc..f213ae62 100644 --- a/src/reducers/searchOffersReducer.spec.js +++ b/src/reducers/searchOffersReducer.spec.js @@ -155,7 +155,7 @@ describe("Search Offers Reducer", () => { adminReason: null, }), new Offer({ - _id: "id1", + _id: "id2", title: "position1", owner: "company_id", ownerName: "company1", @@ -170,7 +170,7 @@ describe("Search Offers Reducer", () => { adminReason: null, }), new Offer({ - _id: "id1", + _id: "id3", title: "position1", owner: "company_id", ownerName: "company1", diff --git a/src/services/offerService.js b/src/services/offerService.js index 58d0e764..8dd2ee88 100644 --- a/src/services/offerService.js +++ b/src/services/offerService.js @@ -55,8 +55,8 @@ export const searchOffers = buildCancelableRequest( } const offers = json.results; - const queryToken = json.queryToken; - + const updatedQueryToken = json.queryToken; + sendSearchReport(filters, `/offers?${query}`); createEvent(EVENT_TYPES.SUCCESS(OFFER_SEARCH_METRIC_ID, query)); From ecb6b42944b385fd2cefc5703b816c49fbf9beef Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Thu, 14 Apr 2022 00:18:54 +0100 Subject: [PATCH 23/24] Add: test for load more offers feature --- .../SearchResultsWidget.spec.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js index 1d8c75e1..ffc9f8dd 100644 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js +++ b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/SearchResultsWidget.spec.js @@ -185,4 +185,52 @@ describe("SearchResults", () => { expect(screen.getByRole("button", { name: "Adjust Filters" })).toBeInTheDocument(); expect(screen.queryByLabelText("Search", { selector: "input" })).not.toBeInTheDocument(); }); + + it("should fetch initial offers and load more until there are no more", async () => { + + // TODO: discover why the last loadMoreOffers was called with the first mock implementation + + searchOffers + .mockImplementationOnce(() => ({ + updatedQueryToken: "123", + results: [], + })) + .mockImplementationOnce(() => ({ + updatedQueryToken: "456", + results: [initialState.offerSearch.offers[0]], + })) + .mockImplementationOnce(() => ({ + updatedQueryToken: "90", + results: [initialState.offerSearch.offers[1]], + })); + + renderWithStoreAndTheme( + + + , + { + initialState: { + ...initialState, + offerSearch: { + ...initialState.offerSearch, + offers: [initialState.offerSearch.offers[0]], + }, + }, + theme, + } + ); + + await act(async () => { + await fireEvent.click(screen.getByRole("button", { name: "Adjust Filters" })); + }); + + await act(async () => { + await fireEvent.submit(screen.getByLabelText("Search Area")); + }); + + await new Promise((r) => setTimeout(r, 2000)); + + // must wait response from server, otherwise it will be 'loading', hence the await + find + expect(await screen.findAllByTestId("offer-item")).toHaveLength(2); + }); }); From b004555220f2e6f34d6e7d59d51e5ab8fc8e2a43 Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Thu, 19 May 2022 20:42:41 +0100 Subject: [PATCH 24/24] Remove: unused file --- .../useOffersSearcher.spec.js | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js diff --git a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js b/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js deleted file mode 100644 index d926e177..00000000 --- a/src/components/HomePage/SearchResultsArea/SearchResultsWidget/useOffersSearcher.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -// import React from "react"; -// import { useSelector, useDispatch } from "react-redux"; - -// import { render } from "../../../../test-utils"; -// import useLoadMoreOffers from "./useLoadMoreOffers"; - -jest.mock("react-redux", () => {}); - -// TODO: complete this - -describe("useLoadMoreOffers hook", () => { - - // const HookWrapper = ({ notifyHookResult }) => { - // const result = useLoadMoreOffers({ shouldFetchMoreOffers: false }); - // notifyHookResult(result); - // return null; - // }; - - it("should return offer data", () => { - - // useSelector.mockImplementation(() => {}); - // useDispatch.mockImplementation(() => {}); - - // const notifyHookResult = jest.fn(); - // render( - // - // ); - - // expect(notifyHookResult).toHaveBeenCalledWith(expect.objectContaining({ - // offers: [], - // hasMoreOffers: false, - // loading: false, - // error: null, - // })); - }); -});