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 = [
|
const VALID_URL_PATTERNS = [
|
||||||
/^http:\/\/images\.neopets\.com\/items\/[a-zA-Z0-9_ -]+\.gif$/,
|
/^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) => {
|
export default async (req, res) => {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@chakra-ui/core";
|
} from "@chakra-ui/core";
|
||||||
|
|
||||||
|
import { safeImageUrl } from "./util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item show a basic summary of an item, in the context of the current outfit!
|
* 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 (
|
return (
|
||||||
<ItemContainer>
|
<ItemContainer>
|
||||||
<Box>
|
<Box>
|
||||||
<ItemThumbnail
|
<ItemThumbnail src={safeImageUrl(item.thumbnailUrl)} isWorn={isWorn} />
|
||||||
src={`/api/assetProxy?url=${encodeURIComponent(item.thumbnailUrl)}`}
|
|
||||||
isWorn={isWorn}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box width="3" />
|
<Box width="3" />
|
||||||
<Box>
|
<Box>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { css } from "emotion";
|
import { css, cx } from "emotion";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
} from "@chakra-ui/core";
|
} from "@chakra-ui/core";
|
||||||
|
|
||||||
import OutfitResetModal from "./OutfitResetModal";
|
import OutfitResetModal from "./OutfitResetModal";
|
||||||
|
import PosePicker from "./PosePicker";
|
||||||
import SpeciesColorPicker from "./SpeciesColorPicker";
|
import SpeciesColorPicker from "./SpeciesColorPicker";
|
||||||
import useOutfitAppearance from "./useOutfitAppearance";
|
import useOutfitAppearance from "./useOutfitAppearance";
|
||||||
|
|
||||||
|
@ -19,6 +20,8 @@ import useOutfitAppearance from "./useOutfitAppearance";
|
||||||
* control things like species/color and sharing links!
|
* control things like species/color and sharing links!
|
||||||
*/
|
*/
|
||||||
function OutfitControls({ outfitState, dispatchToOutfit }) {
|
function OutfitControls({ outfitState, dispatchToOutfit }) {
|
||||||
|
const [focusIsLocked, setFocusIsLocked] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PseudoBox
|
<PseudoBox
|
||||||
role="group"
|
role="group"
|
||||||
|
@ -34,15 +37,19 @@ function OutfitControls({ outfitState, dispatchToOutfit }) {
|
||||||
"space space"
|
"space space"
|
||||||
"picker picker"`}
|
"picker picker"`}
|
||||||
gridTemplateRows="auto minmax(1rem, 1fr) auto"
|
gridTemplateRows="auto minmax(1rem, 1fr) auto"
|
||||||
className={css`
|
className={cx(
|
||||||
|
css`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-within {
|
&:focus-within,
|
||||||
|
&.focus-is-locked {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
`}
|
`,
|
||||||
|
focusIsLocked && "focus-is-locked"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Box gridArea="back">
|
<Box gridArea="back">
|
||||||
<BackButton dispatchToOutfit={dispatchToOutfit} />
|
<BackButton dispatchToOutfit={dispatchToOutfit} />
|
||||||
|
@ -62,10 +69,23 @@ function OutfitControls({ outfitState, dispatchToOutfit }) {
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box gridArea="space" />
|
<Box gridArea="space" />
|
||||||
<Flex gridArea="picker" justify="center">
|
<Flex gridArea="picker" justify="center">
|
||||||
|
{/**
|
||||||
|
* 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
|
<SpeciesColorPicker
|
||||||
outfitState={outfitState}
|
outfitState={outfitState}
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
<Flex flex="1 1 0" align="center" pl="4">
|
||||||
|
<PosePicker
|
||||||
|
onLockFocus={() => setFocusIsLocked(true)}
|
||||||
|
onUnlockFocus={() => setFocusIsLocked(false)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</PseudoBox>
|
</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
|
* useDebounce helps make a rapidly-changing value change less! It waits for a
|
||||||
* pause in the incoming data before outputting the latest value.
|
* pause in the incoming data before outputting the latest value.
|
||||||
|
|
Loading…
Reference in a new issue