From e5b59c21e153b6d43534b01d9e410202ae513e3b Mon Sep 17 00:00:00 2001 From: Miles B Huff Date: Tue, 17 Oct 2023 02:54:32 -0400 Subject: [PATCH] Evolutions display as chains, but not as trees RIP Eevee --- README.md | 6 +++ frontend/TODO.md | 3 +- .../app/src/redux/slices/pokeapi.slice.ts | 19 ++++++- .../src/utilities/get-id-from-url.function.ts | 2 + frontend/app/src/views/pokemon-info.tsx | 21 +++++--- frontend/app/src/views/search-results.tsx | 8 +-- .../app/src/widgets/evolutions-viewer.tsx | 52 ++++++++++++++----- 7 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 frontend/app/src/utilities/get-id-from-url.function.ts diff --git a/README.md b/README.md index c0a3ebb..818d719 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,12 @@ I also went to lengths to try to ensure I did not infringe upon Nintendo's intel ### Unfinished objectives +#### Complex Pokémon evolutions + +I implemented simple (linear) evolutions, but not complex (branching) evolutions (like Eevee's). +Time constraints being what they are, I've decided to go with the current 90% solution. +If I *were* to go about implementing trees, I'd get to have the most "fun" I've had with data structures since college. I might do this later just for the sake of the challenge. + #### Automated unit testing I did not do the bonus objective that called for unit-testing the application. Time constraints being what they were, I decided to triage this, since I know the company does not generally use automated unit testing in its UI (instead relying on a mixture of manual QA and end-to-end testing). I did, however, ensure that [Jest](https://jestjs.io) was at least present and ready to go (by integrating `vite-template-redux` into the project). diff --git a/frontend/TODO.md b/frontend/TODO.md index feb337b..dcdbe8c 100644 --- a/frontend/TODO.md +++ b/frontend/TODO.md @@ -4,10 +4,8 @@ My personal remaining TODO. ## Required: - In the ReadMe, explain what changes might be needed if this app were intended to be run in a "concurrent" environment. -- *Remove console logs* ## Bonus: -- Implement evolution viewing - *Make it so that pressing the down arrow while the search history dropdown is open will move the focus into the dropdown* - *Make it possible to change the orientation and shininess of the sprite* - *When visiting a search page, add it to the search history. (useful for when people visit from a bookmark)* @@ -21,6 +19,7 @@ My personal remaining TODO. - *Implement localization* ## Not doing: +- Implement evolution **trees** (chains are already implemented) - *Add animations to make the site flow* - *Make the "history" dropdown display a list of possible Pokémon names (given the current input) instead of the history once the user has started typing something* - Implement automated unit testing for business functionality diff --git a/frontend/app/src/redux/slices/pokeapi.slice.ts b/frontend/app/src/redux/slices/pokeapi.slice.ts index 5f8dcf4..d72beef 100644 --- a/frontend/app/src/redux/slices/pokeapi.slice.ts +++ b/frontend/app/src/redux/slices/pokeapi.slice.ts @@ -1,7 +1,7 @@ import {urlifyParams} from '@/utilities/urlify-params.function'; import {urlifyPath} from '@/utilities/urlify-path.function'; import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react'; -import {EvolutionChain, NamedAPIResourceList, Pokemon} from 'pokenode-ts'; +import {EvolutionChain, NamedAPIResourceList, Pokemon, PokemonSpecies} from 'pokenode-ts'; //////////////////////////////////////////////////////////////////////////////// export interface PokeapiQueryOptions { @@ -20,21 +20,38 @@ export const pokeapiSlice = createApi({ baseUrl: 'https://pokeapi.co/api/v2', }), endpoints: builder => ({ + evolutionsById: builder.query({ query: (id, options?: PokeapiQueryOptions) => '/evolution-chain' + urlifyPath(id.toString(10)) + urlifyParams(options ?? {}), }), + evolutionsList: builder.query({ + query: (options?: PokeapiQueryOptions) => '/evolution-chain' + urlifyParams(options ?? {}), + }), + + speciesById: builder.query({ + query: (id, options?: PokeapiQueryOptions) => '/pokemon-species' + urlifyPath(id.toString(10)) + urlifyParams(options ?? {}), + }), + speciesList: builder.query({ + query: (options?: PokeapiQueryOptions) => '/pokemon-species' + urlifyParams(options ?? {}), + }), + pokemonById: builder.query({ query: (id, options?: PokeapiQueryOptions) => '/pokemon' + urlifyPath(id.toString(10)) + urlifyParams(options ?? {}), }), pokemonList: builder.query({ query: (options?: PokeapiQueryOptions) => '/pokemon' + urlifyParams(options ?? {}), }), + }), }); //////////////////////////////////////////////////////////////////////////////// export const { useEvolutionsByIdQuery, + + useSpeciesByIdQuery, + useSpeciesListQuery, + usePokemonByIdQuery, usePokemonListQuery, } = pokeapiSlice; diff --git a/frontend/app/src/utilities/get-id-from-url.function.ts b/frontend/app/src/utilities/get-id-from-url.function.ts new file mode 100644 index 0000000..e1fce65 --- /dev/null +++ b/frontend/app/src/utilities/get-id-from-url.function.ts @@ -0,0 +1,2 @@ +//////////////////////////////////////////////////////////////////////////////// +export const getIdFromUrl = (url: string): number => parseInt(url.replace(/^.*\/(\d+)\//, '$1')); diff --git a/frontend/app/src/views/pokemon-info.tsx b/frontend/app/src/views/pokemon-info.tsx index 3d1a3ae..7162f91 100644 --- a/frontend/app/src/views/pokemon-info.tsx +++ b/frontend/app/src/views/pokemon-info.tsx @@ -1,5 +1,6 @@ -import {usePokemonByIdQuery} from '@/redux/slices/pokeapi.slice.ts'; +import {usePokemonByIdQuery, useSpeciesByIdQuery} from '@/redux/slices/pokeapi.slice.ts'; import {displayifyName} from '@/utilities/displayify-name.function'; +import {getIdFromUrl} from '@/utilities/get-id-from-url.function.ts'; import {EvolutionsViewer} from '@/widgets/evolutions-viewer.tsx'; import {PokemonTypes} from '@/widgets/pokemon-types.tsx'; import {Spinner} from '@/widgets/spinner.tsx'; @@ -37,15 +38,17 @@ export const PokemonInfo: FunctionComponent = () => { //////////////////////////////////////////////////////////////////////////////// export const PokemonInfoCore: FunctionComponent<{id: number}> = props => { - const {data: pokemon, error, isLoading: loading} = usePokemonByIdQuery(props.id); - useEffect(() => console.debug(pokemon), [pokemon]); + const {data: pokemon, error: pokemonError, isLoading: pokemonLoading} = usePokemonByIdQuery(props.id); + // useEffect(() => console.debug(pokemon), [pokemon]); + const {data: species, isLoading: speciesLoading} = useSpeciesByIdQuery(props.id); + // useEffect(() => console.debug(species), [species]); // // // // // // // // // // // // // // // // // // // // return <> - {loading ? <> + {pokemonLoading || speciesLoading ? <>

Loading...

- : error ? <> + : pokemonError ? <>

Pokémon #{props.id}

Failed to load data!

Did you enter a valid Pokédex index?

@@ -55,7 +58,7 @@ export const PokemonInfoCore: FunctionComponent<{id: number}> = props => { : <>

{displayifyName(pokemon.name)} (#{pokemon.id})

    - {pokemon.name === pokemon.species.name ? null : + {pokemon.name === pokemon.name ? null :
  • Species: {displayifyName(pokemon.species.name)}
  • @@ -78,8 +81,12 @@ export const PokemonInfoCore: FunctionComponent<{id: number}> = props => { {index === pokemon.moves.length - 1 && pokemon.moves.length > 1 ? '.' : ''} )} + {!species ? null : +
  • Evolution Tree: + +
  • + }
- } ; }; diff --git a/frontend/app/src/views/search-results.tsx b/frontend/app/src/views/search-results.tsx index f34e492..98d4f98 100644 --- a/frontend/app/src/views/search-results.tsx +++ b/frontend/app/src/views/search-results.tsx @@ -1,6 +1,7 @@ -import {usePokemonListQuery} from '@/redux/slices/pokeapi.slice.ts'; +import {useSpeciesListQuery} from '@/redux/slices/pokeapi.slice.ts'; import {BasicPokemonInfo} from '@/types/pokemon.type.ts'; import {displayifyName} from '@/utilities/displayify-name.function'; +import {getIdFromUrl} from '@/utilities/get-id-from-url.function.ts'; import {Spinner} from '@/widgets/spinner.tsx'; import {FunctionComponent, MouseEventHandler, useEffect, useState} from 'react'; import {useNavigate, useSearchParams} from 'react-router-dom'; @@ -22,7 +23,8 @@ export const SearchResults: FunctionComponent = () => { useEffect(parseQuery, [searchParams]); // // // // // // // // // // // // // // // // // // // // - const {data: pokemonsData, error: pokemonsError, isLoading: pokemonsLoading} = usePokemonListQuery({offset: 0, limit: 9999}); //NOTE: `Infinity` doesn't work, so I'm using an arbitrarily high number instead. + const {data: pokemonsData, error: pokemonsError, isLoading: pokemonsLoading} = useSpeciesListQuery({offset: 0, limit: 9999}); //NOTE: `Infinity` doesn't work, so I'm using an arbitrarily high number instead. + // useEffect(() => console.debug(pokemonsData), [pokemonsData]); const [pokemons, setPokemons] = useState([] as Array); /** Get, parse, and save a list of all Pokémon that have a National 'Dex number. */ @@ -32,7 +34,7 @@ export const SearchResults: FunctionComponent = () => { for(const pokemon of pokemonsData.results) { - const id = parseInt(pokemon.url.replace(/^.*\/(\d+)\//, '$1')); + const id = getIdFromUrl(pokemon.url); if(id < 10000) { // IDs greater than `10000` are not real Pokémon IDs. newPokemons.push({ diff --git a/frontend/app/src/widgets/evolutions-viewer.tsx b/frontend/app/src/widgets/evolutions-viewer.tsx index ec3df9b..d9e8f29 100644 --- a/frontend/app/src/widgets/evolutions-viewer.tsx +++ b/frontend/app/src/widgets/evolutions-viewer.tsx @@ -1,23 +1,47 @@ import {useEvolutionsByIdQuery} from '@/redux/slices/pokeapi.slice.ts'; -import {FunctionComponent, useEffect} from 'react'; +import {BasicPokemonInfo} from '@/types/pokemon.type.ts'; +import {getIdFromUrl} from '@/utilities/get-id-from-url.function.ts'; +import {ChainLink} from 'pokenode-ts'; +import {Fragment, FunctionComponent, useEffect, useState} from 'react'; +import {Link} from 'react-router-dom'; //////////////////////////////////////////////////////////////////////////////// export const EvolutionsViewer: FunctionComponent<{id: number;}> = props => { - const {data: evolutions, error, isLoading: loading} = useEvolutionsByIdQuery(props.id); - useEffect(() => console.debug(evolutions), [evolutions]); + + // // // // // // // // // // // // // // // // // // // // + const {data: evolutions} = useEvolutionsByIdQuery(props.id); + // useEffect(() => console.debug(evolutions), [evolutions]); + const [chain, setChain] = useState([] as Array); + + /** Convert the raw chain information into something we can actually display. */ + const compileChain = (): void => { + if(!evolutions) return; + const newChain: Array = []; + + const addToChain = (link: ChainLink): void => { + newChain.push({ + id: getIdFromUrl(link.species.url), + name: link.species.name, + }); + if(link.evolves_to?.[0]) addToChain(link.evolves_to[0]); //FIXME: This won't work for the Eeveelutions. + }; + addToChain(evolutions.chain); + setChain(newChain); + }; + useEffect(compileChain, [evolutions]); // // // // // // // // // // // // // // // // // // // // return <> - {loading ? <> - {/* Load silently. */} - : error ? <> - {/* Fail silently. */} - : !evolutions ? <> - {/* Fail silently. */} - : <> -
    - {/* TODO */} -
- } + + {chain.map((link, index) => ( + + + {link.name} + + {index < chain.length - 1 ? ', ' : ''} + {index === chain.length - 1 && chain.length > 1 ? '.' : ''} + + ))} + ; };