Compare commits

..

182 Commits

Author SHA1 Message Date
Jason Zhu fbfa04a2b7 Clear the import 2023-06-04 16:48:31 +10:00
Jason Zhu c65e946f09 Fix Pokedex.stories.tsx for moving filterPokemonCardsByType, sortPokemonCardsByIdOrName, searchPokemonCardsByName into pokedexSlice, so we can have memorized filtered pokedex 2023-06-04 16:39:51 +10:00
Jason Zhu 6fab4b8217 Put filter pokemonCard logic into redux
(cherry picked from commit e7e4e391d5)
2023-06-04 16:04:59 +10:00
Jason Zhu fa0d194b74 Remove '..' in import 2023-06-04 15:30:29 +10:00
Jason Zhu 7db1852489 Revert "Put filter pokemonCard logic into redux" as it was overfitting redux way
This reverts commit e7e4e391d5.
2023-06-04 15:28:05 +10:00
Jason Zhu e7e4e391d5 Put filter pokemonCard logic into redux 2023-06-04 15:27:28 +10:00
Jason Zhu 9cb23006ed Rename pokeApi to pokeRestApi 2023-06-04 12:54:01 +10:00
Jason Zhu cfd9301793 Remove unnecessary getTypeList initiate() in filterSlice.ts 2023-06-04 12:50:02 +10:00
Jason Zhu 3996e09308 Replace fetch in fetchPokemonsInTheRegion by using RTK Query 2023-06-04 12:37:48 +10:00
Jason Zhu 6f04c341e3 Remove calling async thunk from Filter component using dispatch, but call it directly in slice (reformat) 2023-06-04 12:19:37 +10:00
Jason Zhu 4cfc08448b Remove calling async thunk from Filter component using dispatch, but call it directly in slice 2023-06-04 12:18:41 +10:00
Jason Zhu 3faf494bd0 Added Storybook builds location 2023-06-02 22:53:36 +10:00
Jason Zhu e323616d6d change package.json command from npm to yarn 2023-06-02 22:20:23 +10:00
Jason Zhu ad2317b524 Update webstorm Project.xml 2023-06-02 22:15:35 +10:00
Jason Zhu 22e291c932 Add eslint no-empty-function rule to stop reporting building problem 2023-05-31 19:39:19 +10:00
Jason Zhu d40214f12d Update deploy website 2023-05-31 18:23:26 +10:00
Jason Zhu 2ecf6c82ed Change name of README.md 2023-05-31 18:14:08 +10:00
Jason Zhu 8c1e2806d5 Modified README to add .gif files 2023-05-31 14:43:47 +10:00
Jason Zhu 766a48f7c0 Centralised all infoDialogSlice related storybook preparation in infoDialogSlice.storybook.ts to reduce code footprint 2023-05-20 15:46:28 +10:00
Jason Zhu 8f217d678a Implement onClick action in EvolutionSpecies.tsx, and fix all related storybook 2023-05-20 15:07:12 +10:00
Jason Zhu 99fd0577fd Removed unnecessary code snippets in Pokedex.stories.tsx 2023-05-20 14:29:58 +10:00
Jason Zhu bf3cbe886b Trying to use type alis extension to replace overlap interface definition 2023-05-20 12:42:04 +10:00
Jason Zhu 768c84d6b8 Remove pagination fetching in pokeApi.ts 2023-05-19 23:28:36 +10:00
Jason Zhu bde86898e2 Fixed most warning in pokeApi.ts and Pokedex.tsx 2023-05-19 22:48:15 +10:00
Jason Zhu 4a13b56c98 Remove unnecessary element in PokemonResponseData type 2023-05-19 22:00:22 +10:00
Jason Zhu 2513d4365f Move initialization of Filter Slice into filterSlice.ts 2023-05-19 21:57:52 +10:00
Jason Zhu 54bf00e22c Removed unnecessary action 2023-05-19 19:42:02 +10:00
Jason Zhu 7bb4fde73e Remove redundant code in pokeApi.ts 2023-05-19 18:37:17 +10:00
Jason Zhu c7d7db22da Added todos in README.md 2023-05-19 00:17:22 +10:00
Jason Zhu 974c2e38ff Prettier Header.css 2023-05-19 00:08:06 +10:00
Jason Zhu 81a2b7cb69 Move files into correct place in asset directory, and refactored Header 2023-05-19 00:07:45 +10:00
Jason Zhu 64a3bedde1 Prettier *.css files 2023-05-18 23:37:54 +10:00
Jason Zhu 93e45f79d6 Fixed loading gif 2023-05-18 23:37:35 +10:00
Jason Zhu e4d3344334 Fixed font issues by importing Teko-Regular.ttf, and VT323-Regular.ttf 2023-05-18 23:27:00 +10:00
Jason Zhu 1c279f4efb Fix relative import 2023-05-18 23:11:20 +10:00
Jason Zhu 8a54245ecb Fixed genderRatio in InfoDialog 2023-05-18 23:06:03 +10:00
Jason Zhu bc9ab24c33 Removed unnecessary InfoDialogProps 2023-05-18 23:01:46 +10:00
Jason Zhu 800cecbefc Removed InfoDialog.stories.tsx 2023-05-18 23:00:07 +10:00
Jason Zhu d27ad13e57 Implemented closing of InfoDialog 2023-05-18 22:59:36 +10:00
Jason Zhu ff05a9bf83 Complete InfoDialog feature, now we can click info icon on card to show modal 2023-05-18 22:46:23 +10:00
Jason Zhu 8607a8f1ad Trying to implement InfoDialogSlice and related api endpoints (app is running but infodialog is not showing) 2023-05-18 22:03:36 +10:00
Jason Zhu 281eafc863 Add *.log and *.tsbuildinfo into .gitignore 2023-05-17 21:51:17 +10:00
Jason Zhu 78b7a4a5f4 Trying to impolement InfoDialogSlice and related api endpoints (Code is faulty) 2023-05-17 21:50:34 +10:00
Jason Zhu 8c442946d3 Fixed PokemonCard render issue for single type, by using unit test 2023-05-15 21:21:45 +10:00
Jason Zhu c50862adbf Renamed stories in PokemonTypes.stories.tsx to have correct story names 2023-05-15 21:20:18 +10:00
Jason Zhu 03e973c68d Remove unnecessary import in InfoDialogComponent.stories.tsx 2023-05-15 20:54:22 +10:00
Jason Zhu 02de053d90 Modify all story files, remove deprecated method and use new Meta & StoryObj from Storybook v7 2023-05-15 20:53:17 +10:00
Jason Zhu 50e64b5197 Implemented functional Pokedex.stories.tsx by using decorator to connect Pokedex component with redux store 2023-05-15 20:30:11 +10:00
Jason Zhu 05d31c55a9 Rename .storybook/main.js to main.ts and add type to make it works 2023-05-15 20:03:20 +10:00
Jason Zhu 3eb8eb836e Rename preview.js to preview.ts to use storybook v7 Preview 2023-05-15 19:48:08 +10:00
Jason Zhu 42a4a86e34 Fix import in InfoDialog component so storybook can render successfully 2023-05-15 19:10:22 +10:00
Jason Zhu 987fbbaa79 Upgrade storybook/react to version 7 using `npx storybook@latest upgrade --prerelease` command following https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#from-version-65x-to-700 2023-05-15 18:51:54 +10:00
Jason Zhu 0048a1c064 Trying to implement Pokedex.stories.tsx 2023-05-13 12:41:42 +10:00
Jason Zhu 713b86c14a Removed unused stories directory 2023-05-11 23:39:00 +10:00
Jason Zhu ddb058dce6 Added chromatic script 2023-05-11 23:34:44 +10:00
Jason Zhu cbb8dd9054 Added chromatic as development dependency 2023-05-11 23:30:16 +10:00
Jason Zhu 52ac92fd7a Fixed import directory problem in pokeApi.test.ts 2023-05-11 22:43:58 +10:00
Jason Zhu f8fb41e837 Renamed PokedexState to PokedexStateProps for type 2023-05-11 20:55:53 +10:00
Jason Zhu 08c8af5f4f Implemented getPokemon, getPokemonSpecies, getEvolutionChain endpoints and related jest UTs 2023-05-11 20:04:45 +10:00
Jason Zhu a0adfb0268 Merge all pokeapi related code into one single pokeApi.ts, and relocate it into app/services 2023-05-11 19:05:15 +10:00
Jason Zhu 804c145b11 Separate static part of InfoDialog into InfoDialogComponent, and added stories 2023-05-10 00:13:33 +10:00
Jason Zhu 34af94508e Added name into EvolutionSpecies component 2023-05-10 00:07:14 +10:00
Jason Zhu 4cebf35cf0 Added bulbasaur into PokemonCard storybook 2023-05-08 23:27:44 +10:00
Jason Zhu 6d391d1f9f Refactor colorTypeGradients method again to receive types string list directly 2023-05-08 23:20:47 +10:00
Jason Zhu 25a010e7c6 Refactor colorTypeGradients method to make it shorter 2023-05-08 23:14:03 +10:00
Jason Zhu 57e92b17d3 Prettier format EvolutionSpecies.css 2023-05-08 22:51:07 +10:00
Jason Zhu c20e58db52 Created EvolutionSpecies component with motion 2023-05-08 22:50:43 +10:00
Jason Zhu 23db199ab0 Fixed wrong import in filterApi.test.ts 2023-05-08 21:24:22 +10:00
Jason Zhu 7f563d8c73 Remove listenerMiddleware 2023-05-08 21:23:24 +10:00
Jason Zhu 5317050679 Merge all api types together 2023-05-08 21:15:51 +10:00
Jason Zhu 1055a14457 Put types into correct directory (fix filterApi.ts) 2023-05-08 20:34:13 +10:00
Jason Zhu 6082aa75a0 Put types into correct directory 2023-05-08 20:33:42 +10:00
Jason Zhu 9bb252de00 Fixed pokedex.test.ts after splitting filter out of pokedex component 2023-05-08 20:12:45 +10:00
Jason Zhu db331acbb1 Move Filter related tests in original pokedexApi.test.ts into filterApi.test.ts 2023-05-08 19:59:04 +10:00
Jason Zhu 1d58f41b5f Fixed Filters.test.ts 2023-05-08 00:24:33 +10:00
Jason Zhu 3539b6febe Separate selected Region/Type/Sort/SearchInput out of Pokedex, so it become easier for testing in storybook 2023-05-08 00:24:16 +10:00
Jason Zhu 6acabeb91a Added InfoDialog feature (partially done) 2023-05-07 23:28:14 +10:00
Jason Zhu ff92db699d Prettier GenderRate.css 2023-05-07 23:10:50 +10:00
Jason Zhu f5e0a75c64 Modified index.css file for better global rendering 2023-05-07 23:10:23 +10:00
Jason Zhu 749a745b4a Created GenderRate component for InfoDialog 2023-05-07 23:09:15 +10:00
Jason Zhu 0c4f2d4b67 Import index.css as global css into storybook 2023-05-07 22:39:39 +10:00
Jason Zhu 2ae23cdbfa Refactored PokemonCard a little bit 2023-05-07 22:39:08 +10:00
Jason Zhu 1f0ab75d6e Implemented index.ts for PokemonTypes 2023-05-07 22:32:06 +10:00
Jason Zhu 93fe0cb24a Fixed url import of image in index.css 2023-05-07 22:31:12 +10:00
Jason Zhu 40a64e9033 Implemented GenderRate component for InfoDialog feature 2023-05-07 22:30:20 +10:00
Jason Zhu a21427b91f Remove unnecessary Filter component in Pokedex 2023-05-07 21:34:12 +10:00
Jason Zhu be9d767bfd Moved Filter component into a separate feature 2023-05-07 21:30:49 +10:00
Jason Zhu 6295a6a792 Prettier PokemonCard.css 2023-05-07 19:55:50 +10:00
Jason Zhu dbb7ab99d2 Fixed storybook hierarchy for PokemonCard 2023-05-07 19:54:45 +10:00
Jason Zhu 92d3428c84 Move PokemonCard component into component directory 2023-05-07 19:50:50 +10:00
Jason Zhu ce3d3ce0d7 Fixed width problem of rendering in storybook 2023-05-07 16:10:48 +10:00
Jason Zhu 5068a6f728 Move poke__type css class into PokemonCard component 2023-05-07 15:50:47 +10:00
Jason Zhu b56eae4e40 Addd searchInput selector & remove redundant css 2023-04-19 00:14:45 +10:00
Jason Zhu b2839de8a7 Add filter css (part2: prettier) 2023-04-19 00:03:43 +10:00
Jason Zhu 133884e717 Add filter css 2023-04-19 00:03:02 +10:00
Jason Zhu bb5f6d73c4 Add css for types in PokemonCard back to fix render issue in storybook 2023-04-18 22:46:45 +10:00
Jason Zhu ec0af2f3ac Remove unnecessary images asset and modified PokemonCard for using PokemonTypes component 2023-04-18 21:16:43 +10:00
Jason Zhu 71bc089745 Implemented PokemonTypes common component (part2) 2023-04-18 21:10:57 +10:00
Jason Zhu faf44410bd Implemented PokemonTypes common component 2023-04-18 21:09:53 +10:00
Jason Zhu 3281629dcf Implemented search in the filter bar 2023-04-17 22:52:54 +10:00
Jason Zhu 10442f9dc8 Implemented pokedex list style 2023-04-17 22:34:18 +10:00
Jason Zhu 2857b1d131 Added react-lazy-load-image-component for lazy loading image in PokemonCard 2023-04-17 22:16:25 +10:00
Jason Zhu 3926267d77 Fixed setIsLoadingPokemons calling in fetchPokemonsInRegion 2023-04-17 19:13:28 +10:00
Jason Zhu c10c2f2608 Removed redundant cache clear code 2023-04-17 18:58:34 +10:00
Jason Zhu 00a8e7cba1 Transforms full Pokemon to pokemonCard in PokedexSlice to reduce store size 2023-04-17 18:43:29 +10:00
Jason Zhu 609b5621f1 Change number in PokemonCardProps to id 2023-04-17 00:26:13 +10:00
Jason Zhu 9338a70918 Renamed Pokemon component to PokemonCard 2023-04-17 00:14:29 +10:00
Jason Zhu 2d4426d84d Remove setPokemonList action, and replaced calling pokedexApi using regular fetch 2023-04-17 00:10:26 +10:00
Jason Zhu 91ead7f64f Modify pokedex.test.ts, clear up redundant code 2023-04-12 23:54:12 +10:00
Jason Zhu 8696392dce Implemented sortPokemonsByIdOrName and related unit tests 2023-04-12 23:44:13 +10:00
Jason Zhu 88fb450c5a Fixed pokedexApi.test.ts 2023-04-12 23:24:29 +10:00
Jason Zhu 92f7111943 Fixed filterPokemonByType problem by creating separate function and write unit tests 2023-04-12 23:23:52 +10:00
Jason Zhu fd21848a85 Implemented setTypeOptions and setSelectedType at correct place 2023-04-12 21:57:02 +10:00
Jason Zhu e2bbe1d959 Implemented filteredPokemonList feature 2023-04-11 19:50:59 +10:00
Jason Zhu 968c6c5d95 Added setTypeOptions in useEffect() 2023-04-11 18:46:48 +10:00
Jason Zhu 1801e43192 Recreate useGetRegionOptions and useGetSortOptions again for getting region and sort options in Filter.tsx 2023-04-11 18:33:34 +10:00
Jason Zhu 73fa644a55 initialize Filter by setSelectedRegion and fetchPokemonsInTheRegion 2023-04-11 18:12:38 +10:00
Jason Zhu 790c7828b1 Remove startAppListening import in pokedexSlice.ts 2023-04-11 17:33:28 +10:00
Jason Zhu 183ce62f30 Changed pokedex state variable name; And remove unnecessary listener middleware 2023-04-11 17:31:48 +10:00
Jason Zhu 81fcac97c6 Implement fetchPokemonsInTheRegion to get all pokemons from the region 2023-04-10 15:52:57 +10:00
Jason Zhu a09463a2b4 Implemented side effect of selectedRegion using app listener 2023-04-07 17:01:18 +10:00
Jason Zhu 83ae2f34d7 Change Redux code style by move all slice initialization process into pokedexSlice, Component are majorly for presentation 2023-04-06 23:57:05 +10:00
Jason Zhu fa4fb04efb Remove non-necessary addAppListener (dynamic plugin) 2023-04-05 21:37:57 +10:00
Jason Zhu 40358e3900 Created utils functions within Pokedex 2023-04-05 21:37:03 +10:00
Jason Zhu fed47e34b0 Fix importing from reduxjs/toolkit 2023-04-05 21:36:01 +10:00
Jason Zhu 63e3ce5fb6 Add setRegionPokemonIdsList in Filter 2023-04-05 21:34:53 +10:00
Jason Zhu 19c189c37d Setup typescripted listenerMiddleware 2023-04-04 23:12:22 +10:00
Jason Zhu f30edc9700 Move types with pokedexSlice into types/slice.ts 2023-04-02 23:51:23 +10:00
Jason Zhu 40049ef7b5 Remove unnecessary setRegionPokemonList endpoint 2023-04-02 23:50:22 +10:00
Jason Zhu 89b5b976e1 Removed getRegionPokemonList endpoint and related tests 2023-04-02 21:07:53 +10:00
Jason Zhu f4fd616b34 Steop using setRegionPokemonList, as now we link Region with Pokemon ID 2023-04-02 18:13:44 +10:00
Jason Zhu 5fee30437b Add TODO for removing unnecessary endpoints 2023-04-02 16:01:36 +10:00
Jason Zhu a831e76275 Add correct yarn test:watchAll command in package.json, and remove unnecessary App.test.tsx 2023-04-02 15:49:35 +10:00
Jason Zhu 3ee61e19f9 Install tslint config for webstorm 2023-03-31 23:11:30 +11:00
Jason Zhu be0903bd93 Trying to use getRegionPokemonList endpoint for Loading screen 2023-03-31 21:58:03 +11:00
Jason Zhu 54bd031092 Implement getRegionPokemonList with queryFn and related test cases (fix) 2023-03-28 22:46:53 +11:00
Jason Zhu f4da542ecd Implement getRegionPokemonList with queryFn and related test cases 2023-03-28 22:44:00 +11:00
Jason Zhu 5c87c25620 Modify Filter so it send selectedRegion and selectedType to pokedexSlice 2023-03-27 23:49:12 +11:00
Jason Zhu 0b2de88f4a Show loading image when GetRegionListQuery and GetTypeListQuery is loading 2023-03-27 23:29:10 +11:00
Jason Zhu 9eb1f1f971 Consolidate types/interfaces for api 2023-03-27 23:18:34 +11:00
Jason Zhu 5d6c09642c Implemented pokeApiAllPagesCustomBaseQuery and jest tests 2023-03-27 22:52:15 +11:00
Jason Zhu 6849b97726 Implemented test for getTypeList, getRegionList endpoints 2023-03-26 22:26:15 +11:00
Jason Zhu 6aa1baa5fd Remove unncessary beforeAfter in jest, store was re-created for every test 2023-03-26 20:09:58 +11:00
Jason Zhu 4db65c6375 Changed lint-staged.config.js for json files 2023-03-26 18:39:34 +11:00
Jason Zhu 00556e35d9 Installed msw for mocking server, add JEST tests for simple pokedexApi endpoints; Change lint-staged.config.js for json files 2023-03-26 18:39:04 +11:00
Jason Zhu 9ed782813d Fixed all .css files 2023-03-26 18:20:17 +11:00
Jason Zhu db682dc2a2 Move Loading screen into Pokedex.tsx 2023-03-26 15:56:51 +11:00
Jason Zhu 33b1f4f79c Configure Pokemon CSS correctly 2023-03-23 21:45:20 +11:00
Jason Zhu 3c91954e29 Implemented background image color of Pokemon Card 2023-03-23 21:21:46 +11:00
Jason Zhu 751704a8c2 Implemented jest test for Pokemon Card component (part 2): export formatNumber 2023-03-23 21:04:15 +11:00
Jason Zhu 9e48382026 Implemented jest test for Pokemon Card component 2023-03-23 21:03:36 +11:00
Jason Zhu 27df3a780a Implemented type color for Pokemon Card component 2023-03-23 20:49:31 +11:00
Jason Zhu 23b29ffccc Implement colorTypeGradients utility function 2023-03-23 20:34:09 +11:00
Jason Zhu 4c0f7f4d1c Implemented PokeTypeAsset in Pokemon Card component 2023-03-23 20:32:28 +11:00
Jason Zhu 0d6c393cbb Moved charizard.json into src/features/Pokedex/Pokemon/assets directory 2023-03-23 00:12:50 +11:00
Jason Zhu cf9e5d490d Moved poke types into src/features/Pokedex/Pokemon/assets directory 2023-03-23 00:12:29 +11:00
Jason Zhu 0b5a98d859 Implement first iteration of Pokemon.stories.tsx 2023-03-22 21:50:35 +11:00
Jason Zhu 91f3d51b9b Configured storybook 2023-03-22 21:29:42 +11:00
Jason Zhu aee577aeef Installed storybook in the project 2023-03-22 21:16:47 +11:00
Jason Zhu 87293cbd7f Change package manager from npm to yarn 2023-03-22 21:11:12 +11:00
Jason Zhu a29c471a0b Implemented index.ts for small components 2023-03-22 20:32:04 +11:00
Jason Zhu 47558e17d3 modified .nvmrc file 2023-03-21 17:28:18 +11:00
Jason Zhu af89068929 Implemented setSelectedType and setSelectedSort reducer in pokedexSlice, and getRegionList and getTypeList endpoints 2023-03-19 22:42:09 +11:00
Jason Zhu 452794f4e6 Created pokedexSlice and pokedexApi to perform getPokemonList 2023-03-19 16:53:07 +11:00
Jason Zhu e901a58105 Moved Filters.tsx into Pokedex feature folder 2023-03-19 15:53:49 +11:00
Jason Zhu 3139f53236 Added Pokedex component 2023-03-19 15:38:29 +11:00
Jason Zhu 4a8ad0256b Fixed abosolute path in App.tsx 2023-03-19 15:24:59 +11:00
Jason Zhu a867ce0001 Use Create-React-App absolute path feature 2023-03-19 15:17:58 +11:00
Jason Zhu bbaec4a027 Move Header.tsx into components directory 2023-03-19 13:16:17 +11:00
Jason Zhu 65f0b56a20 Fixed location of pokemon card location 2023-03-19 12:48:37 +11:00
Jason Zhu 610b501d9a Fixed import of charizard image for example 2023-03-18 23:52:12 +11:00
Jason Zhu fe2f07a165 Added configuration for Webstorm in per project setting 2023-03-18 23:39:18 +11:00
Jason Zhu 1b05c69514 Configured eslint by install eslint-plugin-prettier 2023-03-18 23:38:44 +11:00
Jason Zhu b3d48f475c Added husky and lint-staged for pre-commit check 2023-03-18 23:31:08 +11:00
Jason Zhu 8688cc44f0 Fixed lint & format using prettier and eslint 2023-03-18 23:21:20 +11:00
Jason Zhu 5ccfe5a986 Configured eslint and prettier to work on package.json 2023-03-18 22:52:01 +11:00
Jason Zhu 59ce0e2051 Installed eslint and provide configurations 2023-03-18 22:47:48 +11:00
Jason Zhu 53fcf68405 Move dev dependencies into devDependencies 2023-03-18 22:40:36 +11:00
Jason Zhu 36929bcbcc Added all image assets 2023-03-18 22:29:13 +11:00
Jason Zhu bc0c90767f Added .nvmrc file 2023-03-05 22:17:09 +11:00
Jason Zhu 634b15d0a9 Added fonts for the Pokemon card 2023-03-05 21:59:29 +11:00
Jason Zhu 8edd8b1040 Added a sample static Pokemon card 2023-03-05 21:03:15 +11:00
128 changed files with 82037 additions and 17267 deletions

29
.eslintrc 100644
View File

@ -0,0 +1,29 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended"
],
"plugins": ["react", "@typescript-eslint", "prettier"],
"env": {
"browser": true,
"node": true,
"es2021": true
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"prettier/prettier": "error",
"@typescript-eslint/no-empty-function": "off"
}
}

3
.gitignore vendored
View File

@ -21,3 +21,6 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
*.log
*.tsbuildinfo

View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@ -0,0 +1,61 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_QUOTE_STYLE" value="Single" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -2,5 +2,6 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile> </profile>
</component> </component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

1
.nvmrc 100644
View File

@ -0,0 +1 @@
v18.15.0

35
.storybook/main.ts 100644
View File

@ -0,0 +1,35 @@
import { StorybookConfig } from '@storybook/react-webpack5';
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const config: StorybookConfig = {
stories: ['../src/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/preset-create-react-app',
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
webpackFinal: async config => {
if (!config.resolve) {
config.resolve = {};
}
config.resolve.plugins = config.resolve.plugins || [];
config.resolve.plugins.push(
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, '../tsconfig.json'),
}),
);
return config;
},
docs: {
autodocs: true,
},
};
export default config;

View File

@ -0,0 +1,7 @@
import 'index.css';
import { Preview } from '@storybook/react';
const preview: Preview = {};
export default preview;

View File

@ -1,51 +1,47 @@
# Getting Started with Create React App # PokeRtk = Pokedex + Redux-Toolkit
A simple Pokemon catalogue app, build with React, Redux-Toolkit, Material-UI and PokeAPI. A simple PokemonCard catalogue frontend app, build with React, Redux-Toolkit, Material-UI. It uses backend powered by [PokeAPI](https://pokeapi.co/).
It's a learning project following [pokedex](https://github.com/s1varam/pokedex). In this project, I practised following skills: It's a learning project following [pokedex](https://github.com/s1varam/pokedex). In this project, I practised following skills:
* Use React to create View for FE * Use React to create View for FE
* Use redux, react-redux, redux-toolkit (RTK) for state management * Use redux, react-redux, redux-toolkit (RTK) for state management
* Use storybook to design component * Use storybook to design component
* Use [RTK Query](https://redux-toolkit.js.org/rtk-query/overview) for fetching
Feature of the pokedex application:
* Display list of pokemon cards
* Fetching pokemon cards by region
![fetching by region](README/1_fetching_by_region.gif)
* Filter pokemon cards by type
![filter by type](README/2_filter_by_type.gif)
* Sort pokemon cards by name or ID
![sort by name](README/3_sort_by_id_or_name.gif)
* Filter pokemon cards by search naming
![filter by search](README/4_filter_by_search.gif)
* Show pokemon card detail
![show detail](README/5_click_info_icon.gif)
Deployed on [Netlify](https://pokedex-rtk.netlify.app/)
Storybook builds on [Chromatic](https://www.chromatic.com/builds?appId=645cedbb11414edc1f2eb16c)
## Available Scripts ## Available Scripts
In the project directory, you can run: In the project directory, you can run:
### `npm start` * `nvm use`: use node version according to `.nvmrc`
* `yarn start`: run the development server
* `yarn storybook`: start storybook server to check component rendering
* `yarn build`: build the app for production
Runs the app in the development mode.\ ## TODO
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\ * [ ] Fix css of Filter component
You will also see any lint errors in the console. * [ ] Fix Poke logo size via css
* [ ] Add day/night toggle in header
### `npm test` * [ ] Add github icon in header
* [ ] Use material.ui to display pokemon cards
Launches the test runner in the interactive watch mode.\ * [ ] Fix round corner of InfoDialog component
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. * [ ] Add error handling for data fetching across app
* [x] Deploy through netlify
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 MiB

View File

@ -0,0 +1,8 @@
module.exports = {
'*.json': ['npm run format:check'],
'*.{ts,tsx,js,jsx}': [
'npm run lint',
"bash -c 'npm run types:check'",
'npm run format:check',
],
};

17102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,16 +3,15 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.9.3", "@emotion/react": "^11.11.0",
"@testing-library/jest-dom": "^5.16.5", "@emotion/styled": "^11.11.0",
"@testing-library/react": "^13.4.0", "@mui/icons-material": "^5.11.16",
"@testing-library/user-event": "^13.5.0", "@mui/material": "^5.13.1",
"@types/jest": "^27.5.2", "@reduxjs/toolkit": "^1.9.5",
"@types/node": "^16.18.14", "framer-motion": "^10.12.12",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"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-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
@ -22,12 +21,33 @@
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "test:watchAll": "react-scripts test --watchAll",
"eject": "react-scripts eject",
"prettier": "prettier \"src/**/*.{js,jsx,ts,tsx,css,scss,md}\" --write",
"format:check": "yarn prettier -- --check",
"format:write": "yarn prettier -- --write",
"types:check": "tsc --noEmit --pretty",
"lint": "eslint --ext .js,.jsx,.ts,.tsx src --no-error-on-unmatched-pattern",
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx src --fix --no-error-on-unmatched-pattern",
"fix": "yarn format:write && yarn lint:fix",
"storybook": "storybook dev -p 6006 -s public",
"build-storybook": "storybook build -s public",
"chromatic": "npx chromatic --project-token=chpt_df2240e310a4eff"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
] ]
}, },
"browserslist": { "browserslist": {
@ -43,6 +63,44 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"prettier": "^2.8.4" "@storybook/addon-actions": "^7.0.12",
} "@storybook/addon-essentials": "^7.0.12",
"@storybook/addon-interactions": "^7.0.12",
"@storybook/addon-links": "^7.0.12",
"@storybook/node-logger": "^7.0.12",
"@storybook/preset-create-react-app": "^7.0.12",
"@storybook/react": "^7.0.12",
"@storybook/react-webpack5": "^7.0.12",
"@storybook/testing-library": "^0.1.1-future.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.31",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@types/react-lazy-load-image-component": "^1.5.3",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"babel-plugin-named-exports-order": "^0.0.2",
"chromatic": "^6.17.4",
"eslint": "^8.41.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"msw": "^1.2.1",
"prettier": "^2.8.8",
"prop-types": "^15.8.1",
"storybook": "^7.0.12",
"tsconfig-paths-webpack-plugin": "^4.0.1",
"webpack": "^5.83.1"
},
"readme": "ERROR: No README data found!",
"_id": "pokertk@0.1.0"
} }

View File

@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>PokeRTK</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,32 +1,32 @@
.App { .App {
text-align: center; text-align: center;
} }
.App-logo { .App-logo {
height: 40vmin; height: 40vmin;
pointer-events: none; pointer-events: none;
} }
.App-header { .App-header {
background-color: #282c34; background-color: #282c34;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: white; color: white;
} }
.App-link { .App-link {
color: #61dafb; color: #61dafb;
} }
@keyframes App-logo-spin { @keyframes App-logo-spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }

View File

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

View File

@ -1,13 +1,20 @@
import React from 'react'; import React from 'react';
import './App.css'; import './App.css';
import { Header } from './Header'; import Header from 'components/Header/Header';
import { Filters } from './Filters'; import Pokedex from 'features/Pokedex';
import Filters from 'features/Filters';
import InfoDialog from 'features/InfoDialog';
import { useAppSelector } from 'app/hooks';
function App() { function App() {
const selectedRegion = useAppSelector(state => state.filter.selectedRegion);
return ( return (
<div className="App app_container"> <div className="App app_container">
<Header /> <Header />
<Filters /> <Filters />
<Pokedex selectedRegion={selectedRegion} />
<InfoDialog />
</div> </div>
); );
} }

View File

@ -1,36 +0,0 @@
import React from 'react';
export function Filters() {
return (
<>
<div className="filter__container">
<div className="filter__items">
<div>
<div>REGION</div>
<select name="regionSelect">
<option value="Johto (152-251)">Johto (152-251)</option>
</select>
</div>
</div>
<div className="filter__items">
<div>
<div>TYPE</div>
<select name="typeSelect">
<option value="all">all types</option>
</select>
</div>
</div>
<div className="filter__items">
<div>
<div>SORT BY</div>
<select name="sortSelect">
<option value="name">Name</option>
</select>
</div>
</div>
</div>
</>
);
}
export {};

View File

@ -1,12 +0,0 @@
import React from 'react';
import logo from './assets/poke_logo.png';
export function Header() {
return (
<div>
<div className="poke__logos">
<img src={logo} />
</div>
</div>
);
}

6
src/app/hooks.ts 100644
View File

@ -0,0 +1,6 @@
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -0,0 +1,81 @@
{
"baby_trigger_item": null,
"chain": {
"evolution_details": [],
"evolves_to": [
{
"evolution_details": [
{
"gender": null,
"held_item": null,
"item": null,
"known_move": null,
"known_move_type": null,
"location": null,
"min_affection": null,
"min_beauty": null,
"min_happiness": null,
"min_level": 16,
"needs_overworld_rain": false,
"party_species": null,
"party_type": null,
"relative_physical_stats": null,
"time_of_day": "",
"trade_species": null,
"trigger": {
"name": "level-up",
"url": "https://pokeapi.co/api/v2/evolution-trigger/1/"
},
"turn_upside_down": false
}
],
"evolves_to": [
{
"evolution_details": [
{
"gender": null,
"held_item": null,
"item": null,
"known_move": null,
"known_move_type": null,
"location": null,
"min_affection": null,
"min_beauty": null,
"min_happiness": null,
"min_level": 36,
"needs_overworld_rain": false,
"party_species": null,
"party_type": null,
"relative_physical_stats": null,
"time_of_day": "",
"trade_species": null,
"trigger": {
"name": "level-up",
"url": "https://pokeapi.co/api/v2/evolution-trigger/1/"
},
"turn_upside_down": false
}
],
"evolves_to": [],
"is_baby": false,
"species": {
"name": "charizard",
"url": "https://pokeapi.co/api/v2/pokemon-species/6/"
}
}
],
"is_baby": false,
"species": {
"name": "charmeleon",
"url": "https://pokeapi.co/api/v2/pokemon-species/5/"
}
}
],
"is_baby": false,
"species": {
"name": "charmander",
"url": "https://pokeapi.co/api/v2/pokemon-species/4/"
}
},
"id": 2
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
import { pokeRestApi } from 'app/services/pokeRestApi';
import { filterSlice } from 'features/Filters/filterSlice';
import { getIdFromUrl } from 'app/services/pokeRestApi';
import { configureStore } from '@reduxjs/toolkit';
import { AppStore } from 'app/store';
import {
EvolutionChainResponseData,
PokemonResponseData,
PokemonSpeciesResponseData,
TypeListResponseData,
} from 'types/api';
let store: AppStore;
describe('pokeRestApi', () => {
beforeEach(() => {
store = configureStore({
reducer: {
pokedex: pokedexSlice.reducer,
filter: filterSlice.reducer,
[pokeRestApi.reducerPath]: pokeRestApi.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(pokeRestApi.middleware),
});
});
describe('JEST test against mock API', () => {
describe('test getPokemon query', () => {
test('visit https://pokeapi.co/api/v2/pokemon/85 returns Dodrio', async () => {
await store.dispatch(pokeRestApi.endpoints.getPokemon.initiate(85));
const pokemon = pokeRestApi.endpoints.getPokemon.select(85)(
store.getState(),
).data as PokemonResponseData;
expect(pokemon?.name).toBe('dodrio');
expect(pokemon?.types).toHaveLength(2);
expect(pokemon?.types[0].type.name).toBe('normal');
expect(pokemon?.types[1].type.name).toBe('flying');
expect(pokemon?.sprites.other.dream_world.front_default).toBe(
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/85.svg',
);
});
});
describe('test getPokemonSpecies query', () => {
test('visit https://pokeapi.co/api/v2/pokemon-species/6/', async () => {
await store.dispatch(
pokeRestApi.endpoints.getPokemonSpecies.initiate(6),
);
const pokemonSpecies = pokeRestApi.endpoints.getPokemonSpecies.select(
6,
)(store.getState()).data as PokemonSpeciesResponseData;
expect(pokemonSpecies?.evolution_chain.url).toBe(
'https://pokeapi.co/api/v2/evolution-chain/2/',
);
});
});
describe('test getPokemonSpecies query', () => {
test('visit https://pokeapi.co/api/v2/pokemon-species/6/', async () => {
await store.dispatch(
pokeRestApi.endpoints.getPokemonSpeciesFromUrl.initiate(
'https://pokeapi.co/api/v2/pokemon-species/6/',
),
);
const pokemonSpecies =
pokeRestApi.endpoints.getPokemonSpeciesFromUrl.select(
'https://pokeapi.co/api/v2/pokemon-species/6/',
)(store.getState()).data as PokemonSpeciesResponseData;
expect(pokemonSpecies?.evolution_chain.url).toBe(
'https://pokeapi.co/api/v2/evolution-chain/2/',
);
});
});
describe('test getTypeList query', () => {
test('visit https://pokeapi.co/api/v2/type should return correct data in list', async () => {
await store.dispatch(pokeRestApi.endpoints.getTypeList.initiate());
const typeListData = pokeRestApi.endpoints.getTypeList.select()(
store.getState(),
).data as TypeListResponseData;
expect(typeListData?.results).toHaveLength(typeListData.count + 1);
});
});
describe('test getEvolutionChain query', () => {
test('visit https://pokeapi.co/api/v2/evolution-chain/2/', async () => {
await store.dispatch(
pokeRestApi.endpoints.getEvolutionChain.initiate(2),
);
const evolutionChainData =
pokeRestApi.endpoints.getEvolutionChain.select(2)(store.getState())
.data as EvolutionChainResponseData;
expect(evolutionChainData?.chain.species.url).toBe(
'https://pokeapi.co/api/v2/pokemon-species/4/',
);
expect(evolutionChainData?.chain.evolves_to[0].species.url).toBe(
'https://pokeapi.co/api/v2/pokemon-species/5/',
);
expect(
evolutionChainData?.chain.evolves_to[0].evolves_to[0].species.url,
).toBe('https://pokeapi.co/api/v2/pokemon-species/6/');
});
});
describe('test getEvolutionChainFromUrl query', () => {
test('visit https://pokeapi.co/api/v2/evolution-chain/2/', async () => {
await store.dispatch(
pokeRestApi.endpoints.getEvolutionChainFromUrl.initiate(
'https://pokeapi.co/api/v2/evolution-chain/2/',
),
);
const evolutionChainData =
pokeRestApi.endpoints.getEvolutionChainFromUrl.select(
'https://pokeapi.co/api/v2/evolution-chain/2/',
)(store.getState()).data as EvolutionChainResponseData;
expect(evolutionChainData?.chain.species.url).toBe(
'https://pokeapi.co/api/v2/pokemon-species/4/',
);
expect(evolutionChainData?.chain.evolves_to[0].species.url).toBe(
'https://pokeapi.co/api/v2/pokemon-species/5/',
);
expect(
evolutionChainData?.chain.evolves_to[0].evolves_to[0].species.url,
).toBe('https://pokeapi.co/api/v2/pokemon-species/6/');
});
});
});
describe('test helper functions', () => {
test('getIdFromUrl works correctly for PokemonSpecies', () => {
const url = 'https://pokeapi.co/api/v2/pokemon-species/6/';
const id = getIdFromUrl(url);
expect(id).toBe(6);
});
test('getIdFromUrl works correctly for evolution-chain', () => {
const url = 'https://pokeapi.co/api/v2/evolution-chain/2/';
const id = getIdFromUrl(url);
expect(id).toBe(2);
});
});
});

View File

@ -0,0 +1,57 @@
import { fetchBaseQuery, createApi } from '@reduxjs/toolkit/query/react';
import {
RegionListResponseData,
TypeListResponseData,
PokemonResponseData,
EvolutionChainResponseData,
PokemonSpeciesResponseData,
} from 'types/api';
export const getIdFromUrl = (url: string) => {
const urlParts = url.split('/');
return parseInt(urlParts[urlParts.length - 2]);
};
export const pokeRestApi = createApi({
reducerPath: 'pokeRestApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: builder => ({
getTypeList: builder.query<TypeListResponseData, void>({
query: () => ({ url: 'type' }),
transformResponse: (response: RegionListResponseData) => {
return {
...response,
results: [{ name: 'All Types', url: '' }, ...response.results],
};
},
}),
getPokemon: builder.query<PokemonResponseData, string | number>({
query: IdOrName => ({ url: `pokemon/${IdOrName}` }),
}),
getPokemonSpecies: builder.query<PokemonSpeciesResponseData, number>({
query: Id => ({ url: `pokemon-species/${Id}` }),
}),
getPokemonSpeciesFromUrl: builder.query<PokemonSpeciesResponseData, string>(
{
query: url => ({ url: `pokemon-species/${getIdFromUrl(url)}` }),
},
),
getEvolutionChain: builder.query<EvolutionChainResponseData, number>({
query: Id => ({ url: `evolution-chain/${Id}` }),
}),
getEvolutionChainFromUrl: builder.query<EvolutionChainResponseData, string>(
{
query: url => ({ url: `evolution-chain/${getIdFromUrl(url)}` }),
},
),
}),
});
export const {
useGetTypeListQuery,
useGetPokemonQuery,
useGetPokemonSpeciesQuery,
useGetPokemonSpeciesFromUrlQuery,
useGetEvolutionChainQuery,
useGetEvolutionChainFromUrlQuery,
} = pokeRestApi;

24
src/app/store.ts 100644
View File

@ -0,0 +1,24 @@
import { configureStore } from '@reduxjs/toolkit';
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
import { filterSlice } from 'features/Filters/filterSlice';
import { infoDialogSlice } from 'features/InfoDialog/infoDialogSlice';
import { pokeRestApi } from './services/pokeRestApi';
export const store = configureStore({
reducer: {
// component slices
pokedex: pokedexSlice.reducer,
filter: filterSlice.reducer,
infoDialog: infoDialogSlice.reducer,
// api slices
[pokeRestApi.reducerPath]: pokeRestApi.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(pokeRestApi.middleware),
devTools: true,
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export type AppStore = typeof store;

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,18 @@
export { default as pokeType_bug } from './bug.png';
export { default as pokeType_dark } from './dark.png';
export { default as pokeType_dragon } from './dragon.png';
export { default as pokeType_electric } from './electric.png';
export { default as pokeType_fairy } from './fairy.png';
export { default as pokeType_fighting } from './fighting.png';
export { default as pokeType_fire } from './fire.png';
export { default as pokeType_flying } from './flying.png';
export { default as pokeType_ghost } from './ghost.png';
export { default as pokeType_grass } from './grass.png';
export { default as pokeType_ground } from './ground.png';
export { default as pokeType_ice } from './ice.png';
export { default as pokeType_normal } from './normal.png';
export { default as pokeType_poison } from './poison.png';
export { default as pokeType_psychic } from './psychic.png';
export { default as pokeType_rock } from './rock.png';
export { default as pokeType_steel } from './steel.png';
export { default as pokeType_water } from './water.png';

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,20 @@
import React, { useState, useEffect } from 'react';
type DelayedProps = {
waitBeforeShow: number;
children: React.ReactNode;
};
const Delayed = ({ waitBeforeShow, children }: DelayedProps) => {
const [isShown, setIsShown] = useState(false);
useEffect(() => {
setTimeout(() => {
setIsShown(true);
}, waitBeforeShow);
}, [waitBeforeShow]);
return <>{isShown ? children : null}</>;
};
export default Delayed;

View File

@ -0,0 +1,34 @@
.evolution__sub__box {
display: flex;
align-items: center;
text-align: center;
}
.evolution__box .evo_img {
width: 80px;
height: 80px;
margin: auto;
display: flex;
cursor: pointer;
}
/* .evolution__img__div:hover {
transform: scale(1.05);
} */
.evolution__img__div {
height: 113px;
width: 113px;
display: flex;
border-radius: 50%;
box-shadow: 0 5px 15px 4px rgb(0 0 0 / 30%);
align-items: center;
justify-content: center;
transition: all 0.35s;
margin-top: 3vh;
}
.evolution__poke__name {
text-transform: capitalize;
margin: 10px 0;
}

View File

@ -0,0 +1,56 @@
import { Provider } from 'react-redux';
import type { Meta, StoryObj } from '@storybook/react';
import EvolutionSpecies from './EvolutionSpecies';
import {
MockedState,
MockStoreProps,
mockStore,
} from 'features/InfoDialog/infoDialogSlice.storybook';
const Mockstore: React.FC<MockStoreProps> = ({ InfoDialogState, children }) => (
<Provider store={mockStore({ InfoDialogState })}>{children}</Provider>
);
const meta: Meta<typeof EvolutionSpecies> = {
title: 'Component/EvolutionSpecies',
component: EvolutionSpecies,
decorators: [
(story: () => React.ReactNode) => (
<div style={{ padding: '3rem' }}>{story()}</div>
),
],
tags: ['autodocs'],
excludeStories: /.*MockedState$/,
};
export default meta;
type Story = StoryObj<typeof EvolutionSpecies>;
export const Bulbasaur: Story = {
decorators: [
(story: () => React.ReactNode) => (
<Mockstore InfoDialogState={MockedState.infoDialog}>{story()}</Mockstore>
),
],
args: {
types: ['grass', 'poison'],
image_url:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/1.svg',
name: 'Bulbasaur',
},
};
export const Magneton: Story = {
decorators: [
(story: () => React.ReactNode) => (
<Mockstore InfoDialogState={MockedState.infoDialog}>{story()}</Mockstore>
),
],
args: {
types: ['electric', 'steel'],
image_url:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/82.svg',
name: 'Magneton',
},
};

View File

@ -0,0 +1,65 @@
import { motion } from 'framer-motion';
import './EvolutionSpecies.css';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import { colorTypeGradients } from 'components/utils';
import { useAppDispatch } from 'app/hooks';
import { fetchSelectedPokemonInfo } from 'features/InfoDialog/infoDialogSlice';
export type EvolutionSpeciesProps = {
types: string[];
name: string;
image_url: string;
};
const EvolutionSpecies = ({
types,
name,
image_url,
}: EvolutionSpeciesProps) => {
const dispatch = useAppDispatch();
const finalColor = colorTypeGradients(types);
return (
<div className={'evolution__sub__box'}>
<div>
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 2,
ease: 'easeOut',
type: 'spring',
bounce: 0.65,
damping: 25,
}}
whileHover={{ scale: 1.05 }}
>
<div
className="evolution__img__div"
style={{
background: `linear-gradient(${finalColor[0]}, ${finalColor[1]})`,
}}
>
<div className={'transparency__div'}>
<LazyLoadImage
alt={'image-pokemon'}
height={80}
width={80}
src={image_url}
visibleByDefault={false}
delayMethod={'debounce'}
effect={'blur'}
className={'evo_img'}
onClick={() => dispatch(fetchSelectedPokemonInfo(name))}
/>
</div>
</div>
</motion.div>
<div className={'evolution__poke__name'}>{name}</div>
</div>
</div>
);
};
export default EvolutionSpecies;

View File

@ -0,0 +1,2 @@
export { default } from './EvolutionSpecies';
export * from './EvolutionSpecies';

View File

@ -0,0 +1,7 @@
.fa-mars {
color: #7070ff;
}
.fa-venus {
color: rgb(224, 61, 88);
}

View File

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import GenderRate from './GenderRate';
const meta: Meta<typeof GenderRate> = {
title: 'Component/GenderRate',
component: GenderRate,
};
export default meta;
type Story = StoryObj<typeof GenderRate>;
export const Option1: Story = {
args: {
genderRatio: 1,
},
};
export const Option2: Story = {
args: {
genderRatio: 2,
},
};

View File

@ -0,0 +1,122 @@
import './GenderRate.css';
export interface GenderRateProps {
genderRatio: number;
}
const GenderRate = ({ genderRatio }: GenderRateProps) => {
switch (genderRatio) {
case 0:
return (
<div>
<span className="gender-male">
100% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
0% <i className="fa fa-venus"></i>
</span>
</div>
);
case 1:
return (
<div>
<span>
87.5% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
12.5% <i className="fa fa-venus"></i>
</span>
</div>
);
case 2:
return (
<div>
<span>
75% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
25% <i className="fa fa-venus"></i>
</span>
</div>
);
case 3:
return (
<div>
<span>
62.5% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
37.5% <i className="fa fa-venus"></i>
</span>
</div>
);
case 4:
return (
<div>
<span>
50% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
50% <i className="fa fa-venus"></i>
</span>
</div>
);
case 5:
return (
<div>
<span>
37.5% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
62.5% <i className="fa fa-venus"></i>
</span>
</div>
);
case 6:
return (
<div>
<span>
25% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
75% <i className="fa fa-venus"></i>
</span>
</div>
);
case 7:
return (
<div>
<span>
12.5% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
87.5% <i className="fa fa-venus"></i>
</span>
</div>
);
case 8:
return (
<div>
<span>
0% <i className="fa fa-mars"></i>
</span>
<span>
{' '}
100% <i className="fa fa-venus"></i>
</span>
</div>
);
default:
return <span>Loading...</span>;
}
};
export default GenderRate;

View File

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

View File

@ -0,0 +1,12 @@
.poke__logos {
display: flex;
flex-direction: column;
margin-left: 10px;
}
.poke__logo {
margin-top: 10px;
width: 15vw;
-webkit-filter: drop-shadow(5px 0px 0px rgba(0, 0, 0, 0.6));
filter: drop-shadow(5px 0px 0px rgba(0, 0, 0, 0.6));
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import logo from 'assets/images/pokedex.png';
import './Header.css';
const Header = () => {
return (
<div>
<div className="app__header">
<div className="poke__logo noselect">
<img src={logo} alt="Poke Logo" />
</div>
</div>
</div>
);
};
export default Header;

View File

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

View File

@ -0,0 +1,325 @@
html {
--pokename: #000;
--cardborder: #fff;
--pokenumber: hsl(228, 28%, 20%);
--info: #fff;
--bggradient: url('assets/images/bg.png');
--bgcolor: none;
}
html[data-theme='dark'] {
--pokename: hsl(228, 28%, 20%);
--cardborder: hsl(228, 28%, 20%);
--pokenumber: hsl(228, 28%, 20%);
--info: hsl(228, 28%, 20%);
--bggradient: none;
--bgcolor: #16171f;
}
@font-face {
font-family: 'Teko';
src: url('assets/fonts/Teko-Regular.ttf');
}
@font-face {
font-family: 'VT323';
src: url('assets/fonts/VT323-Regular.ttf');
}
.info__container {
width: 100%;
display: flex;
flex-direction: row;
}
.info__container__data {
display: flex;
flex-direction: column;
width: 100%;
margin-left: 40px;
}
.info__container__img {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
border-radius: 1rem;
background: #ffffff78;
padding: 20px 20px;
}
.info__container__img img {
width: 120px;
height: 147px;
padding: 1rem 1rem;
margin: 0.3rem;
border-radius: 1.2rem;
}
.pokemon__name {
font-size: 22px;
text-transform: capitalize;
}
.pokemon__id {
color: var(--pokenumber);
font-family: 'Teko', sans-serif;
font-weight: 400;
font-size: 40px;
}
.info__container__data__header {
align-items: center;
justify-content: space-between;
display: flex;
}
.info__container__data__type {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.info__container__data__type img {
height: 20px;
width: 20px;
}
.info__container__headings {
font-family: 'VT323', monospace;
font-size: 27px;
font-weight: 600;
}
.info__container__data__abilities {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
.info__container__data__data {
display: flex;
flex-direction: row;
flex-wrap: wrap;
grid-gap: 0px 15px;
gap: 0px 15px;
align-items: center;
margin: 0px 0;
justify-content: space-between;
background-color: #ffffff40;
padding: 10px;
border-radius: 1rem;
margin-top: 0.5rem;
}
.info__container__stat__columns {
width: 135px;
margin-right: 0px;
}
.MuiDialog-paperFullWidth {
width: 63%;
border-radius: 1rem;
box-shadow: 0 10px 20px rgba(175, 136, 136, 0.29),
0 6px 6px rgba(0, 0, 0, 0.43);
}
.MuiDialog-container {
background-color: var(--bgcolor);
background-image: var(--bggradient);
}
.MuiBackdrop-root {
background-color: rgba(0, 0, 0, 0);
}
.info__container__data__dimensions {
display: flex;
gap: 0 30px;
}
.info__container__stat__columns__name {
text-transform: uppercase;
color: crimson;
}
.dialog__bg {
background-image: url('assets/images/bg.png');
background-repeat: repeat;
}
.dialog__content {
font-family: 'VT323', monospace;
font-size: 21px;
}
.pokemon__name {
font-family: 'Press Start 2P', cursive;
}
.close__btn {
width: 30px !important;
}
.ability {
text-transform: capitalize;
}
.right__box {
display: flex;
flex-direction: column;
height: -webkit-fill-available;
justify-content: space-evenly;
gap: 3px 0px;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.evolution__box {
display: flex;
gap: 0 0px;
}
.arrow__right {
font-size: 1.75rem;
margin: 0 10px;
}
.MuiDialogContent-root:first-child {
padding: 10px;
}
.ability__list {
display: flex;
gap: 0 30px;
padding-inline-start: 1em;
}
.ability__list__bg {
background-color: #ffffff40;
padding: 5px;
border-radius: 1rem;
margin-top: 0.5rem;
}
.MuiTooltip-tooltip {
font-size: 15px !important;
}
.separator {
border: 0;
border-radius: 20px;
height: 80;
background-image: linear-gradient(
to bottom,
rgb(77, 77, 77),
rgb(77, 77, 77)
);
width: 1%;
margin-left: 2vw;
margin-right: auto;
}
.transparency__div {
background: #ffffff40;
height: 113px;
width: 113px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.desc {
background-color: #ffffff40;
padding: 10px;
border-radius: 1rem;
margin-top: 0.5rem;
}
.fa-mars {
color: #7070ff;
}
.fa-venus {
color: rgb(224, 61, 88);
}
.dimensions {
text-align: center;
}
.pokemon__genera {
padding: 3px 10px;
border-radius: 10px;
margin-top: 1.5vh;
text-align: center;
}
/* phone */
@media screen and (max-width: 767px) {
.MuiDialog-paperFullWidth {
width: 75%;
height: 100%;
}
.info__container {
display: flex;
flex-direction: column;
}
.info__container__img {
margin-left: 0px;
}
.info__container__data {
margin-left: 0px;
}
.right__box {
margin-top: 20px;
justify-content: center;
align-items: center;
}
.info__container__data__data {
justify-content: center;
}
.info__container__stat__columns__name,
.info__container__stat__columns__val,
.stats,
.dimensions,
.ability {
text-align: center;
}
.info__container__data__abilities {
margin-top: 20px;
align-items: center;
gap: 0px;
}
.right__box .desc {
text-align: center;
}
.info__container__headings {
text-align: center;
margin-top: 10px;
}
.evolution__box {
flex-direction: column;
}
.evolution__sub__box {
flex-direction: column;
}
.arrow__right {
transform: rotate(90deg);
}
.evolution__poke__name {
margin: 12px;
}
}

View File

@ -0,0 +1,73 @@
import { Provider } from 'react-redux';
import type { Meta, StoryObj } from '@storybook/react';
import InfoDialogComponent from './InfoDialogComponent';
import {
MockedState,
MockStoreProps,
mockStore,
} from 'features/InfoDialog/infoDialogSlice.storybook';
const Mockstore: React.FC<MockStoreProps> = ({ InfoDialogState, children }) => (
<Provider store={mockStore({ InfoDialogState })}>{children}</Provider>
);
const meta: Meta<typeof InfoDialogComponent> = {
title: 'Component/InfoDialogComponent',
component: InfoDialogComponent,
decorators: [
(story: () => React.ReactNode) => (
<div style={{ padding: '3rem' }}>{story()}</div>
),
],
tags: ['autodocs'],
excludeStories: /.*MockedState$/,
};
export default meta;
type Story = StoryObj<typeof InfoDialogComponent>;
export const Duduo: Story = {
decorators: [
(story: () => React.ReactNode) => (
<Mockstore InfoDialogState={MockedState.infoDialog}>{story()}</Mockstore>
),
],
args: {
openDialog: true,
id: 84,
name: 'Doduo',
genera: 'Twin Bird Pokémon',
image:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/84.svg',
types: ['normal', 'flying'],
height: 14,
weight: 392,
genderRatio: 4,
description:
'A bird that makes up for its poor flying with its fast foot speed. Leaves giant footprints.',
abilities: ['run-away', 'early-bird', 'tangled-feet'],
stats: [
{ stat__name: 'hp', stat__value: 35 },
{ stat__name: 'attack', stat__value: 85 },
{ stat__name: 'defense', stat__value: 45 },
{ stat__name: 'special-attack', stat__value: 35 },
{ stat__name: 'special-defense', stat__value: 35 },
{ stat__name: 'speed', stat__value: 75 },
],
evolutionChain: [
{
types: ['normal', 'flying'],
image_url:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/84.svg',
name: 'Doduo',
},
{
name: 'Dodrio',
types: ['normal', 'flying'],
image_url:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/85.svg',
},
],
},
};

View File

@ -0,0 +1,191 @@
import { Dialog, DialogContent, Tooltip, Zoom } from '@mui/material';
import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt';
import './InfoDialogComponent.css';
import { findPokeTypeAsset } from 'components/PokemonTypes';
import { colorTypeGradients } from 'components/utils';
import GenderRate from 'components/GenderRate';
import Delayed from 'components/Delayed';
import EvolutionSpecies from 'components/EvolutionSpecies';
import { InfoDialogDetails } from 'features/InfoDialog/infoDialogSlice';
export interface Stat {
stat__name: string;
stat__value: number;
}
export type InfoDialogComponentProps = InfoDialogDetails & {
openDialog: boolean;
closeDialog: () => void;
};
const InfoDialog = ({
openDialog,
closeDialog,
id,
name,
types,
genera,
image,
height,
weight,
genderRatio,
description,
abilities,
stats,
evolutionChain,
}: InfoDialogComponentProps) => {
const finalColor = colorTypeGradients(types);
return (
<div>
<Dialog
aria-labelledby="customized-dialog-title"
open={openDialog}
onClose={closeDialog}
fullWidth
maxWidth="md"
className="dialog__bg noselect"
>
<DialogContent
style={{
background: `linear-gradient(${finalColor[0]}, ${finalColor[1]})`,
}}
className="dialog__content"
>
<div className="info__container">
<div className="info__container__img">
<div className="pokemon__id">#{String(id).padStart(3, '0')}</div>
<div className="pokemon__name">{name}</div>
<div
className="pokemon__genera"
style={{ background: finalColor[0] }}
>
{genera}
</div>
<div>
<img src={image} alt="poke-img" />
</div>
<div className="info__container__data__type">
{types.map(type => (
<Tooltip
title={type}
key={type}
TransitionComponent={Zoom}
arrow
>
<div className={`poke__type__bg ${type}`}>
<img src={findPokeTypeAsset(type)} alt="poke-type" />
</div>
</Tooltip>
))}
</div>
<div className="dimensions">
<p>
<span
className="info__container__headings"
style={{ fontSize: '20px' }}
>
Height
</span>
{`${height / 10} m/${`${Math.floor(
(height / 10) * 3.28,
)}'${Math.round((((height / 10) * 3.28) % 1) * 12)}"`} `}
</p>
<p>
<span
className="info__container__headings"
style={{ fontSize: '20px' }}
>
Weight
</span>
{` ${(weight / 10).toFixed(1)} kg/${(weight * 0.2205).toFixed(
1,
)} lbs`}
</p>
</div>
<div className="gender__container">
{genderRatio === -1 ? (
'Genderless'
) : (
<GenderRate genderRatio={genderRatio} />
)}
</div>
</div>
<div className="info__container__data">
<div className={'right__box'}>
<div>
<div className={'info__container__headings'}>About</div>
<div className={'desc'}>{description}</div>
</div>
<div className={'info__container__data__header'}>
<div className={'info__container__data__abilities'}>
<div className={'info__container__headings'}>Abilities</div>
<div className={'ability__list__bg'}>
<ul className={'ability__list'}>
{abilities.map(ability => (
<li key={ability}>
<div className={'ability'}>{ability}&nbsp;</div>
</li>
))}
</ul>
</div>
</div>
</div>
<div>
<div className={'info__container__headings stats'}>
Base Stats
</div>
<div className={'info__container__data__data'}>
{stats.map(stat => (
<div
key={stat['stat__name']}
className="info__container__stat__columns"
>
<div className="info__container__stat__columns__name">
{stat.stat__name}
</div>
<div className="info__container__stat__columns__val">
{stat.stat__value}
</div>
</div>
))}
</div>
</div>
<div>
<div className={'info__container__headings'}>Evolution</div>
<div className={'evolution__box'}>
{evolutionChain.map(evo => (
<Delayed
waitBeforeShow={evolutionChain.indexOf(evo) * 500}
key={evo.name}
>
<EvolutionSpecies
types={evo.types}
image_url={evo.image_url}
name={evo.name}
/>
{evolutionChain.indexOf(evo) + 1 &&
evolutionChain.indexOf(evo) <
evolutionChain.length - 1 && (
<div className={'evolution__sub__box'}>
<ArrowRightAltIcon
className={'arrow__right'}
></ArrowRightAltIcon>
</div>
)}
</Delayed>
))}
</div>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
};
export default InfoDialog;

View File

@ -0,0 +1,2 @@
export { default } from './InfoDialogComponent';
export * from './InfoDialogComponent';

View File

@ -0,0 +1,48 @@
@font-face {
font-family: 'Press Start 2P';
src: url('assets/fonts/PressStart2P-Regular.ttf') format('truetype');
}
.app__container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.app__container .loading__text {
font-family: 'Press Start 2P', cursive;
color: var(--colorPrimary);
}
.app__container .loading__text {
font-family: 'Press Start 2P', cursive;
color: var(--colorPrimary);
}
.loading__gif {
width: 15%;
}
@media screen and (max-width: 767px) {
.loading__gif {
width: 35%;
}
.poke__logo {
width: 35vw;
}
.filter__container {
display: flex;
flex-direction: column;
width: 100vw;
align-items: center;
margin: 7vh 0 5vh 0;
gap: 2vh 2vw;
justify-content: center;
}
select,
.filter__items > input {
width: 40vw;
}
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import './Loading.css';
const Loading = () => {
return (
<div className="loading">
<div className="loading__container">
<div className="loading__text">Loading...</div>
<div className="gif_container">
<img
src="http://i.gifer.com/VgI.gif"
alt="loading gif"
className="loading__gif noselect"
/>
</div>
</div>
</div>
);
};
export default Loading;

View File

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

View File

@ -0,0 +1,251 @@
:root {
--grass: #5fbd58;
--bug: #92bc2c;
--dark: #595761;
--dragon: #0c69c8;
--electric: #f2d94e;
--fairy: #ee90e6;
--fighting: #d3425f;
--fire: #dc872f;
--flying: #a1bbec;
--ghost: #5f6dbc;
--ground: #da7c4d;
--ice: #75d0c1;
--normal: #a0a29f;
--poison: #b763cf;
--psychic: #ff2ca8;
--rock: #a38c21;
--steel: #5695a3;
--water: #539ddf;
}
html {
--pokename: #000;
--cardborder: #fff;
--pokenumber: hsl(228, 28%, 20%);
--info: #fff;
}
@font-face {
font-family: 'Press Start 2P';
src: url('assets/fonts/PressStart2P-Regular.ttf') format('truetype');
}
.thumbnail__container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 1.5rem 0;
margin: 2rem;
/* border: 15px solid var(--cardborder); */
border-radius: 1rem;
width: 220px;
height: 285px;
text-align: center;
/* box-shadow: 0 5px 25px 1px rgb(0 0 0 / 50%); */
box-shadow: 0 1.6px 1.6px rgba(0, 0, 0, 0.023),
0 3.8px 3.8px rgba(0, 0, 0, 0.034), 0 6.9px 6.9px rgba(0, 0, 0, 0.041),
0 11.4px 11.4px rgba(0, 0, 0, 0.049), 0 18.8px 18.8px rgba(0, 0, 0, 0.056),
0 32.8px 32.8px rgba(0, 0, 0, 0.067), 0 71px 71px rgba(0, 0, 0, 0.09);
transition: all 0.2s ease-in-out;
}
.thumbnail__container:hover {
transform: scale(1.1);
}
.info__icon {
margin-right: 15px;
margin-top: -4px;
font-size: 25px;
color: var(--info);
opacity: 0.5;
}
.info__icon:hover {
opacity: 1;
}
h3 {
font-family: 'Press Start 2P', cursive;
margin-bottom: 0.2rem;
color: var(--pokename);
}
.thumbnail__container .poke__number {
border-radius: 1rem;
padding: 0.2rem 0.4rem;
font-weight: 400;
font-size: 35px;
font-family: 'Teko', sans-serif;
}
.thumbnail__container img {
width: 120px;
height: 120px;
}
.thumbnail__container small {
text-transform: capitalize;
}
.card__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.poke__type {
display: flex;
grid-gap: 0 10px;
gap: 0 20px;
align-items: center;
justify-content: center;
margin-top: 30px;
}
.poke__type__bg > img {
width: 20px;
height: 20px;
}
.poke__type__bg {
width: 40px;
height: 40px;
border-radius: 100%;
display: flex;
justify-content: center;
align-content: center;
align-items: center;
}
.grass {
background: var(--grass);
box-shadow: 0 0 20px var(--grass);
}
.bug {
background: var(--bug);
box-shadow: 0 0 20px var(--bug);
}
.dark {
background: var(--dark);
box-shadow: 0 0 20px var(--dark);
}
.dragon {
background: var(--dragon);
box-shadow: 0 0 20px var(--dragon);
}
.electric {
background: var(--electric);
box-shadow: 0 0 20px #796d26;
}
.fairy {
background: var(--fairy);
box-shadow: 0 0 20px var(--fairy);
}
.fighting {
background: var(--fighting);
box-shadow: 0 0 20px var(--fighting);
}
.flying {
background: var(--flying);
box-shadow: 0 0 20px var(--flying);
}
.ghost {
background: var(--ghost);
box-shadow: 0 0 20px var(--ghost);
}
.ground {
background: var(--ground);
box-shadow: 0 0 20px var(--ground);
}
.ice {
background: var(--ice);
box-shadow: 0 0 20px var(--ice);
}
.normal {
background: var(--normal);
box-shadow: 0 0 20px var(--normal);
}
.poison {
background: var(--poison);
box-shadow: 0 0 20px var(--poison);
}
.psychic {
background: var(--psychic);
box-shadow: 0 0 20px var(--psychic);
}
.rock {
background: var(--rock);
box-shadow: 0 0 20px var(--rock);
}
.steel {
background: var(--steel);
box-shadow: 0 0 20px var(--steel);
}
.water {
background: var(--water);
box-shadow: 0 0 20px var(--water);
}
.fire {
background: var(--fire);
box-shadow: 0 0 20px var(--fire);
}
.image__container {
height: 150px;
width: 150px;
}
.poke__name h3 {
margin-top: 0;
}
.MuiTooltip-tooltip {
font-size: 15px;
}
@media screen and (max-width: 767px) {
.thumbnail__container .img__thumbnail {
width: 90px;
height: 90px;
}
.thumbnail__container {
min-width: 175px;
height: 225px;
margin: 1.25rem 0.75rem;
padding: 1.25rem 0;
}
.image__container {
height: 90px;
width: 90px;
}
.thumbnail__container .poke__number {
font-size: 1.75em;
}
.poke__name h3 {
margin-top: 20px;
font-size: 1em;
}
.poke__type {
margin-top: 20px;
}
}

View File

@ -0,0 +1,42 @@
import type { Meta, StoryObj } from '@storybook/react';
import PokemonCard from './PokemonCard';
import charizard_svg from './assets/stories/charizard.svg';
import charizard_info from './assets/stories/charizard.json';
const meta: Meta<typeof PokemonCard> = {
title: 'Component/PokemonCard',
component: PokemonCard,
};
export default meta;
type Story = StoryObj<typeof PokemonCard>;
export const Charizard: Story = {
args: {
id: 6,
name: charizard_info.name,
image: charizard_svg,
types: ['fire', 'flying'],
},
};
export const Bulbasaur: Story = {
args: {
id: 1,
name: 'bulbasaur',
image:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/1.svg',
types: ['grass', 'poison'],
},
};
export const Pikachu: Story = {
args: {
id: 25,
name: 'pikachu',
image:
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/25.svg',
types: ['electric'],
},
};

View File

@ -0,0 +1,15 @@
import { formatNumber } from './PokemonCard';
describe('Test Functions', () => {
describe('formatNumber', () => {
it('should format single digit integer correctly', () => {
expect(formatNumber(6)).toBe('#006');
});
it('should format double digit integer correctly', () => {
expect(formatNumber(16)).toBe('#016');
});
it('should format triple digit integer correctly', () => {
expect(formatNumber(116)).toBe('#116');
});
});
});

View File

@ -0,0 +1,78 @@
import React from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import 'react-lazy-load-image-component/src/effects/blur.css';
import './PokemonCard.css';
import { colorTypeGradients } from 'components/utils';
import PokemonTypes from 'components/PokemonTypes/PokemonTypes';
export interface PokemonCardProps {
id: number;
name: string;
image: string;
types: string[];
}
type PokemonCardPropsActionable = PokemonCardProps & {
onClickAction: () => void;
};
export function formatNumber(num: number) {
return '#' + num.toString().padStart(3, '0');
}
const PokemonCard = ({
id,
name,
image,
types,
onClickAction,
}: PokemonCardPropsActionable) => {
const finalColor = colorTypeGradients(types);
return (
<div
className="thumbnail__container noselect"
style={{
background: `linear-gradient(${finalColor[0]}, ${finalColor[1]})`,
}}
>
<div className="card__header">
<div className="poke__number">{formatNumber(id)}</div>
<div className="info__icon">
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 512 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
onClick={onClickAction}
>
<path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"></path>
</svg>
</div>
</div>
<div className="image__container">
<LazyLoadImage
alt={name}
height={150}
src={image}
visibleByDefault={false}
delayMethod={'debounce'}
effect="blur"
className="img_thumbnail"
/>
</div>
<div className="poke__name">
<h3>{name}</h3>
<div className="poke__type">
<PokemonTypes types={types} />
</div>
</div>
</div>
);
};
export default PokemonCard;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,2 @@
export * from './PokemonCard';
export { default } from './PokemonCard';

View File

@ -0,0 +1,23 @@
.poke__type {
display: flex;
grid-gap: 0 10px;
gap: 0 20px;
align-items: center;
justify-content: center;
margin-top: 30px;
}
.poke__type__bg > img {
width: 20px;
height: 20px;
}
.poke__type__bg {
width: 40px;
height: 40px;
border-radius: 100%;
display: flex;
justify-content: center;
align-content: center;
align-items: center;
}

View File

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react';
import PokemonTypes from './PokemonTypes';
const meta: Meta<typeof PokemonTypes> = {
title: 'Component/PokemonTypes',
component: PokemonTypes,
};
export default meta;
type Story = StoryObj<typeof PokemonTypes>;
export const fireOnly: Story = {
name: 'Fire only',
args: {
types: ['fire'],
},
};
export const grassAndPoinson: Story = {
name: 'Grass and Poison',
args: {
types: ['grass', 'poison'],
},
};
export const fireAndFlying: Story = {
name: 'Fire and Flying',
args: {
types: ['fire', 'flying'],
},
};
export const threeTypes: Story = {
name: 'Fire, Flying and Grass',
args: {
types: ['fire', 'flying', 'grass'],
},
};

View File

@ -0,0 +1,70 @@
import React from 'react';
import { Tooltip, Zoom } from '@mui/material';
import * as pokeTypeAsset from 'assets/types';
import './PokemonTypes.css';
export const findPokeTypeAsset = (pokeType: string) => {
switch (pokeType) {
case 'normal':
return pokeTypeAsset.pokeType_normal;
case 'fire':
return pokeTypeAsset.pokeType_fire;
case 'water':
return pokeTypeAsset.pokeType_water;
case 'electric':
return pokeTypeAsset.pokeType_electric;
case 'grass':
return pokeTypeAsset.pokeType_grass;
case 'ice':
return pokeTypeAsset.pokeType_ice;
case 'fighting':
return pokeTypeAsset.pokeType_fighting;
case 'poison':
return pokeTypeAsset.pokeType_poison;
case 'ground':
return pokeTypeAsset.pokeType_ground;
case 'flying':
return pokeTypeAsset.pokeType_flying;
case 'psychic':
return pokeTypeAsset.pokeType_psychic;
case 'bug':
return pokeTypeAsset.pokeType_bug;
case 'rock':
return pokeTypeAsset.pokeType_rock;
case 'ghost':
return pokeTypeAsset.pokeType_ghost;
case 'dragon':
return pokeTypeAsset.pokeType_dragon;
case 'dark':
return pokeTypeAsset.pokeType_dark;
case 'steel':
return pokeTypeAsset.pokeType_steel;
case 'fairy':
return pokeTypeAsset.pokeType_fairy;
default:
return pokeTypeAsset.pokeType_normal;
}
};
export interface PokemonTypesProps {
types: string[];
}
const PokemonTypes = ({ types }: PokemonTypesProps) => {
return (
// css is set in consumer
<>
{types.map(type => (
<Tooltip title={type} key={type} TransitionComponent={Zoom} arrow>
<div className={`poke__type__bg ${type}`}>
<img src={findPokeTypeAsset(type)} alt={type} />
</div>
</Tooltip>
))}
</>
);
};
export default PokemonTypes;

View File

@ -0,0 +1,2 @@
export * from './PokemonTypes';
export { default } from './PokemonTypes';

View File

@ -0,0 +1,7 @@
import { colorTypeGradients } from './utils';
describe('Test utility functions', () => {
it('should return correct color for each type', () => {
expect(colorTypeGradients(['grass'])).toBe(['#a8ff98', '#a8ff98']);
});
});

View File

@ -0,0 +1,77 @@
const getColor = (type: string): string => {
let returnColor: string;
switch (type) {
case 'grass':
returnColor = '#a8ff98';
break;
case 'poison':
returnColor = '#d6a2e4';
break;
case 'normal':
returnColor = '#dcdcdc';
break;
case 'fire':
returnColor = '#ffb971';
break;
case 'water':
returnColor = '#8cc4e2';
break;
case 'electric':
returnColor = '#ffe662';
break;
case 'ice':
returnColor = '#8cf5e4';
break;
case 'fighting':
returnColor = '#da7589';
break;
case 'ground':
returnColor = '#e69a74';
break;
case 'flying':
returnColor = '#bbc9e4';
break;
case 'psychic':
returnColor = '#ffa5da';
break;
case 'bug':
returnColor = '#bae05f';
break;
case 'rock':
returnColor = '#C9BB8A';
break;
case 'ghost':
returnColor = '#8291e0';
break;
case 'dark':
returnColor = '#8e8c94';
break;
case 'dragon':
returnColor = '#88a2e8';
break;
case 'steel':
returnColor = '#9fb8b9';
break;
case 'fairy':
returnColor = '#fdb9e9';
break;
default:
returnColor = 'gainsboro';
break;
}
return returnColor;
};
export const colorTypeGradients = (types: string[]): string[] => {
const color1: string = getColor(types[0]);
let color2: string = color1;
if (types.length === 2) {
color2 = getColor(types[1]);
} else if (types.length === 1) {
color2 = color1;
}
return [color1, color2];
};

View File

@ -0,0 +1,42 @@
html {
--toggle: hsl(230deg 17% 85%);
--bgcolor: none;
--colorPrimary: #000;
--filterHeading: #000;
--selectBg: #fff;
--selectText: #000;
}
@font-face {
font-family: 'barcadebrawl';
src: url('assets/fonts/BarcadeBrawl.ttf');
}
.filter__container {
display: flex;
flex-direction: row;
margin: 7vh 0 5vh 0;
gap: 0 2vw;
}
.filter__items {
display: flex;
flex-direction: column;
align-items: center;
font-family: 'barcadebrawl';
font-size: 12px;
}
/* TODO: Fix incorrect render of input */
.filter__items > input {
width: 10vw;
margin-top: 5px;
background-color: var(--selectBg);
color: var(--selectText);
border: 1px solid var(--selectText);
}
.filter__items > div,
label {
color: var(--filterHeading);
}

View File

@ -0,0 +1,16 @@
import {
useGetRegionOptions,
createRegionPokemonListOptionElements,
} from './Filters';
describe('Filters', () => {
describe('test utility functions', () => {
test('createOptionElements works correctly', () => {
const { data } = useGetRegionOptions();
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)');
});
});
});

View File

@ -0,0 +1,115 @@
import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from 'app/hooks';
import {
setSelectedRegion,
setSelectedType,
setSelectedSort,
setSearchInput,
initializeFilterSlice,
} from './filterSlice';
import { useGetTypeListQuery } from 'app/services/pokeRestApi';
import { RegionPokemonRange } from './types/slice';
import './Filters.css';
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 Filters = () => {
const dispatch = useAppDispatch();
const typeOptions = useAppSelector(state => state.filter.typeOptions);
const sortOptions = useAppSelector(state => state.filter.sortOptions);
const regionOptions = useAppSelector(state => state.filter.regionOptions);
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 { isLoading: isFetchingTypeOptions } = useGetTypeListQuery();
useEffect(() => {
dispatch(initializeFilterSlice());
}, [dispatch]);
return (
<>
<div className="filter__container noselect">
<div className="filter__items">
<div>
<div>REGION</div>
<select
name="regionSelect"
onChange={e => dispatch(setSelectedRegion(e.target.value))}
value={selectedRegion}
>
{createRegionPokemonListOptionElements(regionOptions)}
</select>
</div>
</div>
<div className="filter__items">
<div>
<div>TYPE</div>
<select
name="regionSelect"
onChange={e => dispatch(setSelectedType(e.target.value))}
value={selectedType}
>
{isFetchingTypeOptions ? (
<option>Loading...</option>
) : (
typeOptions.map(type => (
<option key={type} value={type}>
{type}
</option>
))
)}
</select>
</div>
</div>
<div className="filter__items">
<div>
<div>SORT BY</div>
<select
name="sortSelect"
disabled={isFetchingTypeOptions}
onChange={e => dispatch(setSelectedSort(e.target.value))}
value={selectedSort}
>
{sortOptions.map(option => (
<option key={option.value} value={option.value}>
{option.name}
</option>
))}
</select>
</div>
</div>
<div className="filter__items">
<div>
<div>SEARCH</div>
<input
type="text"
onChange={e => dispatch(setSearchInput(e.target.value))}
value={searchInput}
/>
</div>
</div>
</div>
</>
);
};
export default Filters;

View File

@ -0,0 +1,27 @@
{
"count": 20,
"next": null,
"previous": null,
"results": [
{ "name": "normal", "url": "https://pokeapi.co/api/v2/type/1/" },
{ "name": "fighting", "url": "https://pokeapi.co/api/v2/type/2/" },
{ "name": "flying", "url": "https://pokeapi.co/api/v2/type/3/" },
{ "name": "poison", "url": "https://pokeapi.co/api/v2/type/4/" },
{ "name": "ground", "url": "https://pokeapi.co/api/v2/type/5/" },
{ "name": "rock", "url": "https://pokeapi.co/api/v2/type/6/" },
{ "name": "bug", "url": "https://pokeapi.co/api/v2/type/7/" },
{ "name": "ghost", "url": "https://pokeapi.co/api/v2/type/8/" },
{ "name": "steel", "url": "https://pokeapi.co/api/v2/type/9/" },
{ "name": "fire", "url": "https://pokeapi.co/api/v2/type/10/" },
{ "name": "water", "url": "https://pokeapi.co/api/v2/type/11/" },
{ "name": "grass", "url": "https://pokeapi.co/api/v2/type/12/" },
{ "name": "electric", "url": "https://pokeapi.co/api/v2/type/13/" },
{ "name": "psychic", "url": "https://pokeapi.co/api/v2/type/14/" },
{ "name": "ice", "url": "https://pokeapi.co/api/v2/type/15/" },
{ "name": "dragon", "url": "https://pokeapi.co/api/v2/type/16/" },
{ "name": "dark", "url": "https://pokeapi.co/api/v2/type/17/" },
{ "name": "fairy", "url": "https://pokeapi.co/api/v2/type/18/" },
{ "name": "unknown", "url": "https://pokeapi.co/api/v2/type/10001/" },
{ "name": "shadow", "url": "https://pokeapi.co/api/v2/type/10002/" }
]
}

View File

@ -0,0 +1,113 @@
import {
createAsyncThunk,
createSlice,
PayloadAction,
Slice,
} from '@reduxjs/toolkit';
import { FilterStateProps } from './types/slice';
import { RegionPokemonRange } from './types/slice';
import { pokeRestApi } from 'app/services/pokeRestApi';
import { fetchPokemonsInTheRegion } from 'features/Pokedex/pokedexSlice';
export const initialState: FilterStateProps = {
regionOptions: [],
typeOptions: [],
sortOptions: [],
selectedRegion: '',
selectedType: '',
selectedSort: '',
searchInput: '',
};
export const initializeFilterSlice = createAsyncThunk(
'filter/initializeFilterSlice',
async (_args, thunkAPI) => {
const dispatch = thunkAPI.dispatch;
const regionOptions = [
{ 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 },
];
dispatch(pokeRestApi.endpoints.getTypeList.initiate());
const sortOptions = [
{ name: 'ID', value: 'id' },
{ name: 'Name', value: 'name' },
];
return { regionOptions, sortOptions };
},
);
export const filterSlice: Slice<FilterStateProps> = createSlice({
name: 'filter',
initialState,
reducers: {
setSelectedRegion: (state, action: PayloadAction<string>) => {
state.selectedRegion = action.payload;
fetchPokemonsInTheRegion(state.selectedRegion);
},
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.addCase(initializeFilterSlice.fulfilled, (state, action) => {
if (action.payload) {
state.regionOptions = action.payload.regionOptions;
state.sortOptions = action.payload.sortOptions;
state.selectedRegion = action.payload.regionOptions[0].region;
state.selectedSort = action.payload.sortOptions[0].value;
}
});
builder.addMatcher(
pokeRestApi.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 FilterStateProps = {
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

@ -0,0 +1,36 @@
import { useAppDispatch, useAppSelector } from 'app/hooks';
import InfoDialogComponent from 'components/InfoDialogComponent';
import { setCloseDialog } from './infoDialogSlice';
const InfoDialog = () => {
const dispatch = useAppDispatch();
const isOpen = useAppSelector(state => state.infoDialog.isOpen);
const selectedInfoDialogDetails = useAppSelector(
state => state.infoDialog.InfoDialogDetails,
);
return (
<>
<InfoDialogComponent
openDialog={isOpen}
closeDialog={() => dispatch(setCloseDialog(null))}
id={selectedInfoDialogDetails.id}
name={selectedInfoDialogDetails.name}
types={selectedInfoDialogDetails.types}
genera={selectedInfoDialogDetails.genera}
image={selectedInfoDialogDetails.image}
height={selectedInfoDialogDetails.height}
weight={selectedInfoDialogDetails.weight}
genderRatio={selectedInfoDialogDetails.genderRatio}
description={selectedInfoDialogDetails.description}
abilities={selectedInfoDialogDetails.abilities}
stats={selectedInfoDialogDetails.stats}
evolutionChain={selectedInfoDialogDetails.evolutionChain}
/>
</>
);
};
export default InfoDialog;

Some files were not shown because too many files have changed in this diff Show More