sketched out the pet compatibility chooser

can't actually set things properly yet, but you can scroll through options and see how it fits!
This commit is contained in:
Emi Matchu 2020-08-01 14:12:57 -07:00
parent 488299353a
commit 213f30b2f2
6 changed files with 243 additions and 89 deletions

View file

@ -2,6 +2,10 @@ import * as React from "react";
import { import {
Button, Button,
Box, Box,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
HStack, HStack,
Modal, Modal,
ModalBody, ModalBody,
@ -9,74 +13,80 @@ import {
ModalContent, ModalContent,
ModalHeader, ModalHeader,
ModalOverlay, ModalOverlay,
Radio,
RadioGroup,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { ExternalLinkIcon } from "@chakra-ui/icons"; import { ExternalLinkIcon } from "@chakra-ui/icons";
function ItemSupportAppearanceLayerModal({ item, itemLayer, isOpen, onClose }) { import { OutfitLayers } from "../../components/OutfitPreview";
import SpeciesColorPicker from "../../components/SpeciesColorPicker";
import useOutfitAppearance from "../../components/useOutfitAppearance";
function ItemSupportAppearanceLayerModal({
item,
itemLayer,
outfitState,
isOpen,
onClose,
}) {
return ( return (
<Modal size="xl" isOpen={isOpen} onClose={onClose}> <Modal size="xl" isOpen={isOpen} onClose={onClose}>
<ModalOverlay> <ModalOverlay>
<ModalContent> <ModalContent color="green.800">
<ModalHeader> <ModalHeader>
Layer {itemLayer.id}: {item.name} Layer {itemLayer.id}: {item.name}
</ModalHeader> </ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody mb="4" pb="4"> <ModalBody mb="4" pb="4">
<Box <Metadata>
as="dl" <MetadataLabel>ID:</MetadataLabel>
display="grid" <MetadataValue>{itemLayer.id}</MetadataValue>
gridTemplateColumns="max-content auto" <MetadataLabel>Zone:</MetadataLabel>
gridRowGap="1" <MetadataValue>
gridColumnGap="2"
>
<Box as="dt" gridColumn="1" fontWeight="bold">
ID:
</Box>
<Box as="dd" gridColumn="2">
{itemLayer.id}
</Box>
<Box as="dt" gridColumn="1" fontWeight="bold">
Zone:
</Box>
<Box as="dd" gridColumn="2">
{itemLayer.zone.label} ({itemLayer.zone.id}) {itemLayer.zone.label} ({itemLayer.zone.id})
</Box> </MetadataValue>
<Box as="dt" gridColumn="1" fontWeight="bold"> <MetadataLabel>Assets:</MetadataLabel>
Assets: <MetadataValue>
</Box> <HStack spacing="2">
<HStack as="dd" gridColumn="2" spacing="2"> {itemLayer.svgUrl ? (
{itemLayer.svgUrl ? ( <Button
<Button as="a"
as="a" size="xs"
size="xs" target="_blank"
target="_blank" href={itemLayer.svgUrl}
href={itemLayer.svgUrl} colorScheme="teal"
colorScheme="teal" >
> SVG <ExternalLinkIcon ml="1" />
SVG <ExternalLinkIcon ml="1" /> </Button>
</Button> ) : (
) : ( <Button size="xs" isDisabled>
<Button size="xs" isDisabled> No SVG
No SVG </Button>
</Button> )}
)} {itemLayer.imageUrl ? (
{itemLayer.imageUrl ? ( <Button
<Button as="a"
as="a" size="xs"
size="xs" target="_blank"
target="_blank" href={itemLayer.imageUrl}
href={itemLayer.imageUrl} colorScheme="teal"
colorScheme="teal" >
> PNG <ExternalLinkIcon ml="1" />
PNG <ExternalLinkIcon ml="1" /> </Button>
</Button> ) : (
) : ( <Button size="xs" isDisabled>
<Button size="xs" isDisabled> No PNG
No PNG </Button>
</Button> )}
)} </HStack>
</HStack> </MetadataValue>
</Box> </Metadata>
<Box height="8" />
<ItemSupportAppearanceLayerPetCompatibility
item={item}
itemLayer={itemLayer}
outfitState={outfitState}
/>
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</ModalOverlay> </ModalOverlay>
@ -84,4 +94,122 @@ function ItemSupportAppearanceLayerModal({ item, itemLayer, isOpen, onClose }) {
); );
} }
function ItemSupportAppearanceLayerPetCompatibility({
item,
itemLayer,
outfitState,
}) {
const [selectedBiology, setSelectedBiology] = React.useState({
speciesId: outfitState.speciesId,
colorId: outfitState.colorId,
pose: outfitState.pose,
isValid: true,
});
const [visibleBiology, setVisibleBiology] = React.useState(selectedBiology);
console.log(selectedBiology, visibleBiology);
const { loading, error, visibleLayers } = useOutfitAppearance({
speciesId: visibleBiology.speciesId,
colorId: visibleBiology.colorId,
pose: visibleBiology.pose,
wornItemIds: [item.id],
});
const biologyLayers = visibleLayers.filter((l) => l.source === "pet");
return (
<FormControl isInvalid={error || !selectedBiology.isValid ? true : false}>
<FormLabel>Pet compatibility</FormLabel>
<RadioGroup
colorScheme="green"
value={itemLayer.bodyId}
onChange={(e) => console.log(e)}
marginBottom="4"
>
<Radio value="0">
Fits all pets{" "}
<Box display="inline" color="gray.400" fontSize="sm">
(Body ID: 0)
</Box>
</Radio>
<Radio as="div" value="100" marginTop="2">
Fits all pets with the same body as:{" "}
<Box display="inline" color="gray.400" fontSize="sm">
(Body ID: 100)
</Box>
</Radio>
</RadioGroup>
<Box display="flex" flexDirection="column" alignItems="center">
<Box
width="150px"
height="150px"
marginTop="2"
marginBottom="2"
boxShadow="md"
borderRadius="md"
>
<OutfitLayers
loading={loading}
visibleLayers={[...biologyLayers, itemLayer]}
/>
</Box>
<SpeciesColorPicker
speciesId={selectedBiology.speciesId}
colorId={selectedBiology.colorId}
idealPose={outfitState.pose}
isDisabled={itemLayer.bodyId === "0"}
size="sm"
showPlaceholders
onChange={(species, color, isValid, pose) => {
const speciesId = species.id;
const colorId = color.id;
setSelectedBiology({ speciesId, colorId, isValid, pose });
if (isValid) {
setVisibleBiology({ speciesId, colorId, isValid, pose });
}
}}
/>
<Box height="1" />
{!error && (
<FormHelperText>
If it doesn't look right, try some other options until it does!
</FormHelperText>
)}
{error && <FormErrorMessage>{error.message}</FormErrorMessage>}
</Box>
</FormControl>
);
}
function Metadata({ children }) {
return (
<Box
as="dl"
display="grid"
gridTemplateColumns="max-content auto"
gridRowGap="1"
gridColumnGap="2"
>
{children}
</Box>
);
}
function MetadataLabel({ children }) {
return (
<Box as="dt" gridColumn="1" fontWeight="bold">
{children}
</Box>
);
}
function MetadataValue({ children }) {
return (
<Box as="dd" gridColumn="2">
{children}
</Box>
);
}
export default ItemSupportAppearanceLayerModal; export default ItemSupportAppearanceLayerModal;

View file

@ -250,9 +250,10 @@ function ItemSupportAppearanceFields({ item, outfitState }) {
<HStack spacing="4" overflow="auto" paddingX="1"> <HStack spacing="4" overflow="auto" paddingX="1">
{itemLayers.map((itemLayer) => ( {itemLayers.map((itemLayer) => (
<ItemSupportAppearanceLayer <ItemSupportAppearanceLayer
biologyLayers={biologyLayers}
itemLayer={itemLayer}
item={item} item={item}
itemLayer={itemLayer}
biologyLayers={biologyLayers}
outfitState={outfitState}
/> />
))} ))}
</HStack> </HStack>
@ -261,7 +262,12 @@ function ItemSupportAppearanceFields({ item, outfitState }) {
); );
} }
function ItemSupportAppearanceLayer({ biologyLayers, itemLayer, item }) { function ItemSupportAppearanceLayer({
item,
itemLayer,
biologyLayers,
outfitState,
}) {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
return ( return (
@ -311,6 +317,7 @@ function ItemSupportAppearanceLayer({ biologyLayers, itemLayer, item }) {
<ItemSupportAppearanceLayerModal <ItemSupportAppearanceLayerModal
item={item} item={item}
itemLayer={itemLayer} itemLayer={itemLayer}
outfitState={outfitState}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
/> />

View file

@ -167,7 +167,7 @@ export function OutfitLayers({
opacity={isMounted && loading ? 1 : 0} opacity={isMounted && loading ? 1 : 0}
transition={`opacity 0.2s ${loading ? loadingDelay : "0s"}`} transition={`opacity 0.2s ${loading ? loadingDelay : "0s"}`}
> >
<FullScreenCenter> <FullScreenCenter zIndex="9000">
<Box <Box
width="100%" width="100%"
height="100%" height="100%"
@ -175,7 +175,7 @@ export function OutfitLayers({
opacity="0.8" opacity="0.8"
/> />
</FullScreenCenter> </FullScreenCenter>
<FullScreenCenter> <FullScreenCenter zIndex="9001">
<HangerSpinner color="green.300" boxSize="48px" /> <HangerSpinner color="green.300" boxSize="48px" />
</FullScreenCenter> </FullScreenCenter>
</Box> </Box>

View file

@ -15,7 +15,9 @@ function SpeciesColorPicker({
speciesId, speciesId,
colorId, colorId,
idealPose, idealPose,
showPlaceholders, showPlaceholders = false,
isDisabled = false,
size = "md",
dark = false, dark = false,
onChange, onChange,
}) { }) {
@ -51,30 +53,38 @@ function SpeciesColorPicker({
const backgroundColor = dark ? "gray.600" : "white"; const backgroundColor = dark ? "gray.600" : "white";
const borderColor = dark ? "transparent" : "green.600"; const borderColor = dark ? "transparent" : "green.600";
const textColor = dark ? "gray.50" : "inherit"; const textColor = dark ? "gray.50" : "inherit";
const SpeciesColorSelect = ({ ...props }) => ( const SpeciesColorSelect = ({ isDisabled, isLoading, ...props }) => {
<Select const loadingProps = isLoading
backgroundColor={backgroundColor} ? {
color={textColor} // Visually the disabled state is the same as the normal state, but
border="1px" // with a wait cursor. We don't expect this to take long, and the flash
borderColor={borderColor} // of content is rough! (The caret still flashes, but that's small and
boxShadow="md" // harder to style in Chakra.)
width="auto" opacity: 1,
_hover={{ cursor: "wait",
borderColor: "green.400", }
}} : {};
_disabled={{
// Visually the disabled state is the same as the normal state, but return (
// with a wait cursor. We don't expect this to take long, and the flash <Select
// of content is rough! (The caret still flashes, but that's small and backgroundColor={backgroundColor}
// harder to style in Chakra.) color={textColor}
opacity: 1, size={size}
cursor: "wait", border="1px"
}} borderColor={borderColor}
isInvalid={valids && !pairIsValid(valids, speciesId, colorId)} boxShadow="md"
errorBorderColor="red.300" width="auto"
{...props} _hover={{
/> borderColor: "green.400",
); }}
isInvalid={valids && !pairIsValid(valids, speciesId, colorId)}
isDisabled={isDisabled || isLoading}
errorBorderColor="red.300"
{...props}
{...loadingProps}
/>
);
};
if ((loadingMeta || loadingValids) && !showPlaceholders) { if ((loadingMeta || loadingValids) && !showPlaceholders) {
return ( return (
@ -125,7 +135,8 @@ function SpeciesColorPicker({
<SpeciesColorSelect <SpeciesColorSelect
aria-label="Pet color" aria-label="Pet color"
value={colorId} value={colorId}
isDisabled={allColors.length === 0} isLoading={allColors.length === 0}
isDisabled={isDisabled}
onChange={onChangeColor} onChange={onChangeColor}
> >
{allColors.length === 0 && ( {allColors.length === 0 && (
@ -141,11 +152,12 @@ function SpeciesColorPicker({
</option> </option>
))} ))}
</SpeciesColorSelect> </SpeciesColorSelect>
<Box width="4" /> <Box width={size === "sm" ? 2 : 4} />
<SpeciesColorSelect <SpeciesColorSelect
aria-label="Pet species" aria-label="Pet species"
value={speciesId} value={speciesId}
isDisabled={allSpecies.length === 0} isLoading={allSpecies.length === 0}
isDisabled={isDisabled}
onChange={onChangeSpecies} onChange={onChangeSpecies}
> >
{allSpecies.length === 0 && ( {allSpecies.length === 0 && (

View file

@ -117,6 +117,7 @@ export const itemAppearanceFragment = gql`
id id
svgUrl svgUrl
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
bodyId
zone { zone {
id id
depth depth

View file

@ -104,6 +104,12 @@ const typeDefs = gql`
or if it's not as simple as a single SVG (e.g. animated). or if it's not as simple as a single SVG (e.g. animated).
""" """
svgUrl: String svgUrl: String
"""
This layer can fit on PetAppearances with the same bodyId. "0" is a
special body ID that indicates it fits all PetAppearances.
"""
bodyId: ID!
} }
# Cache for 1 week (unlikely to change) # Cache for 1 week (unlikely to change)