Compare commits

..

1 Commits

Author SHA1 Message Date
Jason Zhu 8844c3b7ae Try to implement graphql 2023-06-04 15:31:19 +10:00
11 changed files with 146 additions and 142 deletions

View File

@ -9,6 +9,7 @@
"@mui/material": "^5.13.1", "@mui/material": "^5.13.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"framer-motion": "^10.12.12", "framer-motion": "^10.12.12",
"graphql-request": "^6.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-lazy-load-image-component": "^1.5.6", "react-lazy-load-image-component": "^1.5.6",

View File

@ -8,12 +8,20 @@ import { useAppSelector } from 'app/hooks';
function App() { function App() {
const selectedRegion = useAppSelector(state => state.filter.selectedRegion); const selectedRegion = useAppSelector(state => state.filter.selectedRegion);
const selectedType = useAppSelector(state => state.filter.selectedType);
const selectedSort = useAppSelector(state => state.filter.selectedSort);
const selectedSearchInput = useAppSelector(state => state.filter.searchInput);
return ( return (
<div className="App app_container"> <div className="App app_container">
<Header /> <Header />
<Filters /> <Filters />
<Pokedex selectedRegion={selectedRegion} /> <Pokedex
selectedRegion={selectedRegion}
selectedType={selectedType}
selectedSort={selectedSort}
searchInput={selectedSearchInput}
/>
<InfoDialog /> <InfoDialog />
</div> </div>
); );

View File

@ -0,0 +1 @@
import { AppStore } from 'app/store';

View File

@ -0,0 +1,37 @@
import { createApi } from '@reduxjs/toolkit/query';
import { request, gql, ClientError } from 'graphql-request';
const graphqlBaseQuery =
({ baseUrl }: { baseUrl: string }) =>
async ({ body }: { body: string }) => {
try {
const result = await request(baseUrl, body);
return { data: result };
} catch (error) {
if (error instanceof ClientError) {
return { error: { status: error.response.status, data: error } };
}
return { error: { status: 500, data: error } };
}
};
export const pokeGraphqlApi = createApi({
baseQuery: graphqlBaseQuery({
baseUrl: 'https://beta.pokeapi.co/graphql/v1beta',
}),
endpoints: builder => ({
getRegionList: builder.query({
query: () => ({
body: gql`
query {
pokemon_v2_region {
id
name
}
}
`,
}),
transformResponse: response => response.pokemon_v2_region.data,
}),
}),
});

View File

@ -4,12 +4,12 @@ import {
PayloadAction, PayloadAction,
Slice, Slice,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import { FilterStateProps } from './types/slice'; import { FilterState } from './types/slice';
import { RegionPokemonRange } from './types/slice'; import { RegionPokemonRange } from './types/slice';
import { pokeRestApi } from 'app/services/pokeRestApi'; import { pokeRestApi } from 'app/services/pokeRestApi';
import { fetchPokemonsInTheRegion } from 'features/Pokedex/pokedexSlice'; import { fetchPokemonsInTheRegion } from 'features/Pokedex/pokedexSlice';
export const initialState: FilterStateProps = { const initialState: FilterState = {
regionOptions: [], regionOptions: [],
typeOptions: [], typeOptions: [],
sortOptions: [], sortOptions: [],
@ -46,7 +46,7 @@ export const initializeFilterSlice = createAsyncThunk(
}, },
); );
export const filterSlice: Slice<FilterStateProps> = createSlice({ export const filterSlice: Slice<FilterState> = createSlice({
name: 'filter', name: 'filter',
initialState, initialState,
reducers: { reducers: {

View File

@ -1,4 +1,4 @@
export type FilterStateProps = { export type FilterState = {
regionOptions: RegionPokemonRange[]; regionOptions: RegionPokemonRange[];
typeOptions: string[]; typeOptions: string[];
sortOptions: { name: string; value: string }[]; sortOptions: { name: string; value: string }[];

View File

@ -3,10 +3,8 @@ import { configureStore, createSlice } from '@reduxjs/toolkit';
import type { Meta } from '@storybook/react'; import type { Meta } from '@storybook/react';
import Pokedex from './Pokedex'; import Pokedex from './Pokedex';
import { initialState as initialPokedexState } from './pokedexSlice'; import { initialState } from './pokedexSlice';
import { initialState as initialFilterState } from '../Filters/filterSlice';
import { PokedexStateProps } from './types/slice'; import { PokedexStateProps } from './types/slice';
import { FilterStateProps } from 'features/Filters/types/slice';
const MockedState = { const MockedState = {
// Copied from Redux DevTools from browser // Copied from Redux DevTools from browser
@ -64,55 +62,30 @@ const MockedState = {
}, },
], ],
}, },
filter: {
regionOptions: [],
typeOptions: [],
sortOptions: [],
selectedRegion: 'kanto',
selectedType: 'All Types',
selectedSort: 'id',
searchInput: '',
},
}; };
interface MockStoreProps { interface MockStoreProps {
pokedexState: PokedexStateProps; pokedexState: PokedexStateProps;
filterState: FilterStateProps;
children: React.ReactNode; children: React.ReactNode;
} }
// Create a mock store // Create a mock store
const mockPokedexSlice = (pokedexState: PokedexStateProps) => { const mockSlice = (pokedexState: PokedexStateProps) => {
return createSlice({ return createSlice({
name: 'pokedex', name: 'pokedex',
initialState: pokedexState, initialState: pokedexState,
reducers: {}, reducers: {},
}); });
}; };
const mockFilterSlice = (filterState: FilterStateProps) => { const mockStore = (pokedexState: PokedexStateProps) => {
return createSlice({
name: 'filter',
initialState: filterState,
reducers: {},
});
};
const mockStore = (
pokedexState: PokedexStateProps,
filterState: FilterStateProps,
) => {
return configureStore({ return configureStore({
reducer: { reducer: {
filter: mockFilterSlice(filterState).reducer, pokedex: mockSlice(pokedexState).reducer,
pokedex: mockPokedexSlice(pokedexState).reducer,
}, },
}); });
}; };
const Mockstore: React.FC<MockStoreProps> = ({ const Mockstore: React.FC<MockStoreProps> = ({ pokedexState, children }) => (
pokedexState, <Provider store={mockStore(pokedexState)}>{children}</Provider>
filterState,
children,
}) => (
<Provider store={mockStore(pokedexState, filterState)}>{children}</Provider>
); );
const meta: Meta<typeof Pokedex> = { const meta: Meta<typeof Pokedex> = {
@ -132,53 +105,21 @@ export default meta;
export const Loding = { export const Loding = {
decorators: [ decorators: [
(story: () => React.ReactNode) => ( (story: () => React.ReactNode) => (
<Mockstore <Mockstore pokedexState={initialState}>{story()}</Mockstore>
pokedexState={initialPokedexState}
filterState={initialFilterState}
>
{story()}
</Mockstore>
), ),
], ],
}; };
export const All = { export const Primary = {
decorators: [ decorators: [
(story: () => React.ReactNode) => ( (story: () => React.ReactNode) => (
<Mockstore <Mockstore pokedexState={MockedState.pokedex}>{story()}</Mockstore>
pokedexState={MockedState.pokedex}
filterState={MockedState.filter}
>
{story()}
</Mockstore>
),
],
args: {
selectedRegion: 'kanto',
},
};
const filterStateOnlyFire = {
regionOptions: [],
typeOptions: [],
sortOptions: [],
selectedRegion: 'kanto',
selectedType: 'fire',
selectedSort: 'id',
searchInput: '',
};
export const typeFireSelected = {
decorators: [
(story: () => React.ReactNode) => (
<Mockstore
pokedexState={MockedState.pokedex}
filterState={filterStateOnlyFire}
>
{story()}
</Mockstore>
), ),
], ],
args: { args: {
selectedRegion: 'kanto', selectedRegion: 'kanto',
selectedType: 'All Types',
selectedSort: 'id',
searchInput: '',
}, },
}; };

View File

@ -1,26 +1,77 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import PokemonCard from 'components/PokemonCard'; import PokemonCard, { PokemonCardProps } from 'components/PokemonCard';
import Loading from 'components/Loading'; import Loading from 'components/Loading';
import { useAppSelector, useAppDispatch } from 'app/hooks'; import { useAppSelector, useAppDispatch } from 'app/hooks';
import { import { fetchPokemonsInTheRegion } from './pokedexSlice';
fetchPokemonsInTheRegion,
searchedSortedFilteredPokemonCardList,
} from './pokedexSlice';
import { fetchSelectedPokemonInfo } from 'features/InfoDialog/infoDialogSlice'; import { fetchSelectedPokemonInfo } from 'features/InfoDialog/infoDialogSlice';
export const filterPokemonCardsByType = (
pokemonList: PokemonCardProps[],
selectedType: string,
) => {
return pokemonList.filter(
pokemon =>
selectedType === 'All Types' ||
pokemon.types.some(type => type === selectedType),
);
};
export const sortPokemonCardsByIdOrName = (
pokemonList: PokemonCardProps[],
selectedSort: string,
) => {
return pokemonList.sort((a, b) => {
if (selectedSort === 'id') {
return a.id - b.id;
} else if (selectedSort === 'name') {
return a.name.localeCompare(b.name);
} else {
return 0;
}
});
};
export const searchPokemonCardsByName = (
pokemonList: PokemonCardProps[],
searchInput: string,
) => {
return pokemonList.filter(pokemon =>
pokemon.name.toLowerCase().includes(searchInput.toLowerCase()),
);
};
export interface PokedexProps { export interface PokedexProps {
selectedRegion: string; selectedRegion: string;
selectedType: string;
selectedSort: string;
searchInput: string;
} }
const Pokedex = ({ selectedRegion }: PokedexProps) => { const Pokedex = ({
selectedRegion,
selectedType,
selectedSort,
searchInput,
}: PokedexProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isLoadingPokemons = useAppSelector( const isLoadingPokemons = useAppSelector(
state => state.pokedex.isLoadingPokemons, state => state.pokedex.isLoadingPokemons,
); );
const pokemonCardListForRendering = searchedSortedFilteredPokemonCardList( const pokemonList = useAppSelector(state => state.pokedex.pokemonCardList);
useAppSelector(state => state),
const filteredPokemonList = filterPokemonCardsByType(
pokemonList,
selectedType,
);
const sortedFilteredPokemonCardList = sortPokemonCardsByIdOrName(
filteredPokemonList,
selectedSort,
);
const searchedPokemonCardList = searchPokemonCardsByName(
sortedFilteredPokemonCardList,
searchInput,
); );
useEffect(() => { useEffect(() => {
@ -33,7 +84,7 @@ const Pokedex = ({ selectedRegion }: PokedexProps) => {
<Loading /> <Loading />
) : ( ) : (
<div className="all__pokemons"> <div className="all__pokemons">
{pokemonCardListForRendering.map(pokemonCard => ( {searchedPokemonCardList.map(pokemonCard => (
<PokemonCard <PokemonCard
key={pokemonCard.id} key={pokemonCard.id}
id={pokemonCard.id} id={pokemonCard.id}

View File

@ -2,7 +2,7 @@ import {
filterPokemonCardsByType, filterPokemonCardsByType,
sortPokemonCardsByIdOrName, sortPokemonCardsByIdOrName,
searchPokemonCardsByName, searchPokemonCardsByName,
} from 'features/Pokedex/pokedexSlice'; } from 'features/Pokedex/Pokedex';
import { PokemonCardProps } from 'components/PokemonCard'; import { PokemonCardProps } from 'components/PokemonCard';
import pokemon3_venusaur_card from 'features/Pokedex/__test__/pokemon3_venusaur_Card.json'; import pokemon3_venusaur_card from 'features/Pokedex/__test__/pokemon3_venusaur_Card.json';
import pokemon4_charmander_card from 'features/Pokedex/__test__/pokemon4_charmandar_Card.json'; import pokemon4_charmander_card from 'features/Pokedex/__test__/pokemon4_charmandar_Card.json';

View File

@ -7,7 +7,6 @@ import { getStartAndEndIdsForRegion } from './utils';
import { PokemonResponseData } from 'types/api'; import { PokemonResponseData } from 'types/api';
import { pokeRestApi } from 'app/services/pokeRestApi'; import { pokeRestApi } from 'app/services/pokeRestApi';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { PokemonCardProps } from 'components/PokemonCard';
export const fetchPokemonsInTheRegion = createAsyncThunk< export const fetchPokemonsInTheRegion = createAsyncThunk<
PokemonResponseData[], PokemonResponseData[],
@ -67,57 +66,3 @@ export const pokedexSlice: Slice<PokedexStateProps> = createSlice({
export const { setIsLoadingPokemons } = pokedexSlice.actions; export const { setIsLoadingPokemons } = pokedexSlice.actions;
export default pokedexSlice.reducer; export default pokedexSlice.reducer;
/// selectors
export const filterPokemonCardsByType = (
pokemonList: PokemonCardProps[],
selectedType: string,
) => {
return pokemonList.filter(
pokemon =>
selectedType === 'All Types' ||
pokemon.types.some(type => type === selectedType),
);
};
export const sortPokemonCardsByIdOrName = (
pokemonList: PokemonCardProps[],
selectedSort: string,
) => {
return pokemonList.sort((a, b) => {
if (selectedSort === 'id') {
return a.id - b.id;
} else if (selectedSort === 'name') {
return a.name.localeCompare(b.name);
} else {
return 0;
}
});
};
export const searchPokemonCardsByName = (
pokemonList: PokemonCardProps[],
searchInput: string,
) => {
return pokemonList.filter(pokemon =>
pokemon.name.toLowerCase().includes(searchInput.toLowerCase()),
);
};
export const filteredPokemonListByType = (state: RootState) =>
filterPokemonCardsByType(
state.pokedex.pokemonCardList,
state.filter.selectedType,
);
export const sortedFilteredPokemonCardList = (state: RootState) =>
sortPokemonCardsByIdOrName(
filteredPokemonListByType(state),
state.filter.selectedSort,
);
export const searchedSortedFilteredPokemonCardList = (state: RootState) =>
searchPokemonCardsByName(
sortedFilteredPokemonCardList(state),
state.filter.searchInput,
);

View File

@ -1833,6 +1833,11 @@
resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4" resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4"
integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ== integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==
"@graphql-typed-document-node/core@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
"@humanwhocodes/config-array@^0.11.8": "@humanwhocodes/config-array@^0.11.8":
version "0.11.8" version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@ -5997,6 +6002,13 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
path-type "^4.0.0" path-type "^4.0.0"
yaml "^1.10.0" yaml "^1.10.0"
cross-fetch@^3.1.5:
version "3.1.6"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c"
integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==
dependencies:
node-fetch "^2.6.11"
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -7999,6 +8011,14 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
graphql-request@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f"
integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==
dependencies:
"@graphql-typed-document-node/core" "^3.2.0"
cross-fetch "^3.1.5"
"graphql@^15.0.0 || ^16.0.0": "graphql@^15.0.0 || ^16.0.0":
version "16.6.0" version "16.6.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb"
@ -10260,7 +10280,7 @@ node-fetch-native@^1.0.2:
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.1.1.tgz#b8977dd7fe6c5599e417301ed3987bca787d3d6f" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.1.1.tgz#b8977dd7fe6c5599e417301ed3987bca787d3d6f"
integrity sha512-9VvspTSUp2Sxbl+9vbZTlFGq9lHwE8GDVVekxx6YsNd1YH59sb3Ba8v3Y3cD8PkLNcileGGcA21PFjVl0jzDaw== integrity sha512-9VvspTSUp2Sxbl+9vbZTlFGq9lHwE8GDVVekxx6YsNd1YH59sb3Ba8v3Y3cD8PkLNcileGGcA21PFjVl0jzDaw==
node-fetch@^2.6.1: node-fetch@^2.6.1, node-fetch@^2.6.11:
version "2.6.11" version "2.6.11"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25"
integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==