Use standard image URLs on Your Outfits page
The old URLs were glitchy because we weren't escaping the `layerUrls` param… and this will let us take better advantage of the same shared caching as other stuff!
This commit is contained in:
parent
ae6b012f88
commit
9a68bd1355
3 changed files with 24 additions and 96 deletions
|
@ -5,12 +5,9 @@ import gql from "graphql-tag";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { ErrorMessage, Heading1, useCommonStyles } from "./util";
|
import { Heading1, MajorErrorMessage, useCommonStyles } from "./util";
|
||||||
import HangerSpinner from "./components/HangerSpinner";
|
import HangerSpinner from "./components/HangerSpinner";
|
||||||
import OutfitThumbnail, {
|
import OutfitThumbnail from "./components/OutfitThumbnail";
|
||||||
outfitThumbnailFragment,
|
|
||||||
getOutfitThumbnailRenderSize,
|
|
||||||
} from "./components/OutfitThumbnail";
|
|
||||||
import useRequireLogin from "./components/useRequireLogin";
|
import useRequireLogin from "./components/useRequireLogin";
|
||||||
import WIPCallout from "./components/WIPCallout";
|
import WIPCallout from "./components/WIPCallout";
|
||||||
|
|
||||||
|
@ -31,14 +28,13 @@ function UserOutfitsPageContent() {
|
||||||
|
|
||||||
const { loading: queryLoading, error, data } = useQuery(
|
const { loading: queryLoading, error, data } = useQuery(
|
||||||
gql`
|
gql`
|
||||||
query UserOutfitsPageContent($size: LayerImageSize) {
|
query UserOutfitsPageContent {
|
||||||
currentUser {
|
currentUser {
|
||||||
id
|
id
|
||||||
outfits {
|
outfits {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
updatedAt
|
||||||
...OutfitThumbnailFragment
|
|
||||||
|
|
||||||
# For alt text
|
# For alt text
|
||||||
petAppearance {
|
petAppearance {
|
||||||
|
@ -58,13 +54,8 @@ function UserOutfitsPageContent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${outfitThumbnailFragment}
|
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
variables: {
|
|
||||||
// NOTE: This parameter is used inside `OutfitThumbnailFragment`!
|
|
||||||
size: "SIZE_" + getOutfitThumbnailRenderSize(),
|
|
||||||
},
|
|
||||||
context: { sendAuth: true },
|
context: { sendAuth: true },
|
||||||
skip: userLoading,
|
skip: userLoading,
|
||||||
}
|
}
|
||||||
|
@ -79,7 +70,7 @@ function UserOutfitsPageContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorMessage>Error loading outfits: {error.message}</ErrorMessage>;
|
return <MajorErrorMessage error={error} variant="network" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outfits = data.currentUser.outfits;
|
const outfits = data.currentUser.outfits;
|
||||||
|
@ -106,8 +97,8 @@ function OutfitCard({ outfit }) {
|
||||||
<ClassNames>
|
<ClassNames>
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
<OutfitThumbnail
|
<OutfitThumbnail
|
||||||
petAppearance={outfit.petAppearance}
|
outfitId={outfit.id}
|
||||||
itemAppearances={outfit.itemAppearances}
|
updatedAt={outfit.updatedAt}
|
||||||
alt={buildOutfitAltText(outfit)}
|
alt={buildOutfitAltText(outfit)}
|
||||||
// Firefox shows alt text as a fallback for images it can't show yet.
|
// Firefox shows alt text as a fallback for images it can't show yet.
|
||||||
// Show our alt text clearly if the image failed to load... but hide
|
// Show our alt text clearly if the image failed to load... but hide
|
||||||
|
@ -118,6 +109,7 @@ function OutfitCard({ outfit }) {
|
||||||
width={150}
|
width={150}
|
||||||
height={150}
|
height={150}
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
|
loading="lazy"
|
||||||
className={css`
|
className={css`
|
||||||
&:-moz-loading {
|
&:-moz-loading {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|
|
@ -4,10 +4,7 @@ import gql from "graphql-tag";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
import OutfitThumbnail, {
|
import OutfitThumbnail from "../components/OutfitThumbnail";
|
||||||
outfitThumbnailFragment,
|
|
||||||
getOutfitThumbnailRenderSize,
|
|
||||||
} from "../components/OutfitThumbnail";
|
|
||||||
import { useOutfitPreview } from "../components/OutfitPreview";
|
import { useOutfitPreview } from "../components/OutfitPreview";
|
||||||
import { loadable, MajorErrorMessage, TestErrorSender } from "../util";
|
import { loadable, MajorErrorMessage, TestErrorSender } from "../util";
|
||||||
|
|
||||||
|
@ -65,21 +62,20 @@ function WardrobePreviewAndControls({
|
||||||
function OutfitThumbnailIfCached({ outfitId }) {
|
function OutfitThumbnailIfCached({ outfitId }) {
|
||||||
const { data } = useQuery(
|
const { data } = useQuery(
|
||||||
gql`
|
gql`
|
||||||
query OutfitThumbnailIfCached($outfitId: ID!, $size: LayerImageSize!) {
|
query OutfitThumbnailIfCached($outfitId: ID!) {
|
||||||
outfit(id: $outfitId) {
|
outfit(id: $outfitId) {
|
||||||
id
|
id
|
||||||
...OutfitThumbnailFragment
|
updatedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${outfitThumbnailFragment}
|
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
variables: {
|
variables: {
|
||||||
outfitId,
|
outfitId,
|
||||||
// NOTE: This parameter is used inside `OutfitThumbnailFragment`!
|
skip: outfitId == null,
|
||||||
size: "SIZE_" + getOutfitThumbnailRenderSize(),
|
|
||||||
},
|
},
|
||||||
fetchPolicy: "cache-only",
|
fetchPolicy: "cache-only",
|
||||||
|
onError: (e) => console.error(e),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -89,8 +85,8 @@ function OutfitThumbnailIfCached({ outfitId }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OutfitThumbnail
|
<OutfitThumbnail
|
||||||
petAppearance={data.outfit.petAppearance}
|
outfitId={data.outfit.id}
|
||||||
itemAppearances={data.outfit.itemAppearances}
|
updatedAt={data.outfit.updatedAt}
|
||||||
alt=""
|
alt=""
|
||||||
objectFit="contain"
|
objectFit="contain"
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|
|
@ -1,81 +1,21 @@
|
||||||
import React from "react";
|
|
||||||
import { Box } from "@chakra-ui/react";
|
import { Box } from "@chakra-ui/react";
|
||||||
import gql from "graphql-tag";
|
|
||||||
|
|
||||||
import getVisibleLayers, {
|
function OutfitThumbnail({ outfitId, updatedAt, ...props }) {
|
||||||
petAppearanceFragmentForGetVisibleLayers,
|
const versionTimestamp = new Date(updatedAt).getTime();
|
||||||
itemAppearanceFragmentForGetVisibleLayers,
|
|
||||||
} from "../../shared/getVisibleLayers";
|
// NOTE: It'd be more reliable for testing to use a relative path, but
|
||||||
|
// generating these on dev is SO SLOW, that I'd rather just not.
|
||||||
|
const thumbnailUrl150 = `https://outfits.openneo-assets.net/outfits/${outfitId}/v/${versionTimestamp}/150.png`;
|
||||||
|
const thumbnailUrl300 = `https://outfits.openneo-assets.net/outfits/${outfitId}/v/${versionTimestamp}/300.png`;
|
||||||
|
|
||||||
function OutfitThumbnail({ petAppearance, itemAppearances, ...props }) {
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="img"
|
as="img"
|
||||||
src={buildOutfitThumbnailUrl(petAppearance, itemAppearances)}
|
src={thumbnailUrl150}
|
||||||
|
srcSet={`${thumbnailUrl150} 1x, ${thumbnailUrl300} 2x`}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
export default OutfitThumbnail;
|
||||||
|
|
Loading…
Reference in a new issue