Add species-face picker to item page previews

Note that it doesn't do any compatibility checking, graying out, hiding unneeded faces, etc. They just exist now is all!
This commit is contained in:
Emi Matchu 2021-01-25 10:24:39 -08:00
parent 7092d86b76
commit f50de9b11e

View file

@ -14,6 +14,10 @@ import {
useColorModeValue, useColorModeValue,
useTheme, useTheme,
useToast, useToast,
useToken,
Stack,
Wrap,
WrapItem,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { import {
CheckIcon, CheckIcon,
@ -27,7 +31,7 @@ import { useQuery, useMutation } from "@apollo/client";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import ItemPageLayout, { SubtleSkeleton } from "./ItemPageLayout"; import ItemPageLayout, { SubtleSkeleton } from "./ItemPageLayout";
import { Delay, usePageTitle } from "./util"; import { Delay, ErrorMessage, usePageTitle } from "./util";
import { import {
itemAppearanceFragment, itemAppearanceFragment,
petAppearanceFragment, petAppearanceFragment,
@ -601,90 +605,539 @@ function ItemPageOutfitPreview({ itemId }) {
const isIncompatible = Array.isArray(layers) && layers.length === 0; const isIncompatible = Array.isArray(layers) && layers.length === 0;
return ( return (
<VStack spacing="3" width="100%"> <Stack direction={{ base: "column", md: "row" }} spacing="8">
<AspectRatio <VStack spacing="3" width="100%">
width="300px" <AspectRatio
maxWidth="100%" width="300px"
ratio="1" maxWidth="100%"
border="1px" ratio="1"
borderColor={borderColor} border="1px"
transition="border-color 0.2s" borderColor={borderColor}
borderRadius="lg" transition="border-color 0.2s"
boxShadow="lg" borderRadius="lg"
overflow="hidden" boxShadow="lg"
> overflow="hidden"
<Box> >
<OutfitPreview <Box>
<OutfitPreview
speciesId={petState.speciesId}
colorId={petState.colorId}
pose={petState.pose}
appearanceId={petState.appearanceId}
wornItemIds={[itemId]}
isLoading={loading}
spinnerVariant="corner"
loadingDelayMs={2000}
engine="canvas"
onChangeHasAnimations={setHasAnimations}
/>
{hasAnimations && (
<PlayPauseButton
isPaused={isPaused}
onClick={() => setIsPaused(!isPaused)}
/>
)}
</Box>
</AspectRatio>
<Box display="flex" width="100%" alignItems="center">
<Box
// This empty box grows at the same rate as the box on the right, so
// the middle box will be centered, if there's space!
flex="1 0 0"
/>
<SpeciesColorPicker
speciesId={petState.speciesId} speciesId={petState.speciesId}
colorId={petState.colorId} colorId={petState.colorId}
pose={petState.pose} pose={petState.pose}
appearanceId={petState.appearanceId} idealPose={idealPose}
wornItemIds={[itemId]} onChange={(species, color, _, closestPose) => {
isLoading={loading} setPetState({
spinnerVariant="corner" speciesId: species.id,
loadingDelayMs={2000} colorId: color.id,
engine="canvas" pose: closestPose,
onChangeHasAnimations={setHasAnimations} appearanceId: null,
});
}}
size="sm"
showPlaceholders
// This is just a UX affordance: while we could handle invalid states
// from a UI perspective, we figure that, if a pet preview is already
// visible and responsive to changes, it feels better to treat the
// changes as atomic and always-valid.
stateMustAlwaysBeValid
/> />
{hasAnimations && ( <Box flex="1 0 0" lineHeight="1">
<IconButton {isIncompatible && (
icon={isPaused ? <MdPlayArrow /> : <MdPause />} <Tooltip label="No data yet" placement="top">
aria-label={isPaused ? "Play" : "Pause"} <WarningIcon
onClick={() => setIsPaused(!isPaused)} color={errorColor}
borderRadius="full" transition="color 0.2"
boxShadow="md" marginLeft="2"
color="gray.50" />
backgroundColor="blackAlpha.700" </Tooltip>
position="absolute" )}
bottom="2" </Box>
left="2"
_hover={{ backgroundColor: "blackAlpha.900" }}
_focus={{ backgroundColor: "blackAlpha.900" }}
/>
)}
</Box> </Box>
</AspectRatio> </VStack>
<Box display="flex" width="100%" alignItems="center"> <Box maxWidth="400px">
<Box <SpeciesFacesPicker
// This empty box grows at the same rate as the box on the right, so itemId={itemId}
// the middle box will be centered, if there's space! selectedSpeciesId={petState.speciesId}
flex="1 0 0" onChange={({ speciesId, colorId }) =>
/>
<SpeciesColorPicker
speciesId={petState.speciesId}
colorId={petState.colorId}
pose={petState.pose}
idealPose={idealPose}
onChange={(species, color, _, closestPose) => {
setPetState({ setPetState({
speciesId: species.id, speciesId,
colorId: color.id, colorId,
pose: closestPose, pose: idealPose,
appearanceId: null, appearanceId: null,
}); })
}} }
size="sm" isLoading={loading}
showPlaceholders
// This is just a UX affordance: while we could handle invalid states
// from a UI perspective, we figure that, if a pet preview is already
// visible and responsive to changes, it feels better to treat the
// changes as atomic and always-valid.
stateMustAlwaysBeValid
/> />
<Box flex="1 0 0" lineHeight="1">
{isIncompatible && (
<Tooltip label="No data yet" placement="top">
<WarningIcon
color={errorColor}
transition="color 0.2"
marginLeft="2"
/>
</Tooltip>
)}
</Box>
</Box> </Box>
</VStack> </Stack>
); );
} }
function PlayPauseButton({ isPaused, onClick }) {
return (
<IconButton
icon={isPaused ? <MdPlayArrow /> : <MdPause />}
aria-label={isPaused ? "Play" : "Pause"}
onClick={onClick}
borderRadius="full"
boxShadow="md"
color="gray.50"
backgroundColor="blackAlpha.700"
position="absolute"
bottom="2"
left="2"
_hover={{ backgroundColor: "blackAlpha.900" }}
_focus={{ backgroundColor: "blackAlpha.900" }}
/>
);
}
function SpeciesFacesPicker({
itemId,
selectedSpeciesId,
onChange,
isLoading,
}) {
const selectedBorderColor = useColorModeValue("green.600", "green.400");
const selectedBackgroundColor = useColorModeValue("green.200", "green.600");
const [
selectedBorderColorValue,
selectedBackgroundColorValue,
] = useToken("colors", [selectedBorderColor, selectedBackgroundColor]);
const allSpeciesFaces = speciesFaces.sort((a, b) =>
a.speciesName.localeCompare(b.speciesName)
);
return (
<ClassNames>
{({ css }) => (
<Wrap
spacing="0"
justify="center"
// On mobile, give this a scroll container, and some extra padding so
// the selected-face effects still fit inside.
maxHeight={{ base: "200px", md: "none" }}
overflow={{ base: "auto", md: "visible" }}
padding={{ base: "8px", md: "0" }}
>
{allSpeciesFaces.map(({ speciesId, speciesName, colorId, src }) => (
<WrapItem
key={speciesId}
as="label"
cursor={isLoading ? "wait" : "pointer"}
position="relative"
>
<VisuallyHidden
as="input"
type="radio"
aria-label={speciesName}
name="species-faces-picker"
value={speciesId}
checked={speciesId === selectedSpeciesId}
disabled={isLoading}
onChange={() => onChange({ speciesId, colorId })}
/>
<Box
overflow="hidden"
transition="all 0.2s"
className={css`
input:checked + & {
background: ${selectedBackgroundColorValue};
border-radius: 6px;
box-shadow: ${selectedBorderColorValue} 0 0 0 3px;
transform: scale(1.2);
z-index: 1;
}
`}
>
<Box
as="img"
src={src}
alt={speciesName}
width={50}
height={50}
filter="saturate(90%)"
opacity="0.9"
transition="all 0.2s"
className={css`
input:checked + * > & {
opacity: 1;
filter: saturate(110%);
}
`}
/>
</Box>
</WrapItem>
))}
</Wrap>
)}
</ClassNames>
);
}
// HACK: I'm just hardcoding all this, rather than connecting up to the
// database and adding a loading state. Tbh I'm not sure it's a good idea
// to load this dynamically until we have SSR to make it come in fast!
// And it's not so bad if this gets out of sync with the database,
// because the SpeciesColorPicker will still be usable!
const colors = { BLUE: "8", RED: "61", GREEN: "34", YELLOW: "84" };
const speciesFaces = [
{
speciesId: "1",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/obxdjm88/1/1.png",
colorId: colors.GREEN,
speciesName: "Acara",
},
{
speciesId: "2",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/n9ozx4z5/1/1.png",
colorId: colors.BLUE,
speciesName: "Aisha",
},
{
speciesId: "3",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/kfonqhdc/1/1.png",
colorId: colors.YELLOW,
speciesName: "Blumaroo",
},
{
speciesId: "4",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/sc2hhvhn/1/1.png",
colorId: colors.YELLOW,
speciesName: "Bori",
},
{
speciesId: "5",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/wqz8xn4t/1/1.png",
colorId: colors.YELLOW,
speciesName: "Bruce",
},
{
speciesId: "6",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/jc9klfxm/1/1.png",
colorId: colors.YELLOW,
speciesName: "Buzz",
},
{
speciesId: "7",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/4lrb4n3f/1/1.png",
colorId: colors.RED,
speciesName: "Chia",
},
{
speciesId: "8",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/bdml26md/1/1.png",
colorId: colors.YELLOW,
speciesName: "Chomby",
},
{
speciesId: "9",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/xl6msllv/1/1.png",
colorId: colors.GREEN,
speciesName: "Cybunny",
},
{
speciesId: "10",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/bob39shq/1/1.png",
colorId: colors.YELLOW,
speciesName: "Draik",
},
{
speciesId: "11",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/jhhhbrww/1/1.png",
colorId: colors.RED,
speciesName: "Elephante",
},
{
speciesId: "12",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/6kngmhvs/1/1.png",
colorId: colors.RED,
speciesName: "Eyrie",
},
{
speciesId: "13",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/47vt32x2/1/1.png",
colorId: colors.GREEN,
speciesName: "Flotsam",
},
{
speciesId: "14",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/5nrd2lvd/1/1.png",
colorId: colors.YELLOW,
speciesName: "Gelert",
},
{
speciesId: "15",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/6c275jcg/1/1.png",
colorId: colors.BLUE,
speciesName: "Gnorbu",
},
{
speciesId: "16",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/j7q65fv4/1/1.png",
colorId: colors.BLUE,
speciesName: "Grarrl",
},
{
speciesId: "17",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/5xn4kjf8/1/1.png",
colorId: colors.GREEN,
speciesName: "Grundo",
},
{
speciesId: "18",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/jsfvcqwt/1/1.png",
colorId: colors.RED,
speciesName: "Hissi",
},
{
speciesId: "19",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/w32r74vo/1/1.png",
colorId: colors.GREEN,
speciesName: "Ixi",
},
{
speciesId: "20",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/kz43rnld/1/1.png",
colorId: colors.YELLOW,
speciesName: "Jetsam",
},
{
speciesId: "21",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/m267j935/1/1.png",
colorId: colors.GREEN,
speciesName: "Jubjub",
},
{
speciesId: "22",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/4gsrb59g/1/1.png",
colorId: colors.YELLOW,
speciesName: "Kacheek",
},
{
speciesId: "23",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/ktlxmrtr/1/1.png",
colorId: colors.BLUE,
speciesName: "Kau",
},
{
speciesId: "24",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/42j5q3zx/1/1.png",
colorId: colors.GREEN,
speciesName: "Kiko",
},
{
speciesId: "25",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/ncfn87wk/1/1.png",
colorId: colors.GREEN,
speciesName: "Koi",
},
{
speciesId: "26",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/omx9c876/1/1.png",
colorId: colors.RED,
speciesName: "Korbat",
},
{
speciesId: "27",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/rfsbh59t/1/1.png",
colorId: colors.BLUE,
speciesName: "Kougra",
},
{
speciesId: "28",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/hxgsm5d4/1/1.png",
colorId: colors.BLUE,
speciesName: "Krawk",
},
{
speciesId: "29",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/blxmjgbk/1/1.png",
colorId: colors.YELLOW,
speciesName: "Kyrii",
},
{
speciesId: "30",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/8r94jhfq/1/1.png",
colorId: colors.YELLOW,
speciesName: "Lenny",
},
{
speciesId: "31",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/z42535zh/1/1.png",
colorId: colors.YELLOW,
speciesName: "Lupe",
},
{
speciesId: "32",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/qgg6z8s7/1/1.png",
colorId: colors.BLUE,
speciesName: "Lutari",
},
{
speciesId: "33",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/kk2nn2jr/1/1.png",
colorId: colors.YELLOW,
speciesName: "Meerca",
},
{
speciesId: "34",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/jgkoro5z/1/1.png",
colorId: colors.GREEN,
speciesName: "Moehog",
},
{
speciesId: "35",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/xwlo9657/1/1.png",
colorId: colors.BLUE,
speciesName: "Mynci",
},
{
speciesId: "36",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/bx7fho8x/1/1.png",
colorId: colors.BLUE,
speciesName: "Nimmo",
},
{
speciesId: "37",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/rjzmx24v/1/1.png",
colorId: colors.YELLOW,
speciesName: "Ogrin",
},
{
speciesId: "38",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/kokc52kh/1/1.png",
colorId: colors.RED,
speciesName: "Peophin",
},
{
speciesId: "39",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/fw6lvf3c/1/1.png",
colorId: colors.GREEN,
speciesName: "Poogle",
},
{
speciesId: "40",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/tjhwbro3/1/1.png",
colorId: colors.RED,
speciesName: "Pteri",
},
{
speciesId: "41",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/jdto7mj4/1/1.png",
colorId: colors.YELLOW,
speciesName: "Quiggle",
},
{
speciesId: "42",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/qsgbm5f6/1/1.png",
colorId: colors.BLUE,
speciesName: "Ruki",
},
{
speciesId: "43",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/hkjoncsx/1/1.png",
colorId: colors.RED,
speciesName: "Scorchio",
},
{
speciesId: "44",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/mmvn4tkg/1/1.png",
colorId: colors.YELLOW,
speciesName: "Shoyru",
},
{
speciesId: "45",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/fc4cxk3t/1/1.png",
colorId: colors.RED,
speciesName: "Skeith",
},
{
speciesId: "46",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/84gvowmj/1/1.png",
colorId: colors.YELLOW,
speciesName: "Techo",
},
{
speciesId: "47",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/jd433863/1/1.png",
colorId: colors.BLUE,
speciesName: "Tonu",
},
{
speciesId: "48",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/q39wn6vq/1/1.png",
colorId: colors.YELLOW,
speciesName: "Tuskaninny",
},
{
speciesId: "49",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/njzvoflw/1/1.png",
colorId: colors.GREEN,
speciesName: "Uni",
},
{
speciesId: "50",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/rox4mgh5/1/1.png",
colorId: colors.RED,
speciesName: "Usul",
},
{
speciesId: "51",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/dnr2kj4b/1/1.png",
colorId: colors.YELLOW,
speciesName: "Wocky",
},
{
speciesId: "52",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/tdkqr2b6/1/1.png",
colorId: colors.RED,
speciesName: "Xweetok",
},
{
speciesId: "53",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/h95cs547/1/1.png",
colorId: colors.RED,
speciesName: "Yurble",
},
{
speciesId: "54",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/x8c57g2l/1/1.png",
colorId: colors.BLUE,
speciesName: "Zafara",
},
{
speciesId: "55",
src: "https://pets.neopets-asset-proxy.openneo.net/cp/xkntzsww/1/1.png",
colorId: colors.YELLOW,
speciesName: "Vandagyre",
},
];
export default ItemPage; export default ItemPage;