diff --git a/.vscode/settings.json b/.vscode/settings.json index 761cd6f..a1d5d55 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "editor.formatOnSave": true, - "editor.tabSize": 2 + "editor.tabSize": 2, + "jest.pathToJest": "yarn test" } \ No newline at end of file diff --git a/src/ItemList.js b/src/ItemList.js index d324d8a..659d2a2 100644 --- a/src/ItemList.js +++ b/src/ItemList.js @@ -26,7 +26,7 @@ function Item({ item, isWorn, onWear }) { cursor="pointer" onClick={onWear} > - + {item.name} diff --git a/src/WardrobePage.js b/src/WardrobePage.js index 7af1483..4fafb59 100644 --- a/src/WardrobePage.js +++ b/src/WardrobePage.js @@ -1,6 +1,4 @@ import React from "react"; -import gql from "graphql-tag"; -import { useQuery } from "@apollo/react-hooks"; import { Box, Editable, @@ -27,24 +25,14 @@ import useOutfitState from "./useOutfitState.js"; import { ITEMS } from "./data"; function WardrobePage() { - const { loading, error, data: datax } = useQuery(gql` - query { - items(ids: [38913, 38911]) { - id - name - } - } - `); - console.log(loading, error, datax); - - const [data, wearItemRaw] = useOutfitState(); + const { loading, error, data, wearItem } = useOutfitState(); const [searchQuery, setSearchQuery] = React.useState(""); const toast = useToast(); const [hasSentToast, setHasSentToast] = React.useState(false); - const wearItem = React.useCallback( + const wearItemAndToast = React.useCallback( (itemIdToAdd) => { - wearItemRaw(itemIdToAdd); + wearItem(itemIdToAdd); if (!hasSentToast) { setTimeout(() => { @@ -62,7 +50,7 @@ function WardrobePage() { setHasSentToast(true); } }, - [toast, wearItemRaw, hasSentToast, setHasSentToast] + [toast, wearItem, hasSentToast, setHasSentToast] ); return ( @@ -104,12 +92,12 @@ function WardrobePage() { ) : ( )} diff --git a/src/data.js b/src/data.js index ef7435d..52d9313 100644 --- a/src/data.js +++ b/src/data.js @@ -1,56 +1,56 @@ export const ITEMS = [ { - id: 1, - name: "Zafara Agent Gloves", - thumbnailSrc: "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", + id: "38913", + // name: "Zafara Agent Gloves", + // thumbnailSrc: "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", zoneName: "Gloves", }, { - id: 2, - name: "Zafara Agent Hood", - thumbnailSrc: "http://images.neopets.com/items/clo_zafara_agent_hood.gif", + id: "38911", + // name: "Zafara Agent Hood", + // thumbnailSrc: "http://images.neopets.com/items/clo_zafara_agent_hood.gif", zoneName: "Hat", }, { - id: 3, - name: "Zafara Agent Robe", - thumbnailSrc: "http://images.neopets.com/items/clo_zafara_agent_robe.gif", + id: "38912", + // name: "Zafara Agent Robe", + // thumbnailSrc: "http://images.neopets.com/items/clo_zafara_agent_robe.gif", zoneName: "Jacket", }, { - id: 4, - name: "Moon and Stars Background", - thumbnailSrc: "http://images.neopets.com/items/bg_moonstars.gif", + id: "37375", + // name: "Moon and Stars Background", + // thumbnailSrc: "http://images.neopets.com/items/bg_moonstars.gif", zoneName: "Background", }, { - id: 5, - name: "Altador Forest Background", - thumbnailSrc: "http://images.neopets.com/items/bg_ddy18_altadorforest.gif", + id: "74166", + // name: "Altador Forest Background", + // thumbnailSrc: "http://images.neopets.com/items/bg_ddy18_altadorforest.gif", zoneName: "Background", }, { - id: 6, - name: "Altador Cup Brooch", - thumbnailSrc: "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", + id: "48313", + // name: "Altador Cup Brooch", + // thumbnailSrc: "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", zoneName: "Collar", }, { - id: 7, - name: "Magic Ball Table", - thumbnailSrc: "http://images.neopets.com/items/gif_magicball_table.gif", + id: "37229", + // name: "Magic Ball Table", + // thumbnailSrc: "http://images.neopets.com/items/gif_magicball_table.gif", zoneName: "Lower Foreground Item", }, { - id: 8, - name: "Green Leaf String Lights", - thumbnailSrc: "http://images.neopets.com/items/toy_stringlight_illleaf.gif", + id: "43014", + // name: "Green Leaf String Lights", + // thumbnailSrc: "http://images.neopets.com/items/toy_stringlight_illleaf.gif", zoneName: "Background Item", }, { - id: 9, - name: "Jewelled Staff", - thumbnailSrc: "http://images.neopets.com/items/mall_staff_jewelled.gif", + id: "43397", + // name: "Jewelled Staff", + // thumbnailSrc: "http://images.neopets.com/items/mall_staff_jewelled.gif", zoneName: "Left-hand item", }, ]; diff --git a/src/server/apollo-server-vercel.js b/src/server/apollo-server-vercel.js index 9e9bb42..082c2bc 100644 --- a/src/server/apollo-server-vercel.js +++ b/src/server/apollo-server-vercel.js @@ -177,10 +177,15 @@ class ApolloServer extends ApolloServerBase { ...this.playgroundOptions, }; - return setHeaders(res, { - "Content-Type": "text/html", - ...requestCorsHeadersObject, - }).send(renderPlaygroundPage(playgroundRenderPageOptions)); + return setHeaders( + res, + new Headers({ + "Content-Type": "text/html", + ...requestCorsHeadersObject, + }) + ) + .status(200) + .send(renderPlaygroundPage(playgroundRenderPageOptions)); } } diff --git a/src/server/index.js b/src/server/index.js index 2d909df..8f95acd 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -7,6 +7,7 @@ const typeDefs = gql` type Item { id: ID! name: String! + thumbnailUrl: String! } type Query { diff --git a/src/server/index.test.js b/src/server/index.test.js index c52c728..269235c 100644 --- a/src/server/index.test.js +++ b/src/server/index.test.js @@ -30,6 +30,7 @@ it("can load items", async () => { items(ids: $ids) { id name + thumbnailUrl } } `, @@ -49,14 +50,17 @@ it("can load items", async () => { Object { "id": "38911", "name": "Zafara Agent Hood", + "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_hood.gif", }, Object { "id": "38912", "name": "Zafara Agent Robe", + "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif", }, Object { "id": "38913", "name": "Zafara Agent Gloves", + "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", }, ], } diff --git a/src/server/loaders.js b/src/server/loaders.js index bf1c026..c4c4275 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -6,8 +6,8 @@ async function loadItems(db, ids) { `SELECT * FROM items WHERE id IN (${qs})`, ids ); - - return rows; + const entities = rows.map(normalizeProperties); + return entities; } const buildItemTranslationLoader = (db) => @@ -17,14 +17,24 @@ const buildItemTranslationLoader = (db) => `SELECT * FROM item_translations WHERE item_id IN (${qs}) AND locale = "en"`, itemIds ); + const entities = rows.map(normalizeProperties); - const rowsByItemId = new Map(rows.map((row) => [row.item_id, row])); + const entitiesByItemId = new Map(entities.map((e) => [e.itemId, e])); return itemIds.map( (itemId) => - rowsByItemId.get(itemId) || + entitiesByItemId.get(itemId) || new Error(`could not find translation for item ${itemId}`) ); }); +function normalizeProperties(row) { + const normalizedRow = {}; + for (const [key, value] of Object.entries(row)) { + const normalizedKey = key.replace(/_([a-z])/gi, (m) => m[1].toUpperCase()); + normalizedRow[normalizedKey] = value; + } + return normalizedRow; +} + module.exports = { loadItems, buildItemTranslationLoader }; diff --git a/src/useItemData.js b/src/useItemData.js new file mode 100644 index 0000000..307fa0d --- /dev/null +++ b/src/useItemData.js @@ -0,0 +1,33 @@ +import gql from "graphql-tag"; +import { useQuery } from "@apollo/react-hooks"; + +import { ITEMS } from "./data"; + +function useItemData(itemIds) { + const { loading, error, data } = useQuery( + gql` + query($itemIds: [ID!]!) { + items(ids: $itemIds) { + id + name + thumbnailUrl + } + } + `, + { variables: { itemIds } } + ); + + const items = (data && data.items) || []; + const itemsById = {}; + for (const item of items) { + const hardcodedItem = ITEMS.find((i) => i.id === item.id); + itemsById[item.id] = { + ...hardcodedItem, + ...item, + }; + } + + return { loading, error, itemsById }; +} + +export default useItemData; diff --git a/src/useOutfitState.js b/src/useOutfitState.js index b52d623..77e0e4a 100644 --- a/src/useOutfitState.js +++ b/src/useOutfitState.js @@ -1,19 +1,23 @@ import React from "react"; -import { ITEMS } from "./data.js"; +import useItemData from "./useItemData"; function useOutfitState() { const [wornItemIds, setWornItemIds] = React.useState([ - 1, - 2, - 3, - 4, - 6, - 7, - 8, - 9, + "38913", + "38911", + "38912", + "37375", + "48313", + "37229", + "43014", + "43397", ]); - const [closetedItemIds, setClosetedItemIds] = React.useState([5]); + const [closetedItemIds, setClosetedItemIds] = React.useState(["74166"]); + + const allItemIds = [...wornItemIds, ...closetedItemIds]; + + const { loading, error, itemsById } = useItemData(allItemIds); const wearItem = React.useCallback( (itemIdToAdd) => { @@ -24,7 +28,7 @@ function useOutfitState() { let newWornItemIds = wornItemIds; let newClosetedItemIds = closetedItemIds; - const itemToAdd = ITEMS.find((item) => item.id === itemIdToAdd); + const itemToAdd = itemsById[itemIdToAdd]; // Move the item out of the closet. newClosetedItemIds = newClosetedItemIds.filter( @@ -33,7 +37,7 @@ function useOutfitState() { // Move conflicting items to the closet. const conflictingItemIds = newWornItemIds.filter((wornItemId) => { - const wornItem = ITEMS.find((item) => item.id === wornItemId); + const wornItem = itemsById[wornItemId]; return wornItem.zoneName === itemToAdd.zoneName; }); newWornItemIds = newWornItemIds.filter( @@ -47,16 +51,26 @@ function useOutfitState() { setWornItemIds(newWornItemIds); setClosetedItemIds(newClosetedItemIds); }, - [wornItemIds, setWornItemIds, closetedItemIds, setClosetedItemIds] + [wornItemIds, closetedItemIds, itemsById] ); - const wornItems = wornItemIds.map((id) => - ITEMS.find((item) => item.id === id) - ); - const closetedItems = closetedItemIds.map((id) => - ITEMS.find((item) => item.id === id) + const zonesAndItems = getZonesAndItems( + itemsById, + wornItemIds, + closetedItemIds ); + const data = { zonesAndItems, wornItemIds }; + + return { loading, error, data, wearItem }; +} + +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); + const allItems = [...wornItems, ...closetedItems]; const allZoneNames = [...new Set(allItems.map((item) => item.zoneName))]; allZoneNames.sort(); @@ -70,9 +84,7 @@ function useOutfitState() { return { zoneName, items, wornItemId }; }); - const data = { zonesAndItems, wornItemIds }; - - return [data, wearItem]; + return zonesAndItems; } export default useOutfitState;