diff --git a/src/app/UserOutfitsPage.js b/src/app/UserOutfitsPage.js index 062d46c..eb4e865 100644 --- a/src/app/UserOutfitsPage.js +++ b/src/app/UserOutfitsPage.js @@ -6,12 +6,11 @@ import { useQuery } from "@apollo/client"; import { Link } from "react-router-dom"; import { ErrorMessage, Heading1, useCommonStyles } from "./util"; -import { - getVisibleLayers, - petAppearanceFragmentForGetVisibleLayers, - itemAppearanceFragmentForGetVisibleLayers, -} from "./components/useOutfitAppearance"; import HangerSpinner from "./components/HangerSpinner"; +import OutfitThumbnail, { + outfitThumbnailFragment, + getOutfitThumbnailRenderSize, +} from "./components/OutfitThumbnail"; import useRequireLogin from "./components/useRequireLogin"; import WIPCallout from "./components/WIPCallout"; @@ -38,13 +37,11 @@ function UserOutfitsPageContent() { outfits { id name + + ...OutfitThumbnailFragment + + # For alt text petAppearance { - id - layers { - id - svgUrl - imageUrl(size: $size) - } species { id name @@ -53,16 +50,6 @@ function UserOutfitsPageContent() { id name } - ...PetAppearanceForGetVisibleLayers - } - itemAppearances { - id - layers { - id - svgUrl - imageUrl(size: $size) - } - ...ItemAppearanceForGetVisibleLayers } wornItems { id @@ -71,10 +58,15 @@ function UserOutfitsPageContent() { } } } - ${petAppearanceFragmentForGetVisibleLayers} - ${itemAppearanceFragmentForGetVisibleLayers} + ${outfitThumbnailFragment} `, - { variables: { size: "SIZE_" + getBestImageSize() }, skip: userLoading } + { + variables: { + // NOTE: This parameter is used inside `OutfitThumbnailFragment`! + size: "SIZE_" + getOutfitThumbnailRenderSize(), + }, + skip: userLoading, + } ); if (userLoading || queryLoading) { @@ -112,14 +104,9 @@ function OutfitCard({ outfit }) { const image = ( {({ css }) => ( - layer.svgUrl || layer.imageUrl - ); - - return `/api/outfitImage?size=${size}&layerUrls=${layerUrls.join(",")}`; -} - function buildOutfitAltText(outfit) { const { petAppearance, wornItems } = outfit; const { species, color } = petAppearance; @@ -216,20 +193,4 @@ function buildOutfitAltText(outfit) { return altText; } -/** - * getBestImageSize returns the right image size to render at 150x150, for the - * current device. - * - * On high-DPI devices, we'll download a 300x300 image to render at 150x150 - * scale. On standard-DPI devices, we'll download a 150x150 image, to save - * bandwidth. - */ -function getBestImageSize() { - if (window.devicePixelRatio > 1) { - return 300; - } else { - return 150; - } -} - export default UserOutfitsPage; diff --git a/src/app/WardrobePage/WardrobeOutfitPreview.js b/src/app/WardrobePage/WardrobeOutfitPreview.js new file mode 100644 index 0000000..5631aa4 --- /dev/null +++ b/src/app/WardrobePage/WardrobeOutfitPreview.js @@ -0,0 +1,78 @@ +import React from "react"; +import gql from "graphql-tag"; +import { useQuery } from "@apollo/client"; + +import OutfitThumbnail, { + outfitThumbnailFragment, + getOutfitThumbnailRenderSize, +} from "../components/OutfitThumbnail"; +import OutfitPreview from "../components/OutfitPreview"; + +function WardrobeOutfitPreview({ + isLoading, + outfitState, + onChangeHasAnimations, +}) { + return ( + } + /> + ); +} + +/** + * OutfitThumbnailIfCached will render an OutfitThumbnail as a placeholder for + * the outfit preview... but only if we already have the data to generate the + * thumbnail stored in our local Apollo GraphQL cache. + * + * This means that, when you come from the Your Outfits page, we can show the + * outfit thumbnail instantly while everything else loads. But on direct + * navigation, this does nothing, and we just wait for the preview to load in + * like usual! + */ +function OutfitThumbnailIfCached({ outfitId }) { + const { data } = useQuery( + gql` + query OutfitThumbnailIfCached($outfitId: ID!, $size: LayerImageSize!) { + outfit(id: $outfitId) { + id + ...OutfitThumbnailFragment + } + } + ${outfitThumbnailFragment} + `, + { + variables: { + outfitId, + // NOTE: This parameter is used inside `OutfitThumbnailFragment`! + size: "SIZE_" + getOutfitThumbnailRenderSize(), + }, + fetchPolicy: "cache-only", + } + ); + + if (!data?.outfit) { + return null; + } + + return ( + + ); +} + +export default WardrobeOutfitPreview; diff --git a/src/app/WardrobePage/index.js b/src/app/WardrobePage/index.js index 83a461e..d931f9b 100644 --- a/src/app/WardrobePage/index.js +++ b/src/app/WardrobePage/index.js @@ -3,11 +3,11 @@ import { useToast } from "@chakra-ui/react"; import loadable from "@loadable/component"; import ItemsAndSearchPanels from "./ItemsAndSearchPanels"; -import OutfitPreview from "../components/OutfitPreview"; import SupportOnly from "./support/SupportOnly"; import useOutfitState, { OutfitStateContext } from "./useOutfitState"; import { usePageTitle } from "../util"; import WardrobePageLayout from "./WardrobePageLayout"; +import WardrobeOutfitPreview from "./WardrobeOutfitPreview"; const OutfitControls = loadable(() => import(/* webpackPreload: true */ "./OutfitControls") @@ -61,13 +61,9 @@ function WardrobePage() { } diff --git a/src/app/components/OutfitPreview.js b/src/app/components/OutfitPreview.js index 54e4de6..e5f739c 100644 --- a/src/app/components/OutfitPreview.js +++ b/src/app/components/OutfitPreview.js @@ -165,6 +165,10 @@ export function OutfitLayers({ // point we fade it out. opacity={visibleLayers.length === 0 ? 1 : 0} transition="opacity 0.2s" + width="100%" + height="100%" + maxWidth="600px" + maxHeight="600px" > {placeholder} diff --git a/src/app/components/OutfitThumbnail.js b/src/app/components/OutfitThumbnail.js new file mode 100644 index 0000000..e0138ef --- /dev/null +++ b/src/app/components/OutfitThumbnail.js @@ -0,0 +1,82 @@ +import React from "react"; +import { Box } from "@chakra-ui/react"; +import gql from "graphql-tag"; + +import { + getVisibleLayers, + petAppearanceFragmentForGetVisibleLayers, + itemAppearanceFragmentForGetVisibleLayers, +} from "./useOutfitAppearance"; + +function OutfitThumbnail({ petAppearance, itemAppearances, ...props }) { + return ( + + ); +} + +function buildOutfitThumbnailUrl(petAppearance, itemAppearances) { + const size = getOutfitThumbnailRenderSize(); + const visibleLayers = getVisibleLayers(petAppearance, itemAppearances); + const layerUrls = visibleLayers.map( + (layer) => layer.svgUrl || layer.imageUrl + ); + + return `/api/outfitImage?size=${size}&layerUrls=${layerUrls.join(",")}`; +} + +/** + * getOutfitThumbnailRenderSize returns the right image size to render at + * 150x150, for the current device. + * + * On high-DPI devices, we'll download a 300x300 image to render at 150x150 + * scale. On standard-DPI devices, we'll download a 150x150 image, to save + * bandwidth. + */ +export function getOutfitThumbnailRenderSize() { + if (window.devicePixelRatio > 1) { + return 300; + } else { + return 150; + } +} + +// NOTE: The query must include a `$size: LayerImageSize` parameter, probably +// with the return value of `getOutfitThumbnailRenderSize`! +export const outfitThumbnailFragment = gql` + fragment OutfitThumbnailFragment on Outfit { + petAppearance { + id + layers { + id + svgUrl + imageUrl(size: $size) + } + species { + id + name + } + color { + id + name + } + ...PetAppearanceForGetVisibleLayers + } + itemAppearances { + id + layers { + id + svgUrl + imageUrl(size: $size) + } + ...ItemAppearanceForGetVisibleLayers + } + } + ${petAppearanceFragmentForGetVisibleLayers} + ${itemAppearanceFragmentForGetVisibleLayers} +`; + +export default OutfitThumbnail;