use item data to detect conflicts
This commit is contained in:
parent
d0c698bf18
commit
abfe854756
6 changed files with 154 additions and 66 deletions
|
@ -18,6 +18,7 @@
|
||||||
"dataloader": "^2.0.0",
|
"dataloader": "^2.0.0",
|
||||||
"emotion-theming": "^10.0.27",
|
"emotion-theming": "^10.0.27",
|
||||||
"graphql": "^15.0.0",
|
"graphql": "^15.0.0",
|
||||||
|
"immer": "^6.0.3",
|
||||||
"mysql2": "^2.1.0",
|
"mysql2": "^2.1.0",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, Image, PseudoBox, Stack, Skeleton } from "@chakra-ui/core";
|
import { Box, Image, PseudoBox, Stack, Skeleton } from "@chakra-ui/core";
|
||||||
|
|
||||||
function ItemList({ items, wornItemIds, onWearItem }) {
|
function ItemList({ items, wornItemIds, dispatchToOutfit }) {
|
||||||
return (
|
return (
|
||||||
<Stack spacing="3">
|
<Stack spacing="3">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
|
@ -9,7 +9,7 @@ function ItemList({ items, wornItemIds, onWearItem }) {
|
||||||
<Item
|
<Item
|
||||||
item={item}
|
item={item}
|
||||||
isWorn={wornItemIds.includes(item.id)}
|
isWorn={wornItemIds.includes(item.id)}
|
||||||
onWear={() => onWearItem(item.id)}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
@ -33,14 +33,14 @@ function ItemListSkeleton() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Item({ item, isWorn, onWear }) {
|
function Item({ item, isWorn, dispatchToOutfit }) {
|
||||||
return (
|
return (
|
||||||
<PseudoBox
|
<PseudoBox
|
||||||
role="group"
|
role="group"
|
||||||
d="flex"
|
d="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={onWear}
|
onClick={() => dispatchToOutfit({ type: "wearItem", itemId: item.id })}
|
||||||
>
|
>
|
||||||
<ItemThumbnail src={item.thumbnailUrl} isWorn={isWorn} />
|
<ItemThumbnail src={item.thumbnailUrl} isWorn={isWorn} />
|
||||||
<Box width="3" />
|
<Box width="3" />
|
||||||
|
|
|
@ -27,7 +27,7 @@ import OutfitPreview from "./OutfitPreview";
|
||||||
import { Delay } from "./util";
|
import { Delay } from "./util";
|
||||||
|
|
||||||
function WardrobePage() {
|
function WardrobePage() {
|
||||||
const { loading, error, data, wearItem } = useOutfitState();
|
const { loading, error, data, dispatch: dispatchToOutfit } = useOutfitState();
|
||||||
const [searchQuery, setSearchQuery] = React.useState("");
|
const [searchQuery, setSearchQuery] = React.useState("");
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
@ -69,8 +69,8 @@ function WardrobePage() {
|
||||||
<Box gridArea="outfit" backgroundColor="gray.900">
|
<Box gridArea="outfit" backgroundColor="gray.900">
|
||||||
<OutfitPreview
|
<OutfitPreview
|
||||||
itemIds={data.wornItemIds}
|
itemIds={data.wornItemIds}
|
||||||
speciesId="54"
|
speciesId={data.speciesId}
|
||||||
colorId="75"
|
colorId={data.colorId}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box gridArea="search" boxShadow="sm">
|
<Box gridArea="search" boxShadow="sm">
|
||||||
|
@ -84,13 +84,13 @@ function WardrobePage() {
|
||||||
<SearchPanel
|
<SearchPanel
|
||||||
query={searchQuery}
|
query={searchQuery}
|
||||||
wornItemIds={data.wornItemIds}
|
wornItemIds={data.wornItemIds}
|
||||||
onWearItem={wearItem}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ItemsPanel
|
<ItemsPanel
|
||||||
zonesAndItems={data.zonesAndItems}
|
zonesAndItems={data.zonesAndItems}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onWearItem={wearItem}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -135,7 +135,7 @@ function SearchToolbar({ query, onChange }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchPanel({ query, wornItemIds, onWearItem }) {
|
function SearchPanel({ query, wornItemIds, dispatchToOutfit }) {
|
||||||
const { loading, error, itemsById } = useItemData(ITEMS.map((i) => i.id));
|
const { loading, error, itemsById } = useItemData(ITEMS.map((i) => i.id));
|
||||||
|
|
||||||
const normalize = (s) => s.toLowerCase();
|
const normalize = (s) => s.toLowerCase();
|
||||||
|
@ -152,13 +152,19 @@ function SearchPanel({ query, wornItemIds, onWearItem }) {
|
||||||
error={error}
|
error={error}
|
||||||
results={results}
|
results={results}
|
||||||
wornItemIds={wornItemIds}
|
wornItemIds={wornItemIds}
|
||||||
onWearItem={onWearItem}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchResults({ loading, error, results, wornItemIds, onWearItem }) {
|
function SearchResults({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
results,
|
||||||
|
wornItemIds,
|
||||||
|
dispatchToOutfit,
|
||||||
|
}) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ItemListSkeleton />;
|
return <ItemListSkeleton />;
|
||||||
}
|
}
|
||||||
|
@ -191,12 +197,12 @@ function SearchResults({ loading, error, results, wornItemIds, onWearItem }) {
|
||||||
<ItemList
|
<ItemList
|
||||||
items={results}
|
items={results}
|
||||||
wornItemIds={wornItemIds}
|
wornItemIds={wornItemIds}
|
||||||
onWearItem={onWearItem}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemsPanel({ zonesAndItems, loading, onWearItem }) {
|
function ItemsPanel({ zonesAndItems, loading, dispatchToOutfit }) {
|
||||||
return (
|
return (
|
||||||
<Box color="green.800">
|
<Box color="green.800">
|
||||||
<OutfitHeading />
|
<OutfitHeading />
|
||||||
|
@ -217,7 +223,7 @@ function ItemsPanel({ zonesAndItems, loading, onWearItem }) {
|
||||||
<ItemList
|
<ItemList
|
||||||
items={items}
|
items={items}
|
||||||
wornItemIds={[wornItemId]}
|
wornItemIds={[wornItemId]}
|
||||||
onWearItem={onWearItem}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -3,18 +3,28 @@ import { useQuery } from "@apollo/react-hooks";
|
||||||
|
|
||||||
import { ITEMS } from "./data";
|
import { ITEMS } from "./data";
|
||||||
|
|
||||||
function useItemData(itemIds) {
|
function useItemData(itemIds, speciesId, colorId) {
|
||||||
const { loading, error, data } = useQuery(
|
const { loading, error, data } = useQuery(
|
||||||
gql`
|
gql`
|
||||||
query($itemIds: [ID!]!) {
|
query($itemIds: [ID!]!, $speciesId: ID!, $colorId: ID!) {
|
||||||
items(ids: $itemIds) {
|
items(ids: $itemIds) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
|
|
||||||
|
# This is used for wearItem actions, to resolve conflicts. We don't
|
||||||
|
# use it directly; we just expect it to be in the cache!
|
||||||
|
appearanceOn(speciesId: $speciesId, colorId: $colorId) {
|
||||||
|
layers {
|
||||||
|
zone {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
{ variables: { itemIds } }
|
{ variables: { itemIds, speciesId, colorId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const items = (data && data.items) || [];
|
const items = (data && data.items) || [];
|
||||||
|
|
|
@ -1,57 +1,42 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import gql from "graphql-tag";
|
||||||
|
import produce, { enableMapSet } from "immer";
|
||||||
|
import { useApolloClient } from "@apollo/react-hooks";
|
||||||
|
|
||||||
import useItemData from "./useItemData";
|
import useItemData from "./useItemData";
|
||||||
|
|
||||||
|
enableMapSet();
|
||||||
|
|
||||||
function useOutfitState() {
|
function useOutfitState() {
|
||||||
const [wornItemIds, setWornItemIds] = React.useState([
|
const apolloClient = useApolloClient();
|
||||||
"38913",
|
const [state, dispatch] = React.useReducer(outfitStateReducer(apolloClient), {
|
||||||
"38911",
|
wornItemIds: new Set([
|
||||||
"38912",
|
"38913",
|
||||||
"37375",
|
"38911",
|
||||||
"48313",
|
"38912",
|
||||||
"37229",
|
"37375",
|
||||||
"43014",
|
"48313",
|
||||||
"43397",
|
"37229",
|
||||||
]);
|
"43014",
|
||||||
const [closetedItemIds, setClosetedItemIds] = React.useState(["74166"]);
|
"43397",
|
||||||
|
]),
|
||||||
|
closetedItemIds: new Set(["74166"]),
|
||||||
|
speciesId: "54", // Starry
|
||||||
|
colorId: "75", // Zafara
|
||||||
|
});
|
||||||
|
|
||||||
const allItemIds = [...wornItemIds, ...closetedItemIds];
|
const { speciesId, colorId } = state;
|
||||||
|
|
||||||
const { loading, error, itemsById } = useItemData(allItemIds);
|
// It's more convenient to manage these as a Set in state, but most callers
|
||||||
|
// will find it more convenient to access them as arrays! e.g. for `.map()`
|
||||||
|
const wornItemIds = Array.from(state.wornItemIds);
|
||||||
|
const closetedItemIds = Array.from(state.closetedItemIds);
|
||||||
|
|
||||||
const wearItem = React.useCallback(
|
const allItemIds = [...state.wornItemIds, ...state.closetedItemIds];
|
||||||
(itemIdToAdd) => {
|
const { loading, error, itemsById } = useItemData(
|
||||||
if (wornItemIds.includes(itemIdToAdd)) {
|
allItemIds,
|
||||||
return;
|
speciesId,
|
||||||
}
|
colorId
|
||||||
|
|
||||||
let newWornItemIds = wornItemIds;
|
|
||||||
let newClosetedItemIds = closetedItemIds;
|
|
||||||
|
|
||||||
const itemToAdd = itemsById[itemIdToAdd];
|
|
||||||
|
|
||||||
// Move the item out of the closet.
|
|
||||||
newClosetedItemIds = newClosetedItemIds.filter(
|
|
||||||
(id) => id !== itemIdToAdd
|
|
||||||
);
|
|
||||||
|
|
||||||
// Move conflicting items to the closet.
|
|
||||||
const conflictingItemIds = newWornItemIds.filter((wornItemId) => {
|
|
||||||
const wornItem = itemsById[wornItemId];
|
|
||||||
return wornItem.zoneName === itemToAdd.zoneName;
|
|
||||||
});
|
|
||||||
newWornItemIds = newWornItemIds.filter(
|
|
||||||
(id) => !conflictingItemIds.includes(id)
|
|
||||||
);
|
|
||||||
newClosetedItemIds = [...newClosetedItemIds, ...conflictingItemIds];
|
|
||||||
|
|
||||||
// Add this item to the worn set.
|
|
||||||
newWornItemIds = [...newWornItemIds, itemIdToAdd];
|
|
||||||
|
|
||||||
setWornItemIds(newWornItemIds);
|
|
||||||
setClosetedItemIds(newClosetedItemIds);
|
|
||||||
},
|
|
||||||
[wornItemIds, closetedItemIds, itemsById]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const zonesAndItems = getZonesAndItems(
|
const zonesAndItems = getZonesAndItems(
|
||||||
|
@ -60,9 +45,90 @@ function useOutfitState() {
|
||||||
closetedItemIds
|
closetedItemIds
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = { zonesAndItems, wornItemIds };
|
const data = { zonesAndItems, wornItemIds, speciesId, colorId };
|
||||||
|
|
||||||
return { loading, error, data, wearItem };
|
return { loading, error, data, dispatch };
|
||||||
|
}
|
||||||
|
|
||||||
|
const outfitStateReducer = (apolloClient) => (baseState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "wearItem":
|
||||||
|
return produce(baseState, (state) => {
|
||||||
|
const { wornItemIds, closetedItemIds, speciesId, colorId } = state;
|
||||||
|
const { itemId } = action;
|
||||||
|
|
||||||
|
// Move the item out of the closet.
|
||||||
|
closetedItemIds.delete(itemId);
|
||||||
|
|
||||||
|
// Move conflicting items to the closet.
|
||||||
|
//
|
||||||
|
// We do this by looking them up in the Apollo Cache, which is going to
|
||||||
|
// include the relevant item data because the `useOutfitState` hook
|
||||||
|
// queries for it!
|
||||||
|
//
|
||||||
|
// (It could be possible to mess up the timing by taking an action
|
||||||
|
// while worn items are still partially loading, but I think it would
|
||||||
|
// require a pretty weird action sequence to make that happen... like,
|
||||||
|
// doing a search and it loads before the worn item data does? Anyway,
|
||||||
|
// Apollo will throw in that case, which should just essentially reject
|
||||||
|
// the action.)
|
||||||
|
const conflictingIds = findItemConflicts(itemId, state, apolloClient);
|
||||||
|
for (const conflictingId of conflictingIds) {
|
||||||
|
wornItemIds.delete(conflictingId);
|
||||||
|
closetedItemIds.add(conflictingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this item to the worn set.
|
||||||
|
wornItemIds.add(itemId);
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error(`unexpected action ${action}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function findItemConflicts(itemIdToAdd, state, apolloClient) {
|
||||||
|
const { wornItemIds, speciesId, colorId } = state;
|
||||||
|
|
||||||
|
const { items } = apolloClient.readQuery({
|
||||||
|
query: gql`
|
||||||
|
query($itemIds: [ID!]!, $speciesId: ID!, $colorId: ID!) {
|
||||||
|
items(ids: $itemIds) {
|
||||||
|
id
|
||||||
|
appearanceOn(speciesId: $speciesId, colorId: $colorId) {
|
||||||
|
layers {
|
||||||
|
zone {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
itemIds: [itemIdToAdd, ...wornItemIds],
|
||||||
|
speciesId,
|
||||||
|
colorId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const itemToAdd = items.find((i) => i.id === itemIdToAdd);
|
||||||
|
const itemToAddZoneIds = itemToAdd.appearanceOn.layers.map((l) => l.zone.id);
|
||||||
|
const wornItems = Array.from(wornItemIds).map((id) =>
|
||||||
|
items.find((i) => i.id === id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const conflictingIds = [];
|
||||||
|
for (const wornItem of wornItems) {
|
||||||
|
const wornItemZoneIds = wornItem.appearanceOn.layers.map((l) => l.zone.id);
|
||||||
|
|
||||||
|
const hasConflict = wornItemZoneIds.some((zid) =>
|
||||||
|
itemToAddZoneIds.includes(zid)
|
||||||
|
);
|
||||||
|
if (hasConflict) {
|
||||||
|
conflictingIds.push(wornItem.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflictingIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) {
|
function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) {
|
||||||
|
|
|
@ -6109,6 +6109,11 @@ immer@1.10.0:
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
||||||
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
|
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
|
||||||
|
|
||||||
|
immer@^6.0.3:
|
||||||
|
version "6.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/immer/-/immer-6.0.3.tgz#94d5051cd724668160a900d66d85ec02816f29bd"
|
||||||
|
integrity sha512-12VvNrfSrXZdm/BJgi/KDW2soq5freVSf3I1+4CLunUM8mAGx2/0Njy0xBVzi5zewQZiwM7z1/1T+8VaI7NkmQ==
|
||||||
|
|
||||||
import-cwd@^2.0.0:
|
import-cwd@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
||||||
|
|
Loading…
Reference in a new issue