diff --git a/app/javascript/wardrobe-2020/ItemPage/SpeciesFacesPicker.js b/app/javascript/wardrobe-2020/ItemPage/SpeciesFacesPicker.js
deleted file mode 100644
index d5fc8ddd..00000000
--- a/app/javascript/wardrobe-2020/ItemPage/SpeciesFacesPicker.js
+++ /dev/null
@@ -1,905 +0,0 @@
-import React from "react";
-import { ClassNames } from "@emotion/react";
-import {
- Box,
- Tooltip,
- useColorModeValue,
- useToken,
- Wrap,
- WrapItem,
- Flex,
-} from "@chakra-ui/react";
-import { WarningTwoIcon } from "@chakra-ui/icons";
-import gql from "graphql-tag";
-import { useQuery } from "@apollo/client";
-
-function SpeciesFacesPicker({
- selectedSpeciesId,
- selectedColorId,
- compatibleBodies,
- couldProbablyModelMoreData,
- onChange,
- isLoading,
-}) {
- // For basic colors (Blue, Green, Red, Yellow), we just use the hardcoded
- // data, which is part of the bundle and loads super-fast. For other colors,
- // we load in all the faces of that color, falling back to basic colors when
- // absent!
- //
- // TODO: Could we move this into our `build-cached-data` script, and just do
- // the query all the time, and have Apollo happen to satisfy it fast?
- // The semantics of returning our colorful random set could be weird…
- const selectedColorIsBasic = colorIsBasic(selectedColorId);
- const {
- loading: loadingGQL,
- error,
- data,
- } = useQuery(
- gql`
- query SpeciesFacesPicker($selectedColorId: ID!) {
- color(id: $selectedColorId) {
- id
- appliedToAllCompatibleSpecies {
- id
- neopetsImageHash
- species {
- id
- }
- body {
- id
- }
- }
- }
- }
- `,
- {
- variables: { selectedColorId },
- skip: selectedColorId == null || selectedColorIsBasic,
- onError: (e) => console.error(e),
- },
- );
-
- const allBodiesAreCompatible = compatibleBodies.some(
- (body) => body.id === "0",
- );
- const compatibleBodyIds = compatibleBodies.map((body) => body.id);
-
- const speciesFacesFromData = data?.color?.appliedToAllCompatibleSpecies || [];
-
- const allSpeciesFaces = DEFAULT_SPECIES_FACES.map((defaultSpeciesFace) => {
- const providedSpeciesFace = speciesFacesFromData.find(
- (f) => f.species.id === defaultSpeciesFace.speciesId,
- );
- if (providedSpeciesFace) {
- return {
- ...defaultSpeciesFace,
- colorId: selectedColorId,
- bodyId: providedSpeciesFace.body.id,
- // If this species/color pair exists, but without an image hash, then
- // we want to provide a face so that it's enabled, but use the fallback
- // image even though it's wrong, so that it looks like _something_.
- neopetsImageHash:
- providedSpeciesFace.neopetsImageHash ||
- defaultSpeciesFace.neopetsImageHash,
- };
- } else {
- return defaultSpeciesFace;
- }
- });
-
- return (
-
-
- {allSpeciesFaces.map((speciesFace) => (
-
-
-
- ))}
-
- {error && (
-
-
-
- Error loading this color's pet photos.
-
- Check your connection and try again.
-
-
- )}
-
- );
-}
-const SpeciesFaceOption = React.memo(
- ({
- speciesId,
- speciesName,
- colorId,
- neopetsImageHash,
- isSelected,
- bodyIsCompatible,
- isValid,
- couldProbablyModelMoreData,
- onChange,
- isLoading,
- }) => {
- const selectedBorderColor = useColorModeValue("green.600", "green.400");
- const selectedBackgroundColor = useColorModeValue("green.200", "green.600");
- const focusBorderColor = "blue.400";
- const focusBackgroundColor = "blue.100";
- const [
- selectedBorderColorValue,
- selectedBackgroundColorValue,
- focusBorderColorValue,
- focusBackgroundColorValue,
- ] = useToken("colors", [
- selectedBorderColor,
- selectedBackgroundColor,
- focusBorderColor,
- focusBackgroundColor,
- ]);
- const xlShadow = useToken("shadows", "xl");
-
- const [labelIsHovered, setLabelIsHovered] = React.useState(false);
- const [inputIsFocused, setInputIsFocused] = React.useState(false);
-
- const isDisabled = isLoading || !isValid || !bodyIsCompatible;
- const isHappy = isLoading || (isValid && bodyIsCompatible);
- const emotionId = isHappy ? "1" : "2";
- const cursor = isLoading ? "wait" : isDisabled ? "not-allowed" : "pointer";
-
- let disabledExplanation = null;
- if (isLoading) {
- // If we're still loading, don't try to explain anything yet!
- } else if (!isValid) {
- disabledExplanation = "(Can't be this color)";
- } else if (!bodyIsCompatible) {
- disabledExplanation = couldProbablyModelMoreData
- ? "(Item needs models)"
- : "(Not compatible)";
- }
-
- const tooltipLabel = (
-
- {speciesName}
- {disabledExplanation && (
-
- {disabledExplanation}
-
- )}
-
- );
-
- // NOTE: Because we render quite a few of these, avoiding using Chakra
- // elements like Box helps with render performance!
- return (
-
- {({ css }) => (
-
-
-
- )}
-
- );
- },
-);
-
-/**
- * CrossFadeImage is like , but listens for successful load events, and
- * fades from the previous image to the new image once it loads.
- *
- * We treat `src` as a unique key representing the image's identity, but we
- * also carry along the rest of the props during the fade, like `srcSet` and
- * `className`.
- */
-function CrossFadeImage(incomingImageProps) {
- const [prevImageProps, setPrevImageProps] = React.useState(null);
- const [currentImageProps, setCurrentImageProps] = React.useState(null);
-
- const incomingImageIsCurrentImage =
- incomingImageProps.src === currentImageProps?.src;
-
- const onLoadNextImage = () => {
- setPrevImageProps(currentImageProps);
- setCurrentImageProps(incomingImageProps);
- };
-
- // The main trick to this component is using React's `key` feature! When
- // diffing the rendered tree, if React sees two nodes with the same `key`, it
- // treats them as the same node and makes the prop changes to match.
- //
- // We usually use this in `.map`, to make sure that adds/removes in a list
- // don't cause our children to shift around and swap their React state or DOM
- // nodes with each other.
- //
- // But here, we use `key` to get React to transition the same DOM node
- // between 3 different states!
- //
- // The image starts its life as the last in the list, from
- // `incomingImageProps`: it's invisible, and still loading. We use its `src`
- // as the `key`.
- //
- // When it loads, we update the state so that this `key` now belongs to the
- // _second_ node, from `currentImageProps`. React will see this and make the
- // correct transition for us: it sets opacity to 0, sets z-index to 2,
- // removes aria-hidden, and removes the `onLoad` handler.
- //
- // Then, when another image is ready to show, we update the state so that
- // this key now belongs to the _first_ node, from `prevImageProps` (and the
- // second node is showing something new). React sees this, and makes the
- // transition back to invisibility, but without the `onLoad` handler this
- // time! (And transitions the current image into view, like it did for this
- // one.)
- //
- // Finally, when yet _another_ image is ready to show, we stop rendering any
- // images with this key anymore, and so React unmounts the image entirely.
- //
- // Thanks, React, for handling our multiple overlapping transitions through
- // this little state machine! This could have been a LOT harder to write,
- // whew!
- return (
-
- {({ css }) => (
-
- )}
-
- );
-}
-/**
- * DeferredTooltip is like Chakra's , but it waits until `isOpen` is
- * true before mounting it, and unmounts it after closing.
- *
- * This can drastically improve render performance when there are lots of
- * tooltip targets to re-render… but it comes with some limitations, like the
- * extra requirement to control `isOpen`, and some additional DOM structure!
- */
-function DeferredTooltip({ children, isOpen, ...props }) {
- const [shouldShowTooltip, setShouldShowToolip] = React.useState(isOpen);
-
- React.useEffect(() => {
- if (isOpen) {
- setShouldShowToolip(true);
- } else {
- const timeoutId = setTimeout(() => setShouldShowToolip(false), 500);
- return () => clearTimeout(timeoutId);
- }
- }, [isOpen]);
-
- return (
-
- {({ css }) => (
-