2020-04-25 00:22:49 -07:00
|
|
|
import React from "react";
|
2021-01-03 19:11:46 -08:00
|
|
|
import { ClassNames } from "@emotion/react";
|
2020-04-25 00:22:49 -07:00
|
|
|
import {
|
|
|
|
Box,
|
|
|
|
Editable,
|
|
|
|
EditablePreview,
|
|
|
|
EditableInput,
|
|
|
|
Flex,
|
|
|
|
IconButton,
|
|
|
|
Skeleton,
|
2020-09-01 17:17:45 -07:00
|
|
|
Tooltip,
|
2020-04-25 15:25:51 -07:00
|
|
|
VisuallyHidden,
|
2021-03-29 19:25:00 -07:00
|
|
|
Menu,
|
|
|
|
MenuButton,
|
|
|
|
MenuList,
|
|
|
|
MenuItem,
|
|
|
|
Portal,
|
2021-03-29 19:46:21 -07:00
|
|
|
Button,
|
2021-04-19 03:56:51 -07:00
|
|
|
useToast,
|
2021-04-22 02:35:59 -07:00
|
|
|
Popover,
|
|
|
|
PopoverTrigger,
|
|
|
|
PopoverContent,
|
|
|
|
PopoverArrow,
|
|
|
|
PopoverBody,
|
2020-12-25 09:08:33 -08:00
|
|
|
} from "@chakra-ui/react";
|
2021-04-22 02:35:59 -07:00
|
|
|
import {
|
|
|
|
CheckIcon,
|
|
|
|
EditIcon,
|
|
|
|
ExternalLinkIcon,
|
|
|
|
QuestionIcon,
|
|
|
|
WarningIcon,
|
|
|
|
} from "@chakra-ui/icons";
|
2020-04-25 00:22:49 -07:00
|
|
|
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
2021-04-19 03:56:51 -07:00
|
|
|
import { useHistory } from "react-router-dom";
|
2020-04-25 00:22:49 -07:00
|
|
|
|
2020-07-20 21:41:26 -07:00
|
|
|
import { Delay, Heading1, Heading2 } from "../util";
|
2020-08-05 00:25:25 -07:00
|
|
|
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
|
2021-03-29 19:46:21 -07:00
|
|
|
import { BiRename } from "react-icons/bi";
|
|
|
|
import { IoCloudUploadOutline } from "react-icons/io5";
|
2021-03-29 19:25:00 -07:00
|
|
|
import { MdMoreVert } from "react-icons/md";
|
2021-03-29 19:50:34 -07:00
|
|
|
import useCurrentUser from "../components/useCurrentUser";
|
2021-04-19 03:56:51 -07:00
|
|
|
import gql from "graphql-tag";
|
2021-04-21 21:27:10 -07:00
|
|
|
import { useMutation } from "@apollo/client";
|
2021-04-22 02:35:59 -07:00
|
|
|
import { outfitStatesAreEqual } from "./useOutfitState";
|
2020-04-25 00:22:49 -07:00
|
|
|
|
2020-04-26 01:14:31 -07:00
|
|
|
/**
|
|
|
|
* ItemsPanel shows the items in the current outfit, and lets the user toggle
|
|
|
|
* between them! It also shows an editable outfit name, to help set context.
|
|
|
|
*
|
|
|
|
* Note that this component provides an effective 1 unit of padding around
|
|
|
|
* itself, which is uncommon in this app: we usually prefer to let parents
|
|
|
|
* control the spacing!
|
|
|
|
*
|
|
|
|
* This is because Item has padding, but it's generally not visible; so, to
|
|
|
|
* *look* aligned with the other elements like the headings, the headings need
|
|
|
|
* to have extra padding. Essentially: while the Items _do_ stretch out the
|
|
|
|
* full width of the container, it doesn't look like it!
|
|
|
|
*/
|
2020-04-25 00:22:49 -07:00
|
|
|
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
2020-09-01 17:17:45 -07:00
|
|
|
const { zonesAndItems, incompatibleItems } = outfitState;
|
2020-04-25 00:22:49 -07:00
|
|
|
|
|
|
|
return (
|
2021-01-03 19:11:46 -08:00
|
|
|
<ClassNames>
|
|
|
|
{({ css }) => (
|
|
|
|
<Box>
|
|
|
|
<Box px="1">
|
|
|
|
<OutfitHeading
|
|
|
|
outfitState={outfitState}
|
|
|
|
dispatchToOutfit={dispatchToOutfit}
|
|
|
|
/>
|
|
|
|
</Box>
|
|
|
|
<Flex direction="column">
|
|
|
|
{loading ? (
|
|
|
|
<ItemZoneGroupsSkeleton
|
|
|
|
itemCount={outfitState.allItemIds.length}
|
2020-09-01 17:17:45 -07:00
|
|
|
/>
|
2021-01-03 19:11:46 -08:00
|
|
|
) : (
|
|
|
|
<TransitionGroup component={null}>
|
|
|
|
{zonesAndItems.map(({ zoneLabel, items }) => (
|
|
|
|
<CSSTransition
|
|
|
|
key={zoneLabel}
|
|
|
|
{...fadeOutAndRollUpTransition(css)}
|
|
|
|
>
|
|
|
|
<ItemZoneGroup
|
|
|
|
zoneLabel={zoneLabel}
|
|
|
|
items={items}
|
|
|
|
outfitState={outfitState}
|
|
|
|
dispatchToOutfit={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>
|
2020-09-01 17:17:45 -07:00
|
|
|
)}
|
2021-01-03 19:11:46 -08:00
|
|
|
</Flex>
|
|
|
|
</Box>
|
|
|
|
)}
|
|
|
|
</ClassNames>
|
2020-04-25 00:22:49 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-26 01:14:31 -07:00
|
|
|
/**
|
|
|
|
* ItemZoneGroup shows the items for a particular zone, and lets the user
|
|
|
|
* toggle between them.
|
|
|
|
*
|
|
|
|
* For each item, it renders a <label> with a visually-hidden radio button and
|
|
|
|
* the Item component (which will visually reflect the radio's state). This
|
|
|
|
* makes the list screen-reader- and keyboard-accessible!
|
|
|
|
*/
|
2020-09-01 17:17:45 -07:00
|
|
|
function ItemZoneGroup({
|
|
|
|
zoneLabel,
|
|
|
|
items,
|
|
|
|
outfitState,
|
|
|
|
dispatchToOutfit,
|
|
|
|
isDisabled = false,
|
|
|
|
afterHeader = null,
|
|
|
|
}) {
|
2020-04-26 01:14:31 -07:00
|
|
|
// onChange is fired when the radio button becomes checked, not unchecked!
|
2020-04-25 15:25:51 -07:00
|
|
|
const onChange = (e) => {
|
|
|
|
const itemId = e.target.value;
|
|
|
|
dispatchToOutfit({ type: "wearItem", itemId });
|
|
|
|
};
|
|
|
|
|
2020-04-26 01:14:31 -07:00
|
|
|
// Clicking the radio button when already selected deselects it - this is how
|
|
|
|
// you can select none!
|
|
|
|
const onClick = (e) => {
|
2020-04-25 15:25:51 -07:00
|
|
|
const itemId = e.target.value;
|
|
|
|
if (outfitState.wornItemIds.includes(itemId)) {
|
|
|
|
// We need the event handler to finish before this, so that simulated
|
|
|
|
// events don't just come back around and undo it - but we can't just
|
|
|
|
// solve that with `preventDefault`, because it breaks the radio's
|
|
|
|
// intended visual updates when we unwear. So, we `setTimeout` to do it
|
|
|
|
// after all event handlers resolve!
|
|
|
|
setTimeout(() => dispatchToOutfit({ type: "unwearItem", itemId }), 0);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-09-24 07:07:05 -07:00
|
|
|
const onRemove = React.useCallback(
|
|
|
|
(itemId) => {
|
|
|
|
dispatchToOutfit({ type: "removeItem", itemId });
|
|
|
|
},
|
|
|
|
[dispatchToOutfit]
|
|
|
|
);
|
|
|
|
|
2020-04-25 15:25:51 -07:00
|
|
|
return (
|
2021-01-03 19:11:46 -08:00
|
|
|
<ClassNames>
|
|
|
|
{({ css }) => (
|
|
|
|
<Box mb="10">
|
|
|
|
<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)}
|
|
|
|
onRemove={onRemove}
|
|
|
|
isDisabled={isDisabled}
|
|
|
|
/>
|
|
|
|
);
|
2020-09-01 17:17:45 -07:00
|
|
|
|
2021-01-03 19:11:46 -08:00
|
|
|
return (
|
|
|
|
<CSSTransition
|
|
|
|
key={item.id}
|
|
|
|
{...fadeOutAndRollUpTransition(css)}
|
|
|
|
>
|
|
|
|
{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>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</TransitionGroup>
|
|
|
|
</ItemListContainer>
|
|
|
|
</Box>
|
|
|
|
)}
|
|
|
|
</ClassNames>
|
2020-04-26 01:14:31 -07:00
|
|
|
);
|
|
|
|
}
|
2020-04-25 23:17:59 -07:00
|
|
|
|
2020-05-19 14:43:21 -07:00
|
|
|
/**
|
|
|
|
* ItemZoneGroupSkeletons is a placeholder for when the items are loading.
|
|
|
|
*
|
|
|
|
* We try to match the approximate size of the incoming data! This is
|
|
|
|
* especially nice for when you start with a fresh pet from the homepage, so
|
|
|
|
* we don't show skeleton items that just clear away!
|
|
|
|
*/
|
|
|
|
function ItemZoneGroupsSkeleton({ itemCount }) {
|
|
|
|
const groups = [];
|
|
|
|
for (let i = 0; i < itemCount; i++) {
|
|
|
|
// NOTE: I initially wrote this to return groups of 3, which looks good for
|
|
|
|
// outfit shares I think, but looks bad for pet loading... once shares
|
|
|
|
// become a more common use case, it might be useful to figure out how
|
|
|
|
// to differentiate these cases and show 1-per-group for pets, but
|
|
|
|
// maybe more for built outfits?
|
|
|
|
groups.push(<ItemZoneGroupSkeleton key={i} itemCount={1} />);
|
|
|
|
}
|
|
|
|
return groups;
|
|
|
|
}
|
|
|
|
|
2020-04-26 01:14:31 -07:00
|
|
|
/**
|
|
|
|
* ItemZoneGroupSkeleton is a placeholder for when an ItemZoneGroup is loading.
|
|
|
|
*/
|
2020-05-19 14:43:21 -07:00
|
|
|
function ItemZoneGroupSkeleton({ itemCount }) {
|
2020-04-26 01:14:31 -07:00
|
|
|
return (
|
|
|
|
<Box mb="10">
|
|
|
|
<Delay>
|
2020-05-19 14:43:21 -07:00
|
|
|
<Skeleton
|
|
|
|
mx="1"
|
|
|
|
// 2.25rem font size, 1.25rem line height
|
|
|
|
height={`${2.25 * 1.25}rem`}
|
|
|
|
width="12rem"
|
|
|
|
/>
|
|
|
|
<ItemListSkeleton count={itemCount} />
|
2020-04-26 01:14:31 -07:00
|
|
|
</Delay>
|
|
|
|
</Box>
|
2020-04-25 15:25:51 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-19 03:56:51 -07:00
|
|
|
function useOutfitSaving(outfitState) {
|
|
|
|
const { isLoggedIn, id: currentUserId } = useCurrentUser();
|
|
|
|
const history = useHistory();
|
|
|
|
const toast = useToast();
|
|
|
|
|
2021-04-22 02:35:59 -07:00
|
|
|
// Whether this outfit is new, i.e. local-only, i.e. has _never_ been saved
|
|
|
|
// to the server.
|
|
|
|
const isNewOutfit = outfitState.id == null;
|
|
|
|
|
|
|
|
// Whether this outfit's latest local changes have been saved to the server.
|
|
|
|
const latestVersionIsSaved =
|
|
|
|
outfitState.savedOutfitState &&
|
|
|
|
outfitStatesAreEqual(outfitState, outfitState.savedOutfitState);
|
2021-04-19 03:56:51 -07:00
|
|
|
|
|
|
|
// Only logged-in users can save outfits - and they can only save new outfits,
|
|
|
|
// or outfits they created.
|
|
|
|
const canSaveOutfit =
|
2021-04-22 02:35:59 -07:00
|
|
|
isLoggedIn && (isNewOutfit || outfitState.creator?.id === currentUserId);
|
2021-04-19 03:56:51 -07:00
|
|
|
|
|
|
|
const [sendSaveOutfitMutation, { loading: isSaving }] = useMutation(
|
|
|
|
gql`
|
|
|
|
mutation UseOutfitSaving_SaveOutfit(
|
|
|
|
$id: ID # Optional, is null when saving new outfits.
|
|
|
|
$name: String # Optional, server may fill in a placeholder.
|
|
|
|
$speciesId: ID!
|
|
|
|
$colorId: ID!
|
|
|
|
$pose: Pose!
|
|
|
|
$wornItemIds: [ID!]!
|
|
|
|
$closetedItemIds: [ID!]!
|
|
|
|
) {
|
|
|
|
outfit: saveOutfit(
|
|
|
|
id: $id
|
|
|
|
name: $name
|
|
|
|
speciesId: $speciesId
|
|
|
|
colorId: $colorId
|
|
|
|
pose: $pose
|
|
|
|
wornItemIds: $wornItemIds
|
|
|
|
closetedItemIds: $closetedItemIds
|
|
|
|
) {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
petAppearance {
|
|
|
|
id
|
|
|
|
species {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
color {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
pose
|
|
|
|
}
|
|
|
|
wornItems {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
closetedItems {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
creator {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
{
|
|
|
|
variables: {
|
|
|
|
id: outfitState.id, // Optional, is null when saving new outfits
|
|
|
|
name: outfitState.name, // Optional, server may fill in a placeholder
|
|
|
|
speciesId: outfitState.speciesId,
|
|
|
|
colorId: outfitState.colorId,
|
|
|
|
pose: outfitState.pose,
|
|
|
|
wornItemIds: outfitState.wornItemIds,
|
|
|
|
closetedItemIds: outfitState.closetedItemIds,
|
|
|
|
},
|
|
|
|
context: { sendAuth: true },
|
2021-04-20 02:26:31 -07:00
|
|
|
update: (cache, { data: { outfit } }) => {
|
|
|
|
// After save, add this outfit to the current user's outfit list. This
|
|
|
|
// will help when navigating back to Your Outfits, to force a refresh.
|
|
|
|
// https://www.apollographql.com/docs/react/caching/cache-interaction/#example-updating-the-cache-after-a-mutation
|
|
|
|
cache.modify({
|
|
|
|
id: cache.identify(outfit.creator),
|
|
|
|
fields: {
|
2021-04-21 21:27:10 -07:00
|
|
|
outfits: (existingOutfitRefs = []) => {
|
2021-04-20 02:26:31 -07:00
|
|
|
const newOutfitRef = cache.writeFragment({
|
|
|
|
data: outfit,
|
|
|
|
fragment: gql`
|
|
|
|
fragment NewOutfit on Outfit {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
return [...existingOutfitRefs, newOutfitRef];
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
2021-04-19 03:56:51 -07:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const saveOutfit = React.useCallback(() => {
|
|
|
|
sendSaveOutfitMutation()
|
|
|
|
.then(({ data: { outfit } }) => {
|
|
|
|
// Navigate to the new saved outfit URL. Our Apollo cache should pick
|
|
|
|
// up the data from this mutation response, and combine it with the
|
|
|
|
// existing cached data, to make this smooth without any loading UI.
|
|
|
|
history.push(`/outfits/${outfit.id}`);
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.error(e);
|
|
|
|
toast({
|
|
|
|
status: "error",
|
|
|
|
title: "Sorry, there was an error saving this outfit!",
|
|
|
|
description: "Maybe check your connection and try again.",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}, [sendSaveOutfitMutation, history, toast]);
|
|
|
|
|
|
|
|
return {
|
|
|
|
canSaveOutfit,
|
2021-04-22 02:35:59 -07:00
|
|
|
isNewOutfit,
|
2021-04-19 03:56:51 -07:00
|
|
|
isSaving,
|
2021-04-22 02:35:59 -07:00
|
|
|
latestVersionIsSaved,
|
2021-04-19 03:56:51 -07:00
|
|
|
saveOutfit,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-04-22 02:35:59 -07:00
|
|
|
/**
|
|
|
|
* OutfitSavingIndicator shows a Save button, or the "Saved" or "Saving" state,
|
|
|
|
* if the user can save this outfit. If not, this is empty!
|
|
|
|
*/
|
|
|
|
function OutfitSavingIndicator({ outfitState }) {
|
|
|
|
const {
|
|
|
|
canSaveOutfit,
|
|
|
|
isNewOutfit,
|
|
|
|
isSaving,
|
|
|
|
latestVersionIsSaved,
|
|
|
|
saveOutfit,
|
|
|
|
} = useOutfitSaving(outfitState);
|
|
|
|
|
|
|
|
if (!canSaveOutfit) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isNewOutfit) {
|
|
|
|
return (
|
|
|
|
<Button
|
|
|
|
variant="outline"
|
|
|
|
size="sm"
|
|
|
|
isLoading={isSaving}
|
|
|
|
loadingText="Saving…"
|
|
|
|
leftIcon={
|
|
|
|
<Box
|
|
|
|
// Adjust the visual balance toward the cloud
|
|
|
|
marginBottom="-2px"
|
|
|
|
>
|
|
|
|
<IoCloudUploadOutline />
|
|
|
|
</Box>
|
|
|
|
}
|
|
|
|
onClick={saveOutfit}
|
|
|
|
data-test-id="wardrobe-save-outfit-button"
|
|
|
|
>
|
|
|
|
Save
|
|
|
|
</Button>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (latestVersionIsSaved) {
|
|
|
|
return (
|
|
|
|
<Flex align="center" fontSize="xs">
|
|
|
|
<CheckIcon
|
|
|
|
marginRight="1"
|
|
|
|
// HACK: Not sure why my various centering things always feel wrong...
|
|
|
|
marginBottom="-2px"
|
|
|
|
/>
|
|
|
|
Saved
|
|
|
|
</Flex>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Popover trigger="hover">
|
|
|
|
<PopoverTrigger>
|
|
|
|
<Flex align="center" fontSize="xs" tabIndex="0">
|
|
|
|
<WarningIcon
|
|
|
|
marginRight="1"
|
|
|
|
// HACK: Not sure why my various centering things always feel wrong...
|
|
|
|
marginBottom="-2px"
|
|
|
|
/>
|
|
|
|
Not saved
|
|
|
|
</Flex>
|
|
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent>
|
|
|
|
<PopoverArrow />
|
|
|
|
<PopoverBody>
|
|
|
|
We're still working on this! For now, use{" "}
|
|
|
|
<Box
|
|
|
|
as="a"
|
|
|
|
href={`https://impress.openneo.net/outfits/${outfitState.id}`}
|
|
|
|
target="_blank"
|
|
|
|
>
|
|
|
|
<Box as="span" textDecoration="underline">
|
|
|
|
Classic DTI
|
|
|
|
</Box>
|
|
|
|
<ExternalLinkIcon marginLeft="1" marginTop="-3px" fontSize="sm" />
|
|
|
|
</Box>{" "}
|
|
|
|
to save existing outfits.
|
|
|
|
</PopoverBody>
|
|
|
|
</PopoverContent>
|
|
|
|
</Popover>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-26 01:14:31 -07:00
|
|
|
/**
|
|
|
|
* OutfitHeading is an editable outfit name, as a big pretty page heading!
|
2021-03-29 19:25:00 -07:00
|
|
|
* It also contains the outfit menu, for saving etc.
|
2020-04-26 01:14:31 -07:00
|
|
|
*/
|
2020-04-25 00:22:49 -07:00
|
|
|
function OutfitHeading({ outfitState, dispatchToOutfit }) {
|
|
|
|
return (
|
2021-03-29 19:25:00 -07:00
|
|
|
// The Editable wraps everything, including the menu, because the menu has
|
|
|
|
// a Rename option.
|
|
|
|
<Editable
|
|
|
|
// Make sure not to ever pass `undefined` into here, or else the
|
|
|
|
// component enters uncontrolled mode, and changing the value
|
|
|
|
// later won't fix it!
|
|
|
|
value={outfitState.name || ""}
|
|
|
|
placeholder="Untitled outfit"
|
|
|
|
onChange={(value) =>
|
|
|
|
dispatchToOutfit({ type: "rename", outfitName: value })
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{({ onEdit }) => (
|
2021-03-29 19:46:21 -07:00
|
|
|
<Flex align="center" marginBottom="6">
|
|
|
|
<Box>
|
2021-03-29 19:25:00 -07:00
|
|
|
<Box role="group" d="inline-block" position="relative" width="100%">
|
|
|
|
<Heading1>
|
2021-04-16 03:04:39 -07:00
|
|
|
<EditablePreview lineHeight="48px" data-test-id="outfit-name" />
|
2021-03-29 19:25:00 -07:00
|
|
|
<EditableInput lineHeight="48px" />
|
|
|
|
</Heading1>
|
|
|
|
</Box>
|
|
|
|
</Box>
|
2021-03-29 19:46:21 -07:00
|
|
|
<Box width="4" flex="1 0 auto" />
|
2021-04-22 02:35:59 -07:00
|
|
|
<Box flex="0 0 auto">
|
|
|
|
<OutfitSavingIndicator outfitState={outfitState} />
|
|
|
|
</Box>
|
|
|
|
<Box width="2" />
|
2021-03-29 19:25:00 -07:00
|
|
|
<Menu placement="bottom-end">
|
|
|
|
<MenuButton
|
|
|
|
as={IconButton}
|
|
|
|
variant="ghost"
|
|
|
|
icon={<MdMoreVert />}
|
|
|
|
aria-label="Outfit menu"
|
|
|
|
borderRadius="full"
|
|
|
|
fontSize="24px"
|
|
|
|
opacity="0.8"
|
|
|
|
/>
|
|
|
|
<Portal>
|
|
|
|
<MenuList>
|
2021-03-29 19:46:21 -07:00
|
|
|
{outfitState.id && (
|
|
|
|
<MenuItem icon={<EditIcon />} isDisabled>
|
|
|
|
Edit a copy <i>(Coming soon)</i>
|
|
|
|
</MenuItem>
|
|
|
|
)}
|
2021-03-29 19:25:00 -07:00
|
|
|
<MenuItem
|
2021-03-29 19:46:21 -07:00
|
|
|
icon={<BiRename />}
|
2021-03-29 19:25:00 -07:00
|
|
|
onClick={() => {
|
|
|
|
// Start the rename after a tick, so finishing up the click
|
|
|
|
// won't just immediately remove focus from the Editable.
|
|
|
|
setTimeout(onEdit, 0);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Rename
|
|
|
|
</MenuItem>
|
|
|
|
</MenuList>
|
|
|
|
</Portal>
|
|
|
|
</Menu>
|
|
|
|
</Flex>
|
2021-01-04 22:29:39 -08:00
|
|
|
)}
|
2021-03-29 19:25:00 -07:00
|
|
|
</Editable>
|
2020-04-25 00:22:49 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-26 01:14:31 -07:00
|
|
|
/**
|
2020-04-27 10:14:13 -07:00
|
|
|
* fadeOutAndRollUpTransition is the props for a CSSTransition, to manage the
|
|
|
|
* fade-out and height decrease when an Item or ItemZoneGroup is removed.
|
|
|
|
*
|
|
|
|
* Note that this _cannot_ be implemented as a wrapper component that returns a
|
|
|
|
* CSSTransition. This is because the CSSTransition must be the direct child of
|
|
|
|
* the TransitionGroup, and a wrapper breaks the parent-child relationship.
|
|
|
|
*
|
2020-04-26 01:14:31 -07:00
|
|
|
* See react-transition-group docs for more info!
|
|
|
|
*/
|
2021-01-03 19:11:46 -08:00
|
|
|
const fadeOutAndRollUpTransition = (css) => ({
|
2020-04-27 10:14:13 -07:00
|
|
|
classNames: css`
|
|
|
|
&-exit {
|
|
|
|
opacity: 1;
|
|
|
|
height: auto;
|
|
|
|
}
|
2020-04-26 01:14:31 -07:00
|
|
|
|
2020-04-27 10:14:13 -07:00
|
|
|
&-exit-active {
|
|
|
|
opacity: 0;
|
|
|
|
height: 0 !important;
|
|
|
|
margin-top: 0 !important;
|
|
|
|
margin-bottom: 0 !important;
|
|
|
|
transition: all 0.5s;
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
timeout: 500,
|
|
|
|
onExit: (e) => {
|
|
|
|
e.style.height = e.offsetHeight + "px";
|
|
|
|
},
|
2021-01-03 19:11:46 -08:00
|
|
|
});
|
2020-04-25 00:22:49 -07:00
|
|
|
|
|
|
|
export default ItemsPanel;
|