Skip to content

Commit

Permalink
Evolutions display as chains, but not as trees
Browse files Browse the repository at this point in the history
RIP Eevee
  • Loading branch information
MilesBHuff committed Oct 17, 2023
1 parent 970cdfd commit e5b59c2
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 27 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
3 changes: 1 addition & 2 deletions frontend/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)*
Expand All @@ -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
Expand Down
19 changes: 18 additions & 1 deletion frontend/app/src/redux/slices/pokeapi.slice.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -20,21 +20,38 @@ export const pokeapiSlice = createApi({
baseUrl: 'https://pokeapi.co/api/v2',
}),
endpoints: builder => ({

evolutionsById: builder.query<EvolutionChain, number>({
query: (id, options?: PokeapiQueryOptions) => '/evolution-chain' + urlifyPath(id.toString(10)) + urlifyParams(options ?? {}),
}),
evolutionsList: builder.query<NamedAPIResourceList, void | PokeapiQueryOptions>({
query: (options?: PokeapiQueryOptions) => '/evolution-chain' + urlifyParams(options ?? {}),
}),

speciesById: builder.query<PokemonSpecies, number>({
query: (id, options?: PokeapiQueryOptions) => '/pokemon-species' + urlifyPath(id.toString(10)) + urlifyParams(options ?? {}),
}),
speciesList: builder.query<NamedAPIResourceList, void | PokeapiQueryOptions>({
query: (options?: PokeapiQueryOptions) => '/pokemon-species' + urlifyParams(options ?? {}),
}),

pokemonById: builder.query<Pokemon, number>({
query: (id, options?: PokeapiQueryOptions) => '/pokemon' + urlifyPath(id.toString(10)) + urlifyParams(options ?? {}),
}),
pokemonList: builder.query<NamedAPIResourceList, void | PokeapiQueryOptions>({
query: (options?: PokeapiQueryOptions) => '/pokemon' + urlifyParams(options ?? {}),
}),

}),
});

////////////////////////////////////////////////////////////////////////////////
export const {
useEvolutionsByIdQuery,

useSpeciesByIdQuery,
useSpeciesListQuery,

usePokemonByIdQuery,
usePokemonListQuery,
} = pokeapiSlice;
2 changes: 2 additions & 0 deletions frontend/app/src/utilities/get-id-from-url.function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
////////////////////////////////////////////////////////////////////////////////
export const getIdFromUrl = (url: string): number => parseInt(url.replace(/^.*\/(\d+)\//, '$1'));
21 changes: 14 additions & 7 deletions frontend/app/src/views/pokemon-info.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 ? <>
<h2>Loading...</h2>
<Spinner />
</> : error ? <>
</> : pokemonError ? <>
<h2>Pokémon #{props.id}</h2>
<p className="error">Failed to load data!</p>
<p>Did you enter a valid Pokédex index?</p>
Expand All @@ -55,7 +58,7 @@ export const PokemonInfoCore: FunctionComponent<{id: number}> = props => {
</> : <>
<h2>{displayifyName(pokemon.name)} (#{pokemon.id})</h2>
<ul>
{pokemon.name === pokemon.species.name ? null :
{pokemon.name === pokemon.name ? null :
<li><strong>Species: </strong>
{displayifyName(pokemon.species.name)}
</li>
Expand All @@ -78,8 +81,12 @@ export const PokemonInfoCore: FunctionComponent<{id: number}> = props => {
{index === pokemon.moves.length - 1 && pokemon.moves.length > 1 ? '.' : ''}
</Fragment>)}
</li>
{!species ? null :
<li><strong>Evolution Tree: </strong>
<EvolutionsViewer id={getIdFromUrl(species.evolution_chain.url)}/>
</li>
}
</ul>
<EvolutionsViewer id={props.id}/>
</>}
</>;
};
8 changes: 5 additions & 3 deletions frontend/app/src/views/search-results.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<BasicPokemonInfo>);

/** Get, parse, and save a list of all Pokémon that have a National 'Dex number. */
Expand All @@ -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({
Expand Down
52 changes: 38 additions & 14 deletions frontend/app/src/widgets/evolutions-viewer.tsx
Original file line number Diff line number Diff line change
@@ -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<BasicPokemonInfo>);

/** Convert the raw chain information into something we can actually display. */
const compileChain = (): void => {
if(!evolutions) return;
const newChain: Array<BasicPokemonInfo> = [];

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. */}
</> : <>
<ul className="evolutions-viewer">
{/* TODO */}
</ul>
</>}
<span className="evolutions-viewer">
{chain.map((link, index) => (
<Fragment key={index}>
<Link to={`/pokemon?id=${link.id}`}>
{link.name}
</Link>
{index < chain.length - 1 ? ', ' : ''}
{index === chain.length - 1 && chain.length > 1 ? '.' : ''}
</Fragment>
))}
</span>
</>;
};

0 comments on commit e5b59c2

Please sign in to comment.