impress-2020/src/app/PosePicker.js

243 lines
6.7 KiB
JavaScript
Raw Normal View History

2020-05-02 15:41:02 -07:00
import React from "react";
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,
useTheme,
} from "@chakra-ui/core";
import { petAppearanceFragment } from "./useOutfitAppearance";
2020-05-02 15:41:02 -07:00
import { safeImageUrl } from "./util";
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";
function PosePicker({ outfitState, onLockFocus, onUnlockFocus }) {
2020-05-02 15:41:02 -07:00
const theme = useTheme();
const { speciesId, colorId } = outfitState;
const { loading, error, poses } = useAvailablePoses({
speciesId,
colorId,
});
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!
const numAvailablePoses = Object.values(poses).filter((p) => p.isAvailable)
.length;
if (numAvailablePoses <= 1) {
return null;
}
2020-05-02 15:41:02 -07:00
return (
<Popover
placement="top-end"
usePortal
onOpen={onLockFocus}
onClose={onUnlockFocus}
>
{({ 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">
<table width="100%" borderSpacing="8px">
<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">
<PoseButton pose={poses.happyMasc} />
2020-05-02 16:03:23 -07:00
</Cell>
<Cell as="td">
<PoseButton pose={poses.sadMasc} />
2020-05-02 16:03:23 -07:00
</Cell>
<Cell as="td">
<PoseButton pose={poses.sickMasc} />
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">
<PoseButton pose={poses.happyFem} />
2020-05-02 16:03:23 -07:00
</Cell>
<Cell as="td">
<PoseButton pose={poses.sadFem} />
2020-05-02 16:03:23 -07:00
</Cell>
<Cell as="td">
<PoseButton pose={poses.sickFem} />
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
);
}
function PoseButton({ pose }) {
if (!pose.isAvailable) {
return null;
}
2020-05-02 15:41:02 -07:00
return (
<Box rounded="full" boxShadow="md" overflow="hidden">
<Button variant="unstyled" width="auto" height="auto">
<Image
src={safeImageUrl(pose.thumbnailUrl)}
2020-05-02 15:41:02 -07:00
width="50px"
height="50px"
className={css`
opacity: 0.01;
&[src] {
opacity: 1;
transition: opacity 0.2s;
}
`}
/>
</Button>
</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" />;
}
function useAvailablePoses({ speciesId, colorId }) {
const { loading, error, data } = useQuery(
gql`
query PosePicker($speciesId: ID!, $colorId: ID!) {
petAppearances(speciesId: $speciesId, colorId: $colorId) {
genderPresentation
emotion
...PetAppearanceForOutfitPreview
}
}
${petAppearanceFragment}
`,
{ variables: { speciesId, colorId } }
);
const petAppearances = data?.petAppearances || [];
const hasAppearanceFor = (e, gp) =>
petAppearances.some(
(pa) => pa.emotion === e && pa.genderPresentation === gp
);
const poses = {
happyMasc: {
isAvailable: hasAppearanceFor("HAPPY", "MASCULINE"),
thumbnailUrl: "http://pets.neopets.com/cp/42j5q3zx/1/1.png",
},
sadMasc: {
isAvailable: hasAppearanceFor("SAD", "MASCULINE"),
thumbnailUrl: "http://pets.neopets.com/cp/42j5q3zx/2/1.png",
},
sickMasc: {
isAvailable: hasAppearanceFor("SICK", "MASCULINE"),
thumbnailUrl: "http://pets.neopets.com/cp/42j5q3zx/4/1.png",
},
happyFem: {
isAvailable: hasAppearanceFor("HAPPY", "FEMININE"),
thumbnailUrl: "http://pets.neopets.com/cp/xgnghng7/1/1.png",
},
sadFem: {
isAvailable: hasAppearanceFor("SAD", "FEMININE"),
thumbnailUrl: "http://pets.neopets.com/cp/xgnghng7/2/1.png",
},
sickFem: {
isAvailable: hasAppearanceFor("SICK", "FEMININE"),
thumbnailUrl: "http://pets.neopets.com/cp/xgnghng7/4/1.png",
},
};
return { loading, error, poses };
}
2020-05-02 15:41:02 -07:00
export default PosePicker;