show incompatible items in the outfit list
This was a subtle little thing for a while! If you switch species/color, such that an item doesn't fit the pet anymore, we used to just hide it. Now, we show it in a list, so that you can understand what went wrong, and have the option to remove it.
This commit is contained in:
parent
b7c958c39b
commit
6d968cc385
3 changed files with 111 additions and 60 deletions
|
@ -52,7 +52,7 @@ function Item({
|
||||||
isWorn,
|
isWorn,
|
||||||
isInOutfit,
|
isInOutfit,
|
||||||
dispatchToOutfit,
|
dispatchToOutfit,
|
||||||
hideSimpleZones = false,
|
isDisabled = false,
|
||||||
}) {
|
}) {
|
||||||
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
|
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
@ -65,15 +65,16 @@ function Item({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ItemContainer>
|
<ItemContainer isDisabled={isDisabled}>
|
||||||
<Box flex="0 0 auto" marginRight="3">
|
<Box flex="0 0 auto" marginRight="3">
|
||||||
<ItemThumbnail
|
<ItemThumbnail
|
||||||
src={safeImageUrl(item.thumbnailUrl)}
|
src={safeImageUrl(item.thumbnailUrl)}
|
||||||
isWorn={isWorn}
|
isWorn={isWorn}
|
||||||
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flex="1 1 0" minWidth="0">
|
<Box flex="1 1 0" minWidth="0">
|
||||||
<ItemName id={itemNameId} isWorn={isWorn}>
|
<ItemName id={itemNameId} isWorn={isWorn} isDisabled={isDisabled}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</ItemName>
|
</ItemName>
|
||||||
<Wrap spacing="2" marginTop="1" opacity="0.7">
|
<Wrap spacing="2" marginTop="1" opacity="0.7">
|
||||||
|
@ -147,7 +148,7 @@ function Item({
|
||||||
*/
|
*/
|
||||||
function ItemSkeleton() {
|
function ItemSkeleton() {
|
||||||
return (
|
return (
|
||||||
<ItemContainer isFocusable={false}>
|
<ItemContainer isDisabled>
|
||||||
<Skeleton width="50px" height="50px" />
|
<Skeleton width="50px" height="50px" />
|
||||||
<Box width="3" />
|
<Box width="3" />
|
||||||
<Skeleton height="1.5rem" width="12rem" />
|
<Skeleton height="1.5rem" width="12rem" />
|
||||||
|
@ -162,7 +163,7 @@ function ItemSkeleton() {
|
||||||
* styles - including for its children, who sometimes reference it as an
|
* styles - including for its children, who sometimes reference it as an
|
||||||
* .item-container parent!
|
* .item-container parent!
|
||||||
*/
|
*/
|
||||||
function ItemContainer({ children, isFocusable = true }) {
|
function ItemContainer({ children, isDisabled = false }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const focusBackgroundColor = useColorModeValue(
|
const focusBackgroundColor = useColorModeValue(
|
||||||
|
@ -187,12 +188,12 @@ function ItemContainer({ children, isFocusable = true }) {
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
d="flex"
|
d="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
cursor={isFocusable ? "pointer" : undefined}
|
cursor={isDisabled ? undefined : "pointer"}
|
||||||
border="1px"
|
border="1px"
|
||||||
borderColor="transparent"
|
borderColor="transparent"
|
||||||
className={cx([
|
className={cx([
|
||||||
"item-container",
|
"item-container",
|
||||||
isFocusable &&
|
!isDisabled &&
|
||||||
css`
|
css`
|
||||||
&:hover,
|
&:hover,
|
||||||
input:focus + & {
|
input:focus + & {
|
||||||
|
@ -218,7 +219,7 @@ function ItemContainer({ children, isFocusable = true }) {
|
||||||
* ItemThumbnail shows a small preview image for the item, including some
|
* ItemThumbnail shows a small preview image for the item, including some
|
||||||
* hover/focus and worn/unworn states.
|
* hover/focus and worn/unworn states.
|
||||||
*/
|
*/
|
||||||
function ItemThumbnail({ src, isWorn }) {
|
function ItemThumbnail({ src, isWorn, isDisabled }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const colorMode = useColorMode();
|
const colorMode = useColorMode();
|
||||||
|
|
||||||
|
@ -243,16 +244,18 @@ function ItemThumbnail({ src, isWorn }) {
|
||||||
{
|
{
|
||||||
transform: "scale(0.8)",
|
transform: "scale(0.8)",
|
||||||
},
|
},
|
||||||
!isWorn && {
|
!isDisabled &&
|
||||||
[containerHasFocus]: {
|
!isWorn && {
|
||||||
opacity: "0.9",
|
[containerHasFocus]: {
|
||||||
transform: "scale(0.9)",
|
opacity: "0.9",
|
||||||
|
transform: "scale(0.9)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
!isDisabled &&
|
||||||
|
isWorn && {
|
||||||
|
opacity: 1,
|
||||||
|
transform: "none",
|
||||||
},
|
},
|
||||||
},
|
|
||||||
isWorn && {
|
|
||||||
opacity: 1,
|
|
||||||
transform: "none",
|
|
||||||
},
|
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
@ -266,11 +269,12 @@ function ItemThumbnail({ src, isWorn }) {
|
||||||
{
|
{
|
||||||
borderColor: `${borderColor} !important`,
|
borderColor: `${borderColor} !important`,
|
||||||
},
|
},
|
||||||
!isWorn && {
|
!isDisabled &&
|
||||||
[containerHasFocus]: {
|
!isWorn && {
|
||||||
borderColor: `${focusBorderColor} !important`,
|
[containerHasFocus]: {
|
||||||
|
borderColor: `${focusBorderColor} !important`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
<Image width="100%" height="100%" src={src} alt="" />
|
<Image width="100%" height="100%" src={src} alt="" />
|
||||||
|
@ -283,7 +287,7 @@ function ItemThumbnail({ src, isWorn }) {
|
||||||
* ItemName shows the item's name, including some hover/focus and worn/unworn
|
* ItemName shows the item's name, including some hover/focus and worn/unworn
|
||||||
* states.
|
* states.
|
||||||
*/
|
*/
|
||||||
function ItemName({ children, ...props }) {
|
function ItemName({ children, isDisabled, ...props }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -293,17 +297,20 @@ function ItemName({ children, ...props }) {
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
className={css`
|
className={
|
||||||
${containerHasFocus} {
|
!isDisabled &&
|
||||||
opacity: 0.9;
|
css`
|
||||||
font-weight: ${theme.fontWeights.medium};
|
${containerHasFocus} {
|
||||||
}
|
opacity: 0.9;
|
||||||
|
font-weight: ${theme.fontWeights.medium};
|
||||||
|
}
|
||||||
|
|
||||||
input:checked + .item-container & {
|
input:checked + .item-container & {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
font-weight: ${theme.fontWeights.bold};
|
font-weight: ${theme.fontWeights.bold};
|
||||||
}
|
}
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -8,9 +8,10 @@ import {
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
IconButton,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
Tooltip,
|
||||||
VisuallyHidden,
|
VisuallyHidden,
|
||||||
} from "@chakra-ui/core";
|
} from "@chakra-ui/core";
|
||||||
import { EditIcon } from "@chakra-ui/icons";
|
import { EditIcon, QuestionIcon } from "@chakra-ui/icons";
|
||||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||||
|
|
||||||
import { Delay, Heading1, Heading2 } from "../util";
|
import { Delay, Heading1, Heading2 } from "../util";
|
||||||
|
@ -30,7 +31,7 @@ import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
|
||||||
* full width of the container, it doesn't look like it!
|
* full width of the container, it doesn't look like it!
|
||||||
*/
|
*/
|
||||||
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
||||||
const { zonesAndItems } = outfitState;
|
const { zonesAndItems, incompatibleItems } = outfitState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
@ -55,6 +56,24 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
||||||
/>
|
/>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
))}
|
))}
|
||||||
|
{incompatibleItems.length > 0 && (
|
||||||
|
<ItemZoneGroup
|
||||||
|
zoneLabel="Incompatible"
|
||||||
|
afterHeader={
|
||||||
|
<Tooltip
|
||||||
|
label="These items don't fit this pet"
|
||||||
|
placement="top"
|
||||||
|
openDelay={100}
|
||||||
|
>
|
||||||
|
<QuestionIcon fontSize="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
items={incompatibleItems}
|
||||||
|
outfitState={outfitState}
|
||||||
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
|
isDisabled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -70,7 +89,14 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
||||||
* the Item component (which will visually reflect the radio's state). This
|
* the Item component (which will visually reflect the radio's state). This
|
||||||
* makes the list screen-reader- and keyboard-accessible!
|
* makes the list screen-reader- and keyboard-accessible!
|
||||||
*/
|
*/
|
||||||
function ItemZoneGroup({ zoneLabel, items, outfitState, dispatchToOutfit }) {
|
function ItemZoneGroup({
|
||||||
|
zoneLabel,
|
||||||
|
items,
|
||||||
|
outfitState,
|
||||||
|
dispatchToOutfit,
|
||||||
|
isDisabled = false,
|
||||||
|
afterHeader = null,
|
||||||
|
}) {
|
||||||
// onChange is fired when the radio button becomes checked, not unchecked!
|
// onChange is fired when the radio button becomes checked, not unchecked!
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
const itemId = e.target.value;
|
const itemId = e.target.value;
|
||||||
|
@ -93,38 +119,52 @@ function ItemZoneGroup({ zoneLabel, items, outfitState, dispatchToOutfit }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mb="10">
|
<Box mb="10">
|
||||||
<Heading2 mx="1">{zoneLabel}</Heading2>
|
<Heading2 display="flex" alignItems="center" mx="1">
|
||||||
|
{zoneLabel}
|
||||||
|
{afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
|
||||||
|
</Heading2>
|
||||||
<ItemListContainer>
|
<ItemListContainer>
|
||||||
<TransitionGroup component={null}>
|
<TransitionGroup component={null}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const itemNameId =
|
const itemNameId =
|
||||||
zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`;
|
zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`;
|
||||||
|
const itemNode = (
|
||||||
|
<Item
|
||||||
|
item={item}
|
||||||
|
itemNameId={itemNameId}
|
||||||
|
isWorn={
|
||||||
|
!isDisabled && outfitState.wornItemIds.includes(item.id)
|
||||||
|
}
|
||||||
|
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
||||||
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CSSTransition key={item.id} {...fadeOutAndRollUpTransition}>
|
<CSSTransition key={item.id} {...fadeOutAndRollUpTransition}>
|
||||||
<label>
|
{isDisabled ? (
|
||||||
<VisuallyHidden
|
itemNode
|
||||||
as="input"
|
) : (
|
||||||
type="radio"
|
<label>
|
||||||
aria-labelledby={itemNameId}
|
<VisuallyHidden
|
||||||
name={zoneLabel}
|
as="input"
|
||||||
value={item.id}
|
type="radio"
|
||||||
checked={outfitState.wornItemIds.includes(item.id)}
|
aria-labelledby={itemNameId}
|
||||||
onChange={onChange}
|
name={zoneLabel}
|
||||||
onClick={onClick}
|
value={item.id}
|
||||||
onKeyUp={(e) => {
|
checked={outfitState.wornItemIds.includes(item.id)}
|
||||||
if (e.key === " ") {
|
onChange={onChange}
|
||||||
onClick(e);
|
onClick={onClick}
|
||||||
}
|
onKeyUp={(e) => {
|
||||||
}}
|
if (e.key === " ") {
|
||||||
/>
|
onClick(e);
|
||||||
<Item
|
}
|
||||||
item={item}
|
}}
|
||||||
itemNameId={itemNameId}
|
/>
|
||||||
isWorn={outfitState.wornItemIds.includes(item.id)}
|
{itemNode}
|
||||||
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
</label>
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
)}
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -81,11 +81,15 @@ function useOutfitState() {
|
||||||
wornItemIds,
|
wornItemIds,
|
||||||
closetedItemIds
|
closetedItemIds
|
||||||
);
|
);
|
||||||
|
const incompatibleItems = items
|
||||||
|
.filter((i) => i.appearanceOn.layers.length === 0)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
const url = buildOutfitUrl(state);
|
const url = buildOutfitUrl(state);
|
||||||
|
|
||||||
const outfitState = {
|
const outfitState = {
|
||||||
zonesAndItems,
|
zonesAndItems,
|
||||||
|
incompatibleItems,
|
||||||
name,
|
name,
|
||||||
wornItemIds,
|
wornItemIds,
|
||||||
closetedItemIds,
|
closetedItemIds,
|
||||||
|
|
Loading…
Reference in a new issue