add item removal, with smooth transitions!
This commit is contained in:
parent
d39c781f3f
commit
5264509b53
7 changed files with 224 additions and 109 deletions
12
src/ItemList.css
Normal file
12
src/ItemList.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
.item-list-row-exit {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.item-list-row-exit-active {
|
||||
opacity: 0;
|
||||
height: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
transition: all 0.5s;
|
||||
}
|
|
@ -1,19 +1,42 @@
|
|||
import React from "react";
|
||||
import { Box, Image, PseudoBox, Stack, Skeleton } from "@chakra-ui/core";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
IconButton,
|
||||
Image,
|
||||
PseudoBox,
|
||||
Stack,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/core";
|
||||
|
||||
function ItemList({ items, wornItemIds, dispatchToOutfit }) {
|
||||
import "./ItemList.css";
|
||||
|
||||
function ItemList({ items, outfitState, dispatchToOutfit }) {
|
||||
return (
|
||||
<Stack spacing="3">
|
||||
{items.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Item
|
||||
item={item}
|
||||
isWorn={wornItemIds.includes(item.id)}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
<Flex direction="column">
|
||||
<TransitionGroup component={null}>
|
||||
{items.map((item) => (
|
||||
<CSSTransition
|
||||
key={item.id}
|
||||
classNames="item-list-row"
|
||||
timeout={500}
|
||||
onExit={(e) => {
|
||||
e.style.height = e.offsetHeight + "px";
|
||||
}}
|
||||
>
|
||||
<PseudoBox mb="2" mt="2">
|
||||
<Item
|
||||
item={item}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</PseudoBox>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -29,7 +52,12 @@ function ItemListSkeleton({ count }) {
|
|||
);
|
||||
}
|
||||
|
||||
function Item({ item, isWorn, dispatchToOutfit }) {
|
||||
function Item({ item, outfitState, dispatchToOutfit }) {
|
||||
const { wornItemIds, allItemIds } = outfitState;
|
||||
|
||||
const isWorn = wornItemIds.includes(item.id);
|
||||
const isInOutfit = allItemIds.includes(item.id);
|
||||
|
||||
return (
|
||||
<PseudoBox
|
||||
role="group"
|
||||
|
@ -46,6 +74,38 @@ function Item({ item, isWorn, dispatchToOutfit }) {
|
|||
<ItemThumbnail src={item.thumbnailUrl} isWorn={isWorn} />
|
||||
<Box width="3" />
|
||||
<ItemName isWorn={isWorn}>{item.name}</ItemName>
|
||||
<Box flexGrow="1" />
|
||||
{isInOutfit && (
|
||||
<Tooltip label="Remove" placement="top">
|
||||
<IconButton
|
||||
icon="delete"
|
||||
aria-label="Remove from outfit"
|
||||
variant="ghost"
|
||||
color="gray.400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dispatchToOutfit({ type: "removeItem", itemId: item.id });
|
||||
}}
|
||||
opacity="0"
|
||||
transitionProperty="opacity color"
|
||||
transitionDuration="0.2s"
|
||||
_groupHover={{
|
||||
opacity: 1,
|
||||
transitionDuration: "0.5s",
|
||||
}}
|
||||
_hover={{
|
||||
opacity: 1,
|
||||
color: "gray.800",
|
||||
backgroundColor: "gray.200",
|
||||
}}
|
||||
_focus={{
|
||||
opacity: 1,
|
||||
color: "gray.800",
|
||||
backgroundColor: "gray.200",
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</PseudoBox>
|
||||
);
|
||||
}
|
||||
|
|
12
src/ItemsPanel.css
Normal file
12
src/ItemsPanel.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
.items-panel-zone-exit {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.items-panel-zone-exit-active {
|
||||
opacity: 0;
|
||||
height: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
transition: all 0.5s;
|
||||
}
|
115
src/ItemsPanel.js
Normal file
115
src/ItemsPanel.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Editable,
|
||||
EditablePreview,
|
||||
EditableInput,
|
||||
Flex,
|
||||
IconButton,
|
||||
PseudoBox,
|
||||
Skeleton,
|
||||
} from "@chakra-ui/core";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Delay, Heading1, Heading2 } from "./util";
|
||||
import ItemList, { ItemListSkeleton } from "./ItemList";
|
||||
|
||||
import "./ItemsPanel.css";
|
||||
|
||||
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
||||
const { zonesAndItems, wornItemIds } = outfitState;
|
||||
|
||||
return (
|
||||
<Box color="green.800">
|
||||
<OutfitHeading
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
<Flex direction="column">
|
||||
{loading &&
|
||||
[1, 2, 3].map((i) => (
|
||||
<Box key={i}>
|
||||
<Delay>
|
||||
<Skeleton height="2.3rem" width="12rem" />
|
||||
<ItemListSkeleton count={3} />
|
||||
</Delay>
|
||||
</Box>
|
||||
))}
|
||||
{!loading && (
|
||||
<TransitionGroup component={null}>
|
||||
{zonesAndItems.map(({ zone, items }) => (
|
||||
<CSSTransition
|
||||
key={zone.id}
|
||||
classNames="items-panel-zone"
|
||||
timeout={500}
|
||||
onExit={(e) => {
|
||||
e.style.height = e.offsetHeight + "px";
|
||||
}}
|
||||
>
|
||||
<Box mb="10">
|
||||
<Heading2>{zone.label}</Heading2>
|
||||
<ItemList
|
||||
items={items}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function OutfitHeading({ outfitState, dispatchToOutfit }) {
|
||||
return (
|
||||
<Box>
|
||||
<PseudoBox role="group" d="inline-block" position="relative" width="100%">
|
||||
<Heading1 mb="6">
|
||||
<Editable
|
||||
value={outfitState.name}
|
||||
placeholder="Untitled outfit (click to edit)"
|
||||
onChange={(value) =>
|
||||
dispatchToOutfit({ type: "rename", outfitName: value })
|
||||
}
|
||||
>
|
||||
{({ isEditing, onRequestEdit }) => (
|
||||
<>
|
||||
<EditablePreview />
|
||||
<EditableInput />
|
||||
{!isEditing && (
|
||||
<OutfitNameEditButton onRequestEdit={onRequestEdit} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Editable>
|
||||
</Heading1>
|
||||
</PseudoBox>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function OutfitNameEditButton({ onRequestEdit }) {
|
||||
return (
|
||||
<PseudoBox
|
||||
d="inline-block"
|
||||
opacity="0"
|
||||
transition="opacity 0.5s"
|
||||
_groupHover={{ opacity: "1" }}
|
||||
onClick={onRequestEdit}
|
||||
position="absolute"
|
||||
>
|
||||
<IconButton
|
||||
icon="edit"
|
||||
variant="link"
|
||||
color="green.600"
|
||||
aria-label="Edit outfit name"
|
||||
title="Edit outfit name"
|
||||
/>
|
||||
</PseudoBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default ItemsPanel;
|
|
@ -94,7 +94,7 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
|
|||
return (
|
||||
<ItemList
|
||||
items={items}
|
||||
wornItemIds={wornItemIds}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Editable,
|
||||
EditablePreview,
|
||||
EditableInput,
|
||||
Grid,
|
||||
Icon,
|
||||
IconButton,
|
||||
|
@ -11,14 +8,10 @@ import {
|
|||
InputGroup,
|
||||
InputLeftElement,
|
||||
InputRightElement,
|
||||
PseudoBox,
|
||||
Skeleton,
|
||||
Stack,
|
||||
useToast,
|
||||
} from "@chakra-ui/core";
|
||||
|
||||
import { Delay, Heading1, Heading2 } from "./util";
|
||||
import ItemList, { ItemListSkeleton } from "./ItemList";
|
||||
import ItemsPanel from "./ItemsPanel";
|
||||
import OutfitPreview from "./OutfitPreview";
|
||||
import SearchPanel from "./SearchPanel";
|
||||
import useOutfitState from "./useOutfitState.js";
|
||||
|
@ -129,90 +122,4 @@ function SearchToolbar({ query, onChange }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
||||
const { zonesAndItems, wornItemIds } = outfitState;
|
||||
|
||||
return (
|
||||
<Box color="green.800">
|
||||
<OutfitHeading
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
<Stack spacing="10">
|
||||
{loading &&
|
||||
[1, 2, 3].map((i) => (
|
||||
<Box key={i}>
|
||||
<Delay>
|
||||
<Skeleton height="2.3rem" width="12rem" mb="3" />
|
||||
<ItemListSkeleton count={3} />
|
||||
</Delay>
|
||||
</Box>
|
||||
))}
|
||||
{!loading &&
|
||||
zonesAndItems.map(({ zone, items }) => (
|
||||
<Box key={zone.id}>
|
||||
<Heading2 mb="3">{zone.label}</Heading2>
|
||||
<ItemList
|
||||
items={items}
|
||||
wornItemIds={items
|
||||
.map((i) => i.id)
|
||||
.filter((id) => wornItemIds.includes(id))}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function OutfitHeading({ outfitState, dispatchToOutfit }) {
|
||||
return (
|
||||
<Box>
|
||||
<PseudoBox role="group" d="inline-block" position="relative" width="100%">
|
||||
<Heading1 mb="6">
|
||||
<Editable
|
||||
value={outfitState.name}
|
||||
placeholder="Untitled outfit (click to edit)"
|
||||
onChange={(value) =>
|
||||
dispatchToOutfit({ type: "rename", outfitName: value })
|
||||
}
|
||||
>
|
||||
{({ isEditing, onRequestEdit }) => (
|
||||
<>
|
||||
<EditablePreview />
|
||||
<EditableInput />
|
||||
{!isEditing && (
|
||||
<OutfitNameEditButton onRequestEdit={onRequestEdit} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Editable>
|
||||
</Heading1>
|
||||
</PseudoBox>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function OutfitNameEditButton({ onRequestEdit }) {
|
||||
return (
|
||||
<PseudoBox
|
||||
d="inline-block"
|
||||
opacity="0"
|
||||
transition="opacity 0.5s"
|
||||
_groupHover={{ opacity: "1" }}
|
||||
onClick={onRequestEdit}
|
||||
position="absolute"
|
||||
>
|
||||
<IconButton
|
||||
icon="edit"
|
||||
variant="link"
|
||||
color="green.600"
|
||||
aria-label="Edit outfit name"
|
||||
title="Edit outfit name"
|
||||
/>
|
||||
</PseudoBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default WardrobePage;
|
||||
|
|
|
@ -133,6 +133,15 @@ const outfitStateReducer = (apolloClient) => (baseState, action) => {
|
|||
wornItemIds.delete(itemId);
|
||||
closetedItemIds.add(itemId);
|
||||
});
|
||||
case "removeItem":
|
||||
return produce(baseState, (state) => {
|
||||
const { wornItemIds, closetedItemIds } = state;
|
||||
const { itemId } = action;
|
||||
|
||||
// Remove this item from both the worn set and the closet.
|
||||
wornItemIds.delete(itemId);
|
||||
closetedItemIds.delete(itemId);
|
||||
});
|
||||
default:
|
||||
throw new Error(`unexpected action ${action}`);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue