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

View file

@ -1,7 +1,6 @@
import React from "react";
import { css, cx } from "emotion";
import {
Badge,
Box,
Flex,
IconButton,
@ -10,24 +9,19 @@ import {
useColorModeValue,
useTheme,
} from "@chakra-ui/core";
import {
EditIcon,
DeleteIcon,
InfoIcon,
NotAllowedIcon,
} from "@chakra-ui/icons";
import { EditIcon, DeleteIcon, InfoIcon } from "@chakra-ui/icons";
import { Link } from "react-router-dom";
import loadable from "@loadable/component";
import {
ItemCardContent,
ItemBadgeList,
ItemBadgeTooltip,
MaybeAnimatedBadge,
NcBadge,
NpBadge,
YouOwnThisBadge,
YouWantThisBadge,
ZoneBadgeList,
} from "../components/ItemCard";
import SupportOnly from "./support/SupportOnly";
import useSupport from "./support/useSupport";
@ -207,11 +201,9 @@ function ItemContainer({ children, isDisabled = false }) {
function ItemBadges({ item }) {
const { isSupportUser } = useSupport();
const occupiedZoneLabels = getZoneLabels(
item.appearanceOn.layers.map((l) => l.zone)
);
const restrictedZoneLabels = getZoneLabels(
item.appearanceOn.restrictedZones.filter((z) => z.isCommonlyUsedByItems)
const occupiedZones = item.appearanceOn.layers.map((l) => l.zone);
const restrictedZones = item.appearanceOn.restrictedZones.filter(
(z) => z.isCommonlyUsedByItems
);
const isMaybeAnimated = item.appearanceOn.layers.some(
(l) => l.canvasMovieLibraryUrl
@ -239,12 +231,8 @@ function ItemBadges({ item }) {
}
{item.currentUserOwnsThis && <YouOwnThisBadge variant="short" />}
{item.currentUserWantsThis && <YouWantThisBadge variant="short" />}
{occupiedZoneLabels.map((zoneLabel) => (
<ZoneBadge key={zoneLabel} variant="occupies" zoneLabel={zoneLabel} />
))}
{restrictedZoneLabels.map((zoneLabel) => (
<ZoneBadge key={zoneLabel} variant="restricts" zoneLabel={zoneLabel} />
))}
<ZoneBadgeList zones={occupiedZones} variant="occupied" />
<ZoneBadgeList zones={restrictedZones} variant="restricts" />
</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
* .item-container is hovered or the adjacent hidden radio/checkbox is

View file

@ -9,7 +9,7 @@ import {
useColorModeValue,
useTheme,
} 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 { Link } from "react-router-dom";
@ -279,6 +279,53 @@ export function YouWantThisBadge({ variant = "long" }) {
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() {
return (
<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));
});
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) =>
new DataLoader(async (petTypeIds) => {
const qs = petTypeIds.map((_) => "?").join(",");
@ -745,6 +765,7 @@ function buildLoaders(db) {
loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader(
db
);
loaders.itemAllOccupiedZonesLoader = buildItemAllOccupiedZonesLoader(db);
loaders.petTypeLoader = buildPetTypeLoader(db, loaders);
loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
db,

View file

@ -26,6 +26,9 @@ describe("Item", () => {
name
}
explicitlyBodySpecific
allOccupiedZones {
label
}
}
}
`,
@ -57,6 +60,22 @@ describe("Item", () => {
"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 [
"SELECT * FROM color_translations
WHERE color_id IN (?) AND locale = \\"en\\"",
@ -64,6 +83,17 @@ describe("Item", () => {
"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 {
"items": Array [
Object {
"allOccupiedZones": Array [
Object {
"label": "Gloves",
},
],
"createdAt": null,
"description": "Dont leave any trace that you were there with these gloves.",
"explicitlyBodySpecific": false,
@ -7994,6 +7999,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif",
},
Object {
"allOccupiedZones": Array [
Object {
"label": "Hat",
},
],
"createdAt": null,
"description": "Hide your face and hair so no one can recognise you.",
"explicitlyBodySpecific": false,
@ -8005,6 +8015,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_hood.gif",
},
Object {
"allOccupiedZones": Array [
Object {
"label": "Jacket",
},
],
"createdAt": null,
"description": "This robe is great for being stealthy.",
"explicitlyBodySpecific": false,
@ -8016,6 +8031,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif",
},
Object {
"allOccupiedZones": Array [
Object {
"label": "Static",
},
],
"createdAt": "2020-01-01T00:00:00.000Z",
"description": "Maybe youll be discovered by some Neopets from the future and thawed out!",
"explicitlyBodySpecific": true,
@ -8027,6 +8047,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/mall_petinice.gif",
},
Object {
"allOccupiedZones": Array [
Object {
"label": "Shirt/Dress",
},
],
"createdAt": "2020-01-01T00:00:00.000Z",
"description": "Made with the finest jewels of the sea!",
"explicitlyBodySpecific": false,
@ -8041,6 +8066,11 @@ Object {
"thumbnailUrl": "http://images.neopets.com/items/mall_clo_marabluegown.gif",
},
Object {
"allOccupiedZones": Array [
Object {
"label": "Background",
},
],
"createdAt": "2020-01-01T00:00:00.000Z",
"description": "You truly are the number one fan of Altador Cup, and your room reflects this!",
"explicitlyBodySpecific": false,

View file

@ -48,6 +48,11 @@ const typeDefs = gql`
# which species this is for by going through the body field on
# 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 {
@ -210,6 +215,11 @@ const resolvers = {
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: {