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

View file

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

View file

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

View file

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

View file

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

View file

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