pose picker foundational UI!
This commit is contained in:
parent
6b70df7e5e
commit
4954d4adcf
5 changed files with 191 additions and 17 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
147
src/app/PosePicker.js
Normal 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;
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue