Moved Filter component into a separate feature

This commit is contained in:
Jason Zhu 2023-05-07 21:30:49 +10:00
parent 6295a6a792
commit be9d767bfd
18 changed files with 159 additions and 199 deletions

View File

@ -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>
);

View File

@ -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,

View File

@ -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 {

View File

@ -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 } =

View 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;

View 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;

View File

@ -0,0 +1 @@
export { default } from './Filters';

View 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;
};

View File

@ -1,2 +0,0 @@
export { default } from './Filters';
export { default as useFilterLoaded } from './useFilterLoaded';

View File

@ -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;

View File

@ -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 />
) : (

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -1,4 +1,4 @@
import { RegionPokemonRange } from './types/slice';
import { RegionPokemonRange } from 'features/Filters/types/slice';
export const getStartAndEndIdsForRegion = (
region: string,

View File

@ -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);
}
};