2020-04-21 20:32:53 -07:00
|
|
|
import React from "react";
|
|
|
|
import {
|
|
|
|
Box,
|
2020-04-22 02:39:06 -07:00
|
|
|
Editable,
|
|
|
|
EditablePreview,
|
|
|
|
EditableInput,
|
2020-04-21 20:32:53 -07:00
|
|
|
Flex,
|
|
|
|
Grid,
|
|
|
|
Heading,
|
2020-04-22 02:39:06 -07:00
|
|
|
Icon,
|
2020-04-22 01:10:24 -07:00
|
|
|
IconButton,
|
2020-04-21 20:32:53 -07:00
|
|
|
Image,
|
2020-04-22 02:39:06 -07:00
|
|
|
Input,
|
|
|
|
InputGroup,
|
|
|
|
InputLeftElement,
|
|
|
|
InputRightElement,
|
2020-04-21 20:32:53 -07:00
|
|
|
PseudoBox,
|
2020-04-22 15:24:38 -07:00
|
|
|
Skeleton,
|
2020-04-22 02:39:06 -07:00
|
|
|
Stack,
|
|
|
|
Text,
|
2020-04-22 02:57:58 -07:00
|
|
|
useToast,
|
2020-04-21 20:32:53 -07:00
|
|
|
} from "@chakra-ui/core";
|
|
|
|
|
2020-04-22 15:24:38 -07:00
|
|
|
import ItemList, { ItemListSkeleton } from "./ItemList";
|
2020-04-21 20:46:53 -07:00
|
|
|
import useOutfitState from "./useOutfitState.js";
|
2020-04-22 02:39:06 -07:00
|
|
|
import { ITEMS } from "./data";
|
2020-04-21 20:32:53 -07:00
|
|
|
|
|
|
|
function WardrobePage() {
|
2020-04-22 14:55:12 -07:00
|
|
|
const { loading, error, data, wearItem } = useOutfitState();
|
2020-04-22 02:39:06 -07:00
|
|
|
const [searchQuery, setSearchQuery] = React.useState("");
|
|
|
|
|
2020-04-22 02:57:58 -07:00
|
|
|
const toast = useToast();
|
|
|
|
const [hasSentToast, setHasSentToast] = React.useState(false);
|
2020-04-22 14:55:12 -07:00
|
|
|
const wearItemAndToast = React.useCallback(
|
2020-04-22 02:57:58 -07:00
|
|
|
(itemIdToAdd) => {
|
2020-04-22 14:55:12 -07:00
|
|
|
wearItem(itemIdToAdd);
|
2020-04-22 02:57:58 -07:00
|
|
|
|
|
|
|
if (!hasSentToast) {
|
2020-04-22 03:01:23 -07:00
|
|
|
setTimeout(() => {
|
|
|
|
toast({
|
|
|
|
title: "So, the outfit didn't change 😅",
|
|
|
|
description:
|
|
|
|
"This is a prototype, and the outfit preview is static right " +
|
|
|
|
"now! But the list animation is good, yeah? Nice and smooth 😊",
|
|
|
|
status: "warning",
|
|
|
|
isClosable: true,
|
|
|
|
duration: 10000,
|
|
|
|
position: window.innerWidth < 992 ? "top" : "bottom-left",
|
|
|
|
});
|
|
|
|
}, 3000);
|
2020-04-22 02:57:58 -07:00
|
|
|
setHasSentToast(true);
|
|
|
|
}
|
|
|
|
},
|
2020-04-22 14:55:12 -07:00
|
|
|
[toast, wearItem, hasSentToast, setHasSentToast]
|
2020-04-22 02:57:58 -07:00
|
|
|
);
|
|
|
|
|
2020-04-22 15:24:38 -07:00
|
|
|
React.useEffect(() => {
|
|
|
|
if (error) {
|
|
|
|
toast({
|
|
|
|
title: "We couldn't load this outfit 😖",
|
|
|
|
description: "Please reload the page to try again. Sorry!",
|
|
|
|
status: "error",
|
|
|
|
isClosable: true,
|
|
|
|
duration: Infinity,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, [error, toast]);
|
|
|
|
|
2020-04-21 20:32:53 -07:00
|
|
|
return (
|
2020-04-22 00:39:35 -07:00
|
|
|
<Grid
|
|
|
|
// Fullscreen, split into a vertical stack on smaller screens
|
|
|
|
// or a horizontal stack on larger ones!
|
2020-04-22 02:39:06 -07:00
|
|
|
templateAreas={{
|
|
|
|
base: `"outfit"
|
|
|
|
"search"
|
|
|
|
"items"`,
|
|
|
|
lg: `"outfit search"
|
|
|
|
"outfit items"`,
|
|
|
|
}}
|
|
|
|
templateRows={{
|
2020-04-22 03:50:57 -07:00
|
|
|
base: "minmax(100px, 1fr) auto minmax(300px, 1fr)",
|
2020-04-22 02:39:06 -07:00
|
|
|
lg: "auto 1fr",
|
|
|
|
}}
|
|
|
|
templateColumns={{
|
|
|
|
base: "100%",
|
|
|
|
lg: "50% 50%",
|
|
|
|
}}
|
2020-04-22 00:39:35 -07:00
|
|
|
position="absolute"
|
|
|
|
top="0"
|
|
|
|
bottom="0"
|
|
|
|
left="0"
|
|
|
|
right="0"
|
|
|
|
>
|
2020-04-22 02:39:06 -07:00
|
|
|
<Box gridArea="outfit">
|
2020-04-21 20:32:53 -07:00
|
|
|
<OutfitPreview />
|
|
|
|
</Box>
|
2020-04-22 02:39:06 -07:00
|
|
|
<Box gridArea="search" boxShadow="sm">
|
|
|
|
<Box px="5" py="3">
|
|
|
|
<SearchToolbar query={searchQuery} onChange={setSearchQuery} />
|
|
|
|
</Box>
|
|
|
|
</Box>
|
|
|
|
<Box gridArea="items" overflow="auto">
|
2020-04-21 20:32:53 -07:00
|
|
|
<Box px="5" py="5">
|
2020-04-22 02:39:06 -07:00
|
|
|
{searchQuery ? (
|
|
|
|
<SearchPanel
|
|
|
|
query={searchQuery}
|
|
|
|
wornItemIds={data.wornItemIds}
|
2020-04-22 14:55:12 -07:00
|
|
|
onWearItem={wearItemAndToast}
|
2020-04-22 02:39:06 -07:00
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<ItemsPanel
|
|
|
|
zonesAndItems={data.zonesAndItems}
|
2020-04-22 15:24:38 -07:00
|
|
|
loading={loading}
|
2020-04-22 14:55:12 -07:00
|
|
|
onWearItem={wearItemAndToast}
|
2020-04-22 02:39:06 -07:00
|
|
|
/>
|
|
|
|
)}
|
2020-04-21 20:32:53 -07:00
|
|
|
</Box>
|
|
|
|
</Box>
|
|
|
|
</Grid>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function OutfitPreview() {
|
|
|
|
return (
|
|
|
|
<Flex
|
|
|
|
alignItems="center"
|
|
|
|
justifyContent="center"
|
|
|
|
height="100%"
|
|
|
|
width="100%"
|
|
|
|
backgroundColor="gray.900"
|
|
|
|
>
|
|
|
|
<Image
|
|
|
|
src="http://pets.neopets.com/cp/wgmdtdwz/1/7.png"
|
|
|
|
maxHeight="100%"
|
|
|
|
maxWidth="100%"
|
|
|
|
/>
|
|
|
|
</Flex>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-22 02:39:06 -07:00
|
|
|
function SearchToolbar({ query, onChange }) {
|
|
|
|
return (
|
|
|
|
<InputGroup>
|
|
|
|
<InputLeftElement>
|
|
|
|
<Icon name="search" color="gray.400" />
|
|
|
|
</InputLeftElement>
|
|
|
|
<Input
|
2020-04-22 03:13:47 -07:00
|
|
|
placeholder="Search for items to add…"
|
2020-04-22 02:39:06 -07:00
|
|
|
focusBorderColor="green.600"
|
|
|
|
color="green.800"
|
|
|
|
value={query}
|
|
|
|
onChange={(e) => onChange(e.target.value)}
|
2020-04-22 04:07:34 -07:00
|
|
|
onKeyDown={(e) => {
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
onChange("");
|
|
|
|
e.target.blur();
|
|
|
|
}
|
|
|
|
}}
|
2020-04-22 02:39:06 -07:00
|
|
|
/>
|
|
|
|
{query && (
|
|
|
|
<InputRightElement>
|
|
|
|
<IconButton
|
|
|
|
icon="close"
|
|
|
|
color="gray.400"
|
|
|
|
variant="ghost"
|
|
|
|
variantColor="green"
|
|
|
|
aria-label="Clear search"
|
|
|
|
onClick={() => onChange("")}
|
|
|
|
/>
|
|
|
|
</InputRightElement>
|
|
|
|
)}
|
|
|
|
</InputGroup>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function SearchPanel({ query, wornItemIds, onWearItem }) {
|
|
|
|
const normalize = (s) => s.toLowerCase();
|
|
|
|
const results = ITEMS.filter((item) =>
|
|
|
|
normalize(item.name).includes(normalize(query))
|
|
|
|
);
|
|
|
|
results.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
|
|
|
const resultsSection =
|
|
|
|
results.length > 0 ? (
|
|
|
|
<ItemList
|
|
|
|
items={results}
|
|
|
|
wornItemIds={wornItemIds}
|
|
|
|
onWearItem={onWearItem}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<Text color="green.500">
|
|
|
|
We couldn't find any matching items{" "}
|
|
|
|
<span role="img" aria-label="(thinking emoji)">
|
|
|
|
🤔
|
|
|
|
</span>{" "}
|
|
|
|
Try again?
|
|
|
|
</Text>
|
|
|
|
);
|
2020-04-21 20:32:53 -07:00
|
|
|
|
2020-04-22 02:39:06 -07:00
|
|
|
return (
|
|
|
|
<Box color="green.800">
|
2020-04-22 03:55:40 -07:00
|
|
|
<Heading1 mb="6">Searching for "{query}"</Heading1>
|
2020-04-22 02:39:06 -07:00
|
|
|
{resultsSection}
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-22 15:24:38 -07:00
|
|
|
function ItemsPanel({ zonesAndItems, loading, onWearItem }) {
|
2020-04-21 20:32:53 -07:00
|
|
|
return (
|
2020-04-22 01:15:51 -07:00
|
|
|
<Box color="green.800">
|
2020-04-22 01:29:07 -07:00
|
|
|
<OutfitHeading />
|
2020-04-22 01:10:24 -07:00
|
|
|
<Stack spacing="10">
|
2020-04-22 15:24:38 -07:00
|
|
|
{loading &&
|
|
|
|
[1, 2, 3].map((i) => (
|
|
|
|
<Box key={i}>
|
|
|
|
<Skeleton height="2.3rem" width="12rem" mb="3" />
|
|
|
|
<ItemListSkeleton />
|
|
|
|
</Box>
|
|
|
|
))}
|
|
|
|
{!loading &&
|
|
|
|
zonesAndItems.map(({ zoneName, items, wornItemId }) => (
|
|
|
|
<Box key={zoneName}>
|
|
|
|
<Heading2 mb="3">{zoneName}</Heading2>
|
|
|
|
<ItemList
|
|
|
|
items={items}
|
|
|
|
wornItemIds={[wornItemId]}
|
|
|
|
onWearItem={onWearItem}
|
|
|
|
/>
|
|
|
|
</Box>
|
|
|
|
))}
|
2020-04-22 01:10:24 -07:00
|
|
|
</Stack>
|
2020-04-22 01:15:51 -07:00
|
|
|
</Box>
|
2020-04-21 20:32:53 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-22 01:29:07 -07:00
|
|
|
function OutfitHeading() {
|
|
|
|
return (
|
2020-04-22 15:24:38 -07:00
|
|
|
<Box>
|
|
|
|
<PseudoBox role="group" d="inline-block" position="relative">
|
|
|
|
<Heading1 mb="6">
|
|
|
|
<Editable defaultValue="Zafara Agent (roopal27)">
|
|
|
|
{({ isEditing, onRequestEdit }) => (
|
|
|
|
<>
|
|
|
|
<EditablePreview />
|
|
|
|
<EditableInput />
|
|
|
|
{!isEditing && (
|
|
|
|
<OutfitNameEditButton onRequestEdit={onRequestEdit} />
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Editable>
|
|
|
|
</Heading1>
|
|
|
|
</PseudoBox>
|
|
|
|
</Box>
|
2020-04-22 01:29:07 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-22 04:04:02 -07:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-22 03:55:40 -07:00
|
|
|
function Heading1({ children, ...props }) {
|
|
|
|
return (
|
|
|
|
<Heading fontFamily="Delicious" fontWeight="800" size="2xl" {...props}>
|
|
|
|
{children}
|
|
|
|
</Heading>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function Heading2({ children, ...props }) {
|
|
|
|
return (
|
|
|
|
<Heading size="xl" color="green.800" fontFamily="Delicious" {...props}>
|
|
|
|
{children}
|
|
|
|
</Heading>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-21 20:32:53 -07:00
|
|
|
export default WardrobePage;
|