Moved Filter component into a separate feature
This commit is contained in:
parent
6295a6a792
commit
be9d767bfd
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
import Header from 'components/Header';
|
||||
import Pokedex from './features/Pokedex';
|
||||
import Pokedex from 'features/Pokedex';
|
||||
import Filters from 'features/Filters';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App app_container">
|
||||
<Header />
|
||||
<Filters />
|
||||
<Pokedex />
|
||||
</div>
|
||||
);
|
||||
|
@ -1,19 +1,21 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { listenerMiddleware } from './listenerMiddleware';
|
||||
import { pokedexApi } from 'features/Pokedex/pokedexApi';
|
||||
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
|
||||
import { filterSlice } from 'features/Filters/filterSlice';
|
||||
import { filterApi } from 'features/Filters/filterApi';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
// component slices
|
||||
pokedex: pokedexSlice.reducer,
|
||||
filter: filterSlice.reducer,
|
||||
|
||||
// api
|
||||
[pokedexApi.reducerPath]: pokedexApi.reducer,
|
||||
// api slices
|
||||
[filterApi.reducerPath]: filterApi.reducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware().concat(
|
||||
pokedexApi.middleware,
|
||||
filterApi.middleware,
|
||||
listenerMiddleware.middleware,
|
||||
),
|
||||
devTools: true,
|
||||
|
@ -28,7 +28,7 @@ html {
|
||||
|
||||
@font-face {
|
||||
font-family: 'Press Start 2P';
|
||||
src: url('../../assets/PressStart2P-Regular.ttf') format('truetype');
|
||||
src: url('assets/PressStart2P-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.thumbnail__container {
|
||||
|
@ -1,17 +1,18 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useGetTypeListQuery } from 'features/Pokedex/pokedexApi';
|
||||
import { useAppDispatch, useAppSelector } from 'app/hooks';
|
||||
import { fetchPokemonsInTheRegion } from 'features/Pokedex/pokedexSlice';
|
||||
|
||||
import {
|
||||
setSelectedRegion,
|
||||
setSelectedType,
|
||||
setSelectedSort,
|
||||
fetchPokemonsInTheRegion,
|
||||
setRegionOptions,
|
||||
setSortOptions,
|
||||
setTypeOptions,
|
||||
setSortOptions,
|
||||
setSearchInput,
|
||||
} from 'features/Pokedex/pokedexSlice';
|
||||
import { RegionPokemonRange } from 'features/Pokedex/types/slice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/hooks';
|
||||
} from './filterSlice';
|
||||
import { useGetTypeListQuery } from './filterApi';
|
||||
import { RegionPokemonRange } from './types/slice';
|
||||
import './Filters.css';
|
||||
|
||||
export const createRegionPokemonListOptionElements = (
|
||||
@ -55,14 +56,12 @@ const useGetSortOptions = () => {
|
||||
|
||||
const Filters = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedRegion = useAppSelector(state => state.pokedex.selectedRegion);
|
||||
const selectedType = useAppSelector(state => state.pokedex.selectedType);
|
||||
const selectedSort = useAppSelector(state => state.pokedex.selectedSort);
|
||||
const searchInput = useAppSelector(state => state.pokedex.searchInput);
|
||||
const selectedRegion = useAppSelector(state => state.filter.selectedRegion);
|
||||
const selectedType = useAppSelector(state => state.filter.selectedType);
|
||||
const selectedSort = useAppSelector(state => state.filter.selectedSort);
|
||||
const searchInput = useAppSelector(state => state.filter.searchInput);
|
||||
|
||||
const regionPokemonList = useAppSelector(
|
||||
state => state.pokedex.regionOptions,
|
||||
);
|
||||
const regionPokemonList = useAppSelector(state => state.filter.regionOptions);
|
||||
|
||||
const { data: fetchedRegionOptions } = useGetRegionOptions();
|
||||
const { data: fetchedTypeOptions, isLoading: isFetchingTypeOptions } =
|
24
src/features/Filters/filterApi.ts
Normal file
24
src/features/Filters/filterApi.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
import { pokeApiBaseQuery } from '../../services/pokeapi/paginationBaseQuery';
|
||||
import {
|
||||
RegionListResponseData,
|
||||
TypeListResponseData,
|
||||
} from '../Pokedex/types/api';
|
||||
|
||||
export const filterApi = createApi({
|
||||
reducerPath: 'filterApi',
|
||||
baseQuery: pokeApiBaseQuery,
|
||||
endpoints: builder => ({
|
||||
getTypeList: builder.query<TypeListResponseData, void>({
|
||||
query: () => ({ url: 'type', fetchAllPages: true }),
|
||||
transformResponse: (response: RegionListResponseData) => {
|
||||
return {
|
||||
...response,
|
||||
results: [{ name: 'All Types', url: '' }, ...response.results],
|
||||
};
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetTypeListQuery } = filterApi;
|
72
src/features/Filters/filterSlice.ts
Normal file
72
src/features/Filters/filterSlice.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit';
|
||||
import { FilterState } from './types/slice';
|
||||
import { RegionPokemonRange } from './types/slice';
|
||||
import { filterApi } from './filterApi';
|
||||
|
||||
filterApi.endpoints.getTypeList.initiate(); // initialize type list fetching
|
||||
|
||||
const initialState: FilterState = {
|
||||
regionOptions: [],
|
||||
typeOptions: [],
|
||||
sortOptions: [],
|
||||
selectedRegion: '',
|
||||
selectedType: '',
|
||||
selectedSort: '',
|
||||
searchInput: '',
|
||||
};
|
||||
|
||||
export const filterSlice: Slice<FilterState> = createSlice({
|
||||
name: 'filter',
|
||||
initialState,
|
||||
reducers: {
|
||||
setSelectedRegion: (state, action: PayloadAction<string>) => {
|
||||
state.selectedRegion = action.payload;
|
||||
},
|
||||
setSelectedType: (state, action: PayloadAction<string>) => {
|
||||
state.selectedType = action.payload;
|
||||
},
|
||||
setSelectedSort: (state, action: PayloadAction<string>) => {
|
||||
state.selectedSort = action.payload;
|
||||
},
|
||||
setRegionOptions: (state, action: PayloadAction<RegionPokemonRange[]>) => {
|
||||
state.regionOptions = action.payload;
|
||||
},
|
||||
setTypeOptions: (state, action: PayloadAction<string[]>) => {
|
||||
state.typeOptions = action.payload;
|
||||
},
|
||||
setSortOptions: (
|
||||
state,
|
||||
action: PayloadAction<{ name: string; value: string }[]>,
|
||||
) => {
|
||||
state.sortOptions = action.payload;
|
||||
},
|
||||
setSearchInput: (state, action: PayloadAction<string>) => {
|
||||
state.searchInput = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addMatcher(
|
||||
filterApi.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;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setSelectedRegion,
|
||||
setSelectedType,
|
||||
setSelectedSort,
|
||||
setRegionOptions,
|
||||
setTypeOptions,
|
||||
setSortOptions,
|
||||
setSearchInput,
|
||||
} = filterSlice.actions;
|
||||
|
||||
export default filterSlice.reducer;
|
1
src/features/Filters/index.ts
Normal file
1
src/features/Filters/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Filters';
|
15
src/features/Filters/types/slice.ts
Normal file
15
src/features/Filters/types/slice.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export type FilterState = {
|
||||
regionOptions: RegionPokemonRange[];
|
||||
typeOptions: string[];
|
||||
sortOptions: { name: string; value: string }[];
|
||||
selectedRegion: string;
|
||||
selectedType: string;
|
||||
selectedSort: string;
|
||||
searchInput: string;
|
||||
};
|
||||
|
||||
export type RegionPokemonRange = {
|
||||
region: string;
|
||||
startId: number;
|
||||
endId: number;
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
export { default } from './Filters';
|
||||
export { default as useFilterLoaded } from './useFilterLoaded';
|
@ -1,18 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useGetRegionListQuery, useGetTypeListQuery } from '../pokedexApi';
|
||||
|
||||
const useFilterLoaded = () => {
|
||||
const { isLoading: isLoadingRegionList } = useGetRegionListQuery();
|
||||
const { isLoading: isLoadingTypeList } = useGetTypeListQuery();
|
||||
const [isFilterLoaded, setIsFilterLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingRegionList && !isLoadingTypeList) {
|
||||
setIsFilterLoaded(true);
|
||||
}
|
||||
}, [isLoadingRegionList, isLoadingTypeList]);
|
||||
|
||||
return isFilterLoaded;
|
||||
};
|
||||
|
||||
export default useFilterLoaded;
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PokemonCard, { PokemonCardProps } from 'components/PokemonCard';
|
||||
import Filters from './Filters';
|
||||
import Filters from 'features/Filters';
|
||||
import Loading from 'components/Loading';
|
||||
|
||||
import { useAppSelector } from 'app/hooks';
|
||||
@ -41,13 +41,13 @@ export const searchPokemonCardsByName = (
|
||||
};
|
||||
|
||||
const Pokedex = () => {
|
||||
const selectedType = useAppSelector(state => state.filter.selectedType);
|
||||
const selectedSort = useAppSelector(state => state.filter.selectedSort);
|
||||
const searchInput = useAppSelector(state => state.filter.searchInput);
|
||||
|
||||
const isLoadingPokemons = useAppSelector(
|
||||
state => state.pokedex.isLoadingPokemons,
|
||||
);
|
||||
const selectedType = useAppSelector(state => state.pokedex.selectedType);
|
||||
const selectedSort = useAppSelector(state => state.pokedex.selectedSort);
|
||||
const searchInput = useAppSelector(state => state.pokedex.searchInput);
|
||||
|
||||
const pokemonList = useAppSelector(state => state.pokedex.pokemonCardList);
|
||||
|
||||
const filteredPokemonList = filterPokemonCardsByType(
|
||||
@ -65,7 +65,6 @@ const Pokedex = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Filters />
|
||||
{isLoadingPokemons ? (
|
||||
<Loading />
|
||||
) : (
|
||||
|
@ -1,80 +0,0 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import {
|
||||
pokeApiAllPagesCustomBaseQuery,
|
||||
pokeApiFullListFetchArgs,
|
||||
} from './paginationBaseQuery';
|
||||
import {
|
||||
AreaResponseData,
|
||||
LocationResponseData,
|
||||
PokemonListResponseData,
|
||||
PokemonResponseData,
|
||||
RegionListResponseData,
|
||||
RegionResponseData,
|
||||
TypeListResponseData,
|
||||
TypeResponseData,
|
||||
PokemonListItem,
|
||||
nameUrlPair,
|
||||
PokemonList,
|
||||
} from './types/api';
|
||||
|
||||
const pokeApiBaseQuery = async (
|
||||
args: pokeApiFullListFetchArgs,
|
||||
api: any,
|
||||
extra: any,
|
||||
) => {
|
||||
const baseUrl = 'https://pokeapi.co/api/v2/';
|
||||
|
||||
if (args.fetchAllPages) {
|
||||
return pokeApiAllPagesCustomBaseQuery(args, api, extra, baseUrl);
|
||||
} else {
|
||||
return fetchBaseQuery({ baseUrl })(args, api, extra);
|
||||
}
|
||||
};
|
||||
|
||||
export const pokedexApi = createApi({
|
||||
reducerPath: 'pokedexApi',
|
||||
baseQuery: pokeApiBaseQuery,
|
||||
endpoints: builder => ({
|
||||
getPokemonList: builder.query<PokemonListResponseData, void>({
|
||||
query: () => ({ url: `pokemon`, fetchAllPages: true }),
|
||||
}),
|
||||
getRegionList: builder.query<RegionListResponseData, void>({
|
||||
query: () => ({ url: 'region', fetchAllPages: true }),
|
||||
}),
|
||||
getTypeList: builder.query<TypeListResponseData, void>({
|
||||
query: () => ({ url: 'type', fetchAllPages: true }),
|
||||
transformResponse: (response: RegionListResponseData) => {
|
||||
return {
|
||||
...response,
|
||||
results: [{ name: 'All Types', url: '' }, ...response.results],
|
||||
};
|
||||
},
|
||||
}),
|
||||
getPokemon: builder.query<PokemonResponseData, number | string>({
|
||||
query: IdOrName => ({ url: `pokemon/${IdOrName}` }),
|
||||
}),
|
||||
getRegion: builder.query<RegionResponseData, number | string>({
|
||||
query: IdOrName => ({ url: `region/${IdOrName}` }),
|
||||
}),
|
||||
getType: builder.query<TypeResponseData, number | string>({
|
||||
query: IdOrName => ({ url: `type/${IdOrName}` }),
|
||||
}),
|
||||
getLocation: builder.query<LocationResponseData, number | string>({
|
||||
query: IdOrName => ({ url: `location/${IdOrName}` }),
|
||||
}),
|
||||
getArea: builder.query<AreaResponseData, number | string>({
|
||||
query: IdOrName => ({ url: `location-area/${IdOrName}` }),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetPokemonListQuery,
|
||||
useGetRegionListQuery,
|
||||
useGetTypeListQuery,
|
||||
useGetPokemonQuery,
|
||||
useGetRegionQuery,
|
||||
useGetTypeQuery,
|
||||
useGetAreaQuery,
|
||||
useGetLocationQuery,
|
||||
} = pokedexApi;
|
@ -1,15 +1,11 @@
|
||||
import { createAsyncThunk, createSlice, Dispatch } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
import type { Slice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { PokedexState, RegionPokemonRange } from 'features/Pokedex/types/slice';
|
||||
import { PokedexState } 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
|
||||
import { RootState } from 'app/store';
|
||||
|
||||
export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||
PokemonResponseData[],
|
||||
@ -17,7 +13,7 @@ export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||
{ state: RootState }
|
||||
>('pokedex/setSelectedRegion', async (region: string, thunkAPI) => {
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const regionOptions = getState().pokedex.regionOptions;
|
||||
const regionOptions = getState().filter.regionOptions;
|
||||
|
||||
dispatch(setIsLoadingPokemons(true));
|
||||
|
||||
@ -44,13 +40,6 @@ export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||
});
|
||||
|
||||
const initialState: PokedexState = {
|
||||
regionOptions: [],
|
||||
typeOptions: [],
|
||||
sortOptions: [],
|
||||
selectedRegion: '',
|
||||
selectedType: '',
|
||||
selectedSort: '',
|
||||
searchInput: '',
|
||||
isLoadingPokemons: true,
|
||||
pokemonCardList: [],
|
||||
};
|
||||
@ -59,30 +48,6 @@ export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||
name: 'pokedex',
|
||||
initialState,
|
||||
reducers: {
|
||||
setSelectedRegion: (state, action: PayloadAction<string>) => {
|
||||
state.selectedRegion = action.payload;
|
||||
},
|
||||
setSelectedType: (state, action: PayloadAction<string>) => {
|
||||
state.selectedType = action.payload;
|
||||
},
|
||||
setSelectedSort: (state, action: PayloadAction<string>) => {
|
||||
state.selectedSort = action.payload;
|
||||
},
|
||||
setRegionOptions: (state, action: PayloadAction<RegionPokemonRange[]>) => {
|
||||
state.regionOptions = action.payload;
|
||||
},
|
||||
setTypeOptions: (state, action: PayloadAction<string[]>) => {
|
||||
state.typeOptions = action.payload;
|
||||
},
|
||||
setSortOptions: (
|
||||
state,
|
||||
action: PayloadAction<{ name: string; value: string }[]>,
|
||||
) => {
|
||||
state.sortOptions = action.payload;
|
||||
},
|
||||
setSearchInput: (state, action: PayloadAction<string>) => {
|
||||
state.searchInput = action.payload;
|
||||
},
|
||||
setIsLoadingPokemons: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoadingPokemons = action.payload;
|
||||
},
|
||||
@ -101,29 +66,9 @@ export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||
types: pokemon.types.map(type => type.type.name),
|
||||
}));
|
||||
});
|
||||
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;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setSelectedRegion,
|
||||
setSelectedType,
|
||||
setSelectedSort,
|
||||
setRegionOptions,
|
||||
setTypeOptions,
|
||||
setSortOptions,
|
||||
setSearchInput,
|
||||
setIsLoadingPokemons,
|
||||
} = pokedexSlice.actions;
|
||||
export const { setIsLoadingPokemons } = pokedexSlice.actions;
|
||||
|
||||
export default pokedexSlice.reducer;
|
||||
|
@ -2,19 +2,6 @@ import { PokemonResponseData } from './api';
|
||||
import { PokemonCardProps } from 'components/PokemonCard';
|
||||
|
||||
export type PokedexState = {
|
||||
regionOptions: RegionPokemonRange[];
|
||||
typeOptions: string[];
|
||||
sortOptions: { name: string; value: string }[];
|
||||
selectedRegion: string;
|
||||
selectedType: string;
|
||||
selectedSort: string;
|
||||
searchInput: string;
|
||||
isLoadingPokemons: boolean;
|
||||
pokemonCardList: PokemonCardProps[];
|
||||
};
|
||||
|
||||
export type RegionPokemonRange = {
|
||||
region: string;
|
||||
startId: number;
|
||||
endId: number;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RegionPokemonRange } from './types/slice';
|
||||
import { RegionPokemonRange } from 'features/Filters/types/slice';
|
||||
|
||||
export const getStartAndEndIdsForRegion = (
|
||||
region: string,
|
||||
|
@ -55,3 +55,17 @@ export const pokeApiAllPagesCustomBaseQuery = async (
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const pokeApiBaseQuery = async (
|
||||
args: pokeApiFullListFetchArgs,
|
||||
api: any,
|
||||
extra: any,
|
||||
) => {
|
||||
const baseUrl = 'https://pokeapi.co/api/v2/';
|
||||
|
||||
if (args.fetchAllPages) {
|
||||
return pokeApiAllPagesCustomBaseQuery(args, api, extra, baseUrl);
|
||||
} else {
|
||||
return fetchBaseQuery({ baseUrl })(args, api, extra);
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user