impress-2020/src/WardrobePage.js

310 lines
7.5 KiB
JavaScript
Raw Normal View History

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
Grid,
Heading,
2020-04-22 02:39:06 -07:00
Icon,
2020-04-22 01:10:24 -07:00
IconButton,
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,
useToast,
2020-04-21 20:32:53 -07:00
} from "@chakra-ui/core";
import { ITEMS } from "./data";
2020-04-22 15:24:38 -07:00
import ItemList, { ItemListSkeleton } from "./ItemList";
import useItemData from "./useItemData";
2020-04-21 20:46:53 -07:00
import useOutfitState from "./useOutfitState.js";
import OutfitPreview from "./OutfitPreview";
import { Delay } from "./util";
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("");
const toast = useToast();
const [hasSentToast, setHasSentToast] = React.useState(false);
2020-04-22 14:55:12 -07:00
const wearItemAndToast = React.useCallback(
(itemIdToAdd) => {
2020-04-22 14:55:12 -07:00
wearItem(itemIdToAdd);
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);
setHasSentToast(true);
}
},
2020-04-22 14:55:12 -07:00
[toast, wearItem, hasSentToast, setHasSentToast]
);
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={{
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"
>
<Box gridArea="outfit" backgroundColor="gray.900">
<OutfitPreview itemIds={data.wornItemIds} speciesId="54" colorId="75" />
2020-04-21 20:32:53 -07:00
</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>
);
}
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 { loading, error, itemsById } = useItemData(ITEMS.map((i) => i.id));
2020-04-22 02:39:06 -07:00
const normalize = (s) => s.toLowerCase();
const results = Object.values(itemsById).filter((item) =>
2020-04-22 02:39:06 -07:00
normalize(item.name).includes(normalize(query))
);
results.sort((a, b) => a.name.localeCompare(b.name));
return (
<Box color="green.800">
<Heading1 mb="6">Searching for "{query}"</Heading1>
<SearchResults
loading={loading}
error={error}
results={results}
2020-04-22 02:39:06 -07:00
wornItemIds={wornItemIds}
onWearItem={onWearItem}
/>
</Box>
);
}
function SearchResults({ loading, error, results, wornItemIds, onWearItem }) {
if (loading) {
return <ItemListSkeleton />;
}
if (error) {
return (
<Text color="green.500">
We hit an error trying to load your search results
<span role="img" aria-label="(sweat emoji)">
😓
</span>{" "}
Try again?
</Text>
);
}
if (results.length === 0) {
return (
2020-04-22 02:39:06 -07:00
<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 (
<ItemList
items={results}
wornItemIds={wornItemIds}
onWearItem={onWearItem}
/>
2020-04-22 02:39:06 -07:00
);
}
2020-04-22 15:24:38 -07:00
function ItemsPanel({ zonesAndItems, loading, onWearItem }) {
2020-04-21 20:32:53 -07:00
return (
<Box color="green.800">
<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}>
<Delay>
<Skeleton height="2.3rem" width="12rem" mb="3" />
<ItemListSkeleton />
</Delay>
2020-04-22 15:24:38 -07:00
</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>
</Box>
2020-04-21 20:32:53 -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 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>
);
}
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;