diff --git a/package-lock.json b/package-lock.json index 77451da..b804fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11814,11 +11814,11 @@ } }, "match-sorter": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-4.2.1.tgz", - "integrity": "sha512-s+3h9TiZU9U1pWhIERHf8/f4LmBN6IXaRgo2CI17+XGByGS1GvG5VvXK9pcGyCjGe3WM3mSYRC3ipGrd5UEVgw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.2.0.tgz", + "integrity": "sha512-yhmUTR5q6JP/ssR1L1y083Wp+C+TdR8LhYTxWI4IRgEUr8IXJu2mE6L3SwryCgX95/5J7qZdEg0G091sOxr1FQ==", "requires": { - "@babel/runtime": "^7.10.5", + "@babel/runtime": "^7.12.5", "remove-accents": "0.4.2" } }, @@ -14465,19 +14465,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-query": { - "version": "2.26.3", - "resolved": "https://registry.npmjs.org/react-query/-/react-query-2.26.3.tgz", - "integrity": "sha512-h4bhZioVY3kXfo+GLNg0zDn9XvbZkvK3I5eTzW82zpRwtJvUtxrkhrAyZ5N6A0Z9WPeRyo+384JubT5RkMaxEQ==", - "requires": { - "@babel/runtime": "^7.5.5" - } - }, - "react-query-devtools": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-query-devtools/-/react-query-devtools-2.6.3.tgz", - "integrity": "sha512-pSvWq5Q8zgIP7QbF0+4BerCHLaLn5HPzce7sIXYqz4XEizcYJHkJtcrAwn6bUkCu5JmAt1Y7fViQtZwOIG2SYA==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.9.6.tgz", + "integrity": "sha512-EA0HDhryEYSsiVBkCtqpPAnKbIayUqJNz2eImEPgyVaaUxQhG53rnruknt4E1l3bL3nj+RbXFf7zUV5Q6kJgBw==", "requires": { - "match-sorter": "^4.1.0" + "@babel/runtime": "^7.5.5", + "match-sorter": "^6.0.2" } }, "react-refresh": { diff --git a/package.json b/package.json index 5200346..9aa908b 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,7 @@ "query-string": "^6.13.8", "react": "^17.0.1", "react-dom": "^17.0.1", - "react-query": "^2.26.3", - "react-query-devtools": "^2.6.3", + "react-query": "^3.9.6", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", "web-vitals": "^0.2.4" diff --git a/src/components/CardItem.jsx b/src/components/CardItem.jsx index 61361f2..0841294 100644 --- a/src/components/CardItem.jsx +++ b/src/components/CardItem.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Text, Image, Box, Flex } from '@chakra-ui/react'; import { Link, useLocation } from 'react-router-dom'; -function CardItem({ item, galleryArray }) { +function CardItem({ item, pagesArray }) { const background = useLocation(); return ( <> @@ -12,7 +12,7 @@ function CardItem({ item, galleryArray }) { state: { modal: true, background, - galleryArray, + pagesArray, item, }, }} @@ -22,10 +22,11 @@ function CardItem({ item, galleryArray }) { borderRadius="lg" background="#b62a07" overflow="hidden" - h="400px" - padding="5" + padding="4" maxW="sm" + height="100%" direction="column" + justifyContent="space-between" margin="auto" boxShadow="1px 1px 4px 1px black" > @@ -33,10 +34,11 @@ function CardItem({ item, galleryArray }) { src={item.image} alt={item.title} fit="cover" - p="10px" - borderRadius="20px" - mb="10px" + p="0px" + borderRadius="lg" + height="100%" /> + {item.title} diff --git a/src/components/CardList.jsx b/src/components/CardList.jsx deleted file mode 100644 index 7c6074c..0000000 --- a/src/components/CardList.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { React } from 'react'; -import { SimpleGrid } from '@chakra-ui/react'; - -import CardItem from './CardItem'; - -function CardList({ items }) { - return ( - - {items && - items.map((item) => { - return ; - })} - - ); -} - -export default CardList; diff --git a/src/components/ItemRecipe.jsx b/src/components/ItemRecipe.jsx index 26bbb26..c58e08d 100644 --- a/src/components/ItemRecipe.jsx +++ b/src/components/ItemRecipe.jsx @@ -20,12 +20,6 @@ function ItemRecipe() { return response.json(); } throw Error(`code ${response.status}`); - }, - { - refetchOnWindowFocus: false, - refetchOnMount: false, - staleTime: 3600000, - cacheTime: 3600000, } ); @@ -89,7 +83,7 @@ function ItemRecipe() { /> ) : ( - Read the detailed instructions on + Read the detailed instructions on{' '} {`${data.sourceName}`} )} diff --git a/src/components/ModalSwitch.js b/src/components/ModalSwitch.jsx similarity index 100% rename from src/components/ModalSwitch.js rename to src/components/ModalSwitch.jsx diff --git a/src/components/NutritionalTable.jsx b/src/components/NutritionalTable.jsx index 25650c0..ae45ab9 100644 --- a/src/components/NutritionalTable.jsx +++ b/src/components/NutritionalTable.jsx @@ -41,7 +41,7 @@ function NutritionalTable({ data }) { {nutrient.name} {`${nutrient.amount} ${nutrient.unit}`} - {nutrient.percentOfDailyNeeds} + {Math.round(nutrient.percentOfDailyNeeds)} ))} diff --git a/src/components/SearchFieldCollapse.jsx b/src/components/SearchFieldCollapse.jsx index 9b76cda..0ceb291 100644 --- a/src/components/SearchFieldCollapse.jsx +++ b/src/components/SearchFieldCollapse.jsx @@ -8,7 +8,7 @@ import { useDisclosure, Collapse, Button, - HStack, + ButtonGroup, } from '@chakra-ui/react'; import { SearchIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'; import { useHistory } from 'react-router-dom'; @@ -65,7 +65,8 @@ function SearchFieldCollapse({ userQueryData }) { }); }; - const SearchQuery = () => { + const SearchQuery = (e) => { + e.preventDefault(); const query = queryString.stringify(userQuery, { arrayFormat: 'comma', skipEmptyString: true, @@ -75,11 +76,11 @@ function SearchFieldCollapse({ userQueryData }) { }; return ( - - + + - - + + } - onClick={() => SearchQuery()} + onClick={(e) => SearchQuery(e)} size="sm" - mr="1rem" bg="#b62a07" + mr="1px" color="white" _hover={{ bg: '#6e1a05', }} _focus={{ border: 'none' }} + type="submit" /> - + diff --git a/src/components/SearchResults.jsx b/src/components/SearchResults.jsx index 679e8e2..e9edfbf 100644 --- a/src/components/SearchResults.jsx +++ b/src/components/SearchResults.jsx @@ -1,31 +1,60 @@ -import React from 'react'; -import { useQuery } from 'react-query'; -import { Text, Flex, Box, Spinner } from '@chakra-ui/react'; +import React, { useCallback, useState } from 'react'; +import { + Box, + SimpleGrid, + Button, + Text, + Flex, + Spinner, + Center, +} from '@chakra-ui/react'; +import { useInfiniteQuery } from 'react-query'; -import CardList from './CardList'; +import CardItem from './CardItem'; +import useIntersectionObserver from '../hooks/useIntersectionObsever'; function SearchResults({ query }) { - const queryURL = `${process.env.REACT_APP_SEARCH_URL + query}&apiKey=${ - process.env.REACT_APP_KEY - }`; + const itemsPerQuery = 50; + const queryURL = (offset) => + `${process.env.REACT_APP_SEARCH_URL + query}&apiKey=${ + process.env.REACT_APP_KEY + }&number=${itemsPerQuery}&offset=${offset}`; - const { isLoading, error, data } = useQuery( - ['foodData', queryURL], - async () => { - const response = await fetch(queryURL); + const getParams = (lastPage) => + lastPage.totalResults - lastPage.offset > itemsPerQuery + ? lastPage.offset + itemsPerQuery + : false; + + const { + isLoading, + error, + data, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + } = useInfiniteQuery( + ['foodData', queryURL(0)], + async ({ pageParam = 0 }) => { + const response = await fetch(queryURL(pageParam)); if (response.ok) { return response.json(); } throw Error(`code ${response.status}`); }, { - refetchOnWindowFocus: false, - refetchOnMount: false, - staleTime: 3600000, - cacheTime: 3600000, + getNextPageParam: (lastPage) => getParams(lastPage), } ); + // could have used useRef, but the ref won't get updated with the DOM element until a rerender + const [buttonRef, setButtonRef] = useState(); + + useIntersectionObserver({ + target: buttonRef, + onIntersect: useCallback(fetchNextPage, [getParams, fetchNextPage]), + enabled: hasNextPage, + }); + if (isLoading) return ( @@ -62,7 +91,38 @@ function SearchResults({ query }) { } return ( - + + {data && + data.pages.map((page) => + page.results.map((item) => { + return ( + + ); + }) + )} + + +
+ +
); } diff --git a/src/components/ViewRecipe.jsx b/src/components/ViewRecipe.jsx index 1405a24..3e44c6b 100644 --- a/src/components/ViewRecipe.jsx +++ b/src/components/ViewRecipe.jsx @@ -21,10 +21,12 @@ function ViewRecipe() { useEffect(onOpen, [onOpen]); const { state = {} } = location; - const { modal, background = {}, galleryArray = [], item = {} } = state; + const { modal, background = {}, pagesArray = [], item = {} } = state; const { pathname, search } = background; + const galleryArray = pagesArray.map((page) => page.results).flat(); + const currentItemIndex = galleryArray.indexOf(item); const maxIndex = galleryArray.length - 1; @@ -75,7 +77,7 @@ function ViewRecipe() { state: { modal, background, - galleryArray, + pagesArray, item: prevItem, }, }} @@ -100,7 +102,7 @@ function ViewRecipe() { state: { modal, background, - galleryArray, + pagesArray, item: nextItem, }, }} diff --git a/src/hooks/useIntersectionObsever.jsx b/src/hooks/useIntersectionObsever.jsx new file mode 100644 index 0000000..4be8a9d --- /dev/null +++ b/src/hooks/useIntersectionObsever.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +export default function useIntersectionObserver({ + root, + target, + onIntersect, + threshold = 1.0, + rootMargin = '0px', + enabled = true, +}) { + // // Target and root must be set with useState, useRef kind of works after the first render, this always works. + + React.useEffect(() => { + // do nothing if not enabled (ie: when there's nothing more to load) + if (!enabled) { + return undefined; + } + // create new observer, and for each observed element, if isIntersecting is true, execute onIntercect function + const observer = new IntersectionObserver( + (entries) => + entries.forEach((entry) => entry.isIntersecting && onIntersect()), + { + root, + rootMargin, + threshold, + } + ); + + // if there is no target, this hook does nothing + if (!target) { + return undefined; + } + + observer.observe(target); + // cleanup function + return () => observer.unobserve(target); + }, [enabled, onIntersect, root, rootMargin, target, threshold]); +} diff --git a/src/index.js b/src/index.js index 4689d02..5cfa263 100644 --- a/src/index.js +++ b/src/index.js @@ -2,22 +2,31 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { ChakraProvider } from '@chakra-ui/react'; import { BrowserRouter } from 'react-router-dom'; -import { QueryCache, ReactQueryCacheProvider } from 'react-query'; -import { ReactQueryDevtools } from 'react-query-devtools'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { ReactQueryDevtools } from 'react-query/devtools'; import './styles/index.css'; import App from './components/App'; -const queryCache = new QueryCache(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + refetchOnMount: false, + staleTime: 3600000, + cacheTime: 3600000, + }, + }, +}); ReactDOM.render( - + - + ,