import React from "react"; import { ClassNames } from "@emotion/react"; import { Badge, Box, Center, Flex, IconButton, Input, InputGroup, InputLeftElement, InputRightElement, Menu, MenuButton, MenuItem, MenuList, Portal, Wrap, WrapItem, VStack, useToast, } from "@chakra-ui/react"; import { ArrowForwardIcon, CheckIcon, EditIcon, EmailIcon, SearchIcon, StarIcon, } from "@chakra-ui/icons"; import gql from "graphql-tag"; import { useHistory, useParams } from "react-router-dom"; import { useQuery, useLazyQuery, useMutation } from "@apollo/client"; import HangerSpinner from "./components/HangerSpinner"; import { Heading1, Heading2, usePageTitle } from "./util"; import SupportOnly from "./WardrobePage/support/SupportOnly"; import useSupport from "./WardrobePage/support/useSupport"; import useCurrentUser from "./components/useCurrentUser"; import { ClosetList, NeopetsStarIcon } from "./UserItemListPage"; const BadgeButton = React.forwardRef((props, ref) => ( )); function UserItemListsIndexPage() { const { userId } = useParams(); const currentUser = useCurrentUser(); const isCurrentUser = currentUser.id === userId; const { loading, error, data } = useQuery( gql` query UserItemListsIndexPage($userId: ID!) { user(id: $userId) { id username contactNeopetsUsername closetLists { id name description ownsOrWantsItems isDefaultList items { id isNc isPb name thumbnailUrl currentUserOwnsThis currentUserWantsThis } creator { id } } } } `, { variables: { userId }, context: { sendAuth: true } } ); let pageTitleText; if (isCurrentUser) { pageTitleText = "Your lists"; } else if (data?.user) { pageTitleText = `${data?.user?.username}'s lists`; } else { pageTitleText = null; } usePageTitle(pageTitleText); if (loading) { return (
); } if (error) { return {error.message}; } // TODO: I'm not sure why, but apparently `data` is `undefined` in the user // not found case, even though the server is clearly returning // `{data: {user: null}}`? That's... weird, right? :/ if (data?.user == null) { return User not found; } const listsOfOwnedItems = data.user.closetLists.filter( (l) => l.ownsOrWantsItems === "OWNS" ); const listsOfWantedItems = data.user.closetLists.filter( (l) => l.ownsOrWantsItems === "WANTS" ); // Sort default list to the end, then sort alphabetically. We use a similar // sort hack that we use for sorting items in ClosetList! listsOfOwnedItems.sort((a, b) => { const aName = `${a.isDefaultList ? "ZZZ" : "AAA"} ${a.name}`; const bName = `${b.isDefaultList ? "ZZZ" : "AAA"} ${b.name}`; return aName.localeCompare(bName); }); listsOfWantedItems.sort((a, b) => { const aName = `${a.isDefaultList ? "ZZZ" : "AAA"} ${a.name}`; const bName = `${b.isDefaultList ? "ZZZ" : "AAA"} ${b.name}`; return aName.localeCompare(bName); }); const allItemsTheyOwn = listsOfOwnedItems.map((l) => l.items).flat(); const allItemsTheyWant = listsOfWantedItems.map((l) => l.items).flat(); const itemsTheyOwnThatYouWant = allItemsTheyOwn.filter( (i) => i.currentUserWantsThis ); const itemsTheyWantThatYouOwn = allItemsTheyWant.filter( (i) => i.currentUserOwnsThis ); // It's important to de-duplicate these! Otherwise, if the same item appears // in multiple lists, we'll double-count it. const numItemsTheyOwnThatYouWant = new Set( itemsTheyOwnThatYouWant.map((i) => i.id) ).size; const numItemsTheyWantThatYouOwn = new Set( itemsTheyWantThatYouOwn.map((i) => i.id) ).size; return ( {({ css }) => ( {pageTitleText} {data.user.contactNeopetsUsername && ( {data.user.contactNeopetsUsername} )} {data.user.contactNeopetsUsername && ( Neomail )} Support {/* 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) => ( ))} {isCurrentUser ? "Items you want" : `Items ${data.user.username} wants`} {listsOfWantedItems.map((closetList) => ( ))} )} ); } function UserSearchForm() { const [query, setQuery] = React.useState(""); const { isSupportUser, supportSecret } = useSupport(); const history = useHistory(); const toast = useToast(); const [loadUserSearch, { loading: loading1 }] = useLazyQuery( gql` query UserSearchForm($name: String!) { userByName(name: $name) { id # Consider preloading UserItemListsIndexPage 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}/lists`); }, onError: (error) => { console.error(error); toast({ status: "error", title: "Error loading user!", description: "Check your connection and try again?", }); }, } ); const [loadUserByEmail, { loading: loading2 }] = useLazyQuery( gql` query UserSearchFormByEmail($email: String!, $supportSecret: String!) { userByEmail(email: $email, supportSecret: $supportSecret) { id # Consider preloading UserItemListsIndexPage fields here, too? } } `, { onCompleted: (data) => { const user = data.userByEmail; if (!user) { toast({ status: "warning", title: "We couldn't find that email address!", description: "Check the spelling and try again?", }); return; } history.push(`/user/${user.id}/lists`); }, onError: (error) => { console.error(error); toast({ status: "error", title: "Error loading user by email!", description: "Check your connection and try again?", }); }, } ); return ( { const isSupportOnlyEmailSearch = isSupportUser && query.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); if (isSupportOnlyEmailSearch) { toast({ status: "info", title: "Searching by email! (💖 Support-only)", description: "The email field is protected from most users.", }); loadUserByEmail({ variables: { email: query, supportSecret } }); } else { loadUserSearch({ variables: { name: query } }); } e.preventDefault(); }} > setQuery(e.target.value)} placeholder="Find another user…" borderRadius="full" /> } aria-label="Search" isLoading={loading1 || loading2} 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 UserSupportMenu({ children, user }) { const { supportSecret } = useSupport(); const toast = useToast(); const { loading, error, data } = useQuery( gql` query UserSupportMenu($userId: ID!, $supportSecret: String!) { user(id: $userId) { id emailForSupportUsers(supportSecret: $supportSecret) } } `, { variables: { userId: user.id, supportSecret, onError: (e) => console.error(e), }, } ); const [sendEditUsernameMutation] = useMutation( gql` mutation UserSupportMenuRename( $userId: ID! $newUsername: String! $supportSecret: String! ) { setUsername( userId: $userId newUsername: $newUsername supportSecret: $supportSecret ) { id username } } `, { onCompleted: (data) => { const updatedUser = data.setUsername; toast({ status: "success", title: `Successfully renamed user ${updatedUser.id} to ${updatedUser.username}!`, }); }, } ); const editUsername = React.useCallback(() => { const newUsername = prompt( "What should this user's username be?", user.username ); if (!newUsername || newUsername === user.username) { toast({ status: "info", title: "Got it, no change!", description: `User ${user.id}'s username will continue to be ${user.username}.`, }); return; } sendEditUsernameMutation({ variables: { userId: user.id, newUsername, supportSecret }, }).catch((e) => { console.error(e); toast({ status: "error", title: "Error renaming user.", description: "See error details in the console!", }); }); }, [sendEditUsernameMutation, user.id, user.username, supportSecret, toast]); return ( {children} Edit username Send email {error && <> (Error: {error.message})} ); } export default UserItemListsIndexPage;