diff --git a/generatePokemonData.ts b/generatePokemonData.ts index ddb4369..74aa813 100644 --- a/generatePokemonData.ts +++ b/generatePokemonData.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import Pokemon from 'pokemongo-json-pokedex/output/pokemon.json'; -import { IPokemon, IStats, League, Grade } from './src/ts/models/Pokemon'; +import { ILeaguePokemon, IPokemon, IStats, League, Grade } from './src/ts/app/models/Pokemon'; interface ICpAndTotalFound { [ key : number ] : Array; @@ -34,12 +34,12 @@ const getClosestCpMultiplierIndex = (value : number) => { const familyOrder : Array = []; const familyEvolutionOrder : { [ key : string ] : Array } = {}; -const familyEncountered : { [ key : string ] : Array } = {}; +const familyEncountered : { [ key : string ] : Array } = {}; Pokemon.forEach((mon) => { const baseAtk = mon.stats.baseAttack; const baseDef = mon.stats.baseDefense; const baseHp = mon.stats.baseStamina; - const pokemon : IPokemon = { + const pokemon : ILeaguePokemon = { id: mon.id, name: mon.name, dex: mon.dex, @@ -184,7 +184,7 @@ Pokemon.forEach((mon) => { }); }); -const pokemonOrder : Array<{ id : string, name : string }> = []; +const pokemonOrder : Array = []; familyOrder.forEach((familyId) => { familyEvolutionOrder[familyId].forEach((id, order) => { familyEncountered[familyId].some((pokemon, index) => { @@ -196,11 +196,9 @@ familyOrder.forEach((familyId) => { }); }); - familyEncountered[familyId].forEach((pokemon) => { - pokemonOrder.push({ - id: pokemon.id, - name: pokemon.name, - }); + familyEncountered[familyId].forEach((leaguePokemon) => { + const { pvp, ...pokemon } = leaguePokemon; + pokemonOrder.push(pokemon); }); }); diff --git a/src/ts/api/AjaxUtils.ts b/src/ts/api/AjaxUtils.ts new file mode 100644 index 0000000..cfaf652 --- /dev/null +++ b/src/ts/api/AjaxUtils.ts @@ -0,0 +1,223 @@ +export interface IRestApiRejection { + message : string; + status : number; + xhr : XMLHttpRequest; + error : Error | null; + type : string | null; + content : any | null; +} + +const buildQueryString = (queryObject : any) => { + const queryParameters : Array = []; + let queryString = ''; + Object.keys(queryObject || {}).forEach((key : string) => { + queryParameters.push(key + '=' + encodeURIComponent(queryObject[key])); + }); + if (queryParameters.length > 0) { + queryString = '?' + queryParameters.join('&'); + } + return queryString; +}; + +const handleXhrOnLoad = (resolve : (value : any) => void, reject : (reason : any) => void, xhr : XMLHttpRequest) : void => { + if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 204)) { + if (xhr.responseType === '' && xhr.responseText === '') { + return resolve(undefined); + } + if (xhr.responseType === 'text') { + return resolve(xhr.response); + } + try { + const responseJSON = JSON.parse(xhr.response); + + if (typeof(responseJSON.success) !== 'undefined' && !responseJSON.success) { + if (responseJSON.error === 'user_required') { + window.location.reload(); + } + } + + return resolve(responseJSON); + } catch (e) { + const rejection : IRestApiRejection = { + message: e.message, + status: xhr.status, + xhr, + error: e, + type: null, + content: null, + }; + // tslint:disable-next-line:no-console + console.error('Failed to parse response', xhr); + return reject(rejection); + } + } else { + return handleXhrOnLoadCaughtException(reject, xhr); + } +}; + +const handleXhrOnLoadCaughtException = (reject : (reason : any) => void, xhr : XMLHttpRequest) : void => { + if (xhr.status === 401) { + window.location.reload(); + } + + if (xhr.responseType === 'text' || xhr.responseType === '') { + const rejection : IRestApiRejection = { + message: xhr.responseText || xhr.statusText, + status: xhr.status, + xhr, + error: null, + type: null, + content: null, + }; + return reject(rejection); + } + try { + const responseJSON = JSON.parse(xhr.response); + + const rejection : IRestApiRejection = { + message: responseJSON.message || xhr.statusText, + status: xhr.status, + xhr, + error: null, + type: responseJSON.exception_type, + content: responseJSON.content, + }; + return reject(rejection); + } catch (e) { + const rejection : IRestApiRejection = { + message: e.message, + status: xhr.status, + xhr, + error: e, + type: null, + content: null, + }; + // tslint:disable-next-line:no-console + console.error('Failed to parse response', xhr); + return reject(rejection); + } +}; + +export const AjaxUtils = { + ajaxGet(url : string, queryObject? : any) : Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + const cacheBustingQueryObject = (queryObject || {}); // IE 11 caches GET requests + cacheBustingQueryObject._cache_busting_arg_ = +new Date(); // this parameter name must be in the backend's excluded parameter set (see api_utils.py) + + xhr.open('GET', url + buildQueryString(cacheBustingQueryObject)); + xhr.withCredentials = true; + xhr.onload = () => { + if (xhr.readyState === 4 && xhr.status === 200) { + try { + return resolve(JSON.parse(xhr.response)); + } catch (e) { + const rejection : IRestApiRejection = { + message: e.message, + status: xhr.status, + xhr, + error: e, + type: null, + content: null, + }; + // tslint:disable-next-line:no-console + console.error('Failure to parse response', xhr); + return reject(rejection); + } + } else { + return handleXhrOnLoadCaughtException(reject, xhr); + } + }; + xhr.onerror = (e) => { + return handleXhrOnLoadCaughtException(reject, xhr); + }; + xhr.send(); + }); + }, + + ajaxPost(url : string, postBody : any, queryObject? : any, type? : 'text' | 'json') : Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url + buildQueryString(queryObject)); + xhr.withCredentials = true; + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN); + xhr.onload = () => { + if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201)) { + if (type === 'text') { + return resolve(xhr.response); + } + try { + return resolve(JSON.parse(xhr.response)); + } catch (e) { + const rejection : IRestApiRejection = { + message: e.message, + status: xhr.status, + xhr, + error: e, + type: null, + content: null, + }; + // tslint:disable-next-line:no-console + console.error('Failure to parse response', xhr); + return reject(rejection); + } + } else { + return handleXhrOnLoadCaughtException(reject, xhr); + } + }; + xhr.onerror = (e) => { + return handleXhrOnLoadCaughtException(reject, xhr); + }; + xhr.send(JSON.stringify(postBody || undefined)); + }); + }, + + ajaxPostForm(url : string, formData? : FormData, queryObject? : any) : Promise { + // Lesman 4/24/2017 + // NOTE: you do not set the Content-Type on this request, + // the browser will automatically encode the form data and + // set the appropriate Content-Type. The encoding is browser + // specific so you MUST allow the browser to set the header + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url + buildQueryString(queryObject)); + xhr.withCredentials = true; + xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN); + xhr.onload = () => handleXhrOnLoad(resolve, reject, xhr); + xhr.onerror = (e) => { + return handleXhrOnLoadCaughtException(reject, xhr); + }; + xhr.send(formData); + }); + }, + + ajaxPut(url : string, postBody : any, queryObject? : any) : Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('PUT', url + buildQueryString(queryObject)); + xhr.withCredentials = true; + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN); + xhr.onload = () => handleXhrOnLoad(resolve, reject, xhr); + xhr.onerror = (e) => { + return handleXhrOnLoadCaughtException(reject, xhr); + }; + xhr.send(JSON.stringify(postBody || undefined)); + }); + }, + + ajaxDelete(url : string, queryObject? : any) : Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('DELETE', url + buildQueryString(queryObject)); + xhr.withCredentials = true; + xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN); + xhr.onload = () => handleXhrOnLoad(resolve, reject, xhr); + xhr.onerror = (e) => { + return handleXhrOnLoadCaughtException(reject, xhr); + }; + xhr.send(); + }); + }, +}; diff --git a/src/ts/api/PokemonService.ts b/src/ts/api/PokemonService.ts new file mode 100644 index 0000000..e542cde --- /dev/null +++ b/src/ts/api/PokemonService.ts @@ -0,0 +1,51 @@ +import { AjaxUtils } from 'src/ts/api/AjaxUtils'; + +import { IPokemon } from 'src/ts/app/models/Pokemon'; + +interface IPokemonService { + +} + +export class PokemonService implements IPokemonService { + public getPokemonList() { + const queryParameters = { + type: 'no touch', + is_active: true, + }; + + return AjaxUtils.ajaxGet('/api/billing/plan', queryParameters) + .then((response : Array) => { + return Promise.resolve(this.serializePokemonList(response)); + }); + } + + // does `object` need to be `any`? + private serializePokemonList(jsonPokemonList : Array) : Array { + const pokemonList = jsonPokemonList.map((jsonPokemon) => { + let pokemon : IPokemon | null = null; + try { + if (typeof jsonPokemon.name !== 'string') { + throw 'pokemon missing name'; + } + if (typeof jsonPokemon.id !== 'string') { + throw 'pokemon missing id'; + } + if (typeof jsonPokemon.family !== 'string') { + throw 'pokemon missing family'; + } + if (typeof jsonPokemon.dex !== 'number') { + throw 'pokemon missing dex'; + } + if (typeof jsonPokemon.stats !== 'object') { + throw 'pokemon missing stats'; + } + pokemon = { ...jsonPokemon }; + } catch (e) { + console.error(jsonPokemon, e.message); + } + return pokemon; + }); + // TODO should we allow `null`s? + return pokemonList; + } +} diff --git a/src/ts/index.tsx b/src/ts/app/index.tsx similarity index 100% rename from src/ts/index.tsx rename to src/ts/app/index.tsx diff --git a/src/ts/models/Pokemon.ts b/src/ts/app/models/Pokemon.ts similarity index 92% rename from src/ts/models/Pokemon.ts rename to src/ts/app/models/Pokemon.ts index 03264aa..e015192 100644 --- a/src/ts/models/Pokemon.ts +++ b/src/ts/app/models/Pokemon.ts @@ -13,13 +13,15 @@ export interface IBaseStats { baseStamina : number; } -export type League = 'great' | 'ultra'; export interface IPokemon { name : string; id : string; family : string; dex : number; stats : IBaseStats; +} +export type League = 'great' | 'ultra'; +export interface ILeaguePokemon extends IPokemon { pvp : { great : Array; ultra : Array;