Compare commits
23 Commits
3ee61e19f9
...
88fb450c5a
Author | SHA1 | Date | |
---|---|---|---|
88fb450c5a | |||
92f7111943 | |||
fd21848a85 | |||
e2bbe1d959 | |||
968c6c5d95 | |||
1801e43192 | |||
73fa644a55 | |||
790c7828b1 | |||
183ce62f30 | |||
81fcac97c6 | |||
a09463a2b4 | |||
83ae2f34d7 | |||
fa4fb04efb | |||
40358e3900 | |||
fed47e34b0 | |||
63e3ce5fb6 | |||
19c189c37d | |||
f30edc9700 | |||
40049ef7b5 | |||
89b5b976e1 | |||
f4fd616b34 | |||
5fee30437b | |||
a831e76275 |
@ -18,6 +18,7 @@
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test:watchAll": "react-scripts test --watchAll",
|
||||
"eject": "react-scripts eject",
|
||||
"prettier": "prettier \"src/**/*.{js,jsx,ts,tsx,css,scss,md}\" --write",
|
||||
"format:check": "npm run prettier -- --check",
|
||||
|
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
9
src/app/listenerMiddleware.ts
Normal file
9
src/app/listenerMiddleware.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
import type { TypedStartListening } from '@reduxjs/toolkit';
|
||||
import { AppDispatch, RootState } from 'app/store';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
export type AppStartListening = TypedStartListening<RootState, AppDispatch>;
|
||||
|
||||
export const startAppListening =
|
||||
listenerMiddleware.startListening as AppStartListening;
|
@ -1,4 +1,5 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { listenerMiddleware } from './listenerMiddleware';
|
||||
import { pokedexApi } from 'features/Pokedex/pokedexApi';
|
||||
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
|
||||
|
||||
@ -11,7 +12,10 @@ export const store = configureStore({
|
||||
[pokedexApi.reducerPath]: pokedexApi.reducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware().concat(pokedexApi.middleware),
|
||||
getDefaultMiddleware().concat(
|
||||
pokedexApi.middleware,
|
||||
listenerMiddleware.middleware,
|
||||
),
|
||||
devTools: true,
|
||||
});
|
||||
|
||||
|
16
src/features/Pokedex/Filters/Filters.test.ts
Normal file
16
src/features/Pokedex/Filters/Filters.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {
|
||||
useGetRegionPokemons,
|
||||
createRegionPokemonListOptionElements,
|
||||
} from './Filters';
|
||||
|
||||
describe('Filters', () => {
|
||||
describe('test utility functions', () => {
|
||||
test('createOptionElements works correctly', () => {
|
||||
const { data } = useGetRegionPokemons();
|
||||
const optionElements = createRegionPokemonListOptionElements(data);
|
||||
expect(optionElements[0].props.children).toBe('Kanto (1-151)');
|
||||
expect(optionElements[1].props.children).toBe('Johto (152-251)');
|
||||
expect(optionElements[2].props.children).toBe('Hoenn (252-386)');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,71 +1,90 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
useGetRegionListQuery,
|
||||
useGetTypeListQuery,
|
||||
useGetRegionPokemonListQuery,
|
||||
} from 'features/Pokedex/pokedexApi';
|
||||
import { useGetTypeListQuery } from 'features/Pokedex/pokedexApi';
|
||||
import {
|
||||
setSelectedRegion,
|
||||
setSelectedType,
|
||||
setSelectedSort,
|
||||
setFetchingRegionPokemonList,
|
||||
fetchPokemonsInTheRegion,
|
||||
setRegionOptions,
|
||||
setSortOptions,
|
||||
setTypeOptions,
|
||||
} from 'features/Pokedex/pokedexSlice';
|
||||
import { RegionPokemonRange } from 'features/Pokedex/types/slice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/hooks';
|
||||
|
||||
export const createRegionPokemonListOptionElements = (
|
||||
data: RegionPokemonRange[],
|
||||
) => {
|
||||
return data.map(({ region, startId, endId }) => {
|
||||
const value = `${region}`;
|
||||
const label = `${
|
||||
region.charAt(0).toUpperCase() + region.slice(1)
|
||||
} (${startId}-${endId})`;
|
||||
return (
|
||||
<option key={region} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const useGetRegionOptions = () => {
|
||||
const data: RegionPokemonRange[] = [
|
||||
{ region: 'kanto', startId: 1, endId: 151 },
|
||||
{ region: 'johto', startId: 152, endId: 251 },
|
||||
{ region: 'hoenn', startId: 252, endId: 386 },
|
||||
{ region: 'sinnoh', startId: 387, endId: 493 },
|
||||
{ region: 'unova', startId: 494, endId: 649 },
|
||||
{ region: 'kalos', startId: 650, endId: 721 },
|
||||
{ region: 'alola', startId: 722, endId: 809 },
|
||||
{ region: 'galar', startId: 810, endId: 898 },
|
||||
];
|
||||
return { data: data };
|
||||
};
|
||||
|
||||
const useGetSortOptions = () => {
|
||||
const sortOptions = [
|
||||
const data = [
|
||||
{ name: 'ID', value: 'id' },
|
||||
{ name: 'Name', value: 'name' },
|
||||
];
|
||||
return { data: sortOptions };
|
||||
|
||||
return { data: data };
|
||||
};
|
||||
|
||||
const Filters = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleRegionChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
dispatch(setSelectedRegion(event.target.value));
|
||||
};
|
||||
const handleTypeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
dispatch(setSelectedType(event.target.value));
|
||||
};
|
||||
const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
dispatch(setSelectedSort(event.target.value));
|
||||
};
|
||||
|
||||
const { data: regionsData, isLoading: regionsLoading } =
|
||||
useGetRegionListQuery();
|
||||
const { data: typesData, isLoading: typesLoading } = useGetTypeListQuery();
|
||||
const { data: sortOptions } = useGetSortOptions();
|
||||
|
||||
// Send the first region as the default selected region
|
||||
useEffect(() => {
|
||||
if (regionsData && regionsData.results.length > 0) {
|
||||
dispatch(setSelectedRegion(regionsData.results[0].name));
|
||||
}
|
||||
}, [regionsData, dispatch]);
|
||||
|
||||
// Send the first type as the default selected type
|
||||
useEffect(() => {
|
||||
if (typesData && typesData.results.length > 0) {
|
||||
dispatch(setSelectedType(typesData.results[0].name));
|
||||
}
|
||||
}, [typesData, dispatch]);
|
||||
|
||||
const selectedRegion = useAppSelector(state => state.pokedex.selectedRegion);
|
||||
const selectedType = useAppSelector(state => state.pokedex.selectedType);
|
||||
const selectedSort = useAppSelector(state => state.pokedex.selectedSort);
|
||||
|
||||
const { refetch: refetchRegionPokemonList } = useGetRegionPokemonListQuery(
|
||||
selectedRegion,
|
||||
{ skip: !selectedRegion },
|
||||
const regionPokemonList = useAppSelector(
|
||||
state => state.pokedex.regionOptions,
|
||||
);
|
||||
|
||||
const { data: fetchedRegionOptions } = useGetRegionOptions();
|
||||
const { data: fetchedTypeOptions, isLoading: isFetchingTypeOptions } =
|
||||
useGetTypeListQuery();
|
||||
const { data: fetchedSortOptions } = useGetSortOptions();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRegion) {
|
||||
dispatch(setFetchingRegionPokemonList(true));
|
||||
refetchRegionPokemonList();
|
||||
dispatch(setFetchingRegionPokemonList(false));
|
||||
dispatch(setRegionOptions(fetchedRegionOptions));
|
||||
dispatch(setSortOptions(fetchedSortOptions));
|
||||
|
||||
dispatch(setSelectedRegion(fetchedRegionOptions[0].region));
|
||||
dispatch(fetchPokemonsInTheRegion(fetchedRegionOptions[0].region));
|
||||
|
||||
dispatch(setSelectedSort(fetchedSortOptions[0].value));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingTypeOptions && fetchedTypeOptions) {
|
||||
dispatch(setTypeOptions(fetchedTypeOptions.results));
|
||||
dispatch(setSelectedType(fetchedTypeOptions.results[0].name));
|
||||
}
|
||||
}, [selectedRegion, refetchRegionPokemonList]);
|
||||
}, [isFetchingTypeOptions]);
|
||||
|
||||
const optionElements =
|
||||
createRegionPokemonListOptionElements(regionPokemonList);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -75,18 +94,13 @@ const Filters = () => {
|
||||
<div>REGION</div>
|
||||
<select
|
||||
name="regionSelect"
|
||||
disabled={regionsLoading}
|
||||
onChange={handleRegionChange}
|
||||
onChange={e => {
|
||||
dispatch(setSelectedRegion(e.target.value));
|
||||
dispatch(fetchPokemonsInTheRegion(e.target.value));
|
||||
}}
|
||||
value={selectedRegion}
|
||||
>
|
||||
{regionsLoading ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
regionsData?.results.map(region => (
|
||||
<option key={region.name} value={region.name}>
|
||||
{region.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
{optionElements}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,13 +109,13 @@ const Filters = () => {
|
||||
<div>TYPE</div>
|
||||
<select
|
||||
name="regionSelect"
|
||||
disabled={regionsLoading}
|
||||
onChange={handleTypeChange}
|
||||
onChange={e => dispatch(setSelectedType(e.target.value))}
|
||||
value={selectedType}
|
||||
>
|
||||
{typesLoading ? (
|
||||
{isFetchingTypeOptions ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
typesData?.results.map(type => (
|
||||
fetchedTypeOptions?.results.map(type => (
|
||||
<option key={type.name} value={type.name}>
|
||||
{type.name}
|
||||
</option>
|
||||
@ -115,10 +129,11 @@ const Filters = () => {
|
||||
<div>SORT BY</div>
|
||||
<select
|
||||
name="sortSelect"
|
||||
disabled={typesLoading}
|
||||
onChange={handleSortChange}
|
||||
disabled={isFetchingTypeOptions}
|
||||
onChange={e => dispatch(setSelectedSort(e.target.value))}
|
||||
value={selectedSort}
|
||||
>
|
||||
{sortOptions.map(option => (
|
||||
{fetchedSortOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</option>
|
||||
|
@ -3,25 +3,46 @@ import Pokemon from './Pokemon';
|
||||
import Filters from './Filters';
|
||||
import Loading from 'components/Loading';
|
||||
|
||||
import charizard from 'features/Pokedex/Pokemon/assets/stories/charizard.svg';
|
||||
import { useAppSelector } from 'app/hooks';
|
||||
import { PokemonResponseData } from './types/api';
|
||||
|
||||
export const filterPokemonByType = (
|
||||
pokemonList: PokemonResponseData[],
|
||||
selectedType: string,
|
||||
) => {
|
||||
return pokemonList.filter(
|
||||
pokemon =>
|
||||
selectedType === 'All Types' ||
|
||||
pokemon.types.some(type => type.type.name === selectedType),
|
||||
);
|
||||
};
|
||||
|
||||
const Pokedex = () => {
|
||||
const isFetchingRegionPokemonList = useAppSelector(
|
||||
state => state.pokedex.fetchingRegionPokemonList,
|
||||
const isLoadingPokemons = useAppSelector(
|
||||
state => state.pokedex.isLoadingPokemons,
|
||||
);
|
||||
const selectedType = useAppSelector(state => state.pokedex.selectedType);
|
||||
const selectedSort = useAppSelector(state => state.pokedex.selectedSort);
|
||||
|
||||
const pokemonList = useAppSelector(state => state.pokedex.pokemonList);
|
||||
|
||||
const filteredPokemonList = filterPokemonByType(pokemonList, selectedType);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Filters />
|
||||
{isFetchingRegionPokemonList ? (
|
||||
{isLoadingPokemons ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Pokemon
|
||||
name={'Charizard'}
|
||||
number={6}
|
||||
image={charizard}
|
||||
types={['fire', 'flying']}
|
||||
/>
|
||||
filteredPokemonList.map(pokemon => (
|
||||
<Pokemon
|
||||
key={pokemon.id}
|
||||
name={pokemon.name}
|
||||
number={pokemon.id}
|
||||
image={pokemon.sprites.other.dream_world.front_default}
|
||||
types={pokemon.types.map(type => type.type.name)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
44
src/features/Pokedex/__test__/pokedex.test.ts
Normal file
44
src/features/Pokedex/__test__/pokedex.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { filterPokemonByType } from 'features/Pokedex/Pokedex';
|
||||
import { PokemonResponseData } from 'features/Pokedex/types/api';
|
||||
import pokemon1 from 'features/Pokedex/__test__/pokemon1.json';
|
||||
import pokemon2 from 'features/Pokedex/__test__/pokemon2.json';
|
||||
import { AppDispatch, AppStore } from 'app/store';
|
||||
import { configureStore, Store } from '@reduxjs/toolkit';
|
||||
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
|
||||
import { pokedexApi } from 'features/Pokedex/pokedexApi';
|
||||
import { listenerMiddleware } from 'app/listenerMiddleware';
|
||||
|
||||
let store: AppStore;
|
||||
let dispatch: AppDispatch;
|
||||
describe('filterPokemonByType works correctly', () => {
|
||||
beforeEach(() => {
|
||||
store = configureStore({
|
||||
reducer: {
|
||||
pokedex: pokedexSlice.reducer,
|
||||
[pokedexApi.reducerPath]: pokedexApi.reducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware().concat(
|
||||
pokedexApi.middleware,
|
||||
listenerMiddleware.middleware,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
const pokemonList: PokemonResponseData[] = [pokemon1, pokemon2];
|
||||
|
||||
it('should return all Pokemon if the selected type is "All Types"', () => {
|
||||
const selectedType = 'All Types';
|
||||
const filteredList = filterPokemonByType(pokemonList, selectedType);
|
||||
expect(filteredList).toEqual(pokemonList);
|
||||
});
|
||||
|
||||
it('should return only Pokemon of the selected type', () => {
|
||||
const selectedType = 'fire';
|
||||
const filteredList = filterPokemonByType(pokemonList, selectedType);
|
||||
const allPokemonAreOfTypeFire = filteredList.every(pokemon =>
|
||||
pokemon.types.some(type => type.type.name === selectedType),
|
||||
);
|
||||
expect(allPokemonAreOfTypeFire).toBe(true);
|
||||
});
|
||||
});
|
@ -3,8 +3,9 @@ import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import region1 from 'features/Pokedex/__test__/responses/region1.json';
|
||||
import pokemon1 from 'features/Pokedex/__test__/responses/pokemon1.json';
|
||||
import { AppStore } from 'app/store';
|
||||
import { RegionListResponseData, TypeListResponseData } from '../types/api';
|
||||
import { AppStore } from 'app/store';
|
||||
import { listenerMiddleware } from '../../../app/listenerMiddleware';
|
||||
|
||||
let store: AppStore;
|
||||
describe('pokedexApi', () => {
|
||||
@ -15,7 +16,10 @@ describe('pokedexApi', () => {
|
||||
[pokedexApi.reducerPath]: pokedexApi.reducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware().concat(pokedexApi.middleware),
|
||||
getDefaultMiddleware().concat(
|
||||
pokedexApi.middleware,
|
||||
listenerMiddleware.middleware,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
@ -114,16 +118,5 @@ describe('pokedexApi', () => {
|
||||
// @ts-ignore
|
||||
expect(pokemonListData?.previous).toBeUndefined();
|
||||
});
|
||||
|
||||
test('query getRegionPokemonList for johto should return correct data in list', async () => {
|
||||
await store.dispatch(
|
||||
pokedexApi.endpoints.getRegionPokemonList.initiate('johto'),
|
||||
);
|
||||
|
||||
const pokemonListData = pokedexApi.endpoints.getRegionPokemonList.select(
|
||||
'johto',
|
||||
)(store.getState()).data;
|
||||
expect(pokemonListData).toHaveLength(19);
|
||||
}, 100000000000);
|
||||
});
|
||||
});
|
||||
|
9974
src/features/Pokedex/__test__/pokemon1.json
Normal file
9974
src/features/Pokedex/__test__/pokemon1.json
Normal file
File diff suppressed because it is too large
Load Diff
11585
src/features/Pokedex/__test__/pokemon2.json
Normal file
11585
src/features/Pokedex/__test__/pokemon2.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,36 +0,0 @@
|
||||
{
|
||||
"areas": [
|
||||
{
|
||||
"name": "blackthorn-city-area",
|
||||
"url": "https://pokeapi.co/api/v2/location-area/249/"
|
||||
}
|
||||
],
|
||||
"game_indices": [
|
||||
{
|
||||
"game_index": 136,
|
||||
"generation": {
|
||||
"name": "generation-iv",
|
||||
"url": "https://pokeapi.co/api/v2/generation/4/"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 65,
|
||||
"name": "blackthorn-city",
|
||||
"names": [
|
||||
{
|
||||
"language": {
|
||||
"name": "fr",
|
||||
"url": "https://pokeapi.co/api/v2/language/5/"
|
||||
},
|
||||
"name": "Ebènelle"
|
||||
},
|
||||
{
|
||||
"language": {
|
||||
"name": "en",
|
||||
"url": "https://pokeapi.co/api/v2/language/9/"
|
||||
},
|
||||
"name": "Blackthorn City"
|
||||
}
|
||||
],
|
||||
"region": { "name": "johto", "url": "https://pokeapi.co/api/v2/region/2/" }
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
{
|
||||
"areas": [
|
||||
{
|
||||
"name": "burned-tower-1f",
|
||||
"url": "https://pokeapi.co/api/v2/location-area/212/"
|
||||
},
|
||||
{
|
||||
"name": "burned-tower-b1f",
|
||||
"url": "https://pokeapi.co/api/v2/location-area/213/"
|
||||
}
|
||||
],
|
||||
"game_indices": [
|
||||
{
|
||||
"game_index": 206,
|
||||
"generation": {
|
||||
"name": "generation-iv",
|
||||
"url": "https://pokeapi.co/api/v2/generation/4/"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 66,
|
||||
"name": "burned-tower",
|
||||
"names": [
|
||||
{
|
||||
"language": {
|
||||
"name": "fr",
|
||||
"url": "https://pokeapi.co/api/v2/language/5/"
|
||||
},
|
||||
"name": "Tour Cendrée"
|
||||
},
|
||||
{
|
||||
"language": {
|
||||
"name": "en",
|
||||
"url": "https://pokeapi.co/api/v2/language/9/"
|
||||
},
|
||||
"name": "Burned Tower"
|
||||
}
|
||||
],
|
||||
"region": { "name": "johto", "url": "https://pokeapi.co/api/v2/region/2/" }
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
{
|
||||
"id": 2,
|
||||
"locations": [
|
||||
{
|
||||
"name": "blackthorn-city",
|
||||
"url": "https://pokeapi.co/api/v2/location/65/"
|
||||
},
|
||||
{ "name": "burned-tower", "url": "https://pokeapi.co/api/v2/location/66/" }
|
||||
],
|
||||
"main_generation": {
|
||||
"name": "generation-ii",
|
||||
"url": "https://pokeapi.co/api/v2/generation/2/"
|
||||
},
|
||||
"name": "johto",
|
||||
"names": [
|
||||
{
|
||||
"language": {
|
||||
"name": "ja-Hrkt",
|
||||
"url": "https://pokeapi.co/api/v2/language/1/"
|
||||
},
|
||||
"name": "ジョウト地方"
|
||||
},
|
||||
{
|
||||
"language": {
|
||||
"name": "ko",
|
||||
"url": "https://pokeapi.co/api/v2/language/3/"
|
||||
},
|
||||
"name": "성도지방"
|
||||
},
|
||||
{
|
||||
"language": {
|
||||
"name": "fr",
|
||||
"url": "https://pokeapi.co/api/v2/language/5/"
|
||||
},
|
||||
"name": "Johto"
|
||||
},
|
||||
{
|
||||
"language": {
|
||||
"name": "de",
|
||||
"url": "https://pokeapi.co/api/v2/language/6/"
|
||||
},
|
||||
"name": "Johto"
|
||||
},
|
||||
{
|
||||
"language": {
|
||||
"name": "it",
|
||||
"url": "https://pokeapi.co/api/v2/language/8/"
|
||||
},
|
||||
"name": "Johto"
|
||||
},
|
||||
{
|
||||
"language": {
|
||||
"name": "en",
|
||||
"url": "https://pokeapi.co/api/v2/language/9/"
|
||||
},
|
||||
"name": "Johto"
|
||||
}
|
||||
],
|
||||
"pokedexes": [
|
||||
{ "name": "original-johto", "url": "https://pokeapi.co/api/v2/pokedex/3/" },
|
||||
{ "name": "updated-johto", "url": "https://pokeapi.co/api/v2/pokedex/7/" }
|
||||
],
|
||||
"version_groups": [
|
||||
{
|
||||
"name": "gold-silver",
|
||||
"url": "https://pokeapi.co/api/v2/version-group/3/"
|
||||
},
|
||||
{ "name": "crystal", "url": "https://pokeapi.co/api/v2/version-group/4/" },
|
||||
{
|
||||
"name": "heartgold-soulsilver",
|
||||
"url": "https://pokeapi.co/api/v2/version-group/10/"
|
||||
}
|
||||
]
|
||||
}
|
@ -3,7 +3,6 @@ import {
|
||||
pokeApiAllPagesCustomBaseQuery,
|
||||
pokeApiFullListFetchArgs,
|
||||
} from './paginationBaseQuery';
|
||||
import { setFetchingRegionPokemonList } from './pokedexSlice';
|
||||
import {
|
||||
AreaResponseData,
|
||||
LocationResponseData,
|
||||
@ -66,67 +65,6 @@ export const pokedexApi = createApi({
|
||||
getArea: builder.query<AreaResponseData, number | string>({
|
||||
query: IdOrName => ({ url: `location-area/${IdOrName}` }),
|
||||
}),
|
||||
getRegionPokemonList: builder.query<PokemonListItem[], number | string>({
|
||||
async queryFn(regionIdOrName, api) {
|
||||
api.dispatch(setFetchingRegionPokemonList(true));
|
||||
|
||||
// Get region data
|
||||
const regionData: RegionResponseData = await api
|
||||
.dispatch(pokedexApi.endpoints.getRegion.initiate(regionIdOrName))
|
||||
.unwrap();
|
||||
|
||||
// Get location data
|
||||
const locationDataList: LocationResponseData[] = await Promise.all(
|
||||
regionData.locations.map(location =>
|
||||
api
|
||||
.dispatch(
|
||||
pokedexApi.endpoints.getLocation.initiate(location.name),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
);
|
||||
|
||||
// Get area datas
|
||||
const areaDataList: AreaResponseData[] = await Promise.all(
|
||||
locationDataList
|
||||
.flatMap(locationData => locationData.areas)
|
||||
.map(area =>
|
||||
api
|
||||
.dispatch(pokedexApi.endpoints.getArea.initiate(area.name))
|
||||
.unwrap(),
|
||||
),
|
||||
);
|
||||
|
||||
// Collect unique Pokemon
|
||||
const uniquePokemonList = new Set<nameUrlPair>();
|
||||
areaDataList.forEach(areaData => {
|
||||
areaData.pokemon_encounters.forEach(pokemon => {
|
||||
uniquePokemonList.add(pokemon.pokemon);
|
||||
});
|
||||
});
|
||||
|
||||
// Get Pokemon data
|
||||
const pokemonDataList: PokemonListItem[] = await Promise.all(
|
||||
Array.from(uniquePokemonList).map(pokemon =>
|
||||
api
|
||||
.dispatch(pokedexApi.endpoints.getPokemon.initiate(pokemon.name))
|
||||
.unwrap()
|
||||
.then(pokemonData => {
|
||||
return {
|
||||
name: pokemonData.name,
|
||||
id: pokemonData.id,
|
||||
type: pokemonData.types.map(type => type.type.name),
|
||||
image: pokemonData.sprites.other.dream_world.front_default,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
api.dispatch(setFetchingRegionPokemonList(false));
|
||||
|
||||
return { data: Array.from(pokemonDataList) };
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -139,5 +77,4 @@ export const {
|
||||
useGetTypeQuery,
|
||||
useGetAreaQuery,
|
||||
useGetLocationQuery,
|
||||
useGetRegionPokemonListQuery,
|
||||
} = pokedexApi;
|
||||
|
@ -1,33 +1,63 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { PokemonProps } from './Pokemon';
|
||||
import type { RootState } from 'app/store';
|
||||
import { nameUrlPair } from './types/api';
|
||||
import { createAsyncThunk, createSlice, Dispatch } from '@reduxjs/toolkit';
|
||||
import type { Slice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface PokedexState {
|
||||
selectedRegion: string;
|
||||
selectedType: string;
|
||||
selectedSort: string;
|
||||
pokemonList: PokemonProps[];
|
||||
regionPokemonList: nameUrlPair[];
|
||||
fetchingRegionPokemonList: boolean;
|
||||
}
|
||||
import { PokedexState, RegionPokemonRange } from 'features/Pokedex/types/slice';
|
||||
|
||||
import { getStartAndEndIdsForRegion } from './utils';
|
||||
import { PokemonResponseData } from './types/api';
|
||||
import { pokedexApi } from './pokedexApi';
|
||||
import { RootState } from '../../app/store';
|
||||
|
||||
pokedexApi.endpoints.getTypeList.initiate(); // initialize type list fetching
|
||||
// typesData will be used in Filters.tsx
|
||||
|
||||
export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||
PokemonResponseData[],
|
||||
string,
|
||||
{ state: RootState }
|
||||
>('pokedex/setSelectedRegion', async (region: string, thunkAPI) => {
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const regionOptions = getState().pokedex.regionOptions;
|
||||
|
||||
const { startId, endId } = getStartAndEndIdsForRegion(region, regionOptions);
|
||||
const pokemonIds = Array.from(
|
||||
{ length: endId - startId + 1 },
|
||||
(_, i) => i + startId,
|
||||
);
|
||||
// use pokemonIds to fetch pokemon data using getPokemonQuery and store in state
|
||||
const pokemonList = await Promise.all(
|
||||
pokemonIds.map(
|
||||
id =>
|
||||
dispatch(pokedexApi.endpoints.getPokemon.initiate(id)) as Promise<{
|
||||
data: PokemonResponseData;
|
||||
}>,
|
||||
),
|
||||
);
|
||||
const pokemonListData = pokemonList.map(
|
||||
(pokemon: { data: PokemonResponseData }) => pokemon.data,
|
||||
);
|
||||
return pokemonListData;
|
||||
});
|
||||
|
||||
const initialState: PokedexState = {
|
||||
regionOptions: [],
|
||||
typeOptions: [],
|
||||
sortOptions: [],
|
||||
selectedRegion: '',
|
||||
selectedType: '',
|
||||
selectedSort: '',
|
||||
isLoadingPokemons: true,
|
||||
pokemonList: [],
|
||||
regionPokemonList: [],
|
||||
fetchingRegionPokemonList: false,
|
||||
};
|
||||
|
||||
export const pokedexSlice = createSlice({
|
||||
export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||
name: 'pokedex',
|
||||
initialState,
|
||||
reducers: {
|
||||
setSelectedRegion: (state, action: PayloadAction<string>) => {
|
||||
state.selectedRegion = action.payload;
|
||||
// call fetchPokemonsInTheRegion
|
||||
fetchPokemonsInTheRegion(action.payload);
|
||||
},
|
||||
setSelectedType: (state, action: PayloadAction<string>) => {
|
||||
state.selectedType = action.payload;
|
||||
@ -35,12 +65,42 @@ export const pokedexSlice = createSlice({
|
||||
setSelectedSort: (state, action: PayloadAction<string>) => {
|
||||
state.selectedSort = action.payload;
|
||||
},
|
||||
setFetchingRegionPokemonList: (state, action: PayloadAction<boolean>) => {
|
||||
state.fetchingRegionPokemonList = action.payload;
|
||||
setRegionOptions: (state, action: PayloadAction<RegionPokemonRange[]>) => {
|
||||
state.regionOptions = action.payload;
|
||||
},
|
||||
setRegionPokemonList: (state, action: PayloadAction<nameUrlPair[]>) => {
|
||||
state.regionPokemonList = action.payload;
|
||||
setTypeOptions: (state, action: PayloadAction<string[]>) => {
|
||||
state.typeOptions = action.payload;
|
||||
},
|
||||
setSortOptions: (
|
||||
state,
|
||||
action: PayloadAction<{ name: string; value: string }[]>,
|
||||
) => {
|
||||
state.sortOptions = action.payload;
|
||||
},
|
||||
setIsLoadingPokemons: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoadingPokemons = action.payload;
|
||||
},
|
||||
setPokemonList: (state, action: PayloadAction<PokemonResponseData[]>) => {
|
||||
state.pokemonList = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
// add fetchPokemonsInTheRegion
|
||||
builder.addCase(fetchPokemonsInTheRegion.fulfilled, (state, action) => {
|
||||
state.isLoadingPokemons = false;
|
||||
state.pokemonList = action.payload;
|
||||
});
|
||||
builder.addMatcher(
|
||||
pokedexApi.endpoints.getTypeList.matchFulfilled,
|
||||
(state, action) => {
|
||||
if (action.payload && action.payload.results.length > 0) {
|
||||
const regionListResults = action.payload.results;
|
||||
state.typeOptions = regionListResults.map(region => region.name);
|
||||
|
||||
state.selectedType = action.payload.results[0].name;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -48,8 +108,10 @@ export const {
|
||||
setSelectedRegion,
|
||||
setSelectedType,
|
||||
setSelectedSort,
|
||||
setFetchingRegionPokemonList,
|
||||
setRegionPokemonList,
|
||||
setRegionOptions,
|
||||
setTypeOptions,
|
||||
setSortOptions,
|
||||
setPokemonList,
|
||||
} = pokedexSlice.actions;
|
||||
|
||||
export default pokedexSlice.reducer;
|
||||
|
18
src/features/Pokedex/types/slice.ts
Normal file
18
src/features/Pokedex/types/slice.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { PokemonResponseData } from './api';
|
||||
|
||||
export type PokedexState = {
|
||||
selectedRegion: string;
|
||||
regionOptions: RegionPokemonRange[];
|
||||
selectedType: string;
|
||||
typeOptions: string[];
|
||||
selectedSort: string;
|
||||
sortOptions: { name: string; value: string }[];
|
||||
isLoadingPokemons: boolean;
|
||||
pokemonList: PokemonResponseData[];
|
||||
};
|
||||
|
||||
export type RegionPokemonRange = {
|
||||
region: string;
|
||||
startId: number;
|
||||
endId: number;
|
||||
};
|
11
src/features/Pokedex/utils.ts
Normal file
11
src/features/Pokedex/utils.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { RegionPokemonRange } from './types/slice';
|
||||
|
||||
export const getStartAndEndIdsForRegion = (
|
||||
region: string,
|
||||
data: RegionPokemonRange[],
|
||||
): { startId: number; endId: number } => {
|
||||
const regionData = data.find(data => data.region === region);
|
||||
return regionData
|
||||
? { startId: regionData.startId, endId: regionData.endId }
|
||||
: { startId: 0, endId: 0 };
|
||||
};
|
@ -5,12 +5,6 @@ import regionList from 'features/Pokedex/__test__/responses/regionList.json';
|
||||
import typeList from 'features/Pokedex/__test__/responses/typeList.json';
|
||||
import pokemonListPg1 from 'features/Pokedex/__test__/responses/pokemonListPage1.json';
|
||||
import pokemonListPg2 from 'features/Pokedex/__test__/responses/pokemonListPage2.json';
|
||||
import region_johto from 'features/Pokedex/__test__/responses/region_johto.json';
|
||||
import location_blackthorn_city from 'features/Pokedex/__test__/responses/location_blackthorn-city.json';
|
||||
import location_burned_tower from 'features/Pokedex/__test__/responses/location_burned-tower.json';
|
||||
import area_blackthorn_city_area from 'features/Pokedex/__test__/responses/area_blackthorn-city-area.json';
|
||||
import area_burned_tower_1f from 'features/Pokedex/__test__/responses/area_burned-tower-1f.json';
|
||||
import area_burned_tower_b1f from 'features/Pokedex/__test__/responses/area_burned-tower-b1f.json';
|
||||
|
||||
export const handlers = [
|
||||
// mock https://pokeapi.co/api/v2/region/1
|
||||
@ -40,24 +34,4 @@ export const handlers = [
|
||||
return res(ctx.json(pokemonListPg2));
|
||||
}
|
||||
}),
|
||||
|
||||
// getRegionPokemonList
|
||||
rest.get('https://pokeapi.co/api/v2/region/johto', (req, res, ctx) => {
|
||||
return res(ctx.json(region_johto));
|
||||
}),
|
||||
rest.get('https://pokeapi.co/api/v2/location/65', (req, res, ctx) => {
|
||||
return res(ctx.json(location_blackthorn_city));
|
||||
}),
|
||||
rest.get('https://pokeapi.co/api/v2/location/66', (req, res, ctx) => {
|
||||
return res(ctx.json(location_burned_tower));
|
||||
}),
|
||||
rest.get('https://pokeapi.co/api/v2/location-area/249', (req, res, ctx) => {
|
||||
return res(ctx.json(area_blackthorn_city_area));
|
||||
}),
|
||||
rest.get('https://pokeapi.co/api/v2/location-area/212', (req, res, ctx) => {
|
||||
return res(ctx.json(area_burned_tower_1f));
|
||||
}),
|
||||
rest.get('https://pokeapi.co/api/v2/location-area/213', (req, res, ctx) => {
|
||||
return res(ctx.json(area_burned_tower_b1f));
|
||||
}),
|
||||
];
|
||||
|
Loading…
x
Reference in New Issue
Block a user