2020-04-21 20:46:53 -07:00
|
|
|
import React from "react";
|
2020-04-24 18:39:38 -07:00
|
|
|
import gql from "graphql-tag";
|
|
|
|
import produce, { enableMapSet } from "immer";
|
2020-04-24 21:17:03 -07:00
|
|
|
import { useQuery, useApolloClient } from "@apollo/react-hooks";
|
2020-04-21 20:46:53 -07:00
|
|
|
|
2020-04-24 21:17:03 -07:00
|
|
|
import { itemAppearanceFragment } from "./OutfitPreview";
|
2020-04-21 20:46:53 -07:00
|
|
|
|
2020-04-24 18:39:38 -07:00
|
|
|
enableMapSet();
|
2020-04-21 20:46:53 -07:00
|
|
|
|
2020-04-24 18:39:38 -07:00
|
|
|
function useOutfitState() {
|
|
|
|
const apolloClient = useApolloClient();
|
2020-04-24 19:16:24 -07:00
|
|
|
const [state, dispatchToOutfit] = React.useReducer(
|
|
|
|
outfitStateReducer(apolloClient),
|
|
|
|
{
|
2020-04-25 22:13:55 -07:00
|
|
|
name: "Dress to Impress demo 💖",
|
|
|
|
wornItemIds: new Set(["51054", "35779", "35780", "37830"]),
|
|
|
|
closetedItemIds: new Set([
|
|
|
|
"76732",
|
|
|
|
"54393",
|
|
|
|
"80087",
|
|
|
|
"75997",
|
|
|
|
"57632",
|
|
|
|
"80052",
|
|
|
|
"67617",
|
|
|
|
"50861",
|
|
|
|
"77778",
|
|
|
|
"51164",
|
|
|
|
"62215",
|
|
|
|
"70660",
|
|
|
|
"74546",
|
|
|
|
"57997",
|
2020-04-24 19:16:24 -07:00
|
|
|
]),
|
2020-04-25 22:13:55 -07:00
|
|
|
speciesId: "24", // Starry
|
|
|
|
colorId: "62", // Zafara
|
2020-04-24 19:16:24 -07:00
|
|
|
}
|
|
|
|
);
|
2020-04-21 20:46:53 -07:00
|
|
|
|
2020-04-25 07:22:03 -07:00
|
|
|
React.useEffect(() => {
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
if (urlParams.has("species")) {
|
|
|
|
dispatchToOutfit({
|
|
|
|
type: "reset",
|
|
|
|
name: urlParams.get("name"),
|
|
|
|
speciesId: urlParams.get("species"),
|
|
|
|
colorId: urlParams.get("color"),
|
|
|
|
wornItemIds: urlParams.getAll("objects[]"),
|
|
|
|
closetedItemIds: urlParams.getAll("closet[]"),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
window.history.replaceState(null, "", window.location.href.split("?")[0]);
|
|
|
|
});
|
|
|
|
|
2020-04-24 23:29:26 -07:00
|
|
|
const { name, speciesId, colorId } = state;
|
2020-04-21 20:46:53 -07:00
|
|
|
|
2020-04-24 18:39:38 -07:00
|
|
|
// 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);
|
2020-04-21 20:46:53 -07:00
|
|
|
|
2020-04-24 18:39:38 -07:00
|
|
|
const allItemIds = [...state.wornItemIds, ...state.closetedItemIds];
|
2020-04-24 21:17:03 -07:00
|
|
|
const { loading, error, data } = useQuery(
|
|
|
|
gql`
|
|
|
|
query($allItemIds: [ID!]!, $speciesId: ID!, $colorId: ID!) {
|
|
|
|
items(ids: $allItemIds) {
|
|
|
|
# TODO: De-dupe this from SearchPanel?
|
|
|
|
id
|
|
|
|
name
|
|
|
|
thumbnailUrl
|
|
|
|
|
|
|
|
appearanceOn(speciesId: $speciesId, colorId: $colorId) {
|
|
|
|
# This enables us to quickly show the item when the user clicks it!
|
|
|
|
...AppearanceForOutfitPreview
|
|
|
|
|
|
|
|
# This is used to group items by zone, and to detect conflicts when
|
|
|
|
# wearing a new item.
|
|
|
|
layers {
|
|
|
|
zone {
|
|
|
|
id
|
|
|
|
label
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
${itemAppearanceFragment}
|
|
|
|
`,
|
|
|
|
{ variables: { allItemIds, speciesId, colorId } }
|
2020-04-21 20:46:53 -07:00
|
|
|
);
|
|
|
|
|
2020-04-24 21:17:03 -07:00
|
|
|
const items = (data && data.items) || [];
|
|
|
|
const itemsById = {};
|
|
|
|
for (const item of items) {
|
|
|
|
itemsById[item.id] = item;
|
|
|
|
}
|
|
|
|
|
2020-04-22 14:55:12 -07:00
|
|
|
const zonesAndItems = getZonesAndItems(
|
|
|
|
itemsById,
|
|
|
|
wornItemIds,
|
|
|
|
closetedItemIds
|
2020-04-21 20:46:53 -07:00
|
|
|
);
|
|
|
|
|
2020-04-24 19:16:24 -07:00
|
|
|
const outfitState = {
|
|
|
|
zonesAndItems,
|
2020-04-24 23:29:26 -07:00
|
|
|
name,
|
2020-04-24 19:16:24 -07:00
|
|
|
wornItemIds,
|
2020-04-25 07:22:03 -07:00
|
|
|
closetedItemIds,
|
2020-04-24 19:16:24 -07:00
|
|
|
allItemIds,
|
|
|
|
speciesId,
|
|
|
|
colorId,
|
|
|
|
};
|
2020-04-24 18:39:38 -07:00
|
|
|
|
2020-04-24 19:16:24 -07:00
|
|
|
return { loading, error, outfitState, dispatchToOutfit };
|
2020-04-24 18:39:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const outfitStateReducer = (apolloClient) => (baseState, action) => {
|
|
|
|
switch (action.type) {
|
2020-04-24 23:29:26 -07:00
|
|
|
case "rename":
|
|
|
|
return { ...baseState, name: action.outfitName };
|
2020-04-25 04:33:05 -07:00
|
|
|
case "changeColor":
|
|
|
|
return { ...baseState, colorId: action.colorId };
|
|
|
|
case "changeSpecies":
|
|
|
|
return { ...baseState, speciesId: action.speciesId };
|
2020-04-24 18:39:38 -07:00
|
|
|
case "wearItem":
|
|
|
|
return produce(baseState, (state) => {
|
2020-04-24 20:19:26 -07:00
|
|
|
// A hack to work around https://github.com/immerjs/immer/issues/586
|
|
|
|
state.wornItemIds.add("fake-id-immer#586").delete("fake-id-immer#586");
|
|
|
|
|
2020-04-24 19:16:24 -07:00
|
|
|
const { wornItemIds, closetedItemIds } = state;
|
2020-04-24 18:39:38 -07:00
|
|
|
const { itemId } = action;
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2020-04-24 20:19:26 -07:00
|
|
|
// Move this item from the closet to the worn set.
|
|
|
|
closetedItemIds.delete(itemId);
|
2020-04-24 18:39:38 -07:00
|
|
|
wornItemIds.add(itemId);
|
|
|
|
});
|
2020-04-24 20:19:26 -07:00
|
|
|
case "unwearItem":
|
|
|
|
return produce(baseState, (state) => {
|
|
|
|
const { wornItemIds, closetedItemIds } = state;
|
|
|
|
const { itemId } = action;
|
|
|
|
|
|
|
|
// Move this item from the worn set to the closet.
|
|
|
|
wornItemIds.delete(itemId);
|
|
|
|
closetedItemIds.add(itemId);
|
|
|
|
});
|
2020-04-25 00:22:49 -07:00
|
|
|
case "removeItem":
|
|
|
|
return produce(baseState, (state) => {
|
|
|
|
const { wornItemIds, closetedItemIds } = state;
|
|
|
|
const { itemId } = action;
|
|
|
|
|
|
|
|
// Remove this item from both the worn set and the closet.
|
|
|
|
wornItemIds.delete(itemId);
|
|
|
|
closetedItemIds.delete(itemId);
|
|
|
|
});
|
2020-04-25 05:29:27 -07:00
|
|
|
case "reset":
|
|
|
|
const { name, speciesId, colorId, wornItemIds, closetedItemIds } = action;
|
|
|
|
return {
|
|
|
|
name,
|
2020-04-25 07:22:03 -07:00
|
|
|
speciesId: speciesId ? String(speciesId) : baseState.speciesId,
|
|
|
|
colorId: colorId ? String(colorId) : baseState.colorId,
|
|
|
|
wornItemIds: wornItemIds
|
|
|
|
? new Set(wornItemIds.map(String))
|
|
|
|
: baseState.wornItemIds,
|
|
|
|
closetedItemIds: closetedItemIds
|
|
|
|
? new Set(closetedItemIds.map(String))
|
|
|
|
: baseState.closetedItemIds,
|
2020-04-25 05:29:27 -07:00
|
|
|
};
|
2020-04-24 18:39:38 -07:00
|
|
|
default:
|
2020-04-25 04:33:05 -07:00
|
|
|
throw new Error(`unexpected action ${JSON.stringify(action)}`);
|
2020-04-24 18:39:38 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-04-25 22:12:05 -07:00
|
|
|
|
|
|
|
restrictedZones {
|
|
|
|
id
|
|
|
|
}
|
2020-04-24 18:39:38 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
variables: {
|
|
|
|
itemIds: [itemIdToAdd, ...wornItemIds],
|
|
|
|
speciesId,
|
|
|
|
colorId,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const itemToAdd = items.find((i) => i.id === itemIdToAdd);
|
2020-04-25 04:46:40 -07:00
|
|
|
if (!itemToAdd.appearanceOn) {
|
|
|
|
return [];
|
|
|
|
}
|
2020-04-25 22:12:05 -07:00
|
|
|
const itemToAddZoneIds = [
|
|
|
|
...itemToAdd.appearanceOn.layers.map((l) => l.zone.id),
|
|
|
|
...itemToAdd.appearanceOn.restrictedZones.map((z) => z.id),
|
|
|
|
];
|
2020-04-24 18:39:38 -07:00
|
|
|
const wornItems = Array.from(wornItemIds).map((id) =>
|
|
|
|
items.find((i) => i.id === id)
|
|
|
|
);
|
|
|
|
|
|
|
|
const conflictingIds = [];
|
|
|
|
for (const wornItem of wornItems) {
|
2020-04-25 04:46:40 -07:00
|
|
|
if (!wornItem.appearanceOn) {
|
|
|
|
continue;
|
|
|
|
}
|
2020-04-25 22:12:05 -07:00
|
|
|
const wornItemZoneIds = [
|
|
|
|
...wornItem.appearanceOn.layers.map((l) => l.zone.id),
|
|
|
|
...wornItem.appearanceOn.restrictedZones.map((z) => z.id),
|
|
|
|
];
|
2020-04-24 18:39:38 -07:00
|
|
|
|
|
|
|
const hasConflict = wornItemZoneIds.some((zid) =>
|
|
|
|
itemToAddZoneIds.includes(zid)
|
|
|
|
);
|
|
|
|
if (hasConflict) {
|
|
|
|
conflictingIds.push(wornItem.id);
|
|
|
|
}
|
|
|
|
}
|
2020-04-22 14:55:12 -07:00
|
|
|
|
2020-04-24 18:39:38 -07:00
|
|
|
return conflictingIds;
|
2020-04-22 14:55:12 -07:00
|
|
|
}
|
|
|
|
|
2020-04-24 19:16:24 -07:00
|
|
|
// TODO: Get this out of here, tbh...
|
2020-04-22 14:55:12 -07:00
|
|
|
function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) {
|
|
|
|
const wornItems = wornItemIds.map((id) => itemsById[id]).filter((i) => i);
|
|
|
|
const closetedItems = closetedItemIds
|
|
|
|
.map((id) => itemsById[id])
|
|
|
|
.filter((i) => i);
|
|
|
|
|
2020-04-25 00:46:25 -07:00
|
|
|
// We use zone label here, rather than ID, because some zones have the same
|
|
|
|
// label and we *want* to over-simplify that in this UI. (e.g. there are
|
|
|
|
// multiple Hat zones, and some items occupy different ones, but mostly let's
|
|
|
|
// just group them and if they don't conflict then all the better!)
|
2020-04-21 20:46:53 -07:00
|
|
|
const allItems = [...wornItems, ...closetedItems];
|
2020-04-25 00:46:25 -07:00
|
|
|
const itemsByZoneLabel = new Map();
|
2020-04-24 19:16:24 -07:00
|
|
|
for (const item of allItems) {
|
2020-04-25 04:38:55 -07:00
|
|
|
if (!item.appearanceOn) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-04-24 19:16:24 -07:00
|
|
|
for (const layer of item.appearanceOn.layers) {
|
2020-04-25 00:46:25 -07:00
|
|
|
const zoneLabel = layer.zone.label;
|
2020-04-24 19:16:24 -07:00
|
|
|
|
2020-04-25 00:46:25 -07:00
|
|
|
if (!itemsByZoneLabel.has(zoneLabel)) {
|
|
|
|
itemsByZoneLabel.set(zoneLabel, []);
|
2020-04-24 19:16:24 -07:00
|
|
|
}
|
2020-04-25 00:46:25 -07:00
|
|
|
itemsByZoneLabel.get(zoneLabel).push(item);
|
2020-04-24 19:16:24 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-25 00:46:25 -07:00
|
|
|
const zonesAndItems = Array.from(itemsByZoneLabel.entries()).map(
|
|
|
|
([zoneLabel, items]) => ({
|
|
|
|
zoneLabel: zoneLabel,
|
2020-04-24 19:16:24 -07:00
|
|
|
items: [...items].sort((a, b) => a.name.localeCompare(b.name)),
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2020-04-25 00:46:25 -07:00
|
|
|
zonesAndItems.sort((a, b) => a.zoneLabel.localeCompare(b.zoneLabel));
|
2020-04-21 20:46:53 -07:00
|
|
|
|
2020-04-22 14:55:12 -07:00
|
|
|
return zonesAndItems;
|
2020-04-21 20:46:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export default useOutfitState;
|