2020-07-31 16:57:37 -07:00
|
|
|
import * as React from "react";
|
2020-07-31 22:11:32 -07:00
|
|
|
import gql from "graphql-tag";
|
2020-07-31 23:00:10 -07:00
|
|
|
import { useQuery, useMutation } from "@apollo/client";
|
2020-08-01 12:50:01 -07:00
|
|
|
import { css } from "emotion";
|
2020-07-31 16:57:37 -07:00
|
|
|
import {
|
|
|
|
Badge,
|
|
|
|
Box,
|
|
|
|
Drawer,
|
|
|
|
DrawerBody,
|
|
|
|
DrawerCloseButton,
|
|
|
|
DrawerContent,
|
|
|
|
DrawerHeader,
|
|
|
|
DrawerOverlay,
|
|
|
|
FormControl,
|
2020-07-31 22:11:32 -07:00
|
|
|
FormErrorMessage,
|
2020-07-31 16:57:37 -07:00
|
|
|
FormHelperText,
|
|
|
|
FormLabel,
|
2020-08-01 01:35:27 -07:00
|
|
|
HStack,
|
2020-07-31 16:57:37 -07:00
|
|
|
Link,
|
|
|
|
Select,
|
2020-07-31 22:11:32 -07:00
|
|
|
Spinner,
|
2020-08-01 01:35:27 -07:00
|
|
|
Stack,
|
2020-07-31 22:11:32 -07:00
|
|
|
useBreakpointValue,
|
2020-08-01 12:50:01 -07:00
|
|
|
useDisclosure,
|
2020-07-31 16:57:37 -07:00
|
|
|
} from "@chakra-ui/core";
|
2020-08-01 12:50:01 -07:00
|
|
|
import { CheckCircleIcon, EditIcon, ExternalLinkIcon } from "@chakra-ui/icons";
|
2020-07-31 23:00:10 -07:00
|
|
|
|
2020-08-01 22:41:03 -07:00
|
|
|
import ItemLayerSupportModal from "./ItemLayerSupportModal";
|
2020-08-01 01:35:27 -07:00
|
|
|
import { OutfitLayers } from "../../components/OutfitPreview";
|
|
|
|
import useOutfitAppearance from "../../components/useOutfitAppearance";
|
2020-08-05 00:25:25 -07:00
|
|
|
import { OutfitStateContext } from "../useOutfitState";
|
2020-07-31 23:00:10 -07:00
|
|
|
import useSupportSecret from "./useSupportSecret";
|
2020-07-31 16:57:37 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* ItemSupportDrawer shows Support UI for the item when open.
|
|
|
|
*
|
|
|
|
* This component controls the drawer element. The actual content is imported
|
|
|
|
* from another lazy-loaded component!
|
|
|
|
*/
|
2020-08-05 00:25:25 -07:00
|
|
|
function ItemSupportDrawer({ item, isOpen, onClose }) {
|
2020-07-31 22:11:32 -07:00
|
|
|
const placement = useBreakpointValue({
|
|
|
|
base: "bottom",
|
|
|
|
lg: "right",
|
|
|
|
|
|
|
|
// TODO: There's a bug in the Chakra RC that doesn't read the breakpoint
|
|
|
|
// specification correctly - we need these extra keys until it's fixed!
|
|
|
|
// https://github.com/chakra-ui/chakra-ui/issues/1444
|
|
|
|
0: "bottom",
|
|
|
|
1: "bottom",
|
|
|
|
2: "right",
|
|
|
|
3: "right",
|
|
|
|
});
|
|
|
|
|
2020-07-31 16:57:37 -07:00
|
|
|
return (
|
|
|
|
<Drawer
|
2020-07-31 22:11:32 -07:00
|
|
|
placement={placement}
|
2020-07-31 16:57:37 -07:00
|
|
|
isOpen={isOpen}
|
|
|
|
onClose={onClose}
|
|
|
|
// blockScrollOnMount doesn't matter on our fullscreen UI, but the
|
|
|
|
// default implementation breaks out layout somehow 🤔 idk, let's not!
|
|
|
|
blockScrollOnMount={false}
|
|
|
|
>
|
|
|
|
<DrawerOverlay>
|
2020-08-01 01:35:27 -07:00
|
|
|
<DrawerContent
|
|
|
|
maxHeight={placement === "bottom" ? "90vh" : undefined}
|
|
|
|
overflow="auto"
|
|
|
|
>
|
2020-07-31 16:57:37 -07:00
|
|
|
<DrawerCloseButton />
|
2020-08-01 01:35:27 -07:00
|
|
|
<DrawerHeader color="green.800">
|
2020-07-31 16:57:37 -07:00
|
|
|
{item.name}
|
2020-07-31 22:11:32 -07:00
|
|
|
<Badge colorScheme="pink" marginLeft="3">
|
2020-07-31 16:57:37 -07:00
|
|
|
Support <span aria-hidden="true">💖</span>
|
|
|
|
</Badge>
|
|
|
|
</DrawerHeader>
|
2020-08-01 01:35:27 -07:00
|
|
|
<DrawerBody color="green.800">
|
2020-07-31 16:57:37 -07:00
|
|
|
<Box paddingBottom="5">
|
2020-08-01 01:35:27 -07:00
|
|
|
<Stack spacing="8">
|
|
|
|
<ItemSupportSpecialColorFields item={item} />
|
2020-08-05 00:25:25 -07:00
|
|
|
<ItemSupportAppearanceFields item={item} />
|
2020-08-01 01:35:27 -07:00
|
|
|
</Stack>
|
2020-07-31 16:57:37 -07:00
|
|
|
</Box>
|
|
|
|
</DrawerBody>
|
|
|
|
</DrawerContent>
|
|
|
|
</DrawerOverlay>
|
|
|
|
</Drawer>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-01 01:35:27 -07:00
|
|
|
function ItemSupportSpecialColorFields({ item }) {
|
2020-07-31 23:00:10 -07:00
|
|
|
const supportSecret = useSupportSecret();
|
|
|
|
|
2020-07-31 23:40:05 -07:00
|
|
|
const { loading: itemLoading, error: itemError, data: itemData } = useQuery(
|
2020-07-31 22:31:28 -07:00
|
|
|
gql`
|
2020-07-31 23:40:05 -07:00
|
|
|
query ItemSupportDrawerManualSpecialColor($itemId: ID!) {
|
2020-07-31 22:31:28 -07:00
|
|
|
item(id: $itemId) {
|
2020-08-01 00:04:11 -07:00
|
|
|
id
|
2020-07-31 22:31:28 -07:00
|
|
|
manualSpecialColor {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
}
|
2020-07-31 22:11:32 -07:00
|
|
|
}
|
2020-07-31 22:31:28 -07:00
|
|
|
`,
|
2020-08-01 00:35:48 -07:00
|
|
|
{
|
|
|
|
variables: { itemId: item.id },
|
|
|
|
|
|
|
|
// HACK: I think it's a bug in @apollo/client 3.1.1 that, if the
|
|
|
|
// optimistic response sets `manualSpecialColor` to null, the query
|
|
|
|
// doesn't update, even though its cache has updated :/
|
|
|
|
//
|
|
|
|
// This cheap trick of changing the display name every re-render
|
|
|
|
// persuades Apollo that this is a different query, so it re-checks
|
|
|
|
// its cache and finds the empty `manualSpecialColor`. Weird!
|
|
|
|
displayName: `ItemSupportDrawerManualSpecialColor-${new Date()}`,
|
|
|
|
}
|
2020-07-31 22:31:28 -07:00
|
|
|
);
|
2020-07-31 22:11:32 -07:00
|
|
|
|
2020-07-31 23:40:05 -07:00
|
|
|
const {
|
|
|
|
loading: colorsLoading,
|
|
|
|
error: colorsError,
|
|
|
|
data: colorsData,
|
|
|
|
} = useQuery(
|
|
|
|
gql`
|
|
|
|
query ItemSupportDrawerAllColors {
|
|
|
|
allColors {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
isStandard
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`
|
|
|
|
);
|
|
|
|
|
2020-07-31 23:00:10 -07:00
|
|
|
const [
|
|
|
|
mutate,
|
2020-07-31 23:40:05 -07:00
|
|
|
{ loading: mutationLoading, error: mutationError, data: mutationData },
|
2020-07-31 23:00:10 -07:00
|
|
|
] = useMutation(gql`
|
|
|
|
mutation ItemSupportDrawerSetManualSpecialColor(
|
|
|
|
$itemId: ID!
|
|
|
|
$colorId: ID
|
|
|
|
$supportSecret: String!
|
|
|
|
) {
|
|
|
|
setManualSpecialColor(
|
|
|
|
itemId: $itemId
|
|
|
|
colorId: $colorId
|
|
|
|
supportSecret: $supportSecret
|
|
|
|
) {
|
2020-08-01 00:04:11 -07:00
|
|
|
id
|
2020-07-31 23:00:10 -07:00
|
|
|
manualSpecialColor {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2020-07-31 23:40:05 -07:00
|
|
|
const nonStandardColors =
|
|
|
|
colorsData?.allColors?.filter((c) => !c.isStandard) || [];
|
2020-07-31 22:11:32 -07:00
|
|
|
nonStandardColors.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
2020-07-31 16:57:37 -07:00
|
|
|
return (
|
2020-07-31 23:40:05 -07:00
|
|
|
<FormControl
|
|
|
|
isInvalid={colorsError || itemError || mutationError ? true : false}
|
|
|
|
>
|
2020-07-31 16:57:37 -07:00
|
|
|
<FormLabel>Special color</FormLabel>
|
2020-07-31 22:11:32 -07:00
|
|
|
<Select
|
2020-07-31 22:31:28 -07:00
|
|
|
placeholder={
|
2020-07-31 23:40:05 -07:00
|
|
|
colorsLoading || itemLoading
|
|
|
|
? "Loading…"
|
|
|
|
: "Default: Auto-detect from item description"
|
2020-07-31 22:31:28 -07:00
|
|
|
}
|
2020-08-01 00:04:11 -07:00
|
|
|
value={itemData?.item?.manualSpecialColor?.id}
|
|
|
|
isDisabled={mutationLoading}
|
2020-07-31 23:00:10 -07:00
|
|
|
icon={
|
2020-07-31 23:40:05 -07:00
|
|
|
colorsLoading || itemLoading || mutationLoading ? (
|
2020-07-31 23:00:10 -07:00
|
|
|
<Spinner />
|
2020-07-31 23:40:05 -07:00
|
|
|
) : mutationData ? (
|
2020-07-31 23:00:10 -07:00
|
|
|
<CheckCircleIcon />
|
|
|
|
) : undefined
|
|
|
|
}
|
|
|
|
onChange={(e) => {
|
2020-08-01 00:04:11 -07:00
|
|
|
const colorId = e.target.value || null;
|
2020-07-31 23:00:10 -07:00
|
|
|
const color =
|
|
|
|
colorId != null ? { __typename: "Color", id: colorId } : null;
|
|
|
|
mutate({
|
|
|
|
variables: {
|
|
|
|
itemId: item.id,
|
|
|
|
colorId,
|
|
|
|
supportSecret,
|
|
|
|
},
|
|
|
|
optimisticResponse: {
|
|
|
|
__typename: "Mutation",
|
|
|
|
setManualSpecialColor: {
|
|
|
|
__typename: "Item",
|
|
|
|
id: item.id,
|
|
|
|
manualSpecialColor: color,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}}
|
2020-07-31 22:11:32 -07:00
|
|
|
>
|
|
|
|
{nonStandardColors.map((color) => (
|
|
|
|
<option key={color.id} value={color.id}>
|
|
|
|
{color.name}
|
|
|
|
</option>
|
|
|
|
))}
|
2020-07-31 16:57:37 -07:00
|
|
|
</Select>
|
2020-07-31 23:40:05 -07:00
|
|
|
{colorsError && (
|
|
|
|
<FormErrorMessage>{colorsError.message}</FormErrorMessage>
|
|
|
|
)}
|
|
|
|
{itemError && <FormErrorMessage>{itemError.message}</FormErrorMessage>}
|
|
|
|
{mutationError && (
|
|
|
|
<FormErrorMessage>{mutationError.message}</FormErrorMessage>
|
|
|
|
)}
|
|
|
|
{!colorsError && !itemError && !mutationError && (
|
2020-07-31 22:11:32 -07:00
|
|
|
<FormHelperText>
|
|
|
|
This controls which previews we show on the{" "}
|
|
|
|
<Link
|
|
|
|
href={`https://impress.openneo.net/items/${
|
|
|
|
item.id
|
|
|
|
}-${item.name.replace(/ /g, "-")}`}
|
|
|
|
color="green.500"
|
|
|
|
isExternal
|
|
|
|
>
|
|
|
|
item page <ExternalLinkIcon />
|
|
|
|
</Link>
|
|
|
|
.
|
|
|
|
</FormHelperText>
|
|
|
|
)}
|
2020-07-31 16:57:37 -07:00
|
|
|
</FormControl>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-05 00:25:25 -07:00
|
|
|
/**
|
|
|
|
* NOTE: This component takes `outfitState` from context, rather than as a prop
|
|
|
|
* from its parent, for performance reasons. We want `Item` to memoize
|
|
|
|
* and generally skip re-rendering on `outfitState` changes, and to make
|
|
|
|
* sure the context isn't accessed when the drawer is closed. So we use
|
|
|
|
* it here, only when the drawer is open!
|
|
|
|
*/
|
|
|
|
function ItemSupportAppearanceFields({ item }) {
|
|
|
|
const outfitState = React.useContext(OutfitStateContext);
|
2020-08-01 01:35:27 -07:00
|
|
|
const { speciesId, colorId, pose } = outfitState;
|
|
|
|
const { error, visibleLayers } = useOutfitAppearance({
|
|
|
|
speciesId,
|
|
|
|
colorId,
|
|
|
|
pose,
|
|
|
|
wornItemIds: [item.id],
|
|
|
|
});
|
|
|
|
|
|
|
|
const biologyLayers = visibleLayers.filter((l) => l.source === "pet");
|
|
|
|
const itemLayers = visibleLayers.filter((l) => l.source === "item");
|
|
|
|
itemLayers.sort((a, b) => a.zone.depth - b.zone.depth);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<FormControl>
|
|
|
|
<FormLabel>Appearance layers</FormLabel>
|
2020-08-01 12:50:01 -07:00
|
|
|
<HStack spacing="4" overflow="auto" paddingX="1">
|
2020-08-01 01:35:27 -07:00
|
|
|
{itemLayers.map((itemLayer) => (
|
|
|
|
<ItemSupportAppearanceLayer
|
2020-08-01 15:30:26 -07:00
|
|
|
key={itemLayer.id}
|
2020-08-01 12:50:01 -07:00
|
|
|
item={item}
|
2020-08-01 14:12:57 -07:00
|
|
|
itemLayer={itemLayer}
|
|
|
|
biologyLayers={biologyLayers}
|
|
|
|
outfitState={outfitState}
|
2020-08-01 01:35:27 -07:00
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</HStack>
|
|
|
|
{error && <FormErrorMessage>{error.message}</FormErrorMessage>}
|
|
|
|
</FormControl>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-01 14:12:57 -07:00
|
|
|
function ItemSupportAppearanceLayer({
|
|
|
|
item,
|
|
|
|
itemLayer,
|
|
|
|
biologyLayers,
|
|
|
|
outfitState,
|
|
|
|
}) {
|
2020-08-01 12:50:01 -07:00
|
|
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
|
|
|
2020-08-01 01:35:27 -07:00
|
|
|
return (
|
2020-08-01 12:50:01 -07:00
|
|
|
<Box
|
|
|
|
as="button"
|
|
|
|
width="150px"
|
|
|
|
textAlign="center"
|
|
|
|
fontSize="xs"
|
|
|
|
onClick={onOpen}
|
|
|
|
>
|
2020-08-01 01:35:27 -07:00
|
|
|
<Box
|
|
|
|
width="150px"
|
|
|
|
height="150px"
|
|
|
|
marginBottom="1"
|
|
|
|
boxShadow="md"
|
|
|
|
borderRadius="md"
|
2020-08-04 23:00:31 -07:00
|
|
|
position="relative"
|
2020-08-01 01:35:27 -07:00
|
|
|
>
|
|
|
|
<OutfitLayers visibleLayers={[...biologyLayers, itemLayer]} />
|
2020-08-04 23:00:31 -07:00
|
|
|
<Box
|
|
|
|
className={css`
|
|
|
|
opacity: 0;
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
|
|
|
|
button:hover &,
|
|
|
|
button:focus & {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* On touch devices, always show the icon, to clarify that this is
|
|
|
|
* an interactable object! (Whereas I expect other devices to
|
|
|
|
* discover things by exploratory hover or focus!) */
|
|
|
|
@media (hover: none) {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
`}
|
|
|
|
background="green.100"
|
|
|
|
borderRadius="full"
|
|
|
|
boxShadow="sm"
|
|
|
|
position="absolute"
|
|
|
|
bottom="2"
|
|
|
|
right="2"
|
|
|
|
padding="2"
|
|
|
|
alignItems="center"
|
|
|
|
justifyContent="center"
|
|
|
|
width="32px"
|
|
|
|
height="32px"
|
|
|
|
>
|
|
|
|
<EditIcon
|
|
|
|
boxSize="16px"
|
|
|
|
position="relative"
|
|
|
|
top="-2px"
|
|
|
|
right="-1px"
|
|
|
|
/>
|
|
|
|
</Box>
|
2020-08-01 01:35:27 -07:00
|
|
|
</Box>
|
2020-08-01 12:50:01 -07:00
|
|
|
<Box fontWeight="bold">{itemLayer.zone.label}</Box>
|
2020-08-01 01:35:27 -07:00
|
|
|
<Box>Zone ID: {itemLayer.zone.id}</Box>
|
2020-08-01 15:30:26 -07:00
|
|
|
<Box>DTI ID: {itemLayer.id}</Box>
|
2020-08-01 22:41:03 -07:00
|
|
|
<ItemLayerSupportModal
|
2020-08-01 12:50:01 -07:00
|
|
|
item={item}
|
|
|
|
itemLayer={itemLayer}
|
2020-08-01 14:12:57 -07:00
|
|
|
outfitState={outfitState}
|
2020-08-01 12:50:01 -07:00
|
|
|
isOpen={isOpen}
|
|
|
|
onClose={onClose}
|
|
|
|
/>
|
2020-08-01 01:35:27 -07:00
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-31 16:57:37 -07:00
|
|
|
export default ItemSupportDrawer;
|