Add basic search results to footer

It looks bad, and only shows page 1, but it does stuff at all! :0
This commit is contained in:
Emi Matchu 2022-10-14 20:40:06 -07:00
parent a83e3a9e0b
commit 3dfc53cda1
4 changed files with 182 additions and 159 deletions

View file

@ -4,17 +4,24 @@ import { Box, Flex } from "@chakra-ui/react";
import SearchToolbar from "./SearchToolbar"; import SearchToolbar from "./SearchToolbar";
import { MajorErrorMessage, TestErrorSender, useLocalStorage } from "../util"; import { MajorErrorMessage, TestErrorSender, useLocalStorage } from "../util";
import PaginationToolbar from "../components/PaginationToolbar"; import PaginationToolbar from "../components/PaginationToolbar";
import { useSearchResults } from "./useSearchResults";
/** /**
* SearchFooter appears on large screens only, to let you search for new items * SearchFooter appears on large screens only, to let you search for new items
* while still keeping the rest of the item screen open! * while still keeping the rest of the item screen open!
*/ */
function SearchFooter({ searchQuery, onChangeSearchQuery }) { function SearchFooter({ searchQuery, onChangeSearchQuery, outfitState }) {
const [canUseSearchFooter, setCanUseSearchFooter] = useLocalStorage( const [canUseSearchFooter, setCanUseSearchFooter] = useLocalStorage(
"DTIFeatureFlagCanUseSearchFooter", "DTIFeatureFlagCanUseSearchFooter",
false false
); );
const { items, numTotalPages } = useSearchResults(
searchQuery,
outfitState,
1
);
React.useEffect(() => { React.useEffect(() => {
if (window.location.search.includes("feature-flag-can-use-search-footer")) { if (window.location.search.includes("feature-flag-can-use-search-footer")) {
setCanUseSearchFooter(true); setCanUseSearchFooter(true);
@ -27,33 +34,46 @@ function SearchFooter({ searchQuery, onChangeSearchQuery }) {
} }
return ( return (
<Box paddingX="4" paddingY="4"> <Sentry.ErrorBoundary fallback={MajorErrorMessage}>
<Sentry.ErrorBoundary fallback={MajorErrorMessage}> <TestErrorSender />
<TestErrorSender /> <Box>
<Flex as="label" align="center"> <Box paddingX="4" paddingY="4">
<Box fontWeight="600" flex="0 0 auto"> <Flex as="label" align="center">
Add new items: <Box fontWeight="600" flex="0 0 auto">
</Box> Add new items:
<Box width="8" /> </Box>
<SearchToolbar <Box width="8" />
query={searchQuery} <SearchToolbar
onChange={onChangeSearchQuery} query={searchQuery}
flex="0 1 100%" onChange={onChangeSearchQuery}
suggestionsPlacement="top" flex="0 1 100%"
/> suggestionsPlacement="top"
<Box width="8" />
<Box flex="0 0 auto">
<PaginationToolbar
numTotalPages={1}
currentPageNumber={1}
goToPageNumber={() => alert("TODO")}
buildPageUrl={() => null}
size="sm"
/> />
<Box width="8" />
{numTotalPages != null && (
<Box flex="0 0 auto">
<PaginationToolbar
numTotalPages={numTotalPages}
currentPageNumber={1}
goToPageNumber={() => alert("TODO")}
buildPageUrl={() => null}
size="sm"
/>
</Box>
)}
</Flex>
</Box>
<Box maxHeight="32" overflow="auto">
<Box as="ul" listStyleType="disc" paddingLeft="8">
{items.map((item) => (
<Box key={item.id} as="li">
{item.name}
</Box>
))}
</Box> </Box>
</Flex> </Box>
</Sentry.ErrorBoundary> </Box>
</Box> </Sentry.ErrorBoundary>
); );
} }

View file

@ -1,15 +1,11 @@
import React from "react"; import React from "react";
import gql from "graphql-tag";
import { Box, Text, useColorModeValue, VisuallyHidden } from "@chakra-ui/react"; 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 Item, { ItemListContainer, ItemListSkeleton } from "./Item";
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
import PaginationToolbar from "../components/PaginationToolbar"; 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 * 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 * serializeQuery stably converts a search query object to a string, for easier
* JS comparison. * JS comparison.

View file

@ -127,6 +127,7 @@ function WardrobePage() {
<SearchFooter <SearchFooter
searchQuery={searchQuery} searchQuery={searchQuery}
onChangeSearchQuery={setSearchQuery} onChangeSearchQuery={setSearchQuery}
outfitState={outfitState}
/> />
} }
/> />

View file

@ -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 };
}