can search for user by name from items page
This commit is contained in:
parent
f1a8277c22
commit
6a43f92438
3 changed files with 216 additions and 80 deletions
|
@ -1,10 +1,29 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { css } from "emotion";
|
import { css } from "emotion";
|
||||||
import { Badge, Box, Center, Wrap, VStack } from "@chakra-ui/core";
|
import {
|
||||||
import { CheckIcon, EmailIcon, StarIcon } from "@chakra-ui/icons";
|
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 gql from "graphql-tag";
|
||||||
import { useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery, useLazyQuery } from "@apollo/client";
|
||||||
import SimpleMarkdown from "simple-markdown";
|
import SimpleMarkdown from "simple-markdown";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
|
@ -117,84 +136,95 @@ function UserItemsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{isCurrentUser && (
|
<Flex align="center" wrap="wrap-reverse">
|
||||||
<Box float="right">
|
<Box>
|
||||||
<WIPCallout details="These lists are read-only for now. To edit, head back to Classic DTI!" />
|
<Heading1>
|
||||||
|
{isCurrentUser ? "Your items" : `${data.user.username}'s items`}
|
||||||
|
</Heading1>
|
||||||
|
<Wrap spacing="2" opacity="0.7">
|
||||||
|
{data.user.contactNeopetsUsername && (
|
||||||
|
<Badge
|
||||||
|
as="a"
|
||||||
|
href={`http://www.neopets.com/userlookup.phtml?user=${data.user.contactNeopetsUsername}`}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<NeopetsStarIcon marginRight="1" />
|
||||||
|
{data.user.contactNeopetsUsername}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{data.user.contactNeopetsUsername && (
|
||||||
|
<Badge
|
||||||
|
as="a"
|
||||||
|
href={`http://www.neopets.com/neomessages.phtml?type=send&recipient=${data.user.contactNeopetsUsername}`}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<EmailIcon marginRight="1" />
|
||||||
|
Neomail
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{/* 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 && (
|
||||||
|
<Badge
|
||||||
|
as="a"
|
||||||
|
href="#owned-items"
|
||||||
|
colorScheme="blue"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<StarIcon marginRight="1" />
|
||||||
|
{numItemsTheyOwnThatYouWant > 1
|
||||||
|
? `${numItemsTheyOwnThatYouWant} items you want`
|
||||||
|
: "1 item you want"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && (
|
||||||
|
<Badge
|
||||||
|
as="a"
|
||||||
|
href="#wanted-items"
|
||||||
|
colorScheme="green"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<CheckIcon marginRight="1" />
|
||||||
|
{numItemsTheyWantThatYouOwn > 1
|
||||||
|
? `${numItemsTheyWantThatYouOwn} items you own`
|
||||||
|
: "1 item you own"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Wrap>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
<Box flex="1 0 auto" width="2" />
|
||||||
<Heading1>
|
<Box marginBottom="1">
|
||||||
{isCurrentUser ? "Your items" : `${data.user.username}'s items`}
|
<UserSearchForm />
|
||||||
</Heading1>
|
</Box>
|
||||||
<Wrap spacing="2" opacity="0.7">
|
</Flex>
|
||||||
{data.user.contactNeopetsUsername && (
|
|
||||||
<Badge
|
<Box marginTop="4">
|
||||||
as="a"
|
{isCurrentUser && (
|
||||||
href={`http://www.neopets.com/userlookup.phtml?user=${data.user.contactNeopetsUsername}`}
|
<Box float="right">
|
||||||
display="flex"
|
<WIPCallout details="These lists are read-only for now. To edit, head back to Classic DTI!" />
|
||||||
alignItems="center"
|
</Box>
|
||||||
>
|
|
||||||
<NeopetsStarIcon marginRight="1" />
|
|
||||||
{data.user.contactNeopetsUsername}
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
{data.user.contactNeopetsUsername && (
|
<Heading2 id="owned-items" marginBottom="2">
|
||||||
<Badge
|
{isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`}
|
||||||
as="a"
|
</Heading2>
|
||||||
href={`http://www.neopets.com/neomessages.phtml?type=send&recipient=${data.user.contactNeopetsUsername}`}
|
<VStack spacing="8" alignItems="stretch">
|
||||||
display="flex"
|
{listsOfOwnedItems.map((closetList) => (
|
||||||
alignItems="center"
|
<ClosetList
|
||||||
>
|
key={closetList.id}
|
||||||
<EmailIcon marginRight="1" />
|
closetList={closetList}
|
||||||
Neomail
|
isCurrentUser={isCurrentUser}
|
||||||
</Badge>
|
showHeading={listsOfOwnedItems.length > 1}
|
||||||
)}
|
/>
|
||||||
{/* Usually I put "Own" before "Want", but this matches the natural
|
))}
|
||||||
* order on the page: the _matches_ for things you want are things
|
</VStack>
|
||||||
* _this user_ owns, so they come first. I think it's also probably a
|
</Box>
|
||||||
* more natural train of thought: you come to someone's list _wanting_
|
|
||||||
* something, and _then_ thinking about what you can offer. */}
|
|
||||||
{!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && (
|
|
||||||
<Badge
|
|
||||||
as="a"
|
|
||||||
href="#owned-items"
|
|
||||||
colorScheme="blue"
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<StarIcon marginRight="1" />
|
|
||||||
{numItemsTheyOwnThatYouWant > 1
|
|
||||||
? `${numItemsTheyOwnThatYouWant} items you want`
|
|
||||||
: "1 item you want"}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && (
|
|
||||||
<Badge
|
|
||||||
as="a"
|
|
||||||
href="#wanted-items"
|
|
||||||
colorScheme="green"
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<CheckIcon marginRight="1" />
|
|
||||||
{numItemsTheyWantThatYouOwn > 1
|
|
||||||
? `${numItemsTheyWantThatYouOwn} items you own`
|
|
||||||
: "1 item you own"}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Wrap>
|
|
||||||
<Heading2 id="owned-items" marginTop="4" marginBottom="2">
|
|
||||||
{isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`}
|
|
||||||
</Heading2>
|
|
||||||
<VStack spacing="8" alignItems="stretch">
|
|
||||||
{listsOfOwnedItems.map((closetList) => (
|
|
||||||
<ClosetList
|
|
||||||
key={closetList.id}
|
|
||||||
closetList={closetList}
|
|
||||||
isCurrentUser={isCurrentUser}
|
|
||||||
showHeading={listsOfOwnedItems.length > 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
<Heading2 id="wanted-items" marginTop="10" marginBottom="2">
|
<Heading2 id="wanted-items" marginTop="10" marginBottom="2">
|
||||||
{isCurrentUser ? "Items you want" : `Items ${data.user.username} wants`}
|
{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 (
|
||||||
|
<Box
|
||||||
|
as="form"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
loadUserSearch({ variables: { name: query } });
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<SearchIcon color="gray.400" />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search for another user…"
|
||||||
|
borderRadius="full"
|
||||||
|
/>
|
||||||
|
<InputRightElement>
|
||||||
|
<IconButton
|
||||||
|
type="submit"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<ArrowForwardIcon />}
|
||||||
|
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"}
|
||||||
|
/>
|
||||||
|
</InputRightElement>
|
||||||
|
</InputGroup>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ClosetList({ closetList, isCurrentUser, showHeading }) {
|
function ClosetList({ closetList, isCurrentUser, showHeading }) {
|
||||||
const hasYouWantThisBadge = (item) =>
|
const hasYouWantThisBadge = (item) =>
|
||||||
!isCurrentUser &&
|
!isCurrentUser &&
|
||||||
|
|
|
@ -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) =>
|
const buildUserClosetHangersLoader = (db) =>
|
||||||
new DataLoader(async (userIds) => {
|
new DataLoader(async (userIds) => {
|
||||||
const qs = userIds.map((_) => "?").join(",");
|
const qs = userIds.map((_) => "?").join(",");
|
||||||
|
@ -891,6 +906,7 @@ function buildLoaders(db) {
|
||||||
loaders.speciesLoader = buildSpeciesLoader(db);
|
loaders.speciesLoader = buildSpeciesLoader(db);
|
||||||
loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db);
|
loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db);
|
||||||
loaders.userLoader = buildUserLoader(db);
|
loaders.userLoader = buildUserLoader(db);
|
||||||
|
loaders.userByNameLoader = buildUserByNameLoader(db);
|
||||||
loaders.userClosetHangersLoader = buildUserClosetHangersLoader(db);
|
loaders.userClosetHangersLoader = buildUserClosetHangersLoader(db);
|
||||||
loaders.userClosetListsLoader = buildUserClosetListsLoader(db);
|
loaders.userClosetListsLoader = buildUserClosetListsLoader(db);
|
||||||
loaders.zoneLoader = buildZoneLoader(db);
|
loaders.zoneLoader = buildZoneLoader(db);
|
||||||
|
|
|
@ -40,6 +40,7 @@ const typeDefs = gql`
|
||||||
|
|
||||||
extend type Query {
|
extend type Query {
|
||||||
user(id: ID!): User
|
user(id: ID!): User
|
||||||
|
userByName(name: String!): User
|
||||||
currentUser: User
|
currentUser: User
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -213,6 +214,16 @@ const resolvers = {
|
||||||
|
|
||||||
return { id };
|
return { id };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
userByName: async (_, { name }, { userByNameLoader }) => {
|
||||||
|
const user = await userByNameLoader.load(name);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: user.id };
|
||||||
|
},
|
||||||
|
|
||||||
currentUser: async (_, __, { currentUserId, userLoader }) => {
|
currentUser: async (_, __, { currentUserId, userLoader }) => {
|
||||||
if (currentUserId == null) {
|
if (currentUserId == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
Loading…
Reference in a new issue