Add "Items you own" and "Items you want" filters
This commit is contained in:
parent
8dc97f1e6f
commit
701eb33391
6 changed files with 128 additions and 18 deletions
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
])}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(" ");
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue