add individual lists to user items page

This commit is contained in:
Emi Matchu 2020-10-27 23:09:42 -07:00
parent 21039ec148
commit 4e00962edc
3 changed files with 149 additions and 83 deletions

View file

@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import { Badge, Box, Center, Wrap } from "@chakra-ui/core"; import { Badge, Box, Center, Wrap, VStack } from "@chakra-ui/core";
import { CheckIcon, EmailIcon, StarIcon } from "@chakra-ui/icons"; import { CheckIcon, EmailIcon, StarIcon } from "@chakra-ui/icons";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import HangerSpinner from "./components/HangerSpinner"; import HangerSpinner from "./components/HangerSpinner";
import { Heading1, Heading2 } from "./util"; import { Heading1, Heading2, Heading3 } from "./util";
import ItemCard, { import ItemCard, {
ItemBadgeList, ItemBadgeList,
ItemCardList, ItemCardList,
@ -32,24 +32,18 @@ function UserItemsPage() {
username username
contactNeopetsUsername contactNeopetsUsername
itemsTheyOwn { closetLists {
id id
isNc
name name
thumbnailUrl ownsOrWantsItems
currentUserWantsThis isDefaultList
allOccupiedZones { items {
id
label @client
}
}
itemsTheyWant {
id id
isNc isNc
name name
thumbnailUrl thumbnailUrl
currentUserOwnsThis currentUserOwnsThis
currentUserWantsThis
allOccupiedZones { allOccupiedZones {
id id
label @client label @client
@ -57,6 +51,7 @@ function UserItemsPage() {
} }
} }
} }
}
`, `,
{ variables: { userId } } { variables: { userId } }
); );
@ -73,36 +68,48 @@ function UserItemsPage() {
return <Box color="red.400">{error.message}</Box>; return <Box color="red.400">{error.message}</Box>;
} }
// This helps you compare your owns/wants to other users! If they own if (data.user == null) {
// something, and you want it, we say "You want this!". And if they want return <Box color="red.400">User not found</Box>;
// something, and you own it, we say "You own this!". }
const showYouOwnThisBadge = (item) =>
!isCurrentUser && item.currentUserOwnsThis;
const showYouWantThisBadge = (item) =>
!isCurrentUser && item.currentUserWantsThis;
const numYouOwnThisBadges = data.user.itemsTheyWant.filter( const listsOfOwnedItems = data.user.closetLists.filter(
showYouOwnThisBadge (l) => l.ownsOrWantsItems === "OWNS"
).length; );
const numYouWantThisBadges = data.user.itemsTheyOwn.filter( const listsOfWantedItems = data.user.closetLists.filter(
showYouWantThisBadge (l) => l.ownsOrWantsItems === "WANTS"
).length; );
const sortedItemsTheyOwn = [...data.user.itemsTheyOwn].sort((a, b) => { // Sort default list to the end, then sort alphabetically. We use a similar
// This is a cute sort hack. We sort first by, bringing "You want this!" to // sort hack that we use for sorting items in ClosetList!
// the top, and then sorting by name _within_ those two groups. listsOfOwnedItems.sort((a, b) => {
const aName = `${showYouWantThisBadge(a) ? "000" : "999"} ${a.name}`; const aName = `${a.isDefaultList ? "ZZZ" : "AAA"} ${a.name}`;
const bName = `${showYouWantThisBadge(b) ? "000" : "999"} ${b.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); return aName.localeCompare(bName);
}); });
const sortedItemsTheyWant = [...data.user.itemsTheyWant].sort((a, b) => { const allItemsTheyOwn = listsOfOwnedItems.map((l) => l.items).flat();
// This is a cute sort hack. We sort first by, bringing "You own this!" to const allItemsTheyWant = listsOfWantedItems.map((l) => l.items).flat();
// the top, and then sorting by name _within_ those two groups.
const aName = `${showYouOwnThisBadge(a) ? "000" : "999"} ${a.name}`; const itemsTheyOwnThatYouWant = allItemsTheyOwn.filter(
const bName = `${showYouOwnThisBadge(b) ? "000" : "999"} ${b.name}`; (i) => i.currentUserWantsThis
return aName.localeCompare(bName); );
}); 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 ( return (
<Box> <Box>
@ -140,7 +147,7 @@ function UserItemsPage() {
* _this user_ owns, so they come first. I think it's also probably a * _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_ * more natural train of thought: you come to someone's list _wanting_
* something, and _then_ thinking about what you can offer. */} * something, and _then_ thinking about what you can offer. */}
{numYouWantThisBadges > 0 && ( {!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && (
<Badge <Badge
as="a" as="a"
href="#owned-items" href="#owned-items"
@ -149,12 +156,12 @@ function UserItemsPage() {
alignItems="center" alignItems="center"
> >
<StarIcon marginRight="1" /> <StarIcon marginRight="1" />
{numYouWantThisBadges > 1 {numItemsTheyOwnThatYouWant > 1
? `${numYouWantThisBadges} items you want` ? `${numItemsTheyOwnThatYouWant} items you want`
: "1 item you want"} : "1 item you want"}
</Badge> </Badge>
)} )}
{numYouOwnThisBadges > 0 && ( {!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && (
<Badge <Badge
as="a" as="a"
href="#wanted-items" href="#wanted-items"
@ -163,48 +170,84 @@ function UserItemsPage() {
alignItems="center" alignItems="center"
> >
<CheckIcon marginRight="1" /> <CheckIcon marginRight="1" />
{numYouOwnThisBadges > 1 {numItemsTheyWantThatYouOwn > 1
? `${numYouOwnThisBadges} items you own` ? `${numItemsTheyWantThatYouOwn} items you own`
: "1 item you own"} : "1 item you own"}
</Badge> </Badge>
)} )}
</Wrap> </Wrap>
<Heading2 id="owned-items" marginTop="4" marginBottom="6"> <Heading2 id="owned-items" marginTop="4" marginBottom="2">
{isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`} {isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`}
</Heading2> </Heading2>
<ItemCardList> <VStack spacing="8" alignItems="stretch">
{sortedItemsTheyOwn.map((item) => { {listsOfOwnedItems.map((closetList) => (
return ( <ClosetList
<ItemCard key={closetList.id}
key={item.id} closetList={closetList}
item={item} isCurrentUser={isCurrentUser}
badges={ showHeading={listsOfOwnedItems.length > 1}
<ItemBadgeList>
{item.isNc ? <NcBadge /> : <NpBadge />}
{showYouWantThisBadge(item) && <YouWantThisBadge />}
<ZoneBadgeList
zones={item.allOccupiedZones}
variant="occupies"
/> />
</ItemBadgeList> ))}
} </VStack>
/>
);
})}
</ItemCardList>
<Heading2 id="wanted-items" marginBottom="6" marginTop="8"> <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`}
</Heading2> </Heading2>
<VStack spacing="4" alignItems="stretch">
{listsOfWantedItems.map((closetList) => (
<ClosetList
key={closetList.id}
closetList={closetList}
isCurrentUser={isCurrentUser}
showHeading={listsOfWantedItems.length > 1}
/>
))}
</VStack>
</Box>
);
}
function ClosetList({ closetList, isCurrentUser, showHeading }) {
const hasYouWantThisBadge = (item) =>
!isCurrentUser &&
closetList.ownsOrWantsItems === "OWNS" &&
item.currentUserWantsThis;
const hasYouOwnThisBadge = (item) =>
!isCurrentUser &&
closetList.ownsOrWantsItems === "WANTS" &&
item.currentUserOwnsThis;
const hasAnyTradeBadge = (item) =>
hasYouOwnThisBadge(item) || hasYouWantThisBadge(item);
const sortedItems = [...closetList.items].sort((a, b) => {
// This is a cute sort hack. We sort first by, bringing "You own/want
// this!" to the top, and then sorting by name _within_ those two groups.
const aName = `${hasAnyTradeBadge(a) ? "000" : "999"} ${a.name}`;
const bName = `${hasAnyTradeBadge(b) ? "000" : "999"} ${b.name}`;
return aName.localeCompare(bName);
});
return (
<Box>
{showHeading && (
<Heading3
marginBottom="2"
fontStyle={closetList.isDefaultList ? "italic" : "normal"}
>
{closetList.name}
</Heading3>
)}
{sortedItems.length > 0 ? (
<ItemCardList> <ItemCardList>
{sortedItemsTheyWant.map((item) => ( {sortedItems.map((item) => (
<ItemCard <ItemCard
key={item.id} key={item.id}
item={item} item={item}
badges={ badges={
<ItemBadgeList> <ItemBadgeList>
{item.isNc ? <NcBadge /> : <NpBadge />} {item.isNc ? <NcBadge /> : <NpBadge />}
{showYouOwnThisBadge(item) && <YouOwnThisBadge />} {hasYouOwnThisBadge(item) && <YouOwnThisBadge />}
{hasYouWantThisBadge(item) && <YouWantThisBadge />}
<ZoneBadgeList <ZoneBadgeList
zones={item.allOccupiedZones} zones={item.allOccupiedZones}
variant="occupies" variant="occupies"
@ -214,6 +257,9 @@ function UserItemsPage() {
/> />
))} ))}
</ItemCardList> </ItemCardList>
) : (
<Box fontStyle="italic">This list is empty!</Box>
)}
</Box> </Box>
); );
} }

View file

@ -34,6 +34,7 @@ export function Delay({ children, ms = 300 }) {
export function Heading1({ children, ...props }) { export function Heading1({ children, ...props }) {
return ( return (
<Heading <Heading
as="h1"
size="2xl" size="2xl"
fontFamily="Delicious, sans-serif" fontFamily="Delicious, sans-serif"
fontWeight="800" fontWeight="800"
@ -51,6 +52,7 @@ export function Heading1({ children, ...props }) {
export function Heading2({ children, ...props }) { export function Heading2({ children, ...props }) {
return ( return (
<Heading <Heading
as="h2"
size="xl" size="xl"
fontFamily="Delicious, sans-serif" fontFamily="Delicious, sans-serif"
fontWeight="700" fontWeight="700"
@ -61,6 +63,24 @@ export function Heading2({ children, ...props }) {
); );
} }
/**
* Heading2 is a minor subheading, with our DTI-brand-y Delicious font and some
* special typographical styles!!
*/
export function Heading3({ children, ...props }) {
return (
<Heading
as="h3"
size="lg"
fontFamily="Delicious, sans-serif"
fontWeight="700"
{...props}
>
{children}
</Heading>
);
}
/** /**
* safeImageUrl returns an HTTPS-safe image URL for Neopets assets! * safeImageUrl returns an HTTPS-safe image URL for Neopets assets!
*/ */

View file

@ -168,7 +168,7 @@ const resolvers = {
if (isCurrentUser || user.ownedClosetHangersVisibility >= 1) { if (isCurrentUser || user.ownedClosetHangersVisibility >= 1) {
closetListNodes.push({ closetListNodes.push({
id: `user-${id}-default-list-OWNS`, id: `user-${id}-default-list-OWNS`,
name: "(Not in a list)", name: "Not in a list",
ownsOrWantsItems: "OWNS", ownsOrWantsItems: "OWNS",
isDefaultList: true, isDefaultList: true,
items: allClosetHangers items: allClosetHangers
@ -180,7 +180,7 @@ const resolvers = {
if (isCurrentUser || user.wantedClosetHangersVisibility >= 1) { if (isCurrentUser || user.wantedClosetHangersVisibility >= 1) {
closetListNodes.push({ closetListNodes.push({
id: `user-${id}-default-list-WANTS`, id: `user-${id}-default-list-WANTS`,
name: "(Not in a list)", name: "Not in a list",
ownsOrWantsItems: "WANTS", ownsOrWantsItems: "WANTS",
isDefaultList: true, isDefaultList: true,
items: allClosetHangers items: allClosetHangers