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 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 (
|
return (
|
||||||
<Stack spacing="3">
|
<Flex direction="column">
|
||||||
|
<TransitionGroup component={null}>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Box key={item.id}>
|
<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={item}
|
item={item}
|
||||||
isWorn={wornItemIds.includes(item.id)}
|
outfitState={outfitState}
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</PseudoBox>
|
||||||
|
</CSSTransition>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</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 (
|
return (
|
||||||
<PseudoBox
|
<PseudoBox
|
||||||
role="group"
|
role="group"
|
||||||
|
@ -46,6 +74,38 @@ function Item({ item, isWorn, dispatchToOutfit }) {
|
||||||
<ItemThumbnail src={item.thumbnailUrl} isWorn={isWorn} />
|
<ItemThumbnail src={item.thumbnailUrl} isWorn={isWorn} />
|
||||||
<Box width="3" />
|
<Box width="3" />
|
||||||
<ItemName isWorn={isWorn}>{item.name}</ItemName>
|
<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>
|
</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 (
|
return (
|
||||||
<ItemList
|
<ItemList
|
||||||
items={items}
|
items={items}
|
||||||
wornItemIds={wornItemIds}
|
outfitState={outfitState}
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Editable,
|
|
||||||
EditablePreview,
|
|
||||||
EditableInput,
|
|
||||||
Grid,
|
Grid,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
@ -11,14 +8,10 @@ import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputLeftElement,
|
InputLeftElement,
|
||||||
InputRightElement,
|
InputRightElement,
|
||||||
PseudoBox,
|
|
||||||
Skeleton,
|
|
||||||
Stack,
|
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/core";
|
} from "@chakra-ui/core";
|
||||||
|
|
||||||
import { Delay, Heading1, Heading2 } from "./util";
|
import ItemsPanel from "./ItemsPanel";
|
||||||
import ItemList, { ItemListSkeleton } from "./ItemList";
|
|
||||||
import OutfitPreview from "./OutfitPreview";
|
import OutfitPreview from "./OutfitPreview";
|
||||||
import SearchPanel from "./SearchPanel";
|
import SearchPanel from "./SearchPanel";
|
||||||
import useOutfitState from "./useOutfitState.js";
|
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;
|
export default WardrobePage;
|
||||||
|
|
|
@ -133,6 +133,15 @@ const outfitStateReducer = (apolloClient) => (baseState, action) => {
|
||||||
wornItemIds.delete(itemId);
|
wornItemIds.delete(itemId);
|
||||||
closetedItemIds.add(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:
|
default:
|
||||||
throw new Error(`unexpected action ${action}`);
|
throw new Error(`unexpected action ${action}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue