diff --git a/src/app/UserItemsPage.js b/src/app/UserItemsPage.js
index 04d7b25..c630865 100644
--- a/src/app/UserItemsPage.js
+++ b/src/app/UserItemsPage.js
@@ -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`}
- {sortedItemsTheyOwn.map((item) => (
-
- {item.isNc ? : }
- {showYouWantThisBadge(item) && }
-
- }
- />
- ))}
+ {sortedItemsTheyOwn.map((item) => {
+ return (
+
+ {item.isNc ? : }
+ {showYouWantThisBadge(item) && }
+
+
+ }
+ />
+ );
+ })}
@@ -140,6 +155,10 @@ function UserItemsPage() {
{item.isNc ? : }
{showYouOwnThisBadge(item) && }
+
}
/>
diff --git a/src/app/WardrobePage/Item.js b/src/app/WardrobePage/Item.js
index 64b2b67..cea5a02 100644
--- a/src/app/WardrobePage/Item.js
+++ b/src/app/WardrobePage/Item.js
@@ -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 && }
{item.currentUserWantsThis && }
- {occupiedZoneLabels.map((zoneLabel) => (
-
- ))}
- {restrictedZoneLabels.map((zoneLabel) => (
-
- ))}
+
+
);
}
@@ -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 (
-
-
-
- {shorthand}
-
-
-
- );
- }
-
- if (shorthand !== zoneLabel) {
- return (
-
- {shorthand}
-
- );
- }
-
- return {shorthand};
-}
-
/**
* containerHasFocus is a common CSS selector, for the case where our parent
* .item-container is hovered or the adjacent hidden radio/checkbox is
diff --git a/src/app/components/ItemCard.js b/src/app/components/ItemCard.js
index df4b136..9bffeb3 100644
--- a/src/app/components/ItemCard.js
+++ b/src/app/components/ItemCard.js
@@ -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 (
+
+
+
+ {shorthand}
+
+
+
+ );
+ }
+
+ if (shorthand !== zoneLabel) {
+ return (
+
+ {shorthand}
+
+ );
+ }
+
+ return {shorthand};
+}
+
+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) => (
+
+ ));
+}
+
export function MaybeAnimatedBadge() {
return (
diff --git a/src/server/loaders.js b/src/server/loaders.js
index 68d8de8..4636cc0 100644
--- a/src/server/loaders.js
+++ b/src/server/loaders.js
@@ -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,
diff --git a/src/server/query-tests/Item.test.js b/src/server/query-tests/Item.test.js
index 0adac31..27945bc 100644
--- a/src/server/query-tests/Item.test.js
+++ b/src/server/query-tests/Item.test.js
@@ -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",
+ ],
+ ],
]
`);
});
diff --git a/src/server/query-tests/__snapshots__/Item.test.js.snap b/src/server/query-tests/__snapshots__/Item.test.js.snap
index c933c99..4b104b4 100644
--- a/src/server/query-tests/__snapshots__/Item.test.js.snap
+++ b/src/server/query-tests/__snapshots__/Item.test.js.snap
@@ -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,
diff --git a/src/server/types/Item.js b/src/server/types/Item.js
index ea51a06..f276b52 100644
--- a/src/server/types/Item.js
+++ b/src/server/types/Item.js
@@ -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: {