2020-04-24 21:17:03 -07:00
|
|
|
import React from "react";
|
|
|
|
import gql from "graphql-tag";
|
|
|
|
import { Box, Text } from "@chakra-ui/core";
|
|
|
|
import { useQuery } from "@apollo/react-hooks";
|
|
|
|
|
|
|
|
import { Delay, Heading1, useDebounce } from "./util";
|
|
|
|
import ItemList, { ItemListSkeleton } from "./ItemList";
|
|
|
|
import { itemAppearanceFragment } from "./OutfitPreview";
|
|
|
|
|
2020-04-25 01:55:48 -07:00
|
|
|
function SearchPanel({
|
|
|
|
query,
|
|
|
|
outfitState,
|
|
|
|
dispatchToOutfit,
|
|
|
|
getScrollParent,
|
|
|
|
}) {
|
2020-04-24 21:17:03 -07:00
|
|
|
return (
|
|
|
|
<Box color="green.800">
|
2020-04-25 00:43:01 -07:00
|
|
|
<Heading1 mb="4">Searching for "{query}"</Heading1>
|
2020-04-24 21:17:03 -07:00
|
|
|
<SearchResults
|
|
|
|
query={query}
|
|
|
|
outfitState={outfitState}
|
|
|
|
dispatchToOutfit={dispatchToOutfit}
|
2020-04-25 01:55:48 -07:00
|
|
|
getScrollParent={getScrollParent}
|
2020-04-24 21:17:03 -07:00
|
|
|
/>
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function SearchResults({ query, outfitState, dispatchToOutfit }) {
|
2020-04-25 00:43:01 -07:00
|
|
|
const { speciesId, colorId } = outfitState;
|
2020-04-24 21:17:03 -07:00
|
|
|
|
|
|
|
const debouncedQuery = useDebounce(query, 300, { waitForFirstPause: true });
|
2020-04-25 02:18:03 -07:00
|
|
|
const [isEndOfResults, setIsEndOfResults] = React.useState(false);
|
2020-04-24 21:17:03 -07:00
|
|
|
|
2020-04-25 02:18:03 -07:00
|
|
|
React.useEffect(() => {
|
|
|
|
setIsEndOfResults(false);
|
|
|
|
}, [query]);
|
|
|
|
|
|
|
|
const { loading, error, data, fetchMore } = useQuery(
|
2020-04-24 21:17:03 -07:00
|
|
|
gql`
|
2020-04-25 01:55:48 -07:00
|
|
|
query($query: String!, $speciesId: ID!, $colorId: ID!, $offset: Int!) {
|
2020-04-25 00:43:01 -07:00
|
|
|
itemSearchToFit(
|
|
|
|
query: $query
|
|
|
|
speciesId: $speciesId
|
|
|
|
colorId: $colorId
|
2020-04-25 01:55:48 -07:00
|
|
|
offset: $offset
|
|
|
|
limit: 50
|
2020-04-25 00:43:01 -07:00
|
|
|
) {
|
2020-04-25 01:55:48 -07:00
|
|
|
query
|
|
|
|
items {
|
|
|
|
# TODO: De-dupe this from useOutfitState?
|
|
|
|
id
|
|
|
|
name
|
|
|
|
thumbnailUrl
|
|
|
|
|
|
|
|
appearanceOn(speciesId: $speciesId, colorId: $colorId) {
|
|
|
|
# This enables us to quickly show the item when the user clicks it!
|
|
|
|
...AppearanceForOutfitPreview
|
|
|
|
|
|
|
|
# This is used to group items by zone, and to detect conflicts when
|
|
|
|
# wearing a new item.
|
|
|
|
layers {
|
|
|
|
zone {
|
|
|
|
id
|
|
|
|
label
|
|
|
|
}
|
2020-04-24 21:17:03 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
${itemAppearanceFragment}
|
|
|
|
`,
|
|
|
|
{
|
2020-04-25 01:55:48 -07:00
|
|
|
variables: { query: debouncedQuery, speciesId, colorId, offset: 0 },
|
2020-04-24 21:17:03 -07:00
|
|
|
skip: debouncedQuery === null,
|
2020-04-25 01:55:48 -07:00
|
|
|
notifyOnNetworkStatusChange: true,
|
2020-04-25 02:18:03 -07:00
|
|
|
onCompleted: (d) => {
|
|
|
|
const items = d && d.itemSearchToFit && d.itemSearchToFit.items;
|
|
|
|
if (items && items.length < 30) {
|
|
|
|
setIsEndOfResults(true);
|
|
|
|
}
|
|
|
|
},
|
2020-04-24 21:17:03 -07:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2020-04-25 01:55:48 -07:00
|
|
|
const result = data && data.itemSearchToFit;
|
|
|
|
const resultQuery = result && result.query;
|
|
|
|
const items = (result && result.items) || [];
|
|
|
|
|
|
|
|
const onScrolledToBottom = React.useCallback(() => {
|
2020-04-25 02:18:03 -07:00
|
|
|
if (!loading && !isEndOfResults) {
|
2020-04-25 01:55:48 -07:00
|
|
|
fetchMore({
|
|
|
|
variables: {
|
|
|
|
offset: items.length,
|
|
|
|
},
|
|
|
|
updateQuery: (prev, { fetchMoreResult }) => {
|
|
|
|
if (!fetchMoreResult) return prev;
|
2020-04-25 02:18:03 -07:00
|
|
|
|
|
|
|
// Note: This is a bit awkward because, if the results count ends on
|
|
|
|
// a multiple of 30, the user will see a flash of loading before
|
|
|
|
// getting told it's actually the end. Ah well :/
|
|
|
|
//
|
|
|
|
// We could maybe make this more rigorous later with
|
|
|
|
// react-virtualized to have a better scrollbar anyway, and then
|
|
|
|
// we'd need to return the total result count... a bit annoying to
|
|
|
|
// potentially double the query runtime? We'd need to see how slow it
|
|
|
|
// actually makes things.
|
|
|
|
if (fetchMoreResult.itemSearchToFit.items.length < 30) {
|
|
|
|
setIsEndOfResults(true);
|
|
|
|
}
|
|
|
|
|
2020-04-25 01:55:48 -07:00
|
|
|
return {
|
|
|
|
...prev,
|
|
|
|
itemSearchToFit: {
|
|
|
|
...prev.itemSearchToFit,
|
|
|
|
items: [
|
|
|
|
...prev.itemSearchToFit.items,
|
|
|
|
...fetchMoreResult.itemSearchToFit.items,
|
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2020-04-25 02:18:03 -07:00
|
|
|
}, [loading, isEndOfResults, fetchMore, items.length]);
|
2020-04-25 01:55:48 -07:00
|
|
|
|
|
|
|
if (resultQuery !== query || (loading && items.length === 0)) {
|
2020-04-24 21:17:03 -07:00
|
|
|
return (
|
|
|
|
<Delay ms={500}>
|
2020-04-24 21:23:03 -07:00
|
|
|
<ItemListSkeleton count={8} />
|
2020-04-24 21:17:03 -07:00
|
|
|
</Delay>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
return (
|
|
|
|
<Text color="green.500">
|
|
|
|
We hit an error trying to load your search results{" "}
|
|
|
|
<span role="img" aria-label="(sweat emoji)">
|
|
|
|
😓
|
|
|
|
</span>{" "}
|
|
|
|
Try again?
|
|
|
|
</Text>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
return (
|
|
|
|
<Text color="green.500">
|
|
|
|
We couldn't find any matching items{" "}
|
|
|
|
<span role="img" aria-label="(thinking emoji)">
|
|
|
|
🤔
|
|
|
|
</span>{" "}
|
|
|
|
Try again?
|
|
|
|
</Text>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2020-04-25 01:55:48 -07:00
|
|
|
<ScrollTracker threshold={300} onScrolledToBottom={onScrolledToBottom}>
|
|
|
|
<ItemList
|
|
|
|
items={items}
|
|
|
|
outfitState={outfitState}
|
|
|
|
dispatchToOutfit={dispatchToOutfit}
|
|
|
|
/>
|
|
|
|
{items && loading && <ItemListSkeleton count={8} />}
|
|
|
|
</ScrollTracker>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-25 02:18:03 -07:00
|
|
|
function ScrollTracker({ children, query, threshold, onScrolledToBottom }) {
|
2020-04-25 01:55:48 -07:00
|
|
|
const containerRef = React.useRef();
|
|
|
|
const scrollParent = React.useRef();
|
|
|
|
|
|
|
|
const onScroll = React.useCallback(
|
|
|
|
(e) => {
|
|
|
|
const topEdgeScrollPosition = e.target.scrollTop;
|
|
|
|
const bottomEdgeScrollPosition =
|
|
|
|
topEdgeScrollPosition + e.target.clientHeight;
|
|
|
|
const remainingScrollDistance =
|
|
|
|
e.target.scrollHeight - bottomEdgeScrollPosition;
|
|
|
|
if (remainingScrollDistance < threshold) {
|
|
|
|
onScrolledToBottom();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[onScrolledToBottom, threshold]
|
2020-04-24 21:17:03 -07:00
|
|
|
);
|
2020-04-25 01:55:48 -07:00
|
|
|
|
|
|
|
React.useLayoutEffect(() => {
|
|
|
|
if (!containerRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (let el = containerRef.current; el.parentNode; el = el.parentNode) {
|
2020-04-25 02:18:03 -07:00
|
|
|
if (getComputedStyle(el).overflow === "auto") {
|
2020-04-25 01:55:48 -07:00
|
|
|
scrollParent.current = el;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollParent.current.addEventListener("scroll", onScroll);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
if (scrollParent.current) {
|
|
|
|
scrollParent.current.removeEventListener("scroll", onScroll);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [onScroll]);
|
|
|
|
|
|
|
|
return <Box ref={containerRef}>{children}</Box>;
|
2020-04-24 21:17:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export default SearchPanel;
|