Add "Items you own" and "Items you want" filters

This commit is contained in:
Emi Matchu 2021-01-21 15:58:24 -08:00
parent 8dc97f1e6f
commit 701eb33391
6 changed files with 128 additions and 18 deletions

View file

@ -48,6 +48,7 @@ function useSearchQueryInUrl() {
value: value || "", value: value || "",
filterToZoneLabel: searchParams.get("zone") || null, filterToZoneLabel: searchParams.get("zone") || null,
filterToItemKind: searchParams.get("kind") || null, filterToItemKind: searchParams.get("kind") || null,
filterToCurrentUserOwnsOrWants: searchParams.get("user") || null,
}; };
const setQuery = React.useCallback( const setQuery = React.useCallback(
(newQuery) => { (newQuery) => {
@ -64,6 +65,9 @@ function useSearchQueryInUrl() {
if (newQuery.filterToZoneLabel) { if (newQuery.filterToZoneLabel) {
newParams.append("zone", newQuery.filterToZoneLabel); newParams.append("zone", newQuery.filterToZoneLabel);
} }
if (newQuery.filterToCurrentUserOwnsOrWants) {
newParams.append("user", newQuery.filterToCurrentUserOwnsOrWants);
}
const search = newParams.toString(); const search = newParams.toString();
if (search) { if (search) {
url += "?" + search; url += "?" + search;
@ -116,11 +120,13 @@ function ItemSearchPageResults({ query: latestQuery }) {
query ItemSearchPageResults( query ItemSearchPageResults(
$query: String! $query: String!
$itemKind: ItemKindSearchFilter $itemKind: ItemKindSearchFilter
$currentUserOwnsOrWants: OwnsOrWants
$zoneIds: [ID!]! $zoneIds: [ID!]!
) { ) {
itemSearch( itemSearch(
query: $query query: $query
itemKind: $itemKind itemKind: $itemKind
currentUserOwnsOrWants: $currentUserOwnsOrWants
zoneIds: $zoneIds zoneIds: $zoneIds
offset: 0 offset: 0
limit: 30 limit: 30
@ -137,8 +143,10 @@ function ItemSearchPageResults({ query: latestQuery }) {
variables: { variables: {
query: query.value, query: query.value,
itemKind: query.filterToItemKind, itemKind: query.filterToItemKind,
currentUserOwnsOrWants: query.filterToCurrentUserOwnsOrWants,
zoneIds: filterToZoneIds, zoneIds: filterToZoneIds,
}, },
context: { sendAuth: true },
skip: skipSearchResults, skip: skipSearchResults,
} }
); );

View file

@ -2,7 +2,10 @@ import React from "react";
import { Box, Flex } from "@chakra-ui/react"; import { Box, Flex } from "@chakra-ui/react";
import ItemsPanel from "./ItemsPanel"; import ItemsPanel from "./ItemsPanel";
import SearchToolbar, { emptySearchQuery } from "./SearchToolbar"; import SearchToolbar, {
emptySearchQuery,
searchQueryIsEmpty,
} from "./SearchToolbar";
import SearchPanel from "./SearchPanel"; import SearchPanel from "./SearchPanel";
/** /**
@ -34,9 +37,7 @@ function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) {
onChange={setSearchQuery} onChange={setSearchQuery}
/> />
</Box> </Box>
{searchQuery.value || {!searchQueryIsEmpty(searchQuery) ? (
searchQuery.filterToItemKind ||
searchQuery.filterToZoneLabel ? (
<Box <Box
key="search-panel" key="search-panel"
gridArea="items" gridArea="items"

View file

@ -253,6 +253,7 @@ function useSearchResults(query, outfitState) {
query SearchPanel( query SearchPanel(
$query: String! $query: String!
$itemKind: ItemKindSearchFilter $itemKind: ItemKindSearchFilter
$currentUserOwnsOrWants: OwnsOrWants
$zoneIds: [ID!]! $zoneIds: [ID!]!
$speciesId: ID! $speciesId: ID!
$colorId: ID! $colorId: ID!
@ -261,6 +262,7 @@ function useSearchResults(query, outfitState) {
itemSearchToFit( itemSearchToFit(
query: $query query: $query
itemKind: $itemKind itemKind: $itemKind
currentUserOwnsOrWants: $currentUserOwnsOrWants
zoneIds: $zoneIds zoneIds: $zoneIds
speciesId: $speciesId speciesId: $speciesId
colorId: $colorId colorId: $colorId
@ -308,6 +310,7 @@ function useSearchResults(query, outfitState) {
variables: { variables: {
query: debouncedQuery.value, query: debouncedQuery.value,
itemKind: debouncedQuery.filterToItemKind, itemKind: debouncedQuery.filterToItemKind,
currentUserOwnsOrWants: debouncedQuery.filterToCurrentUserOwnsOrWants,
zoneIds: filterToZoneIds, zoneIds: filterToZoneIds,
speciesId, speciesId,
colorId, colorId,
@ -317,7 +320,8 @@ function useSearchResults(query, outfitState) {
skip: skip:
!debouncedQuery.value && !debouncedQuery.value &&
!debouncedQuery.filterToItemKind && !debouncedQuery.filterToItemKind &&
!debouncedQuery.filterToZoneLabel, !debouncedQuery.filterToZoneLabel &&
!debouncedQuery.filterToCurrentUserOwnsOrWants,
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
onCompleted: (d) => { onCompleted: (d) => {
// This is called each time the query completes, including on // This is called each time the query completes, including on
@ -450,6 +454,7 @@ function serializeQuery(query) {
query.value, query.value,
query.filterToItemKind, query.filterToItemKind,
query.filterToZoneLabel, query.filterToZoneLabel,
query.filterToCurrentUserOwnsOrWants,
])}`; ])}`;
} }

View file

@ -15,10 +15,13 @@ import { CloseIcon, SearchIcon } from "@chakra-ui/icons";
import { ClassNames } from "@emotion/react"; import { ClassNames } from "@emotion/react";
import Autosuggest from "react-autosuggest"; import Autosuggest from "react-autosuggest";
import useCurrentUser from "../components/useCurrentUser";
export const emptySearchQuery = { export const emptySearchQuery = {
value: "", value: "",
filterToZoneLabel: null, filterToZoneLabel: null,
filterToItemKind: null, filterToItemKind: null,
filterToCurrentUserOwnsOrWants: null,
}; };
export function searchQueryIsEmpty(query) { export function searchQueryIsEmpty(query) {
@ -44,6 +47,7 @@ function SearchToolbar({
boxShadow = null, boxShadow = null,
}) { }) {
const [suggestions, setSuggestions] = React.useState([]); const [suggestions, setSuggestions] = React.useState([]);
const { isLoggedIn } = useCurrentUser();
// NOTE: This query should always load ~instantly, from the client cache. // NOTE: This query should always load ~instantly, from the client cache.
const { data } = useQuery(gql` const { data } = useQuery(gql`
@ -121,7 +125,11 @@ function SearchToolbar({
// When we change the query filters, clear out the suggestions. // When we change the query filters, clear out the suggestions.
React.useEffect(() => { React.useEffect(() => {
setSuggestions([]); setSuggestions([]);
}, [query.filterToItemKind, query.filterToZoneLabel]); }, [
query.filterToItemKind,
query.filterToZoneLabel,
query.filterToCurrentUserOwnsOrWants,
]);
let queryFilterText = getQueryFilterText(query); let queryFilterText = getQueryFilterText(query);
if (showItemsLabel) { if (showItemsLabel) {
@ -149,7 +157,7 @@ function SearchToolbar({
// set to the _chosen suggestion_ after choosing it? Has that // set to the _chosen suggestion_ after choosing it? Has that
// always happened? Idk? Let's just, gate around it, I guess? // always happened? Idk? Let's just, gate around it, I guess?
if (typeof value === "string") { if (typeof value === "string") {
setSuggestions(getSuggestions(value, query, zoneLabels)); setSuggestions(getSuggestions(value, query, zoneLabels, isLoggedIn));
} }
}} }}
onSuggestionSelected={(e, { suggestion }) => { onSuggestionSelected={(e, { suggestion }) => {
@ -159,6 +167,8 @@ function SearchToolbar({
value: valueWithoutLastWord, value: valueWithoutLastWord,
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel, filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
filterToItemKind: suggestion.itemKind || query.filterToItemKind, filterToItemKind: suggestion.itemKind || query.filterToItemKind,
filterToCurrentUserOwnsOrWants:
suggestion.userOwnsOrWants || query.filterToCurrentUserOwnsOrWants,
}); });
}} }}
getSuggestionValue={(zl) => zl} getSuggestionValue={(zl) => zl}
@ -232,6 +242,7 @@ function SearchToolbar({
...query, ...query,
filterToItemKind: null, filterToItemKind: null,
filterToZoneLabel: null, filterToZoneLabel: null,
filterToCurrentUserOwnsOrWants: null,
}); });
} }
}, },
@ -240,7 +251,7 @@ function SearchToolbar({
); );
} }
function getSuggestions(value, query, zoneLabels) { function getSuggestions(value, query, zoneLabels, isLoggedIn) {
if (!value) { if (!value) {
return []; return [];
} }
@ -267,6 +278,16 @@ function getSuggestions(value, query, zoneLabels) {
} }
} }
if (isLoggedIn && query.filterToCurrentUserOwnsOrWants == null) {
if (wordMatches("Items you own", lastWord)) {
suggestions.push({ userOwnsOrWants: "OWNS", text: "Items you own" });
}
if (wordMatches("Items you want", lastWord)) {
suggestions.push({ userOwnsOrWants: "WANTS", text: "Items you want" });
}
}
if (query.filterToZoneLabel == null) { if (query.filterToZoneLabel == null) {
for (const zoneLabel of zoneLabels) { for (const zoneLabel of zoneLabels) {
if (wordMatches(zoneLabel, lastWord)) { if (wordMatches(zoneLabel, lastWord)) {
@ -293,6 +314,18 @@ function getQueryFilterText(query) {
textWords.push(query.filterToZoneLabel); textWords.push(query.filterToZoneLabel);
} }
if (query.filterToCurrentUserOwnsOrWants === "OWNS") {
if (textWords.length === 0) {
textWords.push("Items");
}
textWords.push("you own");
} else if (query.filterToCurrentUserOwnsOrWants === "WANTS") {
if (textWords.length === 0) {
textWords.push("Items");
}
textWords.push("you want");
}
return textWords.join(" "); return textWords.join(" ");
} }

View file

@ -288,7 +288,15 @@ const buildItemSearchLoader = (db, loaders) =>
// This isn't actually optimized as a batch query, we're just using a // This isn't actually optimized as a batch query, we're just using a
// DataLoader API consistency with our other loaders! // DataLoader API consistency with our other loaders!
const queryPromises = queries.map( const queryPromises = queries.map(
async ({ query, itemKind, zoneIds = [], offset, limit }) => { async ({
query,
itemKind,
currentUserOwnsOrWants,
currentUserId,
zoneIds = [],
offset,
limit,
}) => {
const actualOffset = offset || 0; const actualOffset = offset || 0;
const actualLimit = Math.min(limit || 30, 30); const actualLimit = Math.min(limit || 30, 30);
@ -307,17 +315,35 @@ const buildItemSearchLoader = (db, loaders) =>
zoneIds.length > 0 zoneIds.length > 0
? `swf_assets.zone_id IN (${zoneIds.map((_) => "?").join(", ")})` ? `swf_assets.zone_id IN (${zoneIds.map((_) => "?").join(", ")})`
: "1"; : "1";
const currentUserJoin = currentUserOwnsOrWants
? `INNER JOIN closet_hangers ch ON ch.item_id = items.id`
: "";
const currentUserCondition = currentUserOwnsOrWants
? `ch.user_id = ? AND ch.owned = ?`
: "1";
const currentUserValues = currentUserOwnsOrWants
? [currentUserId, currentUserOwnsOrWants === "OWNS" ? "1" : "0"]
: [];
const [rows, _] = await db.execute( const [rows, _] = await db.execute(
`SELECT DISTINCT items.*, t.name FROM items `SELECT DISTINCT items.*, t.name FROM items
INNER JOIN item_translations t ON t.item_id = items.id INNER JOIN item_translations t ON t.item_id = items.id
INNER JOIN parents_swf_assets rel INNER JOIN parents_swf_assets rel
ON rel.parent_type = "Item" AND rel.parent_id = items.id ON rel.parent_type = "Item" AND rel.parent_id = items.id
INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id
${currentUserJoin}
WHERE ${matcherPlaceholders} AND t.locale = "en" AND WHERE ${matcherPlaceholders} AND t.locale = "en" AND
${zoneIdsPlaceholder} AND ${itemKindCondition} ${zoneIdsPlaceholder} AND ${itemKindCondition} AND
${currentUserCondition}
ORDER BY t.name ORDER BY t.name
LIMIT ? OFFSET ?`, LIMIT ? OFFSET ?`,
[...wordMatchersForMysql, ...zoneIds, actualLimit, actualOffset] [
...wordMatchersForMysql,
...zoneIds,
...currentUserValues,
actualLimit,
actualOffset,
]
); );
const entities = rows.map(normalizeRow); const entities = rows.map(normalizeRow);
@ -340,7 +366,16 @@ const buildItemSearchToFitLoader = (db, loaders) =>
// This isn't actually optimized as a batch query, we're just using a // This isn't actually optimized as a batch query, we're just using a
// DataLoader API consistency with our other loaders! // DataLoader API consistency with our other loaders!
const queryPromises = queryAndBodyIdPairs.map( const queryPromises = queryAndBodyIdPairs.map(
async ({ query, bodyId, itemKind, zoneIds = [], offset, limit }) => { async ({
query,
bodyId,
itemKind,
currentUserOwnsOrWants,
currentUserId,
zoneIds = [],
offset,
limit,
}) => {
const actualOffset = offset || 0; const actualOffset = offset || 0;
const actualLimit = Math.min(limit || 30, 30); const actualLimit = Math.min(limit || 30, 30);
@ -355,25 +390,38 @@ const buildItemSearchToFitLoader = (db, loaders) =>
.join(" AND "); .join(" AND ");
const itemKindCondition = itemSearchKindConditions[itemKind] || "1"; const itemKindCondition = itemSearchKindConditions[itemKind] || "1";
const zoneIdsPlaceholder = const zoneIdsCondition =
zoneIds.length > 0 zoneIds.length > 0
? `swf_assets.zone_id IN (${zoneIds.map((_) => "?").join(", ")})` ? `swf_assets.zone_id IN (${zoneIds.map((_) => "?").join(", ")})`
: "1"; : "1";
const currentUserJoin = currentUserOwnsOrWants
? `INNER JOIN closet_hangers ch ON ch.item_id = items.id`
: "";
const currentUserCondition = currentUserOwnsOrWants
? `ch.user_id = ? AND ch.owned = ?`
: "1";
const currentUserValues = currentUserOwnsOrWants
? [currentUserId, currentUserOwnsOrWants === "OWNS" ? "1" : "0"]
: [];
const [rows, _] = await db.execute( const [rows, _] = await db.execute(
`SELECT DISTINCT items.*, t.name FROM items `SELECT DISTINCT items.*, t.name FROM items
INNER JOIN item_translations t ON t.item_id = items.id INNER JOIN item_translations t ON t.item_id = items.id
INNER JOIN parents_swf_assets rel INNER JOIN parents_swf_assets rel
ON rel.parent_type = "Item" AND rel.parent_id = items.id ON rel.parent_type = "Item" AND rel.parent_id = items.id
INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id
${currentUserJoin}
WHERE ${matcherPlaceholders} AND t.locale = "en" AND WHERE ${matcherPlaceholders} AND t.locale = "en" AND
(swf_assets.body_id = ? OR swf_assets.body_id = 0) AND (swf_assets.body_id = ? OR swf_assets.body_id = 0) AND
${zoneIdsPlaceholder} AND ${itemKindCondition} ${zoneIdsCondition} AND ${itemKindCondition} AND
${currentUserCondition}
ORDER BY t.name ORDER BY t.name
LIMIT ? OFFSET ?`, LIMIT ? OFFSET ?`,
[ [
...wordMatchersForMysql, ...wordMatchersForMysql,
bodyId, bodyId,
...zoneIds, ...zoneIds,
...currentUserValues,
actualLimit, actualLimit,
actualOffset, actualOffset,
] ]

View file

@ -114,6 +114,7 @@ const typeDefs = gql`
itemSearch( itemSearch(
query: String! query: String!
itemKind: ItemKindSearchFilter itemKind: ItemKindSearchFilter
currentUserOwnsOrWants: OwnsOrWants
zoneIds: [ID!] zoneIds: [ID!]
offset: Int offset: Int
limit: Int limit: Int
@ -121,6 +122,7 @@ const typeDefs = gql`
itemSearchToFit( itemSearchToFit(
query: String! query: String!
itemKind: ItemKindSearchFilter itemKind: ItemKindSearchFilter
currentUserOwnsOrWants: OwnsOrWants
zoneIds: [ID!] zoneIds: [ID!]
speciesId: ID! speciesId: ID!
colorId: ID! colorId: ID!
@ -372,12 +374,14 @@ const resolvers = {
}, },
itemSearch: async ( itemSearch: async (
_, _,
{ query, itemKind, zoneIds = [], offset, limit }, { query, itemKind, currentUserOwnsOrWants, zoneIds = [], offset, limit },
{ itemSearchLoader } { itemSearchLoader, currentUserId }
) => { ) => {
const items = await itemSearchLoader.load({ const items = await itemSearchLoader.load({
query: query.trim(), query: query.trim(),
itemKind, itemKind,
currentUserOwnsOrWants,
currentUserId,
zoneIds, zoneIds,
offset, offset,
limit, limit,
@ -387,8 +391,17 @@ const resolvers = {
}, },
itemSearchToFit: async ( itemSearchToFit: async (
_, _,
{ query, speciesId, colorId, itemKind, zoneIds = [], offset, limit }, {
{ petTypeBySpeciesAndColorLoader, itemSearchToFitLoader } query,
speciesId,
colorId,
itemKind,
currentUserOwnsOrWants,
zoneIds = [],
offset,
limit,
},
{ petTypeBySpeciesAndColorLoader, itemSearchToFitLoader, currentUserId }
) => { ) => {
const petType = await petTypeBySpeciesAndColorLoader.load({ const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId, speciesId,
@ -398,6 +411,8 @@ const resolvers = {
const items = await itemSearchToFitLoader.load({ const items = await itemSearchToFitLoader.load({
query: query.trim(), query: query.trim(),
itemKind, itemKind,
currentUserOwnsOrWants,
currentUserId,
zoneIds, zoneIds,
bodyId, bodyId,
offset, offset,