diff --git a/src/App.tsx b/src/App.tsx index 23c5316..411946f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 (
+
); diff --git a/src/app/store.ts b/src/app/store.ts index bf908e1..f67aa1b 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -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, diff --git a/src/components/PokemonCard/PokemonCard.css b/src/components/PokemonCard/PokemonCard.css index c348dc4..1ccead3 100644 --- a/src/components/PokemonCard/PokemonCard.css +++ b/src/components/PokemonCard/PokemonCard.css @@ -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 { diff --git a/src/features/Pokedex/Filters/Filters.css b/src/features/Filters/Filters.css similarity index 100% rename from src/features/Pokedex/Filters/Filters.css rename to src/features/Filters/Filters.css diff --git a/src/features/Pokedex/Filters/Filters.test.ts b/src/features/Filters/Filters.test.ts similarity index 100% rename from src/features/Pokedex/Filters/Filters.test.ts rename to src/features/Filters/Filters.test.ts diff --git a/src/features/Pokedex/Filters/Filters.tsx b/src/features/Filters/Filters.tsx similarity index 87% rename from src/features/Pokedex/Filters/Filters.tsx rename to src/features/Filters/Filters.tsx index abdc808..0a44f2d 100644 --- a/src/features/Pokedex/Filters/Filters.tsx +++ b/src/features/Filters/Filters.tsx @@ -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 } = diff --git a/src/features/Filters/filterApi.ts b/src/features/Filters/filterApi.ts new file mode 100644 index 0000000..d06f03d --- /dev/null +++ b/src/features/Filters/filterApi.ts @@ -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({ + query: () => ({ url: 'type', fetchAllPages: true }), + transformResponse: (response: RegionListResponseData) => { + return { + ...response, + results: [{ name: 'All Types', url: '' }, ...response.results], + }; + }, + }), + }), +}); + +export const { useGetTypeListQuery } = filterApi; diff --git a/src/features/Filters/filterSlice.ts b/src/features/Filters/filterSlice.ts new file mode 100644 index 0000000..4bbb8c5 --- /dev/null +++ b/src/features/Filters/filterSlice.ts @@ -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 = createSlice({ + name: 'filter', + initialState, + reducers: { + setSelectedRegion: (state, action: PayloadAction) => { + state.selectedRegion = action.payload; + }, + setSelectedType: (state, action: PayloadAction) => { + state.selectedType = action.payload; + }, + setSelectedSort: (state, action: PayloadAction) => { + state.selectedSort = action.payload; + }, + setRegionOptions: (state, action: PayloadAction) => { + state.regionOptions = action.payload; + }, + setTypeOptions: (state, action: PayloadAction) => { + state.typeOptions = action.payload; + }, + setSortOptions: ( + state, + action: PayloadAction<{ name: string; value: string }[]>, + ) => { + state.sortOptions = action.payload; + }, + setSearchInput: (state, action: PayloadAction) => { + 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; diff --git a/src/features/Filters/index.ts b/src/features/Filters/index.ts new file mode 100644 index 0000000..5b18d95 --- /dev/null +++ b/src/features/Filters/index.ts @@ -0,0 +1 @@ +export { default } from './Filters'; diff --git a/src/features/Filters/types/slice.ts b/src/features/Filters/types/slice.ts new file mode 100644 index 0000000..0d5a602 --- /dev/null +++ b/src/features/Filters/types/slice.ts @@ -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; +}; diff --git a/src/features/Pokedex/Filters/index.ts b/src/features/Pokedex/Filters/index.ts deleted file mode 100644 index 3fdb3d0..0000000 --- a/src/features/Pokedex/Filters/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Filters'; -export { default as useFilterLoaded } from './useFilterLoaded'; diff --git a/src/features/Pokedex/Filters/useFilterLoaded.ts b/src/features/Pokedex/Filters/useFilterLoaded.ts deleted file mode 100644 index 52beedc..0000000 --- a/src/features/Pokedex/Filters/useFilterLoaded.ts +++ /dev/null @@ -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; diff --git a/src/features/Pokedex/Pokedex.tsx b/src/features/Pokedex/Pokedex.tsx index 4868f58..ecd6962 100644 --- a/src/features/Pokedex/Pokedex.tsx +++ b/src/features/Pokedex/Pokedex.tsx @@ -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 ( <> - {isLoadingPokemons ? ( ) : ( diff --git a/src/features/Pokedex/pokedexApi.ts b/src/features/Pokedex/pokedexApi.ts deleted file mode 100644 index 6a754ac..0000000 --- a/src/features/Pokedex/pokedexApi.ts +++ /dev/null @@ -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({ - query: () => ({ url: `pokemon`, fetchAllPages: true }), - }), - getRegionList: builder.query({ - query: () => ({ url: 'region', fetchAllPages: true }), - }), - getTypeList: builder.query({ - query: () => ({ url: 'type', fetchAllPages: true }), - transformResponse: (response: RegionListResponseData) => { - return { - ...response, - results: [{ name: 'All Types', url: '' }, ...response.results], - }; - }, - }), - getPokemon: builder.query({ - query: IdOrName => ({ url: `pokemon/${IdOrName}` }), - }), - getRegion: builder.query({ - query: IdOrName => ({ url: `region/${IdOrName}` }), - }), - getType: builder.query({ - query: IdOrName => ({ url: `type/${IdOrName}` }), - }), - getLocation: builder.query({ - query: IdOrName => ({ url: `location/${IdOrName}` }), - }), - getArea: builder.query({ - query: IdOrName => ({ url: `location-area/${IdOrName}` }), - }), - }), -}); - -export const { - useGetPokemonListQuery, - useGetRegionListQuery, - useGetTypeListQuery, - useGetPokemonQuery, - useGetRegionQuery, - useGetTypeQuery, - useGetAreaQuery, - useGetLocationQuery, -} = pokedexApi; diff --git a/src/features/Pokedex/pokedexSlice.ts b/src/features/Pokedex/pokedexSlice.ts index aa345f3..18b4e29 100644 --- a/src/features/Pokedex/pokedexSlice.ts +++ b/src/features/Pokedex/pokedexSlice.ts @@ -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 = createSlice({ name: 'pokedex', initialState, reducers: { - setSelectedRegion: (state, action: PayloadAction) => { - state.selectedRegion = action.payload; - }, - setSelectedType: (state, action: PayloadAction) => { - state.selectedType = action.payload; - }, - setSelectedSort: (state, action: PayloadAction) => { - state.selectedSort = action.payload; - }, - setRegionOptions: (state, action: PayloadAction) => { - state.regionOptions = action.payload; - }, - setTypeOptions: (state, action: PayloadAction) => { - state.typeOptions = action.payload; - }, - setSortOptions: ( - state, - action: PayloadAction<{ name: string; value: string }[]>, - ) => { - state.sortOptions = action.payload; - }, - setSearchInput: (state, action: PayloadAction) => { - state.searchInput = action.payload; - }, setIsLoadingPokemons: (state, action: PayloadAction) => { state.isLoadingPokemons = action.payload; }, @@ -101,29 +66,9 @@ export const pokedexSlice: Slice = 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; diff --git a/src/features/Pokedex/types/slice.ts b/src/features/Pokedex/types/slice.ts index 3f97e49..849d0be 100644 --- a/src/features/Pokedex/types/slice.ts +++ b/src/features/Pokedex/types/slice.ts @@ -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; -}; diff --git a/src/features/Pokedex/utils.ts b/src/features/Pokedex/utils.ts index 340eee8..ddb78d2 100644 --- a/src/features/Pokedex/utils.ts +++ b/src/features/Pokedex/utils.ts @@ -1,4 +1,4 @@ -import { RegionPokemonRange } from './types/slice'; +import { RegionPokemonRange } from 'features/Filters/types/slice'; export const getStartAndEndIdsForRegion = ( region: string, diff --git a/src/features/Pokedex/paginationBaseQuery.ts b/src/services/pokeapi/paginationBaseQuery.ts similarity index 80% rename from src/features/Pokedex/paginationBaseQuery.ts rename to src/services/pokeapi/paginationBaseQuery.ts index 31becee..b32a3c3 100644 --- a/src/features/Pokedex/paginationBaseQuery.ts +++ b/src/services/pokeapi/paginationBaseQuery.ts @@ -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); + } +};