From 274a4f716fdf420af74ca5c8d4c643a51c7cb430 Mon Sep 17 00:00:00 2001 From: Matchu Date: Fri, 23 Oct 2020 23:29:50 -0700 Subject: [PATCH] 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 --- src/app/UserItemsPage.js | 43 ++++++++--- src/app/WardrobePage/Item.js | 73 ++----------------- src/app/components/ItemCard.js | 49 ++++++++++++- src/server/loaders.js | 21 ++++++ src/server/query-tests/Item.test.js | 30 ++++++++ .../__snapshots__/Item.test.js.snap | 30 ++++++++ src/server/types/Item.js | 10 +++ 7 files changed, 177 insertions(+), 79 deletions(-) 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: {