diff --git a/dev-todos.txt b/dev-todos.txt index 1c2e865..ae169b6 100644 --- a/dev-todos.txt +++ b/dev-todos.txt @@ -3,3 +3,9 @@ * Restore good download behavior: use crossOrigin for everything, and remove cache-buster in the URL we use for canvas * Undo the local linking we did for @chakra-ui/core, react, and react-dom on Matchu's machine 😅 * Present invalidated items somewhere on the screen, instead of them just vanishing? :/ + +Features: + * Outfit sharing + * Search by zone + * Outfit saving + * Use the space better on big screens? diff --git a/package.json b/package.json index 05ae20f..6a3d290 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "graphql": "^15.0.0", "immer": "^6.0.3", "mysql2": "^2.1.0", + "node-fetch": "^2.6.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.1", diff --git a/src/OutfitResetModal.js b/src/OutfitResetModal.js index 97989da..b7001a2 100644 --- a/src/OutfitResetModal.js +++ b/src/OutfitResetModal.js @@ -1,4 +1,6 @@ import React from "react"; +import gql from "graphql-tag"; +import { useQuery } from "@apollo/react-hooks"; import { Text, Modal, @@ -17,24 +19,45 @@ import { function OutfitResetModal({ isOpen, onClose, dispatchToOutfit }) { const [petName, setPetName] = React.useState(""); + const [submittedPetName, submitPetName] = React.useState(""); - const onComplete = ({ custom_pet, object_info_registry }) => { - dispatchToOutfit({ - type: "reset", - name: custom_pet.name, - speciesId: custom_pet.species_id, - colorId: custom_pet.color_id, - wornItemIds: Object.values(object_info_registry).map( - (o) => o.obj_info_id - ), - closetedItemIds: [], - }); - onClose(); - setPetName(""); - }; - const { loading, error, loadOutfitData } = useLoadOutfitData( - petName, - onComplete + const { loading, error } = useQuery( + gql` + query($petName: String!) { + petOnNeopetsDotCom(petName: $petName) { + color { + id + } + species { + id + } + items { + id + } + } + } + `, + { + variables: { petName: submittedPetName }, + skip: !submittedPetName, + fetchPolicy: "network-only", + onCompleted: (data) => { + if (!data) return; + + const { species, color, items } = data.petOnNeopetsDotCom; + dispatchToOutfit({ + type: "reset", + name: petName, + speciesId: species.id, + colorId: color.id, + wornItemIds: items.map((i) => i.id), + closetedItemIds: [], + }); + onClose(); + setPetName(""); + submitPetName(""); + }, + } ); const clearOutfit = () => { @@ -46,6 +69,7 @@ function OutfitResetModal({ isOpen, onClose, dispatchToOutfit }) { }); onClose(); setPetName(""); + submitPetName(""); }; return ( @@ -55,7 +79,7 @@ function OutfitResetModal({ isOpen, onClose, dispatchToOutfit }) {
{ e.preventDefault(); - loadOutfitData(); + submitPetName(petName); }} > @@ -110,38 +134,4 @@ function OutfitResetModal({ isOpen, onClose, dispatchToOutfit }) { ); } -function useLoadOutfitData(petName, onComplete) { - const [loading, setLoading] = React.useState(false); - const [error, setError] = React.useState(null); - - const loadOutfitData = async () => { - setLoading(true); - setError(null); - - let json; - try { - const res = await fetch( - `http://www.neopets.com/amfphp/json.php/CustomPetService.getViewerData` + - `/${petName}` - ); - if (!res.ok) { - throw new Error(res.statusText); - } - json = await res.json(); - if (!json.custom_pet) { - throw new Error(`missing custom_pet data`); - } - } catch (e) { - setLoading(false); - setError(e); - return; - } - - setLoading(false); - onComplete(json); - }; - - return { loading, error, loadOutfitData }; -} - export default OutfitResetModal; diff --git a/src/server/__snapshots__/index.test.js.snap b/src/server/__snapshots__/index.test.js.snap index 3557663..fed95f9 100644 --- a/src/server/__snapshots__/index.test.js.snap +++ b/src/server/__snapshots__/index.test.js.snap @@ -1,5 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Pet looks up a pet 1`] = ` +Object { + "petOnNeopetsDotCom": Object { + "color": Object { + "id": "75", + }, + "items": Array [ + Object { + "id": "37229", + }, + Object { + "id": "37375", + }, + Object { + "id": "38911", + }, + Object { + "id": "38912", + }, + Object { + "id": "38913", + }, + Object { + "id": "43014", + }, + Object { + "id": "43397", + }, + Object { + "id": "48313", + }, + ], + "species": Object { + "id": "54", + }, + }, +} +`; + exports[`SpeciesColorPair gets them all 1`] = ` Object { "allValidSpeciesColorPairs": Array [ diff --git a/src/server/index.js b/src/server/index.js index e9f56a9..672bcb3 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -2,6 +2,7 @@ const { gql } = require("apollo-server"); const connectToDb = require("./db"); const buildLoaders = require("./loaders"); +const neopets = require("./neopets"); const { capitalize } = require("./util"); const typeDefs = gql` @@ -55,6 +56,12 @@ const typeDefs = gql` color: Color! } + type Outfit { + species: Species! + color: Color! + items: [Item!]! + } + type Query { allColors: [Color!]! allSpecies: [Species!]! @@ -70,6 +77,8 @@ const typeDefs = gql` limit: Int ): ItemSearchResult! petAppearance(speciesId: ID!, colorId: ID!): Appearance + + petOnNeopetsDotCom(petName: String!): Outfit } `; @@ -210,6 +219,17 @@ const resolvers = { const swfAssets = await petSwfAssetLoader.load(petState.id); return { layers: swfAssets, restrictedZones: [] }; }, + petOnNeopetsDotCom: async (_, { petName }) => { + const petData = await neopets.loadPetData(petName); + const outfit = { + species: { id: petData.custom_pet.species_id }, + color: { id: petData.custom_pet.color_id }, + items: Object.values(petData.object_info_registry).map((o) => ({ + id: o.obj_info_id, + })), + }; + return outfit; + }, }, }; diff --git a/src/server/index.test.js b/src/server/index.test.js index 95d7093..31c2ca2 100644 --- a/src/server/index.test.js +++ b/src/server/index.test.js @@ -1680,6 +1680,32 @@ describe("SpeciesColorPair", () => { }); }); +describe("Pet", () => { + it("looks up a pet", async () => { + const res = await query({ + query: gql` + query { + petOnNeopetsDotCom(petName: "roopal27") { + species { + id + } + color { + id + } + items { + id + } + } + } + `, + }); + + expect(res).toHaveNoErrors(); + expect(res.data).toMatchSnapshot(); + expect(queryFn.mock.calls).toMatchInlineSnapshot(`Array []`); + }); +}); + expect.extend({ toHaveNoErrors(res) { if (res.errors) { diff --git a/src/server/neopets.js b/src/server/neopets.js new file mode 100644 index 0000000..945e1f9 --- /dev/null +++ b/src/server/neopets.js @@ -0,0 +1,20 @@ +const fetch = require("node-fetch"); + +async function loadPetData(petName) { + const res = await fetch( + `http://www.neopets.com/amfphp/json.php/CustomPetService.getViewerData` + + `/${petName}` + ); + if (!res.ok) { + throw new Error(`neopets.com returned: ${res.statusText}`); + } + + const json = await res.json(); + if (!json.custom_pet) { + throw new Error(`missing custom_pet data`); + } + + return json; +} + +module.exports = { loadPetData }; diff --git a/yarn.lock b/yarn.lock index ecc790b..78b0f1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7905,7 +7905,7 @@ no-case@^3.0.3: lower-case "^2.0.1" tslib "^1.10.0" -node-fetch@^2.1.2, node-fetch@^2.2.0: +node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==