diff --git a/src/app/ItemPage.js b/src/app/ItemPage.js
index 756b141..f4bf645 100644
--- a/src/app/ItemPage.js
+++ b/src/app/ItemPage.js
@@ -14,9 +14,6 @@ import {
useColorModeValue,
useTheme,
useToast,
- useToken,
- Wrap,
- WrapItem,
Flex,
usePrefersReducedMotion,
Grid,
@@ -27,7 +24,6 @@ import {
EditIcon,
StarIcon,
WarningIcon,
- WarningTwoIcon,
} from "@chakra-ui/icons";
import { MdPause, MdPlayArrow } from "react-icons/md";
import gql from "graphql-tag";
@@ -49,6 +45,9 @@ import SpeciesColorPicker, {
} from "./components/SpeciesColorPicker";
import useCurrentUser from "./components/useCurrentUser";
import { useLocalStorage } from "./util";
+import SpeciesFacesPicker, {
+ colorIsBasic,
+} from "./ItemPage/SpeciesFacesPicker";
function ItemPage() {
const { itemId } = useParams();
@@ -983,318 +982,10 @@ function PlayPauseButton({ isPaused, onClick }) {
);
}
-function SpeciesFacesPicker({
- selectedSpeciesId,
- selectedColorId,
- compatibleBodies,
- couldProbablyModelMoreData,
- onChange,
- isLoading,
+export function ItemZonesInfo({
+ compatibleBodiesAndTheirZones,
+ restrictedZones,
}) {
- // 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.representsAllBodies
- );
- 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 }) => (
-
-
-
- )}
-
- );
- }
-);
-
-function ItemZonesInfo({ compatibleBodiesAndTheirZones, restrictedZones }) {
// Reorganize the body-and-zones data, into zone-and-bodies data. Also, we're
// merging zones with the same label, because that's how user-facing zone UI
// generally works!
@@ -1466,577 +1157,4 @@ function buildSortKeyForZoneLabelsAndTheirBodies({ zoneLabel, bodies }) {
return `${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`;
}
-/**
- * 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 }) => (
-
+ );
+
+ // 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 }) => (
+