diff --git a/src/app/UserItemsPage.js b/src/app/UserItemsPage.js index 9ad401f..ac45038 100644 --- a/src/app/UserItemsPage.js +++ b/src/app/UserItemsPage.js @@ -1,10 +1,29 @@ import React from "react"; import { css } from "emotion"; -import { Badge, Box, Center, Wrap, VStack } from "@chakra-ui/core"; -import { CheckIcon, EmailIcon, StarIcon } from "@chakra-ui/icons"; +import { + Badge, + Box, + Center, + Flex, + IconButton, + Input, + InputGroup, + InputLeftElement, + InputRightElement, + Wrap, + VStack, + useToast, +} from "@chakra-ui/core"; +import { + ArrowForwardIcon, + CheckIcon, + EmailIcon, + SearchIcon, + StarIcon, +} from "@chakra-ui/icons"; import gql from "graphql-tag"; -import { useParams } from "react-router-dom"; -import { useQuery } from "@apollo/client"; +import { useHistory, useParams } from "react-router-dom"; +import { useQuery, useLazyQuery } from "@apollo/client"; import SimpleMarkdown from "simple-markdown"; import DOMPurify from "dompurify"; @@ -117,84 +136,95 @@ function UserItemsPage() { return ( - {isCurrentUser && ( - - + + + + {isCurrentUser ? "Your items" : `${data.user.username}'s items`} + + + {data.user.contactNeopetsUsername && ( + + + {data.user.contactNeopetsUsername} + + )} + {data.user.contactNeopetsUsername && ( + + + Neomail + + )} + {/* Usually I put "Own" before "Want", but this matches the natural + * order on the page: the _matches_ for things you want are things + * _this user_ owns, so they come first. I think it's also probably a + * more natural train of thought: you come to someone's list _wanting_ + * something, and _then_ thinking about what you can offer. */} + {!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && ( + + + {numItemsTheyOwnThatYouWant > 1 + ? `${numItemsTheyOwnThatYouWant} items you want` + : "1 item you want"} + + )} + {!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && ( + + + {numItemsTheyWantThatYouOwn > 1 + ? `${numItemsTheyWantThatYouOwn} items you own` + : "1 item you own"} + + )} + - )} - - {isCurrentUser ? "Your items" : `${data.user.username}'s items`} - - - {data.user.contactNeopetsUsername && ( - - - {data.user.contactNeopetsUsername} - + + + + + + + + {isCurrentUser && ( + + + )} - {data.user.contactNeopetsUsername && ( - - - Neomail - - )} - {/* Usually I put "Own" before "Want", but this matches the natural - * order on the page: the _matches_ for things you want are things - * _this user_ owns, so they come first. I think it's also probably a - * more natural train of thought: you come to someone's list _wanting_ - * something, and _then_ thinking about what you can offer. */} - {!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && ( - - - {numItemsTheyOwnThatYouWant > 1 - ? `${numItemsTheyOwnThatYouWant} items you want` - : "1 item you want"} - - )} - {!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && ( - - - {numItemsTheyWantThatYouOwn > 1 - ? `${numItemsTheyWantThatYouOwn} items you own` - : "1 item you own"} - - )} - - - {isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`} - - - {listsOfOwnedItems.map((closetList) => ( - 1} - /> - ))} - + + {isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`} + + + {listsOfOwnedItems.map((closetList) => ( + 1} + /> + ))} + + {isCurrentUser ? "Items you want" : `Items ${data.user.username} wants`} @@ -213,6 +243,85 @@ function UserItemsPage() { ); } +function UserSearchForm() { + const [query, setQuery] = React.useState(""); + const history = useHistory(); + const toast = useToast(); + + const [loadUserSearch, { loading }] = useLazyQuery( + gql` + query UserSearchForm($name: String!) { + userByName(name: $name) { + id + # Consider preloading UserItemsPage fields here, too? + } + } + `, + { + onCompleted: (data) => { + const user = data.userByName; + if (!user) { + toast({ + status: "warning", + title: "We couldn't find that user!", + description: "Check the spelling and try again?", + }); + return; + } + + history.push(`/user/${user.id}/items`); + }, + onError: (error) => { + console.error(error); + toast({ + status: "error", + title: "Error loading user!", + description: "Check your connection and try again?", + }); + }, + } + ); + + return ( + { + loadUserSearch({ variables: { name: query } }); + e.preventDefault(); + }} + > + + + + + setQuery(e.target.value)} + placeholder="Search for another user…" + borderRadius="full" + /> + + } + aria-label="Search" + isLoading={loading} + minWidth="1.5rem" + minHeight="1.5rem" + width="1.5rem" + height="1.5rem" + borderRadius="full" + opacity={query ? 1 : 0} + transition="opacity 0.2s" + aria-hidden={query ? "false" : "true"} + /> + + + + ); +} + function ClosetList({ closetList, isCurrentUser, showHeading }) { const hasYouWantThisBadge = (item) => !isCurrentUser && diff --git a/src/server/loaders.js b/src/server/loaders.js index b40dab5..c6521cf 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -759,6 +759,21 @@ const buildUserLoader = (db) => ); }); +const buildUserByNameLoader = (db) => + new DataLoader(async (names) => { + const qs = names.map((_) => "?").join(","); + const [rows, _] = await db.execute( + `SELECT * FROM users WHERE name IN (${qs})`, + names + ); + + const entities = rows.map(normalizeRow); + + return names.map((name) => + entities.find((e) => e.name.toLowerCase() === name.toLowerCase()) + ); + }); + const buildUserClosetHangersLoader = (db) => new DataLoader(async (userIds) => { const qs = userIds.map((_) => "?").join(","); @@ -891,6 +906,7 @@ function buildLoaders(db) { loaders.speciesLoader = buildSpeciesLoader(db); loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db); loaders.userLoader = buildUserLoader(db); + loaders.userByNameLoader = buildUserByNameLoader(db); loaders.userClosetHangersLoader = buildUserClosetHangersLoader(db); loaders.userClosetListsLoader = buildUserClosetListsLoader(db); loaders.zoneLoader = buildZoneLoader(db); diff --git a/src/server/types/User.js b/src/server/types/User.js index 67072f0..e6231a9 100644 --- a/src/server/types/User.js +++ b/src/server/types/User.js @@ -40,6 +40,7 @@ const typeDefs = gql` extend type Query { user(id: ID!): User + userByName(name: String!): User currentUser: User } `; @@ -213,6 +214,16 @@ const resolvers = { return { id }; }, + + userByName: async (_, { name }, { userByNameLoader }) => { + const user = await userByNameLoader.load(name); + if (!user) { + return null; + } + + return { id: user.id }; + }, + currentUser: async (_, __, { currentUserId, userLoader }) => { if (currentUserId == null) { return null;