add zones to user items page

idk the labels section was feeling empty, and I didn't see a way to streamline it more, so I figured, add info that might be useful! lol
This commit is contained in:
Emi Matchu 2020-10-23 23:29:50 -07:00
parent 5111ebcbfe
commit 274a4f716f
7 changed files with 177 additions and 79 deletions

View file

@ -13,6 +13,7 @@ import ItemCard, {
NpBadge, NpBadge,
YouOwnThisBadge, YouOwnThisBadge,
YouWantThisBadge, YouWantThisBadge,
ZoneBadgeList,
} from "./components/ItemCard"; } from "./components/ItemCard";
import useCurrentUser from "./components/useCurrentUser"; import useCurrentUser from "./components/useCurrentUser";
import WIPCallout from "./components/WIPCallout"; import WIPCallout from "./components/WIPCallout";
@ -35,6 +36,10 @@ function UserItemsPage() {
isNc isNc
name name
thumbnailUrl thumbnailUrl
allOccupiedZones {
id
label @client
}
} }
itemsTheyWant { itemsTheyWant {
@ -42,6 +47,10 @@ function UserItemsPage() {
isNc isNc
name name
thumbnailUrl thumbnailUrl
allOccupiedZones {
id
label @client
}
} }
} }
@ -114,18 +123,24 @@ function UserItemsPage() {
{isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`} {isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`}
</Heading2> </Heading2>
<ItemCardList> <ItemCardList>
{sortedItemsTheyOwn.map((item) => ( {sortedItemsTheyOwn.map((item) => {
<ItemCard return (
key={item.id} <ItemCard
item={item} key={item.id}
badges={ item={item}
<ItemBadgeList> badges={
{item.isNc ? <NcBadge /> : <NpBadge />} <ItemBadgeList>
{showYouWantThisBadge(item) && <YouWantThisBadge />} {item.isNc ? <NcBadge /> : <NpBadge />}
</ItemBadgeList> {showYouWantThisBadge(item) && <YouWantThisBadge />}
} <ZoneBadgeList
/> zones={item.allOccupiedZones}
))} variant="occupies"
/>
</ItemBadgeList>
}
/>
);
})}
</ItemCardList> </ItemCardList>
<Heading2 marginBottom="6" marginTop="8"> <Heading2 marginBottom="6" marginTop="8">
@ -140,6 +155,10 @@ function UserItemsPage() {
<ItemBadgeList> <ItemBadgeList>
{item.isNc ? <NcBadge /> : <NpBadge />} {item.isNc ? <NcBadge /> : <NpBadge />}
{showYouOwnThisBadge(item) && <YouOwnThisBadge />} {showYouOwnThisBadge(item) && <YouOwnThisBadge />}
<ZoneBadgeList
zones={item.allOccupiedZones}
variant="occupies"
/>
</ItemBadgeList> </ItemBadgeList>
} }
/> />

View file

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { css, cx } from "emotion"; import { css, cx } from "emotion";
import { import {
Badge,
Box, Box,
Flex, Flex,
IconButton, IconButton,
@ -10,24 +9,19 @@ import {
useColorModeValue, useColorModeValue,
useTheme, useTheme,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { import { EditIcon, DeleteIcon, InfoIcon } from "@chakra-ui/icons";
EditIcon,
DeleteIcon,
InfoIcon,
NotAllowedIcon,
} from "@chakra-ui/icons";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import loadable from "@loadable/component"; import loadable from "@loadable/component";
import { import {
ItemCardContent, ItemCardContent,
ItemBadgeList, ItemBadgeList,
ItemBadgeTooltip,
MaybeAnimatedBadge, MaybeAnimatedBadge,
NcBadge, NcBadge,
NpBadge, NpBadge,
YouOwnThisBadge, YouOwnThisBadge,
YouWantThisBadge, YouWantThisBadge,
ZoneBadgeList,
} from "../components/ItemCard"; } from "../components/ItemCard";
import SupportOnly from "./support/SupportOnly"; import SupportOnly from "./support/SupportOnly";
import useSupport from "./support/useSupport"; import useSupport from "./support/useSupport";
@ -207,11 +201,9 @@ function ItemContainer({ children, isDisabled = false }) {
function ItemBadges({ item }) { function ItemBadges({ item }) {
const { isSupportUser } = useSupport(); const { isSupportUser } = useSupport();
const occupiedZoneLabels = getZoneLabels( const occupiedZones = item.appearanceOn.layers.map((l) => l.zone);
item.appearanceOn.layers.map((l) => l.zone) const restrictedZones = item.appearanceOn.restrictedZones.filter(
); (z) => z.isCommonlyUsedByItems
const restrictedZoneLabels = getZoneLabels(
item.appearanceOn.restrictedZones.filter((z) => z.isCommonlyUsedByItems)
); );
const isMaybeAnimated = item.appearanceOn.layers.some( const isMaybeAnimated = item.appearanceOn.layers.some(
(l) => l.canvasMovieLibraryUrl (l) => l.canvasMovieLibraryUrl
@ -239,12 +231,8 @@ function ItemBadges({ item }) {
} }
{item.currentUserOwnsThis && <YouOwnThisBadge variant="short" />} {item.currentUserOwnsThis && <YouOwnThisBadge variant="short" />}
{item.currentUserWantsThis && <YouWantThisBadge variant="short" />} {item.currentUserWantsThis && <YouWantThisBadge variant="short" />}
{occupiedZoneLabels.map((zoneLabel) => ( <ZoneBadgeList zones={occupiedZones} variant="occupied" />
<ZoneBadge key={zoneLabel} variant="occupies" zoneLabel={zoneLabel} /> <ZoneBadgeList zones={restrictedZones} variant="restricts" />
))}
{restrictedZoneLabels.map((zoneLabel) => (
<ZoneBadge key={zoneLabel} variant="restricts" zoneLabel={zoneLabel} />
))}
</ItemBadgeList> </ItemBadgeList>
); );
} }
@ -323,53 +311,6 @@ export function ItemListSkeleton({ count }) {
); );
} }
/**
* getZoneLabels returns the set of labels for the given zones. Sometimes an
* item occupies multiple zones of the same name, so it's especially important
* to de-duplicate them here!
*/
function getZoneLabels(zones) {
let labels = zones.map((z) => z.label);
labels = new Set(labels);
labels = [...labels].sort();
return labels;
}
function ZoneBadge({ variant, zoneLabel }) {
// Shorten the label when necessary, to make the badges less bulky
const shorthand = zoneLabel
.replace("Background Item", "BG Item")
.replace("Foreground Item", "FG Item")
.replace("Lower-body", "Lower")
.replace("Upper-body", "Upper")
.replace("Transient", "Trans")
.replace("Biology", "Bio");
if (variant === "restricts") {
return (
<ItemBadgeTooltip
label={`Restricted: This item can't be worn with ${zoneLabel} items`}
>
<Badge>
<Box display="flex" alignItems="center">
{shorthand} <NotAllowedIcon marginLeft="1" />
</Box>
</Badge>
</ItemBadgeTooltip>
);
}
if (shorthand !== zoneLabel) {
return (
<ItemBadgeTooltip label={zoneLabel}>
<Badge>{shorthand}</Badge>
</ItemBadgeTooltip>
);
}
return <Badge>{shorthand}</Badge>;
}
/** /**
* containerHasFocus is a common CSS selector, for the case where our parent * containerHasFocus is a common CSS selector, for the case where our parent
* .item-container is hovered or the adjacent hidden radio/checkbox is * .item-container is hovered or the adjacent hidden radio/checkbox is

View file

@ -9,7 +9,7 @@ import {
useColorModeValue, useColorModeValue,
useTheme, useTheme,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { CheckIcon, StarIcon } from "@chakra-ui/icons"; import { CheckIcon, NotAllowedIcon, StarIcon } from "@chakra-ui/icons";
import { HiSparkles } from "react-icons/hi"; import { HiSparkles } from "react-icons/hi";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -279,6 +279,53 @@ export function YouWantThisBadge({ variant = "long" }) {
return badge; return badge;
} }
function ZoneBadge({ variant, zoneLabel }) {
// Shorten the label when necessary, to make the badges less bulky
const shorthand = zoneLabel
.replace("Background Item", "BG Item")
.replace("Foreground Item", "FG Item")
.replace("Lower-body", "Lower")
.replace("Upper-body", "Upper")
.replace("Transient", "Trans")
.replace("Biology", "Bio");
if (variant === "restricts") {
return (
<ItemBadgeTooltip
label={`Restricted: This item can't be worn with ${zoneLabel} items`}
>
<Badge>
<Box display="flex" alignItems="center">
{shorthand} <NotAllowedIcon marginLeft="1" />
</Box>
</Badge>
</ItemBadgeTooltip>
);
}
if (shorthand !== zoneLabel) {
return (
<ItemBadgeTooltip label={zoneLabel}>
<Badge>{shorthand}</Badge>
</ItemBadgeTooltip>
);
}
return <Badge>{shorthand}</Badge>;
}
export function ZoneBadgeList({ zones, variant }) {
// Get the sorted zone labels. Sometimes an item occupies multiple zones of
// the same name, so it's important to de-duplicate them!
let labels = zones.map((z) => z.label);
labels = new Set(labels);
labels = [...labels].sort();
return labels.map((label) => (
<ZoneBadge key={label} zoneLabel={label} variant={variant} />
));
}
export function MaybeAnimatedBadge() { export function MaybeAnimatedBadge() {
return ( return (
<ItemBadgeTooltip label="Maybe animated? (Support only)"> <ItemBadgeTooltip label="Maybe animated? (Support only)">

View file

@ -324,6 +324,26 @@ const buildItemBodiesWithAppearanceDataLoader = (db) =>
return itemIds.map((itemId) => entities.filter((e) => e.itemId === itemId)); return itemIds.map((itemId) => entities.filter((e) => e.itemId === itemId));
}); });
const buildItemAllOccupiedZonesLoader = (db) =>
new DataLoader(async (itemIds) => {
const qs = itemIds.map((_) => "?").join(", ");
const [rows, _] = await db.execute(
`SELECT items.id, GROUP_CONCAT(DISTINCT sa.zone_id) AS zone_ids FROM items
INNER JOIN parents_swf_assets psa
ON psa.parent_type = "Item" AND psa.parent_id = items.id
INNER JOIN swf_assets sa ON sa.id = psa.swf_asset_id
WHERE items.id IN (${qs})
GROUP BY items.id;`,
itemIds
);
const entities = rows.map(normalizeRow);
return itemIds.map((itemId) =>
entities.find((e) => e.id === itemId).zoneIds.split(",")
);
});
const buildPetTypeLoader = (db, loaders) => const buildPetTypeLoader = (db, loaders) =>
new DataLoader(async (petTypeIds) => { new DataLoader(async (petTypeIds) => {
const qs = petTypeIds.map((_) => "?").join(","); const qs = petTypeIds.map((_) => "?").join(",");
@ -745,6 +765,7 @@ function buildLoaders(db) {
loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader( loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader(
db db
); );
loaders.itemAllOccupiedZonesLoader = buildItemAllOccupiedZonesLoader(db);
loaders.petTypeLoader = buildPetTypeLoader(db, loaders); loaders.petTypeLoader = buildPetTypeLoader(db, loaders);
loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader( loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
db, db,

View file

@ -26,6 +26,9 @@ describe("Item", () => {
name name
} }
explicitlyBodySpecific explicitlyBodySpecific
allOccupiedZones {
label
}
} }
} }
`, `,
@ -57,6 +60,22 @@ describe("Item", () => {
"78104", "78104",
], ],
], ],
Array [
"SELECT items.id, GROUP_CONCAT(DISTINCT sa.zone_id) AS zone_ids FROM items
INNER JOIN parents_swf_assets psa
ON psa.parent_type = \\"Item\\" AND psa.parent_id = items.id
INNER JOIN swf_assets sa ON sa.id = psa.swf_asset_id
WHERE items.id IN (?, ?, ?, ?, ?, ?)
GROUP BY items.id;",
Array [
"38913",
"38911",
"38912",
"55788",
"77530",
"78104",
],
],
Array [ Array [
"SELECT * FROM color_translations "SELECT * FROM color_translations
WHERE color_id IN (?) AND locale = \\"en\\"", WHERE color_id IN (?) AND locale = \\"en\\"",
@ -64,6 +83,17 @@ describe("Item", () => {
"44", "44",
], ],
], ],
Array [
"SELECT * FROM zone_translations WHERE zone_id IN (?,?,?,?,?,?) AND locale = \\"en\\"",
Array [
"25",
"40",
"26",
"46",
"23",
"3",
],
],
] ]
`); `);
}); });

View file

@ -7983,6 +7983,11 @@ exports[`Item loads metadata 1`] = `
Object { Object {
"items": Array [ "items": Array [
Object { Object {
"allOccupiedZones": Array [
Object {
"label": "Gloves",
},
],
"createdAt": null, "createdAt": null,
"description": "Dont leave any trace that you were there with these gloves.", "description": "Dont leave any trace that you were there with these gloves.",
"explicitlyBodySpecific": false, "explicitlyBodySpecific": false,
@ -7994,6 +7999,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif",
}, },
Object { Object {
"allOccupiedZones": Array [
Object {
"label": "Hat",
},
],
"createdAt": null, "createdAt": null,
"description": "Hide your face and hair so no one can recognise you.", "description": "Hide your face and hair so no one can recognise you.",
"explicitlyBodySpecific": false, "explicitlyBodySpecific": false,
@ -8005,6 +8015,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_hood.gif", "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_hood.gif",
}, },
Object { Object {
"allOccupiedZones": Array [
Object {
"label": "Jacket",
},
],
"createdAt": null, "createdAt": null,
"description": "This robe is great for being stealthy.", "description": "This robe is great for being stealthy.",
"explicitlyBodySpecific": false, "explicitlyBodySpecific": false,
@ -8016,6 +8031,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif", "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif",
}, },
Object { Object {
"allOccupiedZones": Array [
Object {
"label": "Static",
},
],
"createdAt": "2020-01-01T00:00:00.000Z", "createdAt": "2020-01-01T00:00:00.000Z",
"description": "Maybe youll be discovered by some Neopets from the future and thawed out!", "description": "Maybe youll be discovered by some Neopets from the future and thawed out!",
"explicitlyBodySpecific": true, "explicitlyBodySpecific": true,
@ -8027,6 +8047,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/mall_petinice.gif", "thumbnailUrl": "http://images.neopets.com/items/mall_petinice.gif",
}, },
Object { Object {
"allOccupiedZones": Array [
Object {
"label": "Shirt/Dress",
},
],
"createdAt": "2020-01-01T00:00:00.000Z", "createdAt": "2020-01-01T00:00:00.000Z",
"description": "Made with the finest jewels of the sea!", "description": "Made with the finest jewels of the sea!",
"explicitlyBodySpecific": false, "explicitlyBodySpecific": false,
@ -8041,6 +8066,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/mall_clo_marabluegown.gif", "thumbnailUrl": "http://images.neopets.com/items/mall_clo_marabluegown.gif",
}, },
Object { Object {
"allOccupiedZones": Array [
Object {
"label": "Background",
},
],
"createdAt": "2020-01-01T00:00:00.000Z", "createdAt": "2020-01-01T00:00:00.000Z",
"description": "You truly are the number one fan of Altador Cup, and your room reflects this!", "description": "You truly are the number one fan of Altador Cup, and your room reflects this!",
"explicitlyBodySpecific": false, "explicitlyBodySpecific": false,

View file

@ -48,6 +48,11 @@ const typeDefs = gql`
# which species this is for by going through the body field on # which species this is for by going through the body field on
# ItemAppearance!) # ItemAppearance!)
canonicalAppearance: ItemAppearance canonicalAppearance: ItemAppearance
# All zones that this item occupies, for at least one body. That is, it's
# a union of zones for all of its appearances! We use this for overview
# info about the item.
allOccupiedZones: [Zone!]!
} }
type ItemAppearance { type ItemAppearance {
@ -210,6 +215,11 @@ const resolvers = {
body: { id: canonicalBodyId, species: { id: rows[0].speciesId } }, body: { id: canonicalBodyId, species: { id: rows[0].speciesId } },
}; };
}, },
allOccupiedZones: async ({ id }, _, { itemAllOccupiedZonesLoader }) => {
const zoneIds = await itemAllOccupiedZonesLoader.load(id);
const zones = zoneIds.map((id) => ({ id }));
return zones;
},
}, },
ItemAppearance: { ItemAppearance: {