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,
|
||||
isInOutfit,
|
||||
dispatchToOutfit,
|
||||
hideSimpleZones = false,
|
||||
isDisabled = false,
|
||||
}) {
|
||||
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
|
||||
|
||||
|
@ -65,15 +65,16 @@ function Item({
|
|||
|
||||
return (
|
||||
<>
|
||||
<ItemContainer>
|
||||
<ItemContainer isDisabled={isDisabled}>
|
||||
<Box flex="0 0 auto" marginRight="3">
|
||||
<ItemThumbnail
|
||||
src={safeImageUrl(item.thumbnailUrl)}
|
||||
isWorn={isWorn}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Box>
|
||||
<Box flex="1 1 0" minWidth="0">
|
||||
<ItemName id={itemNameId} isWorn={isWorn}>
|
||||
<ItemName id={itemNameId} isWorn={isWorn} isDisabled={isDisabled}>
|
||||
{item.name}
|
||||
</ItemName>
|
||||
<Wrap spacing="2" marginTop="1" opacity="0.7">
|
||||
|
@ -147,7 +148,7 @@ function Item({
|
|||
*/
|
||||
function ItemSkeleton() {
|
||||
return (
|
||||
<ItemContainer isFocusable={false}>
|
||||
<ItemContainer isDisabled>
|
||||
<Skeleton width="50px" height="50px" />
|
||||
<Box width="3" />
|
||||
<Skeleton height="1.5rem" width="12rem" />
|
||||
|
@ -162,7 +163,7 @@ function ItemSkeleton() {
|
|||
* styles - including for its children, who sometimes reference it as an
|
||||
* .item-container parent!
|
||||
*/
|
||||
function ItemContainer({ children, isFocusable = true }) {
|
||||
function ItemContainer({ children, isDisabled = false }) {
|
||||
const theme = useTheme();
|
||||
|
||||
const focusBackgroundColor = useColorModeValue(
|
||||
|
@ -187,12 +188,12 @@ function ItemContainer({ children, isFocusable = true }) {
|
|||
borderRadius="lg"
|
||||
d="flex"
|
||||
alignItems="center"
|
||||
cursor={isFocusable ? "pointer" : undefined}
|
||||
cursor={isDisabled ? undefined : "pointer"}
|
||||
border="1px"
|
||||
borderColor="transparent"
|
||||
className={cx([
|
||||
"item-container",
|
||||
isFocusable &&
|
||||
!isDisabled &&
|
||||
css`
|
||||
&:hover,
|
||||
input:focus + & {
|
||||
|
@ -218,7 +219,7 @@ function ItemContainer({ children, isFocusable = true }) {
|
|||
* ItemThumbnail shows a small preview image for the item, including some
|
||||
* hover/focus and worn/unworn states.
|
||||
*/
|
||||
function ItemThumbnail({ src, isWorn }) {
|
||||
function ItemThumbnail({ src, isWorn, isDisabled }) {
|
||||
const theme = useTheme();
|
||||
const colorMode = useColorMode();
|
||||
|
||||
|
@ -243,16 +244,18 @@ function ItemThumbnail({ src, isWorn }) {
|
|||
{
|
||||
transform: "scale(0.8)",
|
||||
},
|
||||
!isWorn && {
|
||||
[containerHasFocus]: {
|
||||
opacity: "0.9",
|
||||
transform: "scale(0.9)",
|
||||
!isDisabled &&
|
||||
!isWorn && {
|
||||
[containerHasFocus]: {
|
||||
opacity: "0.9",
|
||||
transform: "scale(0.9)",
|
||||
},
|
||||
},
|
||||
!isDisabled &&
|
||||
isWorn && {
|
||||
opacity: 1,
|
||||
transform: "none",
|
||||
},
|
||||
},
|
||||
isWorn && {
|
||||
opacity: 1,
|
||||
transform: "none",
|
||||
},
|
||||
])}
|
||||
>
|
||||
<Box
|
||||
|
@ -266,11 +269,12 @@ function ItemThumbnail({ src, isWorn }) {
|
|||
{
|
||||
borderColor: `${borderColor} !important`,
|
||||
},
|
||||
!isWorn && {
|
||||
[containerHasFocus]: {
|
||||
borderColor: `${focusBorderColor} !important`,
|
||||
!isDisabled &&
|
||||
!isWorn && {
|
||||
[containerHasFocus]: {
|
||||
borderColor: `${focusBorderColor} !important`,
|
||||
},
|
||||
},
|
||||
},
|
||||
])}
|
||||
>
|
||||
<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
|
||||
* states.
|
||||
*/
|
||||
function ItemName({ children, ...props }) {
|
||||
function ItemName({ children, isDisabled, ...props }) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
|
@ -293,17 +297,20 @@ function ItemName({ children, ...props }) {
|
|||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
className={css`
|
||||
${containerHasFocus} {
|
||||
opacity: 0.9;
|
||||
font-weight: ${theme.fontWeights.medium};
|
||||
}
|
||||
className={
|
||||
!isDisabled &&
|
||||
css`
|
||||
${containerHasFocus} {
|
||||
opacity: 0.9;
|
||||
font-weight: ${theme.fontWeights.medium};
|
||||
}
|
||||
|
||||
input:checked + .item-container & {
|
||||
opacity: 1;
|
||||
font-weight: ${theme.fontWeights.bold};
|
||||
}
|
||||
`}
|
||||
input:checked + .item-container & {
|
||||
opacity: 1;
|
||||
font-weight: ${theme.fontWeights.bold};
|
||||
}
|
||||
`
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -8,9 +8,10 @@ import {
|
|||
Flex,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
VisuallyHidden,
|
||||
} 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 { 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!
|
||||
*/
|
||||
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
||||
const { zonesAndItems } = outfitState;
|
||||
const { zonesAndItems, incompatibleItems } = outfitState;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
@ -55,6 +56,24 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
|||
/>
|
||||
</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>
|
||||
)}
|
||||
</Flex>
|
||||
|
@ -70,7 +89,14 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
|||
* the Item component (which will visually reflect the radio's state). This
|
||||
* 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!
|
||||
const onChange = (e) => {
|
||||
const itemId = e.target.value;
|
||||
|
@ -93,38 +119,52 @@ function ItemZoneGroup({ zoneLabel, items, outfitState, dispatchToOutfit }) {
|
|||
|
||||
return (
|
||||
<Box mb="10">
|
||||
<Heading2 mx="1">{zoneLabel}</Heading2>
|
||||
<Heading2 display="flex" alignItems="center" mx="1">
|
||||
{zoneLabel}
|
||||
{afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
|
||||
</Heading2>
|
||||
<ItemListContainer>
|
||||
<TransitionGroup component={null}>
|
||||
{items.map((item) => {
|
||||
const itemNameId =
|
||||
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 (
|
||||
<CSSTransition key={item.id} {...fadeOutAndRollUpTransition}>
|
||||
<label>
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="radio"
|
||||
aria-labelledby={itemNameId}
|
||||
name={zoneLabel}
|
||||
value={item.id}
|
||||
checked={outfitState.wornItemIds.includes(item.id)}
|
||||
onChange={onChange}
|
||||
onClick={onClick}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === " ") {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Item
|
||||
item={item}
|
||||
itemNameId={itemNameId}
|
||||
isWorn={outfitState.wornItemIds.includes(item.id)}
|
||||
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</label>
|
||||
{isDisabled ? (
|
||||
itemNode
|
||||
) : (
|
||||
<label>
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="radio"
|
||||
aria-labelledby={itemNameId}
|
||||
name={zoneLabel}
|
||||
value={item.id}
|
||||
checked={outfitState.wornItemIds.includes(item.id)}
|
||||
onChange={onChange}
|
||||
onClick={onClick}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === " ") {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{itemNode}
|
||||
</label>
|
||||
)}
|
||||
</CSSTransition>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -81,11 +81,15 @@ function useOutfitState() {
|
|||
wornItemIds,
|
||||
closetedItemIds
|
||||
);
|
||||
const incompatibleItems = items
|
||||
.filter((i) => i.appearanceOn.layers.length === 0)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const url = buildOutfitUrl(state);
|
||||
|
||||
const outfitState = {
|
||||
zonesAndItems,
|
||||
incompatibleItems,
|
||||
name,
|
||||
wornItemIds,
|
||||
closetedItemIds,
|
||||
|
|
Loading…
Reference in a new issue