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 { 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 gql from "graphql-tag";
import { useParams } from "react-router-dom";
import { useQuery } from "@apollo/client";
import HangerSpinner from "./components/HangerSpinner";
import { Heading1, Heading2 } from "./util";
import { Heading1, Heading2, Heading3 } from "./util";
import ItemCard, {
ItemBadgeList,
ItemCardList,
@ -32,27 +32,22 @@ function UserItemsPage() {
username
contactNeopetsUsername
itemsTheyOwn {
closetLists {
id
isNc
name
thumbnailUrl
currentUserWantsThis
allOccupiedZones {
ownsOrWantsItems
isDefaultList
items {
id
label @client
}
}
itemsTheyWant {
id
isNc
name
thumbnailUrl
currentUserOwnsThis
allOccupiedZones {
id
label @client
isNc
name
thumbnailUrl
currentUserOwnsThis
currentUserWantsThis
allOccupiedZones {
id
label @client
}
}
}
}
@ -73,36 +68,48 @@ function UserItemsPage() {
return <Box color="red.400">{error.message}</Box>;
}
// This helps you compare your owns/wants to other users! If they own
// something, and you want it, we say "You want this!". And if they want
// something, and you own it, we say "You own this!".
const showYouOwnThisBadge = (item) =>
!isCurrentUser && item.currentUserOwnsThis;
const showYouWantThisBadge = (item) =>
!isCurrentUser && item.currentUserWantsThis;
if (data.user == null) {
return <Box color="red.400">User not found</Box>;
}
const numYouOwnThisBadges = data.user.itemsTheyWant.filter(
showYouOwnThisBadge
).length;
const numYouWantThisBadges = data.user.itemsTheyOwn.filter(
showYouWantThisBadge
).length;
const listsOfOwnedItems = data.user.closetLists.filter(
(l) => l.ownsOrWantsItems === "OWNS"
);
const listsOfWantedItems = data.user.closetLists.filter(
(l) => l.ownsOrWantsItems === "WANTS"
);
const sortedItemsTheyOwn = [...data.user.itemsTheyOwn].sort((a, b) => {
// This is a cute sort hack. We sort first by, bringing "You want this!" to
// the top, and then sorting by name _within_ those two groups.
const aName = `${showYouWantThisBadge(a) ? "000" : "999"} ${a.name}`;
const bName = `${showYouWantThisBadge(b) ? "000" : "999"} ${b.name}`;
// 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 sortedItemsTheyWant = [...data.user.itemsTheyWant].sort((a, b) => {
// This is a cute sort hack. We sort first by, bringing "You own this!" to
// the top, and then sorting by name _within_ those two groups.
const aName = `${showYouOwnThisBadge(a) ? "000" : "999"} ${a.name}`;
const bName = `${showYouOwnThisBadge(b) ? "000" : "999"} ${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 (
<Box>
@ -140,7 +147,7 @@ function UserItemsPage() {
* _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. */}
{numYouWantThisBadges > 0 && (
{!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && (
<Badge
as="a"
href="#owned-items"
@ -149,12 +156,12 @@ function UserItemsPage() {
alignItems="center"
>
<StarIcon marginRight="1" />
{numYouWantThisBadges > 1
? `${numYouWantThisBadges} items you want`
{numItemsTheyOwnThatYouWant > 1
? `${numItemsTheyOwnThatYouWant} items you want`
: "1 item you want"}
</Badge>
)}
{numYouOwnThisBadges > 0 && (
{!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && (
<Badge
as="a"
href="#wanted-items"
@ -163,25 +170,84 @@ function UserItemsPage() {
alignItems="center"
>
<CheckIcon marginRight="1" />
{numYouOwnThisBadges > 1
? `${numYouOwnThisBadges} items you own`
{numItemsTheyWantThatYouOwn > 1
? `${numItemsTheyWantThatYouOwn} items you own`
: "1 item you own"}
</Badge>
)}
</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`}
</Heading2>
<ItemCardList>
{sortedItemsTheyOwn.map((item) => {
return (
<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">
{isCurrentUser ? "Items you want" : `Items ${data.user.username} wants`}
</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>
{sortedItems.map((item) => (
<ItemCard
key={item.id}
item={item}
badges={
<ItemBadgeList>
{item.isNc ? <NcBadge /> : <NpBadge />}
{showYouWantThisBadge(item) && <YouWantThisBadge />}
{hasYouOwnThisBadge(item) && <YouOwnThisBadge />}
{hasYouWantThisBadge(item) && <YouWantThisBadge />}
<ZoneBadgeList
zones={item.allOccupiedZones}
variant="occupies"
@ -189,31 +255,11 @@ function UserItemsPage() {
</ItemBadgeList>
}
/>
);
})}
</ItemCardList>
<Heading2 id="wanted-items" marginBottom="6" marginTop="8">
{isCurrentUser ? "Items you want" : `Items ${data.user.username} wants`}
</Heading2>
<ItemCardList>
{sortedItemsTheyWant.map((item) => (
<ItemCard
key={item.id}
item={item}
badges={
<ItemBadgeList>
{item.isNc ? <NcBadge /> : <NpBadge />}
{showYouOwnThisBadge(item) && <YouOwnThisBadge />}
<ZoneBadgeList
zones={item.allOccupiedZones}
variant="occupies"
/>
</ItemBadgeList>
}
/>
))}
</ItemCardList>
))}
</ItemCardList>
) : (
<Box fontStyle="italic">This list is empty!</Box>
)}
</Box>
);
}

View file

@ -34,6 +34,7 @@ export function Delay({ children, ms = 300 }) {
export function Heading1({ children, ...props }) {
return (
<Heading
as="h1"
size="2xl"
fontFamily="Delicious, sans-serif"
fontWeight="800"
@ -51,6 +52,7 @@ export function Heading1({ children, ...props }) {
export function Heading2({ children, ...props }) {
return (
<Heading
as="h2"
size="xl"
fontFamily="Delicious, sans-serif"
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!
*/

View file

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