diff --git a/src/app/WardrobePage/SearchFooter.js b/src/app/WardrobePage/SearchFooter.js index 0ca07da..8c5bbd8 100644 --- a/src/app/WardrobePage/SearchFooter.js +++ b/src/app/WardrobePage/SearchFooter.js @@ -4,17 +4,24 @@ import { Box, Flex } from "@chakra-ui/react"; import SearchToolbar from "./SearchToolbar"; import { MajorErrorMessage, TestErrorSender, useLocalStorage } from "../util"; import PaginationToolbar from "../components/PaginationToolbar"; +import { useSearchResults } from "./useSearchResults"; /** * SearchFooter appears on large screens only, to let you search for new items * while still keeping the rest of the item screen open! */ -function SearchFooter({ searchQuery, onChangeSearchQuery }) { +function SearchFooter({ searchQuery, onChangeSearchQuery, outfitState }) { const [canUseSearchFooter, setCanUseSearchFooter] = useLocalStorage( "DTIFeatureFlagCanUseSearchFooter", false ); + const { items, numTotalPages } = useSearchResults( + searchQuery, + outfitState, + 1 + ); + React.useEffect(() => { if (window.location.search.includes("feature-flag-can-use-search-footer")) { setCanUseSearchFooter(true); @@ -27,33 +34,46 @@ function SearchFooter({ searchQuery, onChangeSearchQuery }) { } return ( - - - - - - Add new items: - - - - - - alert("TODO")} - buildPageUrl={() => null} - size="sm" + + + + + + + Add new items: + + + + + {numTotalPages != null && ( + + alert("TODO")} + buildPageUrl={() => null} + size="sm" + /> + + )} + + + + + {items.map((item) => ( + + {item.name} + + ))} - - - + + + ); } diff --git a/src/app/WardrobePage/SearchPanel.js b/src/app/WardrobePage/SearchPanel.js index e59b0ca..54adb48 100644 --- a/src/app/WardrobePage/SearchPanel.js +++ b/src/app/WardrobePage/SearchPanel.js @@ -1,15 +1,11 @@ import React from "react"; -import gql from "graphql-tag"; import { Box, Text, useColorModeValue, VisuallyHidden } from "@chakra-ui/react"; -import { useQuery } from "@apollo/client"; -import { useDebounce } from "../util"; -import { emptySearchQuery } from "./SearchToolbar"; import Item, { ItemListContainer, ItemListSkeleton } from "./Item"; -import { itemAppearanceFragment } from "../components/useOutfitAppearance"; import PaginationToolbar from "../components/PaginationToolbar"; +import { useSearchResults } from "./useSearchResults"; -const SEARCH_PER_PAGE = 30; +export const SEARCH_PER_PAGE = 30; /** * SearchPanel shows item search results to the user, so they can preview them @@ -271,133 +267,6 @@ function SearchResultItem({ ); } -/** - * useSearchResults manages the actual querying and state management of search! - */ -function useSearchResults( - query, - outfitState, - currentPageNumber, - { skip = false } = {} -) { - const { speciesId, colorId } = outfitState; - - // We debounce the search query, so that we don't resend a new query whenever - // the user types anything. - const debouncedQuery = useDebounce(query, 300, { - waitForFirstPause: true, - initialValue: emptySearchQuery, - }); - - // NOTE: This query should always load ~instantly, from the client cache. - const { data: zoneData } = useQuery(gql` - query SearchPanelZones { - allZones { - id - label - } - } - `); - const allZones = zoneData?.allZones || []; - const filterToZones = query.filterToZoneLabel - ? allZones.filter((z) => z.label === query.filterToZoneLabel) - : []; - const filterToZoneIds = filterToZones.map((z) => z.id); - - const currentPageIndex = currentPageNumber - 1; - const offset = currentPageIndex * SEARCH_PER_PAGE; - - // Here's the actual GQL query! At the bottom we have more config than usual! - const { loading: loadingGQL, error, data } = useQuery( - gql` - query SearchPanel( - $query: String! - $fitsPet: FitsPetSearchFilter - $itemKind: ItemKindSearchFilter - $currentUserOwnsOrWants: OwnsOrWants - $zoneIds: [ID!]! - $speciesId: ID! - $colorId: ID! - $offset: Int! - $perPage: Int! - ) { - itemSearch: itemSearchV2( - query: $query - fitsPet: $fitsPet - itemKind: $itemKind - currentUserOwnsOrWants: $currentUserOwnsOrWants - zoneIds: $zoneIds - ) { - id - numTotalItems - items(offset: $offset, limit: $perPage) { - # TODO: De-dupe this from useOutfitState? - id - name - thumbnailUrl - isNc - isPb - currentUserOwnsThis - currentUserWantsThis - - appearanceOn(speciesId: $speciesId, colorId: $colorId) { - # This enables us to quickly show the item when the user clicks it! - ...ItemAppearanceForOutfitPreview - - # This is used to group items by zone, and to detect conflicts when - # wearing a new item. - layers { - zone { - id - label @client - } - } - restrictedZones { - id - label @client - isCommonlyUsedByItems @client - } - } - } - } - } - ${itemAppearanceFragment} - `, - { - variables: { - query: debouncedQuery.value, - fitsPet: { speciesId, colorId }, - itemKind: debouncedQuery.filterToItemKind, - currentUserOwnsOrWants: debouncedQuery.filterToCurrentUserOwnsOrWants, - zoneIds: filterToZoneIds, - speciesId, - colorId, - offset, - perPage: SEARCH_PER_PAGE, - }, - context: { sendAuth: true }, - skip: - skip || - (!debouncedQuery.value && - !debouncedQuery.filterToItemKind && - !debouncedQuery.filterToZoneLabel && - !debouncedQuery.filterToCurrentUserOwnsOrWants), - onError: (e) => { - console.error("Error loading search results", e); - }, - // Return `numTotalItems` from the GQL cache while waiting for next page! - returnPartialData: true, - } - ); - - const loading = debouncedQuery !== query || loadingGQL; - const items = data?.itemSearch?.items ?? []; - const numTotalItems = data?.itemSearch?.numTotalItems ?? null; - const numTotalPages = Math.ceil(numTotalItems / SEARCH_PER_PAGE); - - return { loading, error, items, numTotalPages }; -} - /** * serializeQuery stably converts a search query object to a string, for easier * JS comparison. diff --git a/src/app/WardrobePage/index.js b/src/app/WardrobePage/index.js index ac26753..5b7151d 100644 --- a/src/app/WardrobePage/index.js +++ b/src/app/WardrobePage/index.js @@ -127,6 +127,7 @@ function WardrobePage() { } /> diff --git a/src/app/WardrobePage/useSearchResults.js b/src/app/WardrobePage/useSearchResults.js new file mode 100644 index 0000000..73107f0 --- /dev/null +++ b/src/app/WardrobePage/useSearchResults.js @@ -0,0 +1,133 @@ +import gql from "graphql-tag"; +import { useQuery } from "@apollo/client"; +import { useDebounce } from "../util"; +import { emptySearchQuery } from "./SearchToolbar"; +import { itemAppearanceFragment } from "../components/useOutfitAppearance"; +import { SEARCH_PER_PAGE } from "./SearchPanel"; + +/** + * useSearchResults manages the actual querying and state management of search! + */ +export function useSearchResults( + query, + outfitState, + currentPageNumber, + { skip = false } = {} +) { + const { speciesId, colorId } = outfitState; + + // We debounce the search query, so that we don't resend a new query whenever + // the user types anything. + const debouncedQuery = useDebounce(query, 300, { + waitForFirstPause: true, + initialValue: emptySearchQuery, + }); + + // NOTE: This query should always load ~instantly, from the client cache. + const { data: zoneData } = useQuery(gql` + query SearchPanelZones { + allZones { + id + label + } + } + `); + const allZones = zoneData?.allZones || []; + const filterToZones = query.filterToZoneLabel + ? allZones.filter((z) => z.label === query.filterToZoneLabel) + : []; + const filterToZoneIds = filterToZones.map((z) => z.id); + + const currentPageIndex = currentPageNumber - 1; + const offset = currentPageIndex * SEARCH_PER_PAGE; + + // Here's the actual GQL query! At the bottom we have more config than usual! + const { loading: loadingGQL, error, data } = useQuery( + gql` + query SearchPanel( + $query: String! + $fitsPet: FitsPetSearchFilter + $itemKind: ItemKindSearchFilter + $currentUserOwnsOrWants: OwnsOrWants + $zoneIds: [ID!]! + $speciesId: ID! + $colorId: ID! + $offset: Int! + $perPage: Int! + ) { + itemSearch: itemSearchV2( + query: $query + fitsPet: $fitsPet + itemKind: $itemKind + currentUserOwnsOrWants: $currentUserOwnsOrWants + zoneIds: $zoneIds + ) { + id + numTotalItems + items(offset: $offset, limit: $perPage) { + # TODO: De-dupe this from useOutfitState? + id + name + thumbnailUrl + isNc + isPb + currentUserOwnsThis + currentUserWantsThis + + appearanceOn(speciesId: $speciesId, colorId: $colorId) { + # This enables us to quickly show the item when the user clicks it! + ...ItemAppearanceForOutfitPreview + + # This is used to group items by zone, and to detect conflicts when + # wearing a new item. + layers { + zone { + id + label @client + } + } + restrictedZones { + id + label @client + isCommonlyUsedByItems @client + } + } + } + } + } + ${itemAppearanceFragment} + `, + { + variables: { + query: debouncedQuery.value, + fitsPet: { speciesId, colorId }, + itemKind: debouncedQuery.filterToItemKind, + currentUserOwnsOrWants: debouncedQuery.filterToCurrentUserOwnsOrWants, + zoneIds: filterToZoneIds, + speciesId, + colorId, + offset, + perPage: SEARCH_PER_PAGE, + }, + context: { sendAuth: true }, + skip: + skip || + (!debouncedQuery.value && + !debouncedQuery.filterToItemKind && + !debouncedQuery.filterToZoneLabel && + !debouncedQuery.filterToCurrentUserOwnsOrWants), + onError: (e) => { + console.error("Error loading search results", e); + }, + // Return `numTotalItems` from the GQL cache while waiting for next page! + returnPartialData: true, + } + ); + + const loading = debouncedQuery !== query || loadingGQL; + const items = data?.itemSearch?.items ?? []; + const numTotalItems = data?.itemSearch?.numTotalItems ?? null; + const numTotalPages = Math.ceil(numTotalItems / SEARCH_PER_PAGE); + + return { loading, error, items, numTotalPages }; +}