basic outfits page with pet-only thumbnails
This commit is contained in:
parent
b8d919b247
commit
30e0a149be
4 changed files with 89 additions and 21 deletions
|
@ -10,8 +10,8 @@ const beeline = require("honeycomb-beeline")({
|
||||||
const { renderOutfitImage } = require("../src/server/outfit-images");
|
const { renderOutfitImage } = require("../src/server/outfit-images");
|
||||||
|
|
||||||
const VALID_LAYER_URLS = [
|
const VALID_LAYER_URLS = [
|
||||||
/^https:\/\/impress-asset-images\.s3\.amazonaws\.com\/(biology|object)\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[0-9]+\/150x150\.png$/,
|
/^https:\/\/impress-asset-images\.s3\.amazonaws\.com\/(biology|object)\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[0-9]+\/(150|300)x(150|300)\.png(\?[a-zA-Z0-9_-]+)?$/,
|
||||||
/^http:\/\/images\.neopets\.com\/cp\/(biology|object)\/data\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+\.svg$/,
|
/^http:\/\/images\.neopets\.com\/cp\/(bio|object)\/data\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\.svg$/,
|
||||||
];
|
];
|
||||||
|
|
||||||
async function handle(req, res) {
|
async function handle(req, res) {
|
||||||
|
@ -19,9 +19,14 @@ async function handle(req, res) {
|
||||||
res.setHeader("Content-Type", "text/plain");
|
res.setHeader("Content-Type", "text/plain");
|
||||||
return res.status(400).send(`Missing required parameter: layerUrls`);
|
return res.status(400).send(`Missing required parameter: layerUrls`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const layerUrls = req.query.layerUrls.split(",");
|
const layerUrls = req.query.layerUrls.split(",");
|
||||||
|
|
||||||
|
const size = parseInt(req.query.size);
|
||||||
|
if (size !== 150 && size !== 300) {
|
||||||
|
res.setHeader("Content-Type", "text/plain");
|
||||||
|
return res.status(400).send(`Size must be 150 or 300`);
|
||||||
|
}
|
||||||
|
|
||||||
for (const layerUrl of layerUrls) {
|
for (const layerUrl of layerUrls) {
|
||||||
if (!VALID_LAYER_URLS.some((pattern) => layerUrl.match(pattern))) {
|
if (!VALID_LAYER_URLS.some((pattern) => layerUrl.match(pattern))) {
|
||||||
return res.status(400).send(`Unexpected layer URL format: ${layerUrl}`);
|
return res.status(400).send(`Unexpected layer URL format: ${layerUrl}`);
|
||||||
|
@ -30,7 +35,7 @@ async function handle(req, res) {
|
||||||
|
|
||||||
let imageResult;
|
let imageResult;
|
||||||
try {
|
try {
|
||||||
imageResult = await renderOutfitImage(layerUrls, 150);
|
imageResult = await renderOutfitImage(layerUrls, size);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
res.setHeader("Content-Type", "text/plain");
|
res.setHeader("Content-Type", "text/plain");
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, Center } from "@chakra-ui/react";
|
import { Box, Center, Flex, Wrap, WrapItem } from "@chakra-ui/react";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
|
|
||||||
import { ErrorMessage, Heading1 } from "./util";
|
import { ErrorMessage, Heading1 } from "./util";
|
||||||
|
import {
|
||||||
|
getVisibleLayers,
|
||||||
|
petAppearanceFragmentForGetVisibleLayers,
|
||||||
|
} from "./components/useOutfitAppearance";
|
||||||
import HangerSpinner from "./components/HangerSpinner";
|
import HangerSpinner from "./components/HangerSpinner";
|
||||||
import useRequireLogin from "./components/useRequireLogin";
|
import useRequireLogin from "./components/useRequireLogin";
|
||||||
|
|
||||||
function UserOutfitsPage() {
|
function UserOutfitsPage() {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Heading1>Your outfits</Heading1>
|
<Heading1 marginBottom="4">Your outfits</Heading1>
|
||||||
<UserOutfitsPageContent />
|
<UserOutfitsPageContent />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -21,22 +25,26 @@ function UserOutfitsPageContent() {
|
||||||
|
|
||||||
const { loading: queryLoading, error, data } = useQuery(
|
const { loading: queryLoading, error, data } = useQuery(
|
||||||
gql`
|
gql`
|
||||||
query UserOutfitsPageContent {
|
query UserOutfitsPageContent($size: LayerImageSize) {
|
||||||
currentUser {
|
currentUser {
|
||||||
outfits {
|
outfits {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
petAppearance {
|
petAppearance {
|
||||||
id
|
id
|
||||||
}
|
layers {
|
||||||
wornItems {
|
|
||||||
id
|
id
|
||||||
|
svgUrl
|
||||||
|
imageUrl(size: $size)
|
||||||
|
}
|
||||||
|
...PetAppearanceForGetVisibleLayers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${petAppearanceFragmentForGetVisibleLayers}
|
||||||
`,
|
`,
|
||||||
{ skip: userLoading }
|
{ variables: { size: "SIZE_" + getBestImageSize() }, skip: userLoading }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userLoading || queryLoading) {
|
if (userLoading || queryLoading) {
|
||||||
|
@ -51,11 +59,54 @@ function UserOutfitsPageContent() {
|
||||||
return <ErrorMessage>Error loading outfits: {error.message}</ErrorMessage>;
|
return <ErrorMessage>Error loading outfits: {error.message}</ErrorMessage>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outfits = data.currentUser.outfits;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<code>
|
<Wrap spacing="4">
|
||||||
<pre>Data: {JSON.stringify(data, null, 4)}</pre>
|
{outfits.map((outfit) => (
|
||||||
</code>
|
<WrapItem key={outfit.id}>
|
||||||
|
<OutfitCard outfit={outfit} />
|
||||||
|
</WrapItem>
|
||||||
|
))}
|
||||||
|
</Wrap>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OutfitCard({ outfit }) {
|
||||||
|
const thumbnailUrl = buildOutfitThumbnailUrl(outfit.petAppearance, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
textAlign="center"
|
||||||
|
boxShadow="md"
|
||||||
|
borderRadius="md"
|
||||||
|
padding="3"
|
||||||
|
width="calc(150px + 2em)"
|
||||||
|
>
|
||||||
|
<Box as="img" src={thumbnailUrl} width={150} height={150} />
|
||||||
|
<Box>{outfit.name}</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOutfitThumbnailUrl(petAppearance, itemAppearances) {
|
||||||
|
const size = getBestImageSize();
|
||||||
|
const visibleLayers = getVisibleLayers(petAppearance, itemAppearances);
|
||||||
|
const layerUrls = visibleLayers.map(
|
||||||
|
(layer) => layer.svgUrl || layer.imageUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
return `/api/outfitImage?size=${size}&layerUrls=${layerUrls.join(",")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBestImageSize() {
|
||||||
|
if (window.devicePixelRatio > 1) {
|
||||||
|
return 300;
|
||||||
|
} else {
|
||||||
|
return 150;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default UserOutfitsPage;
|
export default UserOutfitsPage;
|
||||||
|
|
|
@ -160,15 +160,11 @@ export const itemAppearanceFragment = gql`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const petAppearanceFragment = gql`
|
export const petAppearanceFragmentForGetVisibleLayers = gql`
|
||||||
fragment PetAppearanceForOutfitPreview on PetAppearance {
|
fragment PetAppearanceForGetVisibleLayers on PetAppearance {
|
||||||
id
|
id
|
||||||
bodyId
|
|
||||||
layers {
|
layers {
|
||||||
id
|
id
|
||||||
svgUrl
|
|
||||||
canvasMovieLibraryUrl
|
|
||||||
imageUrl(size: SIZE_600)
|
|
||||||
zone {
|
zone {
|
||||||
id
|
id
|
||||||
depth @client
|
depth @client
|
||||||
|
@ -179,3 +175,19 @@ export const petAppearanceFragment = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const petAppearanceFragment = gql`
|
||||||
|
fragment PetAppearanceForOutfitPreview on PetAppearance {
|
||||||
|
id
|
||||||
|
bodyId
|
||||||
|
layers {
|
||||||
|
id
|
||||||
|
svgUrl
|
||||||
|
canvasMovieLibraryUrl
|
||||||
|
imageUrl(size: SIZE_600)
|
||||||
|
}
|
||||||
|
...PetAppearanceForGetVisibleLayers
|
||||||
|
}
|
||||||
|
|
||||||
|
${petAppearanceFragmentForGetVisibleLayers}
|
||||||
|
`;
|
||||||
|
|
|
@ -91,7 +91,7 @@ const resolvers = {
|
||||||
const layer = await swfAssetLoader.load(id);
|
const layer = await swfAssetLoader.load(id);
|
||||||
return layer.url;
|
return layer.url;
|
||||||
},
|
},
|
||||||
imageUrl: async ({ id }, { size }, { swfAssetLoader }) => {
|
imageUrl: async ({ id }, { size = "SIZE_150" }, { swfAssetLoader }) => {
|
||||||
const layer = await swfAssetLoader.load(id);
|
const layer = await swfAssetLoader.load(id);
|
||||||
|
|
||||||
// If there's no image, return null. (In the development db, which isn't
|
// If there's no image, return null. (In the development db, which isn't
|
||||||
|
|
Loading…
Reference in a new issue