pose picker foundational UI!

This commit is contained in:
Matt Dunn-Rankin 2020-05-02 15:41:02 -07:00
parent 6b70df7e5e
commit 4954d4adcf
5 changed files with 191 additions and 17 deletions

View file

@ -6,6 +6,7 @@ const streamPipeline = util.promisify(stream.pipeline);
const VALID_URL_PATTERNS = [
/^http:\/\/images\.neopets\.com\/items\/[a-zA-Z0-9_ -]+\.gif$/,
/^http:\/\/pets\.neopets\.com\/cp\/[a-z0-9]+\/[0-9]+\/[0-9]+\.png$/,
];
export default async (req, res) => {

View file

@ -10,6 +10,8 @@ import {
useTheme,
} from "@chakra-ui/core";
import { safeImageUrl } from "./util";
/**
* Item show a basic summary of an item, in the context of the current outfit!
*
@ -29,10 +31,7 @@ export function Item({ item, itemNameId, outfitState, dispatchToOutfit }) {
return (
<ItemContainer>
<Box>
<ItemThumbnail
src={`/api/assetProxy?url=${encodeURIComponent(item.thumbnailUrl)}`}
isWorn={isWorn}
/>
<ItemThumbnail src={safeImageUrl(item.thumbnailUrl)} isWorn={isWorn} />
</Box>
<Box width="3" />
<Box>

View file

@ -1,5 +1,5 @@
import React from "react";
import { css } from "emotion";
import { css, cx } from "emotion";
import {
Box,
Flex,
@ -11,6 +11,7 @@ import {
} from "@chakra-ui/core";
import OutfitResetModal from "./OutfitResetModal";
import PosePicker from "./PosePicker";
import SpeciesColorPicker from "./SpeciesColorPicker";
import useOutfitAppearance from "./useOutfitAppearance";
@ -19,6 +20,8 @@ import useOutfitAppearance from "./useOutfitAppearance";
* control things like species/color and sharing links!
*/
function OutfitControls({ outfitState, dispatchToOutfit }) {
const [focusIsLocked, setFocusIsLocked] = React.useState(false);
return (
<PseudoBox
role="group"
@ -34,15 +37,19 @@ function OutfitControls({ outfitState, dispatchToOutfit }) {
"space space"
"picker picker"`}
gridTemplateRows="auto minmax(1rem, 1fr) auto"
className={css`
opacity: 0;
transition: opacity 0.2s;
className={cx(
css`
opacity: 0;
transition: opacity 0.2s;
&:hover,
&:focus-within {
opacity: 1;
}
`}
&:hover,
&:focus-within,
&.focus-is-locked {
opacity: 1;
}
`,
focusIsLocked && "focus-is-locked"
)}
>
<Box gridArea="back">
<BackButton dispatchToOutfit={dispatchToOutfit} />
@ -62,10 +69,23 @@ function OutfitControls({ outfitState, dispatchToOutfit }) {
</Stack>
<Box gridArea="space" />
<Flex gridArea="picker" justify="center">
<SpeciesColorPicker
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
{/**
* We try to center the species/color picker, but the left spacer will
* shrink more than the pose picker container if we run out of space!
*/}
<Box flex="1 1 0" />
<Box flex="0 0 auto">
<SpeciesColorPicker
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
</Box>
<Flex flex="1 1 0" align="center" pl="4">
<PosePicker
onLockFocus={() => setFocusIsLocked(true)}
onUnlockFocus={() => setFocusIsLocked(false)}
/>
</Flex>
</Flex>
</PseudoBox>
);

147
src/app/PosePicker.js Normal file
View file

@ -0,0 +1,147 @@
import React from "react";
import { css, cx } from "emotion";
import {
Box,
Button,
Flex,
Image,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
useTheme,
} from "@chakra-ui/core";
import { safeImageUrl } from "./util";
function PosePicker({ onLockFocus, onUnlockFocus }) {
const theme = useTheme();
return (
<Popover
placement="top-end"
usePortal
onOpen={onLockFocus}
onClose={onUnlockFocus}
>
{({ isOpen }) => (
<>
<PopoverTrigger>
<Button
variant="unstyled"
boxShadow="md"
d="flex"
alignItems="center"
justifyContent="center"
border="2px solid transparent"
_focus={{ borderColor: "gray.50" }}
_hover={{ borderColor: "gray.50" }}
outline="initial"
className={cx(
css`
border: 2px solid transparent;
&:focus,
&:hover,
&.is-open {
border-color: ${theme.colors.gray["50"]};
}
`,
isOpen && "is-open"
)}
>
<span role="img" aria-label="Choose a pose">
😊
</span>
</Button>
</PopoverTrigger>
<PopoverContent>
<Box p="4">
<table width="100%" borderSpacing="8px">
<thead>
<tr>
<th />
<Box as="th" textAlign="center">
😊
</Box>
<Box as="th" textAlign="center">
😢
</Box>
<Box as="th" textAlign="center">
🤒
</Box>
</tr>
</thead>
<tbody>
<tr>
<Box as="th" textAlign="right">
🙍
</Box>
<PoseCell>
<PoseButton src="http://pets.neopets.com/cp/42j5q3zx/1/1.png" />
</PoseCell>
<PoseCell>
<PoseButton src="http://pets.neopets.com/cp/42j5q3zx/2/1.png" />
</PoseCell>
<PoseCell>
<PoseButton src="http://pets.neopets.com/cp/42j5q3zx/4/1.png" />
</PoseCell>
</tr>
<tr>
<Box as="th" textAlign="right">
🙍
</Box>
<PoseCell>
<PoseButton src="http://pets.neopets.com/cp/xgnghng7/1/1.png" />
</PoseCell>
<PoseCell>
<PoseButton src="http://pets.neopets.com/cp/xgnghng7/2/1.png" />
</PoseCell>
<PoseCell>
<PoseButton src="http://pets.neopets.com/cp/xgnghng7/4/1.png" />
</PoseCell>
</tr>
</tbody>
</table>
</Box>
<PopoverArrow />
</PopoverContent>
</>
)}
</Popover>
);
}
function PoseCell({ children }) {
return (
<td>
<Flex justify="center" p="1">
{children}
</Flex>
</td>
);
}
function PoseButton({ src }) {
return (
<Box rounded="full" boxShadow="md" overflow="hidden">
<Button variant="unstyled" width="auto" height="auto">
<Image
src={safeImageUrl(src)}
width="50px"
height="50px"
className={css`
opacity: 0.01;
&[src] {
opacity: 1;
transition: opacity 0.2s;
}
`}
/>
</Button>
</Box>
);
}
export default PosePicker;

View file

@ -51,6 +51,13 @@ export function Heading2({ children, ...props }) {
);
}
/**
* safeImageUrl returns an HTTPS-safe image URL for Neopets assets!
*/
export function safeImageUrl(url) {
return `/api/assetProxy?url=${encodeURIComponent(url)}`;
}
/**
* useDebounce helps make a rapidly-changing value change less! It waits for a
* pause in the incoming data before outputting the latest value.