diff --git a/src/app/ItemSearchPage.js b/src/app/ItemSearchPage.js index 28c1cbd1..8c0c5535 100644 --- a/src/app/ItemSearchPage.js +++ b/src/app/ItemSearchPage.js @@ -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, } ); diff --git a/src/app/WardrobePage/ItemsAndSearchPanels.js b/src/app/WardrobePage/ItemsAndSearchPanels.js index 62c6ee0e..8141a99d 100644 --- a/src/app/WardrobePage/ItemsAndSearchPanels.js +++ b/src/app/WardrobePage/ItemsAndSearchPanels.js @@ -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} /> - {searchQuery.value || - searchQuery.filterToItemKind || - searchQuery.filterToZoneLabel ? ( + {!searchQueryIsEmpty(searchQuery) ? ( { // 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, ])}`; } diff --git a/src/app/WardrobePage/SearchToolbar.js b/src/app/WardrobePage/SearchToolbar.js index 6edcc0b7..b33d4611 100644 --- a/src/app/WardrobePage/SearchToolbar.js +++ b/src/app/WardrobePage/SearchToolbar.js @@ -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(" "); } diff --git a/src/server/loaders.js b/src/server/loaders.js index 352dc965..ee6c089a 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -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, ] diff --git a/src/server/types/Item.js b/src/server/types/Item.js index 83134db1..89c3ed58 100644 --- a/src/server/types/Item.js +++ b/src/server/types/Item.js @@ -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,