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",
|
||||
"emotion-theming": "^10.0.27",
|
||||
"graphql": "^15.0.0",
|
||||
"immer": "^6.0.3",
|
||||
"mysql2": "^2.1.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { Box, Image, PseudoBox, Stack, Skeleton } from "@chakra-ui/core";
|
||||
|
||||
function ItemList({ items, wornItemIds, onWearItem }) {
|
||||
function ItemList({ items, wornItemIds, dispatchToOutfit }) {
|
||||
return (
|
||||
<Stack spacing="3">
|
||||
{items.map((item) => (
|
||||
|
@ -9,7 +9,7 @@ function ItemList({ items, wornItemIds, onWearItem }) {
|
|||
<Item
|
||||
item={item}
|
||||
isWorn={wornItemIds.includes(item.id)}
|
||||
onWear={() => onWearItem(item.id)}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
|
@ -33,14 +33,14 @@ function ItemListSkeleton() {
|
|||
);
|
||||
}
|
||||
|
||||
function Item({ item, isWorn, onWear }) {
|
||||
function Item({ item, isWorn, dispatchToOutfit }) {
|
||||
return (
|
||||
<PseudoBox
|
||||
role="group"
|
||||
d="flex"
|
||||
alignItems="center"
|
||||
cursor="pointer"
|
||||
onClick={onWear}
|
||||
onClick={() => dispatchToOutfit({ type: "wearItem", itemId: item.id })}
|
||||
>
|
||||
<ItemThumbnail src={item.thumbnailUrl} isWorn={isWorn} />
|
||||
<Box width="3" />
|
||||
|
|
|
@ -27,7 +27,7 @@ import OutfitPreview from "./OutfitPreview";
|
|||
import { Delay } from "./util";
|
||||
|
||||
function WardrobePage() {
|
||||
const { loading, error, data, wearItem } = useOutfitState();
|
||||
const { loading, error, data, dispatch: dispatchToOutfit } = useOutfitState();
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const toast = useToast();
|
||||
|
||||
|
@ -69,8 +69,8 @@ function WardrobePage() {
|
|||
<Box gridArea="outfit" backgroundColor="gray.900">
|
||||
<OutfitPreview
|
||||
itemIds={data.wornItemIds}
|
||||
speciesId="54"
|
||||
colorId="75"
|
||||
speciesId={data.speciesId}
|
||||
colorId={data.colorId}
|
||||
/>
|
||||
</Box>
|
||||
<Box gridArea="search" boxShadow="sm">
|
||||
|
@ -84,13 +84,13 @@ function WardrobePage() {
|
|||
<SearchPanel
|
||||
query={searchQuery}
|
||||
wornItemIds={data.wornItemIds}
|
||||
onWearItem={wearItem}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
) : (
|
||||
<ItemsPanel
|
||||
zonesAndItems={data.zonesAndItems}
|
||||
loading={loading}
|
||||
onWearItem={wearItem}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
)}
|
||||
</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 normalize = (s) => s.toLowerCase();
|
||||
|
@ -152,13 +152,19 @@ function SearchPanel({ query, wornItemIds, onWearItem }) {
|
|||
error={error}
|
||||
results={results}
|
||||
wornItemIds={wornItemIds}
|
||||
onWearItem={onWearItem}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResults({ loading, error, results, wornItemIds, onWearItem }) {
|
||||
function SearchResults({
|
||||
loading,
|
||||
error,
|
||||
results,
|
||||
wornItemIds,
|
||||
dispatchToOutfit,
|
||||
}) {
|
||||
if (loading) {
|
||||
return <ItemListSkeleton />;
|
||||
}
|
||||
|
@ -191,12 +197,12 @@ function SearchResults({ loading, error, results, wornItemIds, onWearItem }) {
|
|||
<ItemList
|
||||
items={results}
|
||||
wornItemIds={wornItemIds}
|
||||
onWearItem={onWearItem}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemsPanel({ zonesAndItems, loading, onWearItem }) {
|
||||
function ItemsPanel({ zonesAndItems, loading, dispatchToOutfit }) {
|
||||
return (
|
||||
<Box color="green.800">
|
||||
<OutfitHeading />
|
||||
|
@ -217,7 +223,7 @@ function ItemsPanel({ zonesAndItems, loading, onWearItem }) {
|
|||
<ItemList
|
||||
items={items}
|
||||
wornItemIds={[wornItemId]}
|
||||
onWearItem={onWearItem}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
|
|
|
@ -3,18 +3,28 @@ import { useQuery } from "@apollo/react-hooks";
|
|||
|
||||
import { ITEMS } from "./data";
|
||||
|
||||
function useItemData(itemIds) {
|
||||
function useItemData(itemIds, speciesId, colorId) {
|
||||
const { loading, error, data } = useQuery(
|
||||
gql`
|
||||
query($itemIds: [ID!]!) {
|
||||
query($itemIds: [ID!]!, $speciesId: ID!, $colorId: ID!) {
|
||||
items(ids: $itemIds) {
|
||||
id
|
||||
name
|
||||
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) || [];
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import React from "react";
|
||||
import gql from "graphql-tag";
|
||||
import produce, { enableMapSet } from "immer";
|
||||
import { useApolloClient } from "@apollo/react-hooks";
|
||||
|
||||
import useItemData from "./useItemData";
|
||||
|
||||
enableMapSet();
|
||||
|
||||
function useOutfitState() {
|
||||
const [wornItemIds, setWornItemIds] = React.useState([
|
||||
const apolloClient = useApolloClient();
|
||||
const [state, dispatch] = React.useReducer(outfitStateReducer(apolloClient), {
|
||||
wornItemIds: new Set([
|
||||
"38913",
|
||||
"38911",
|
||||
"38912",
|
||||
|
@ -12,46 +19,24 @@ function useOutfitState() {
|
|||
"37229",
|
||||
"43014",
|
||||
"43397",
|
||||
]);
|
||||
const [closetedItemIds, setClosetedItemIds] = React.useState(["74166"]);
|
||||
|
||||
const allItemIds = [...wornItemIds, ...closetedItemIds];
|
||||
|
||||
const { loading, error, itemsById } = useItemData(allItemIds);
|
||||
|
||||
const wearItem = React.useCallback(
|
||||
(itemIdToAdd) => {
|
||||
if (wornItemIds.includes(itemIdToAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
]),
|
||||
closetedItemIds: new Set(["74166"]),
|
||||
speciesId: "54", // Starry
|
||||
colorId: "75", // Zafara
|
||||
});
|
||||
newWornItemIds = newWornItemIds.filter(
|
||||
(id) => !conflictingItemIds.includes(id)
|
||||
);
|
||||
newClosetedItemIds = [...newClosetedItemIds, ...conflictingItemIds];
|
||||
|
||||
// Add this item to the worn set.
|
||||
newWornItemIds = [...newWornItemIds, itemIdToAdd];
|
||||
const { speciesId, colorId } = state;
|
||||
|
||||
setWornItemIds(newWornItemIds);
|
||||
setClosetedItemIds(newClosetedItemIds);
|
||||
},
|
||||
[wornItemIds, closetedItemIds, itemsById]
|
||||
// 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 allItemIds = [...state.wornItemIds, ...state.closetedItemIds];
|
||||
const { loading, error, itemsById } = useItemData(
|
||||
allItemIds,
|
||||
speciesId,
|
||||
colorId
|
||||
);
|
||||
|
||||
const zonesAndItems = getZonesAndItems(
|
||||
|
@ -60,9 +45,90 @@ function useOutfitState() {
|
|||
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) {
|
||||
|
|
|
@ -6109,6 +6109,11 @@ immer@1.10.0:
|
|||
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
||||
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:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
||||
|
|
Loading…
Reference in a new issue