Add Support view for all appearances of an item
I think this will be generally useful to minimize switching around for common operations, but also I'm thinking of building a bulk assign tool for things with broken body IDs, and this will be the place for it to live, I think
This commit is contained in:
parent
535abec228
commit
7c5e7ab21a
10 changed files with 334 additions and 96 deletions
179
src/app/WardrobePage/support/AllItemLayersSupportModal.js
Normal file
179
src/app/WardrobePage/support/AllItemLayersSupportModal.js
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Wrap,
|
||||||
|
WrapItem,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { gql, useQuery } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
itemAppearanceFragment,
|
||||||
|
petAppearanceFragment,
|
||||||
|
} from "../../components/useOutfitAppearance";
|
||||||
|
import HangerSpinner from "../../components/HangerSpinner";
|
||||||
|
import { ErrorMessage, useCommonStyles } from "../../util";
|
||||||
|
import ItemSupportAppearanceLayer from "./ItemSupportAppearanceLayer";
|
||||||
|
|
||||||
|
function AllItemLayersSupportModal({ item, isOpen, onClose }) {
|
||||||
|
const { bodyBackground } = useCommonStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size="4xl" isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay>
|
||||||
|
<ModalContent background={bodyBackground}>
|
||||||
|
<ModalHeader as="h1">
|
||||||
|
<Box as="span" fontWeight="700">
|
||||||
|
Layers on all pets:
|
||||||
|
</Box>{" "}
|
||||||
|
<Box as="span" fontWeight="normal">
|
||||||
|
{item.name}
|
||||||
|
</Box>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody paddingBottom="12">
|
||||||
|
<AllItemLayersSupportModalContent item={item} />
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</ModalOverlay>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AllItemLayersSupportModalContent({ item }) {
|
||||||
|
const { loading, error, data } = useQuery(
|
||||||
|
gql`
|
||||||
|
query AllItemLayersSupportModal($itemId: ID!) {
|
||||||
|
item(id: $itemId) {
|
||||||
|
id
|
||||||
|
allAppearances {
|
||||||
|
id
|
||||||
|
body {
|
||||||
|
id
|
||||||
|
representsAllBodies
|
||||||
|
canonicalAppearance {
|
||||||
|
id
|
||||||
|
species {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
color {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isStandard
|
||||||
|
}
|
||||||
|
pose
|
||||||
|
...PetAppearanceForOutfitPreview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...ItemAppearanceForOutfitPreview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${itemAppearanceFragment}
|
||||||
|
${petAppearanceFragment}
|
||||||
|
`,
|
||||||
|
{ variables: { itemId: item.id } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Flex align="center" justify="center" minHeight="64">
|
||||||
|
<HangerSpinner />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorMessage>{error.message}</ErrorMessage>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemAppearances = [...(data.item?.allAppearances || [])].sort(
|
||||||
|
(a, b) => {
|
||||||
|
const aKey = getSortKeyForPetAppearance(a.body.canonicalAppearance);
|
||||||
|
const bKey = getSortKeyForPetAppearance(b.body.canonicalAppearance);
|
||||||
|
return aKey.localeCompare(bKey);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrap justify="center" spacing="4">
|
||||||
|
{itemAppearances.map((itemAppearance) => (
|
||||||
|
<WrapItem key={itemAppearance.id}>
|
||||||
|
<ItemAppearanceCard item={item} itemAppearance={itemAppearance} />
|
||||||
|
</WrapItem>
|
||||||
|
))}
|
||||||
|
</Wrap>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemAppearanceCard({ item, itemAppearance }) {
|
||||||
|
const petAppearance = itemAppearance.body.canonicalAppearance;
|
||||||
|
const biologyLayers = petAppearance.layers;
|
||||||
|
const itemLayers = [...itemAppearance.layers].sort(
|
||||||
|
(a, b) => a.zone.depth - b.zone.depth
|
||||||
|
);
|
||||||
|
|
||||||
|
const { brightBackground } = useCommonStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
background={brightBackground}
|
||||||
|
paddingX="4"
|
||||||
|
paddingY="3"
|
||||||
|
boxShadow="lg"
|
||||||
|
borderRadius="lg"
|
||||||
|
>
|
||||||
|
<Heading as="h2" size="sm" fontWeight="600">
|
||||||
|
{getBodyName(itemAppearance.body)}
|
||||||
|
</Heading>
|
||||||
|
<Box height="3" />
|
||||||
|
<Wrap paddingX="3" spacing="5">
|
||||||
|
{itemLayers.map((itemLayer) => (
|
||||||
|
<WrapItem key={itemLayer.id}>
|
||||||
|
<ItemSupportAppearanceLayer
|
||||||
|
item={item}
|
||||||
|
itemLayer={itemLayer}
|
||||||
|
biologyLayers={biologyLayers}
|
||||||
|
outfitState={{
|
||||||
|
speciesId: petAppearance.species.id,
|
||||||
|
colorId: petAppearance.color.id,
|
||||||
|
pose: petAppearance.pose,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WrapItem>
|
||||||
|
))}
|
||||||
|
</Wrap>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortKeyForPetAppearance({ color, species }) {
|
||||||
|
// Sort standard colors first, then special colors by name, then by species
|
||||||
|
// within each color.
|
||||||
|
return `${color.isStandard ? "A" : "Z"}-${color.name}-${species.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBodyName(body) {
|
||||||
|
if (body.representsAllBodies) {
|
||||||
|
return "All bodies";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { species, color } = body.canonicalAppearance;
|
||||||
|
const speciesName = capitalize(species.name);
|
||||||
|
const colorName = color.isStandard ? "Standard" : capitalize(color.name);
|
||||||
|
return `${colorName} ${speciesName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function capitalize(str) {
|
||||||
|
return str[0].toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AllItemLayersSupportModal;
|
|
@ -43,7 +43,7 @@ import useSupport from "./useSupport";
|
||||||
function ItemLayerSupportModal({
|
function ItemLayerSupportModal({
|
||||||
item,
|
item,
|
||||||
itemLayer,
|
itemLayer,
|
||||||
outfitState,
|
outfitState, // speciesId, colorId, pose
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
}) {
|
}) {
|
||||||
|
@ -574,7 +574,9 @@ function ItemLayerSupportModalRemoveButton({
|
||||||
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(bio|items)\/swf\/(.+?)_([a-z0-9]+)\.swf$/;
|
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(bio|items)\/swf\/(.+?)_([a-z0-9]+)\.swf$/;
|
||||||
|
|
||||||
function convertSwfUrlToPossibleManifestUrls(swfUrl) {
|
function convertSwfUrlToPossibleManifestUrls(swfUrl) {
|
||||||
const match = swfUrl.match(SWF_URL_PATTERN);
|
const match = new URL(swfUrl, "http://images.neopets.com")
|
||||||
|
.toString()
|
||||||
|
.match(SWF_URL_PATTERN);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error(`unexpected SWF URL format: ${JSON.stringify(swfUrl)}`);
|
throw new Error(`unexpected SWF URL format: ${JSON.stringify(swfUrl)}`);
|
||||||
}
|
}
|
||||||
|
|
92
src/app/WardrobePage/support/ItemSupportAppearanceLayer.js
Normal file
92
src/app/WardrobePage/support/ItemSupportAppearanceLayer.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { ClassNames } from "@emotion/react";
|
||||||
|
import { Box, useColorModeValue, useDisclosure } from "@chakra-ui/react";
|
||||||
|
import { EditIcon } from "@chakra-ui/icons";
|
||||||
|
import ItemLayerSupportModal from "./ItemLayerSupportModal";
|
||||||
|
import { OutfitLayers } from "../../components/OutfitPreview";
|
||||||
|
|
||||||
|
function ItemSupportAppearanceLayer({
|
||||||
|
item,
|
||||||
|
itemLayer,
|
||||||
|
biologyLayers,
|
||||||
|
outfitState,
|
||||||
|
}) {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
const iconButtonBgColor = useColorModeValue("green.100", "green.300");
|
||||||
|
const iconButtonColor = useColorModeValue("green.800", "gray.900");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClassNames>
|
||||||
|
{({ css }) => (
|
||||||
|
<Box
|
||||||
|
as="button"
|
||||||
|
width="150px"
|
||||||
|
textAlign="center"
|
||||||
|
fontSize="xs"
|
||||||
|
onClick={onOpen}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width="150px"
|
||||||
|
height="150px"
|
||||||
|
marginBottom="1"
|
||||||
|
boxShadow="md"
|
||||||
|
borderRadius="md"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<OutfitLayers visibleLayers={[...biologyLayers, itemLayer]} />
|
||||||
|
<Box
|
||||||
|
className={css`
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
button:hover &,
|
||||||
|
button:focus & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On touch devices, always show the icon, to clarify that this is
|
||||||
|
* an interactable object! (Whereas I expect other devices to
|
||||||
|
* discover things by exploratory hover or focus!) */
|
||||||
|
@media (hover: none) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
background={iconButtonBgColor}
|
||||||
|
color={iconButtonColor}
|
||||||
|
borderRadius="full"
|
||||||
|
boxShadow="sm"
|
||||||
|
position="absolute"
|
||||||
|
bottom="2"
|
||||||
|
right="2"
|
||||||
|
padding="2"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
width="32px"
|
||||||
|
height="32px"
|
||||||
|
>
|
||||||
|
<EditIcon
|
||||||
|
boxSize="16px"
|
||||||
|
position="relative"
|
||||||
|
top="-2px"
|
||||||
|
right="-1px"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box fontWeight="bold">{itemLayer.zone.label}</Box>
|
||||||
|
<Box>Zone ID: {itemLayer.zone.id}</Box>
|
||||||
|
<Box>DTI ID: {itemLayer.id}</Box>
|
||||||
|
<ItemLayerSupportModal
|
||||||
|
item={item}
|
||||||
|
itemLayer={itemLayer}
|
||||||
|
outfitState={outfitState}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</ClassNames>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ItemSupportAppearanceLayer;
|
|
@ -1,16 +1,17 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import { useQuery, useMutation } from "@apollo/client";
|
import { useQuery, useMutation } from "@apollo/client";
|
||||||
import { ClassNames } from "@emotion/react";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerBody,
|
DrawerBody,
|
||||||
DrawerCloseButton,
|
DrawerCloseButton,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerOverlay,
|
DrawerOverlay,
|
||||||
|
Flex,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
FormHelperText,
|
FormHelperText,
|
||||||
|
@ -25,14 +26,18 @@ import {
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { CheckCircleIcon, EditIcon, ExternalLinkIcon } from "@chakra-ui/icons";
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
} from "@chakra-ui/icons";
|
||||||
|
|
||||||
import ItemLayerSupportModal from "./ItemLayerSupportModal";
|
import AllItemLayersSupportModal from "./AllItemLayersSupportModal";
|
||||||
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
|
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
|
||||||
import { OutfitLayers } from "../../components/OutfitPreview";
|
|
||||||
import useOutfitAppearance from "../../components/useOutfitAppearance";
|
import useOutfitAppearance from "../../components/useOutfitAppearance";
|
||||||
import { OutfitStateContext } from "../useOutfitState";
|
import { OutfitStateContext } from "../useOutfitState";
|
||||||
import useSupport from "./useSupport";
|
import useSupport from "./useSupport";
|
||||||
|
import ItemSupportAppearanceLayer from "./ItemSupportAppearanceLayer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ItemSupportDrawer shows Support UI for the item when open.
|
* ItemSupportDrawer shows Support UI for the item when open.
|
||||||
|
@ -423,9 +428,22 @@ function ItemSupportAppearanceLayers({ item }) {
|
||||||
const itemLayers = visibleLayers.filter((l) => l.source === "item");
|
const itemLayers = visibleLayers.filter((l) => l.source === "item");
|
||||||
itemLayers.sort((a, b) => a.zone.depth - b.zone.depth);
|
itemLayers.sort((a, b) => a.zone.depth - b.zone.depth);
|
||||||
|
|
||||||
|
const modalState = useDisclosure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Appearance layers</FormLabel>
|
<Flex align="center">
|
||||||
|
<FormLabel>Appearance layers</FormLabel>
|
||||||
|
<Box width="4" flex="1 0 auto" />
|
||||||
|
<Button size="xs" onClick={modalState.onOpen}>
|
||||||
|
View on all pets <ChevronRightIcon />
|
||||||
|
</Button>
|
||||||
|
<AllItemLayersSupportModal
|
||||||
|
item={item}
|
||||||
|
isOpen={modalState.isOpen}
|
||||||
|
onClose={modalState.onClose}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
<HStack spacing="4" overflow="auto" paddingX="1">
|
<HStack spacing="4" overflow="auto" paddingX="1">
|
||||||
{itemLayers.map((itemLayer) => (
|
{itemLayers.map((itemLayer) => (
|
||||||
<ItemSupportAppearanceLayer
|
<ItemSupportAppearanceLayer
|
||||||
|
@ -442,88 +460,4 @@ function ItemSupportAppearanceLayers({ item }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemSupportAppearanceLayer({
|
|
||||||
item,
|
|
||||||
itemLayer,
|
|
||||||
biologyLayers,
|
|
||||||
outfitState,
|
|
||||||
}) {
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
|
|
||||||
const iconButtonBgColor = useColorModeValue("green.100", "green.300");
|
|
||||||
const iconButtonColor = useColorModeValue("green.800", "gray.900");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClassNames>
|
|
||||||
{({ css }) => (
|
|
||||||
<Box
|
|
||||||
as="button"
|
|
||||||
width="150px"
|
|
||||||
textAlign="center"
|
|
||||||
fontSize="xs"
|
|
||||||
onClick={onOpen}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
width="150px"
|
|
||||||
height="150px"
|
|
||||||
marginBottom="1"
|
|
||||||
boxShadow="md"
|
|
||||||
borderRadius="md"
|
|
||||||
position="relative"
|
|
||||||
>
|
|
||||||
<OutfitLayers visibleLayers={[...biologyLayers, itemLayer]} />
|
|
||||||
<Box
|
|
||||||
className={css`
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
|
|
||||||
button:hover &,
|
|
||||||
button:focus & {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* On touch devices, always show the icon, to clarify that this is
|
|
||||||
* an interactable object! (Whereas I expect other devices to
|
|
||||||
* discover things by exploratory hover or focus!) */
|
|
||||||
@media (hover: none) {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
background={iconButtonBgColor}
|
|
||||||
color={iconButtonColor}
|
|
||||||
borderRadius="full"
|
|
||||||
boxShadow="sm"
|
|
||||||
position="absolute"
|
|
||||||
bottom="2"
|
|
||||||
right="2"
|
|
||||||
padding="2"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
width="32px"
|
|
||||||
height="32px"
|
|
||||||
>
|
|
||||||
<EditIcon
|
|
||||||
boxSize="16px"
|
|
||||||
position="relative"
|
|
||||||
top="-2px"
|
|
||||||
right="-1px"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box fontWeight="bold">{itemLayer.zone.label}</Box>
|
|
||||||
<Box>Zone ID: {itemLayer.zone.id}</Box>
|
|
||||||
<Box>DTI ID: {itemLayer.id}</Box>
|
|
||||||
<ItemLayerSupportModal
|
|
||||||
item={item}
|
|
||||||
itemLayer={itemLayer}
|
|
||||||
outfitState={outfitState}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</ClassNames>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ItemSupportDrawer;
|
export default ItemSupportDrawer;
|
||||||
|
|
|
@ -56,7 +56,10 @@ export default function useOutfitAppearance(outfitState) {
|
||||||
pose,
|
pose,
|
||||||
appearanceId,
|
appearanceId,
|
||||||
},
|
},
|
||||||
skip: speciesId == null || colorId == null || pose == null,
|
skip:
|
||||||
|
speciesId == null ||
|
||||||
|
colorId == null ||
|
||||||
|
(pose == null && appearanceId == null),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,7 @@ export function ErrorMessage({ children, ...props }) {
|
||||||
export function useCommonStyles() {
|
export function useCommonStyles() {
|
||||||
return {
|
return {
|
||||||
brightBackground: useColorModeValue("white", "gray.700"),
|
brightBackground: useColorModeValue("white", "gray.700"),
|
||||||
|
bodyBackground: useColorModeValue("gray.50", "gray.800"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -934,12 +934,15 @@ const buildCanonicalPetStateForBodyLoader = (db, loaders) =>
|
||||||
// creates an even distribution.
|
// creates an even distribution.
|
||||||
const gender = bodyId % 2 === 0 ? "masc" : "fem";
|
const gender = bodyId % 2 === 0 ? "masc" : "fem";
|
||||||
|
|
||||||
|
const bodyCondition = bodyId !== "0" ? `pet_types.body_id = ?` : `1`;
|
||||||
|
const bodyValues = bodyId !== "0" ? [bodyId] : [];
|
||||||
|
|
||||||
const [rows, _] = await db.execute(
|
const [rows, _] = await db.execute(
|
||||||
{
|
{
|
||||||
sql: `
|
sql: `
|
||||||
SELECT pet_states.*, pet_types.* FROM pet_states
|
SELECT pet_states.*, pet_types.* FROM pet_states
|
||||||
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
|
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
|
||||||
WHERE pet_types.body_id = ?
|
WHERE ${bodyCondition}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
pet_types.color_id = ? DESC, -- Prefer preferredColorId
|
pet_types.color_id = ? DESC, -- Prefer preferredColorId
|
||||||
pet_types.color_id = ? DESC, -- Prefer fallbackColorId
|
pet_types.color_id = ? DESC, -- Prefer fallbackColorId
|
||||||
|
@ -951,7 +954,7 @@ const buildCanonicalPetStateForBodyLoader = (db, loaders) =>
|
||||||
nestTables: true,
|
nestTables: true,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
bodyId,
|
...bodyValues,
|
||||||
preferredColorId || "<ignore>",
|
preferredColorId || "<ignore>",
|
||||||
fallbackColorId,
|
fallbackColorId,
|
||||||
gender === "fem",
|
gender === "fem",
|
||||||
|
|
|
@ -37,7 +37,9 @@ async function loadAssetManifest(swfUrl) {
|
||||||
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(bio|items)\/swf\/(.+?)_([a-z0-9]+)\.swf$/;
|
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(bio|items)\/swf\/(.+?)_([a-z0-9]+)\.swf$/;
|
||||||
|
|
||||||
function convertSwfUrlToPossibleManifestUrls(swfUrl) {
|
function convertSwfUrlToPossibleManifestUrls(swfUrl) {
|
||||||
const match = swfUrl.match(SWF_URL_PATTERN);
|
const match = new URL(swfUrl, "http://images.neopets.com")
|
||||||
|
.toString()
|
||||||
|
.match(SWF_URL_PATTERN);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error(`unexpected SWF URL format: ${JSON.stringify(swfUrl)}`);
|
throw new Error(`unexpected SWF URL format: ${JSON.stringify(swfUrl)}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,6 +101,10 @@ const typeDefs = gql`
|
||||||
# occupies for that body. Note that this might return the special
|
# occupies for that body. Note that this might return the special
|
||||||
# representsAllPets body, e.g. if this is just a Background!
|
# representsAllPets body, e.g. if this is just a Background!
|
||||||
compatibleBodiesAndTheirZones: [BodyAndZones!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
|
compatibleBodiesAndTheirZones: [BodyAndZones!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneWeek})
|
||||||
|
|
||||||
|
# All appearances for this item. Used in Support tools, to show and manage
|
||||||
|
# how this item fits every pet body.
|
||||||
|
allAppearances: [ItemAppearance!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type ItemAppearance {
|
type ItemAppearance {
|
||||||
|
@ -462,6 +466,24 @@ const resolvers = {
|
||||||
zones: row.zoneIds.split(",").map((zoneId) => ({ id: zoneId })),
|
zones: row.zoneIds.split(",").map((zoneId) => ({ id: zoneId })),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
allAppearances: async ({ id }, _, { db }) => {
|
||||||
|
// HACK: Copy-pasted from `compatibleBodies`. Could be a loader?
|
||||||
|
const [rows, __] = await db.query(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT swf_assets.body_id
|
||||||
|
FROM items
|
||||||
|
INNER JOIN parents_swf_assets ON
|
||||||
|
items.id = parents_swf_assets.parent_id AND
|
||||||
|
parents_swf_assets.parent_type = "Item"
|
||||||
|
INNER JOIN swf_assets ON
|
||||||
|
parents_swf_assets.swf_asset_id = swf_assets.id
|
||||||
|
WHERE items.id = ?
|
||||||
|
`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const bodyIds = rows.map((row) => String(row.body_id));
|
||||||
|
return bodyIds.map((bodyId) => ({ item: { id }, bodyId }));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ItemAppearance: {
|
ItemAppearance: {
|
||||||
|
|
|
@ -175,7 +175,7 @@ const resolvers = {
|
||||||
const petState = await canonicalPetStateForBodyLoader.load({
|
const petState = await canonicalPetStateForBodyLoader.load({
|
||||||
bodyId: id,
|
bodyId: id,
|
||||||
preferredColorId,
|
preferredColorId,
|
||||||
fallbackColorId: FALLBACK_COLOR_IDS[species.id] || "8",
|
fallbackColorId: FALLBACK_COLOR_IDS[species?.id] || "8",
|
||||||
});
|
});
|
||||||
if (!petState) {
|
if (!petState) {
|
||||||
return null;
|
return null;
|
||||||
|
|
Loading…
Reference in a new issue