2020-05-02 15:41:02 -07:00
|
|
|
import React from "react";
|
2020-05-02 17:22:46 -07:00
|
|
|
import gql from "graphql-tag";
|
|
|
|
import { useQuery } from "@apollo/react-hooks";
|
2020-05-02 15:41:02 -07:00
|
|
|
import { css, cx } from "emotion";
|
|
|
|
import {
|
|
|
|
Box,
|
|
|
|
Button,
|
|
|
|
Flex,
|
|
|
|
Image,
|
|
|
|
Popover,
|
|
|
|
PopoverArrow,
|
|
|
|
PopoverContent,
|
|
|
|
PopoverTrigger,
|
2020-05-02 22:04:20 -07:00
|
|
|
VisuallyHidden,
|
2020-05-02 15:41:02 -07:00
|
|
|
useTheme,
|
|
|
|
} from "@chakra-ui/core";
|
|
|
|
|
2020-05-02 21:04:54 -07:00
|
|
|
import { getVisibleLayers, petAppearanceFragment } from "./useOutfitAppearance";
|
2020-05-02 15:41:02 -07:00
|
|
|
|
2020-05-02 16:03:23 -07:00
|
|
|
// From https://twemoji.twitter.com/, thank you!
|
|
|
|
import twemojiSmile from "../images/twemoji/smile.svg";
|
|
|
|
import twemojiCry from "../images/twemoji/cry.svg";
|
|
|
|
import twemojiSick from "../images/twemoji/sick.svg";
|
|
|
|
import twemojiMasc from "../images/twemoji/masc.svg";
|
|
|
|
import twemojiFem from "../images/twemoji/fem.svg";
|
2020-05-02 21:04:54 -07:00
|
|
|
import { OutfitLayers } from "./OutfitPreview";
|
2020-05-02 16:03:23 -07:00
|
|
|
|
2020-05-02 22:20:17 -07:00
|
|
|
function PosePicker({
|
|
|
|
outfitState,
|
|
|
|
dispatchToOutfit,
|
|
|
|
onLockFocus,
|
|
|
|
onUnlockFocus,
|
|
|
|
}) {
|
2020-05-02 15:41:02 -07:00
|
|
|
const theme = useTheme();
|
2020-05-02 22:04:20 -07:00
|
|
|
const checkedInputRef = React.useRef();
|
2020-05-02 22:20:17 -07:00
|
|
|
const { loading, error, poses } = usePoses(outfitState);
|
2020-05-02 22:04:20 -07:00
|
|
|
|
2020-05-02 17:22:46 -07:00
|
|
|
if (loading) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is a low-stakes enough control, where enough pairs don't have data
|
|
|
|
// anyway, that I think I want to just not draw attention to failures.
|
|
|
|
if (error) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there's only one pose anyway, don't bother showing a picker!
|
2020-05-02 21:04:54 -07:00
|
|
|
const numAvailablePoses = Object.values(poses).filter((p) => p).length;
|
2020-05-02 17:22:46 -07:00
|
|
|
if (numAvailablePoses <= 1) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-05-02 22:20:17 -07:00
|
|
|
const onChange = (e) => {
|
|
|
|
const [emotion, genderPresentation] = e.target.value.split("-");
|
|
|
|
dispatchToOutfit({
|
|
|
|
type: "setPose",
|
|
|
|
emotion,
|
|
|
|
genderPresentation,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-05-02 15:41:02 -07:00
|
|
|
return (
|
|
|
|
<Popover
|
2020-05-02 22:04:20 -07:00
|
|
|
placement="bottom-end"
|
2020-05-02 15:41:02 -07:00
|
|
|
usePortal
|
|
|
|
onOpen={onLockFocus}
|
|
|
|
onClose={onUnlockFocus}
|
2020-05-02 22:04:20 -07:00
|
|
|
initialFocusRef={checkedInputRef}
|
2020-05-02 15:41:02 -07:00
|
|
|
>
|
|
|
|
{({ isOpen }) => (
|
|
|
|
<>
|
|
|
|
<PopoverTrigger>
|
|
|
|
<Button
|
|
|
|
variant="unstyled"
|
|
|
|
boxShadow="md"
|
|
|
|
d="flex"
|
|
|
|
alignItems="center"
|
|
|
|
justifyContent="center"
|
|
|
|
_focus={{ borderColor: "gray.50" }}
|
|
|
|
_hover={{ borderColor: "gray.50" }}
|
|
|
|
outline="initial"
|
|
|
|
className={cx(
|
|
|
|
css`
|
2020-05-02 16:03:23 -07:00
|
|
|
border: 1px solid transparent !important;
|
|
|
|
transition: border-color 0.2s !important;
|
2020-05-02 15:41:02 -07:00
|
|
|
|
|
|
|
&:focus,
|
|
|
|
&:hover,
|
|
|
|
&.is-open {
|
2020-05-02 16:03:23 -07:00
|
|
|
border-color: ${theme.colors.gray["50"]} !important;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.is-open {
|
|
|
|
border-width: 2px !important;
|
2020-05-02 15:41:02 -07:00
|
|
|
}
|
|
|
|
`,
|
|
|
|
isOpen && "is-open"
|
|
|
|
)}
|
|
|
|
>
|
2020-05-02 16:03:23 -07:00
|
|
|
<EmojiImage src={twemojiSmile} aria-label="Choose a pose" />
|
2020-05-02 15:41:02 -07:00
|
|
|
</Button>
|
|
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent>
|
|
|
|
<Box p="4">
|
2020-05-02 22:20:17 -07:00
|
|
|
<table width="100%">
|
2020-05-02 15:41:02 -07:00
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th />
|
2020-05-02 16:03:23 -07:00
|
|
|
<Cell as="th">
|
|
|
|
<EmojiImage src={twemojiSmile} aria-label="Happy" />
|
|
|
|
</Cell>
|
|
|
|
<Cell as="th">
|
|
|
|
<EmojiImage src={twemojiCry} aria-label="Sad" />
|
|
|
|
</Cell>
|
|
|
|
<Cell as="th">
|
|
|
|
<EmojiImage src={twemojiSick} aria-label="Sick" />
|
|
|
|
</Cell>
|
2020-05-02 15:41:02 -07:00
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
<tr>
|
2020-05-02 16:03:23 -07:00
|
|
|
<Cell as="th">
|
|
|
|
<EmojiImage src={twemojiMasc} aria-label="Masculine" />
|
|
|
|
</Cell>
|
|
|
|
<Cell as="td">
|
2020-05-02 21:04:54 -07:00
|
|
|
<PoseButton
|
|
|
|
pose={poses.happyMasc}
|
2020-05-02 22:20:17 -07:00
|
|
|
onChange={onChange}
|
2020-05-02 22:07:47 -07:00
|
|
|
inputRef={poses.happyMasc.isSelected && checkedInputRef}
|
2020-05-02 21:04:54 -07:00
|
|
|
/>
|
2020-05-02 16:03:23 -07:00
|
|
|
</Cell>
|
|
|
|
<Cell as="td">
|
2020-05-02 22:07:47 -07:00
|
|
|
<PoseButton
|
|
|
|
pose={poses.sadMasc}
|
2020-05-02 22:20:17 -07:00
|
|
|
onChange={onChange}
|
2020-05-02 22:07:47 -07:00
|
|
|
inputRef={poses.sadMasc.isSelected && checkedInputRef}
|
|
|
|
/>
|
2020-05-02 16:03:23 -07:00
|
|
|
</Cell>
|
|
|
|
<Cell as="td">
|
2020-05-02 22:07:47 -07:00
|
|
|
<PoseButton
|
|
|
|
pose={poses.sickMasc}
|
2020-05-02 22:20:17 -07:00
|
|
|
onChange={onChange}
|
2020-05-02 22:07:47 -07:00
|
|
|
inputRef={poses.sickMasc.isSelected && checkedInputRef}
|
|
|
|
/>
|
2020-05-02 16:03:23 -07:00
|
|
|
</Cell>
|
2020-05-02 15:41:02 -07:00
|
|
|
</tr>
|
|
|
|
<tr>
|
2020-05-02 16:03:23 -07:00
|
|
|
<Cell as="th">
|
|
|
|
<EmojiImage src={twemojiFem} aria-label="Feminine" />
|
|
|
|
</Cell>
|
|
|
|
<Cell as="td">
|
2020-05-02 22:07:47 -07:00
|
|
|
<PoseButton
|
|
|
|
pose={poses.happyFem}
|
2020-05-02 22:20:17 -07:00
|
|
|
onChange={onChange}
|
2020-05-02 22:07:47 -07:00
|
|
|
inputRef={poses.happyFem.isSelected && checkedInputRef}
|
|
|
|
/>
|
2020-05-02 16:03:23 -07:00
|
|
|
</Cell>
|
|
|
|
<Cell as="td">
|
2020-05-02 22:07:47 -07:00
|
|
|
<PoseButton
|
|
|
|
pose={poses.sadFem}
|
2020-05-02 22:20:17 -07:00
|
|
|
onChange={onChange}
|
2020-05-02 22:07:47 -07:00
|
|
|
inputRef={poses.sadFem.isSelected && checkedInputRef}
|
|
|
|
/>
|
2020-05-02 16:03:23 -07:00
|
|
|
</Cell>
|
|
|
|
<Cell as="td">
|
2020-05-02 22:07:47 -07:00
|
|
|
<PoseButton
|
|
|
|
pose={poses.sickFem}
|
2020-05-02 22:20:17 -07:00
|
|
|
onChange={onChange}
|
2020-05-02 22:07:47 -07:00
|
|
|
inputRef={poses.sickFem.isSelected && checkedInputRef}
|
|
|
|
/>
|
2020-05-02 16:03:23 -07:00
|
|
|
</Cell>
|
2020-05-02 15:41:02 -07:00
|
|
|
</tr>
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</Box>
|
|
|
|
<PopoverArrow />
|
|
|
|
</PopoverContent>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-02 16:03:23 -07:00
|
|
|
function Cell({ children, as }) {
|
|
|
|
const Tag = as;
|
2020-05-02 15:41:02 -07:00
|
|
|
return (
|
2020-05-02 16:03:23 -07:00
|
|
|
<Tag>
|
2020-05-02 15:41:02 -07:00
|
|
|
<Flex justify="center" p="1">
|
|
|
|
{children}
|
|
|
|
</Flex>
|
2020-05-02 16:03:23 -07:00
|
|
|
</Tag>
|
2020-05-02 15:41:02 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-02 22:04:20 -07:00
|
|
|
const EMOTION_STRINGS = {
|
|
|
|
HAPPY: "Happy",
|
|
|
|
SAD: "Sad",
|
|
|
|
SICK: "Sick",
|
|
|
|
};
|
|
|
|
|
|
|
|
const GENDER_PRESENTATION_STRINGS = {
|
|
|
|
MASCULINE: "Masculine",
|
|
|
|
FEMININE: "Feminine",
|
|
|
|
};
|
|
|
|
|
2020-05-02 22:20:17 -07:00
|
|
|
function PoseButton({ pose, onChange, inputRef }) {
|
2020-05-02 22:04:20 -07:00
|
|
|
const theme = useTheme();
|
|
|
|
|
2020-05-02 21:04:54 -07:00
|
|
|
if (!pose) {
|
2020-05-02 17:22:46 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-05-02 22:04:20 -07:00
|
|
|
const genderPresentationStr =
|
|
|
|
GENDER_PRESENTATION_STRINGS[pose.genderPresentation];
|
|
|
|
const emotionStr = EMOTION_STRINGS[pose.emotion];
|
|
|
|
|
2020-05-02 15:41:02 -07:00
|
|
|
return (
|
2020-05-02 21:04:54 -07:00
|
|
|
<Box
|
2020-05-02 22:04:20 -07:00
|
|
|
as="label"
|
|
|
|
cursor="pointer"
|
|
|
|
onClick={(e) => {
|
|
|
|
// HACK: We need the timeout to beat the popover's focus stealing!
|
|
|
|
const input = e.currentTarget.querySelector("input");
|
|
|
|
setTimeout(() => input.focus(), 0);
|
|
|
|
}}
|
2020-05-02 21:04:54 -07:00
|
|
|
>
|
2020-05-02 22:04:20 -07:00
|
|
|
<VisuallyHidden
|
|
|
|
as="input"
|
|
|
|
type="radio"
|
|
|
|
aria-label={`${emotionStr} and ${genderPresentationStr}`}
|
|
|
|
name="pose"
|
|
|
|
value={`${pose.emotion}-${pose.genderPresentation}`}
|
|
|
|
checked={pose.isSelected}
|
2020-05-02 22:20:17 -07:00
|
|
|
onChange={onChange}
|
2020-05-02 22:07:47 -07:00
|
|
|
ref={inputRef || null}
|
2020-05-02 22:04:20 -07:00
|
|
|
/>
|
|
|
|
<Box
|
|
|
|
rounded="full"
|
|
|
|
boxShadow="md"
|
|
|
|
overflow="hidden"
|
|
|
|
width="50px"
|
|
|
|
height="50px"
|
2020-05-02 22:40:34 -07:00
|
|
|
title={
|
|
|
|
// A lil debug output, so that we can quickly identify glitched
|
|
|
|
// PetStates and manually mark them as glitched!
|
|
|
|
window.location.hostname.includes("localhost") &&
|
|
|
|
`#${pose.petStateId}`
|
|
|
|
}
|
2020-05-02 22:04:20 -07:00
|
|
|
position="relative"
|
|
|
|
className={css`
|
|
|
|
transform: scale(0.8);
|
|
|
|
opacity: 0.8;
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
input:checked + & {
|
|
|
|
transform: scale(1);
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
`}
|
|
|
|
>
|
|
|
|
<Box
|
|
|
|
rounded="full"
|
|
|
|
position="absolute"
|
|
|
|
top="0"
|
|
|
|
bottom="0"
|
|
|
|
left="0"
|
|
|
|
right="0"
|
|
|
|
zIndex="2"
|
|
|
|
className={css`
|
|
|
|
border: 0px solid ${theme.colors.green["600"]};
|
|
|
|
transition: border-width 0.2s;
|
|
|
|
|
|
|
|
input:checked + * & {
|
|
|
|
border-width: 1px;
|
|
|
|
}
|
|
|
|
|
|
|
|
input:focus + * & {
|
|
|
|
border-width: 3px;
|
|
|
|
}
|
|
|
|
`}
|
|
|
|
/>
|
2020-05-02 21:04:54 -07:00
|
|
|
<Box
|
2020-05-02 22:04:20 -07:00
|
|
|
width="50px"
|
|
|
|
height="50px"
|
2020-05-02 21:04:54 -07:00
|
|
|
transform={
|
2020-05-02 22:20:17 -07:00
|
|
|
transformsBySpeciesId[pose.speciesId] ||
|
|
|
|
transformsBySpeciesId.default
|
2020-05-02 21:04:54 -07:00
|
|
|
}
|
|
|
|
>
|
|
|
|
<OutfitLayers visibleLayers={getVisibleLayers(pose, [])} />
|
|
|
|
</Box>
|
2020-05-02 22:04:20 -07:00
|
|
|
</Box>
|
2020-05-02 15:41:02 -07:00
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-02 16:03:23 -07:00
|
|
|
function EmojiImage({ src, "aria-label": ariaLabel }) {
|
|
|
|
return <Image src={src} aria-label={ariaLabel} width="16px" height="16px" />;
|
|
|
|
}
|
|
|
|
|
2020-05-02 22:20:17 -07:00
|
|
|
function usePoses(outfitState) {
|
2020-05-02 22:40:34 -07:00
|
|
|
const { speciesId, colorId } = outfitState;
|
2020-05-02 22:04:20 -07:00
|
|
|
|
2020-05-02 17:22:46 -07:00
|
|
|
const { loading, error, data } = useQuery(
|
|
|
|
gql`
|
|
|
|
query PosePicker($speciesId: ID!, $colorId: ID!) {
|
|
|
|
petAppearances(speciesId: $speciesId, colorId: $colorId) {
|
2020-05-02 21:04:54 -07:00
|
|
|
id
|
2020-05-02 22:40:34 -07:00
|
|
|
petStateId
|
2020-05-02 17:22:46 -07:00
|
|
|
genderPresentation
|
|
|
|
emotion
|
2020-05-02 21:04:54 -07:00
|
|
|
approximateThumbnailUrl
|
2020-05-02 17:22:46 -07:00
|
|
|
...PetAppearanceForOutfitPreview
|
|
|
|
}
|
|
|
|
}
|
|
|
|
${petAppearanceFragment}
|
|
|
|
`,
|
|
|
|
{ variables: { speciesId, colorId } }
|
|
|
|
);
|
|
|
|
|
|
|
|
const petAppearances = data?.petAppearances || [];
|
2020-05-02 22:04:20 -07:00
|
|
|
const buildPose = (e, gp) => ({
|
|
|
|
...petAppearances.find(
|
2020-05-02 17:22:46 -07:00
|
|
|
(pa) => pa.emotion === e && pa.genderPresentation === gp
|
2020-05-02 22:04:20 -07:00
|
|
|
),
|
2020-05-02 22:20:17 -07:00
|
|
|
speciesId,
|
2020-05-02 22:04:20 -07:00
|
|
|
isSelected:
|
2020-05-02 22:20:17 -07:00
|
|
|
outfitState.emotion === e && outfitState.genderPresentation === gp,
|
2020-05-02 22:04:20 -07:00
|
|
|
});
|
2020-05-02 22:20:17 -07:00
|
|
|
console.log(outfitState.emotion, outfitState.genderPresentation, outfitState);
|
2020-05-02 17:22:46 -07:00
|
|
|
|
|
|
|
const poses = {
|
2020-05-02 22:04:20 -07:00
|
|
|
happyMasc: buildPose("HAPPY", "MASCULINE"),
|
|
|
|
sadMasc: buildPose("SAD", "MASCULINE"),
|
|
|
|
sickMasc: buildPose("SICK", "MASCULINE"),
|
|
|
|
happyFem: buildPose("HAPPY", "FEMININE"),
|
|
|
|
sadFem: buildPose("SAD", "FEMININE"),
|
|
|
|
sickFem: buildPose("SICK", "FEMININE"),
|
2020-05-02 17:22:46 -07:00
|
|
|
};
|
|
|
|
|
2020-05-02 22:20:17 -07:00
|
|
|
return { loading, error, poses };
|
2020-05-02 17:22:46 -07:00
|
|
|
}
|
|
|
|
|
2020-05-02 21:04:54 -07:00
|
|
|
const transformsBySpeciesId = {
|
|
|
|
"1": "translate(-5px, 10px) scale(2.8)",
|
|
|
|
"2": "translate(-8px, 8px) scale(2.9)",
|
|
|
|
"3": "translate(-1px, 17px) scale(3)",
|
|
|
|
"4": "translate(-21px, 22px) scale(3.2)",
|
|
|
|
"5": "translate(2px, 15px) scale(3.3)",
|
|
|
|
"6": "translate(-14px, 28px) scale(3.4)",
|
|
|
|
"7": "translate(-4px, 8px) scale(2.9)",
|
|
|
|
"8": "translate(-26px, 30px) scale(3.0)",
|
|
|
|
"9": "translate(-4px, 8px) scale(3.1)",
|
|
|
|
"10": "translate(-14px, 18px) scale(3.0)",
|
|
|
|
"11": "translate(-7px, 24px) scale(2.9)",
|
|
|
|
"12": "translate(-16px, 20px) scale(3.5)",
|
|
|
|
"13": "translate(-11px, 18px) scale(3.0)",
|
|
|
|
"14": "translate(-14px, 26px) scale(3.5)",
|
|
|
|
"15": "translate(-13px, 24px) scale(3.1)",
|
|
|
|
"16": "translate(-6px, 29px) scale(3.1)",
|
|
|
|
"17": "translate(3px, 13px) scale(3.1)",
|
|
|
|
"18": "translate(2px, 27px) scale(3.4)",
|
|
|
|
"19": "translate(-7px, 16px) scale(3.1)",
|
|
|
|
"20": "translate(-2px, 15px) scale(3.0)",
|
|
|
|
"21": "translate(-2px, -17px) scale(3.0)",
|
|
|
|
"22": "translate(-14px, 16px) scale(3.6)",
|
|
|
|
"23": "translate(-16px, 16px) scale(3.2)",
|
|
|
|
"24": "translate(-2px, 6px) scale(3.2)",
|
|
|
|
"25": "translate(-3px, 6px) scale(3.7)",
|
|
|
|
"26": "translate(-7px, 19px) scale(5.2)",
|
|
|
|
"27": "translate(-16px, 20px) scale(3.5)",
|
|
|
|
"28": "translate(-3px, 24px) scale(3.2)",
|
|
|
|
"29": "translate(-9px, 15px) scale(3.4)",
|
|
|
|
"30": "translate(3px, 57px) scale(4.4)",
|
|
|
|
"31": "translate(-28px, 35px) scale(3.8)",
|
|
|
|
"32": "translate(-8px, 33px) scale(3.5)",
|
|
|
|
"33": "translate(-8px, -6px) scale(3.2)",
|
|
|
|
"34": "translate(-14px, 14px) scale(3.1)",
|
|
|
|
"35": "translate(-12px, 0px) scale(3.4)",
|
|
|
|
"36": "translate(6px, 23px) scale(3.3)",
|
|
|
|
"37": "translate(-20px, 25px) scale(3.6)",
|
|
|
|
"38": "translate(-16px, 28px) scale(4.0)",
|
|
|
|
"39": "translate(-8px, 11px) scale(3.0)",
|
|
|
|
"40": "translate(2px, 12px) scale(3.5)",
|
|
|
|
"41": "translate(-3px, 18px) scale(3.0)",
|
|
|
|
"42": "translate(-18px, 46px) scale(4.4)",
|
|
|
|
"43": "translate(-6px, 22px) scale(3.2)",
|
|
|
|
"44": "translate(-2px, 19px) scale(3.4)",
|
|
|
|
"45": "translate(-11px, 32px) scale(3.3)",
|
|
|
|
"46": "translate(-13px, 23px) scale(3.3)",
|
|
|
|
"47": "translate(-14px, 4px) scale(3.1)",
|
|
|
|
"48": "translate(-9px, 24px) scale(3.5)",
|
|
|
|
"49": "translate(-14px, 25px) scale(3.4)",
|
|
|
|
"50": "translate(-7px, 4px) scale(3.6)",
|
|
|
|
"51": "translate(-13px, 16px) scale(3.2)",
|
|
|
|
"52": "translate(-2px, 13px) scale(3.2)",
|
|
|
|
"53": "translate(-6px, 4px) scale(3.1)",
|
|
|
|
"54": "translate(-15px, 22px) scale(3.6)",
|
|
|
|
"55": "translate(1px, 14px) scale(3.1)",
|
|
|
|
default: "scale(2.5)",
|
|
|
|
};
|
|
|
|
|
2020-05-02 15:41:02 -07:00
|
|
|
export default PosePicker;
|