├── .gitignore ├── LICENSE ├── README.md ├── craco.config.js ├── images └── screenshot.gif ├── package.json ├── postcss.config.js ├── public ├── _redirects ├── assets │ ├── fonts │ │ └── montserrat │ │ │ ├── Montserrat-Black.ttf │ │ │ ├── Montserrat-BlackItalic.ttf │ │ │ ├── Montserrat-Bold.ttf │ │ │ ├── Montserrat-BoldItalic.ttf │ │ │ ├── Montserrat-ExtraBold.ttf │ │ │ ├── Montserrat-ExtraBoldItalic.ttf │ │ │ ├── Montserrat-ExtraLight.ttf │ │ │ ├── Montserrat-ExtraLightItalic.ttf │ │ │ ├── Montserrat-Italic.ttf │ │ │ ├── Montserrat-Light.ttf │ │ │ ├── Montserrat-LightItalic.ttf │ │ │ ├── Montserrat-Medium.ttf │ │ │ ├── Montserrat-MediumItalic.ttf │ │ │ ├── Montserrat-Regular.ttf │ │ │ ├── Montserrat-SemiBold.ttf │ │ │ ├── Montserrat-SemiBoldItalic.ttf │ │ │ ├── Montserrat-Thin.ttf │ │ │ ├── Montserrat-ThinItalic.ttf │ │ │ └── OFL.txt │ ├── images │ │ ├── female.png │ │ ├── logo.png │ │ ├── male.png │ │ └── pokeball.png │ └── pokemons │ │ ├── aerodactyl.png │ │ ├── blastoise.png │ │ ├── bulbasaur.png │ │ ├── butterfree.png │ │ ├── charizard.png │ │ ├── charmander.png │ │ ├── chespin.png │ │ ├── chikorita.png │ │ ├── chimcar.png │ │ ├── clefable.png │ │ ├── cyndaquil.png │ │ ├── diglett.png │ │ ├── dragonite.png │ │ ├── fennekin.png │ │ ├── froakie.png │ │ ├── ganger.png │ │ ├── klinklang.png │ │ ├── litten.png │ │ ├── lucario.png │ │ ├── mew.png │ │ ├── mudkip.png │ │ ├── onix.png │ │ ├── oshawott.png │ │ ├── pikachu.png │ │ ├── piplup.png │ │ ├── popplio.png │ │ ├── regice.png │ │ ├── rowlet.png │ │ ├── seviper.png │ │ ├── snivy.png │ │ ├── squirtle.png │ │ ├── tepig.png │ │ ├── togepi.png │ │ ├── torchic.png │ │ ├── totodile.png │ │ ├── treecko.png │ │ ├── turtwig.png │ │ ├── unown.png │ │ ├── venusaur.png │ │ └── weavile.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── Routes.tsx ├── api │ ├── axios.ts │ └── fromApi.ts ├── components │ ├── InfiniteScroll.tsx │ ├── Layout.tsx │ ├── LoadButton.test.tsx │ ├── LoadButton.tsx │ ├── Modal.tsx │ ├── Navbar.tsx │ ├── PokemonCard.tsx │ ├── PokemonDetailsBiography.tsx │ ├── PokemonDetailsEvolutions.tsx │ ├── PokemonDetailsHeader.tsx │ ├── PokemonDetailsStats.tsx │ ├── PokemonEvolution.tsx │ ├── PokemonForm.tsx │ ├── PokemonGenerationCard.tsx │ ├── PokemonGenerations.tsx │ ├── PokemonIcon.tsx │ ├── PokemonInformation.tsx │ ├── PokemonSkeleton.tsx │ ├── PokemonStats.tsx │ ├── SplashScreen.tsx │ ├── Tab.tsx │ ├── Trail.tsx │ └── __snapshots__ │ │ └── LoadButton.test.tsx.snap ├── features │ ├── cachedPokemonsSlice.ts │ ├── evolutionChainSlice.ts │ ├── pokemonSlice.ts │ ├── speciesSlice.ts │ ├── store.ts │ ├── types.ts │ └── utilities.ts ├── globals.ts ├── hooks │ ├── useResize.ts │ ├── useScrollDirection.ts │ └── useTrigger.ts ├── index.css ├── index.tsx ├── logo.svg ├── pages │ ├── PokemonDetailsPage.tsx │ └── PokemonsPage.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts ├── svg │ └── pokeball.svg └── utils │ ├── camelcaseObject.test.ts │ ├── camelcaseObject.ts │ ├── capitalize.test.ts │ ├── capitalize.ts │ ├── leftPad.test.ts │ ├── leftPad.ts │ ├── randomize.test.ts │ ├── randomize.ts │ ├── romanize.test.ts │ ├── romanize.ts │ └── shuffle.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven Hansel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 21 | 22 | [![Contributors][contributors-shield]][contributors-url] 23 | [![Forks][forks-shield]][forks-url] 24 | [![Stargazers][stars-shield]][stars-url] 25 | [![Issues][issues-shield]][issues-url] 26 | [![MIT License][license-shield]][license-url] 27 | 28 | 29 | 30 |
31 |

32 | 33 |

34 | React Pokédex 35 |

36 | 37 | 38 |

39 | A simple pokédex made with React & PokéAPI 40 |
41 |
42 | View Demo 43 | · 44 | Report Bug 45 | · 46 | Request Feature 47 |

48 |

49 | 50 | ![GIF Screenshot](https://github.com/ShinteiMai/react-pokedex/blob/master/images/screenshot.gif) 51 | 52 | 53 | 54 | 55 | ## Table of Contents 56 | 57 | - [Built With](#built-with) 58 | - [Getting Started](#getting-started) 59 | - [Prerequisites](#prerequisites) 60 | - [Installation](#installation) 61 | - [Contributing](#contributing) 62 | - [License](#license) 63 | 64 | 65 | 66 | 67 | ## Built With 68 | 69 | - [PokéAPI](https://pokeapi.co/) 70 | - [React](https://reactjs.org/) 71 | - [TypeScript](https://www.typescriptlang.org/) 72 | - [Tailwind CSS](https://tailwindcss.com/) 73 | - [redux-toolkit](https://redux-toolkit.js.org/) 74 | 75 | 76 | 77 | ## Getting Started 78 | 79 | To get a local copy up and running follow these simple steps. 80 | 81 | ### Prerequisites 82 | 83 | This is an example of how to list things you need to use the software and how to install them. 84 | 85 | - npm 86 | 87 | ```sh 88 | npm install npm@latest -g 89 | ``` 90 | 91 | ### Installation 92 | 93 | 1. Clone the repo 94 | 95 | ```sh 96 | git clone https://github.com/ShinteiMai/react-pokedex.git 97 | ``` 98 | 99 | 2. Install NPM packages 100 | 101 | ```sh 102 | yarn 103 | ``` 104 | 105 | 3. Run the local development server 106 | 107 | 108 | 109 | ## Contributing 110 | 111 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 112 | 113 | 1. Fork the Project 114 | 2. Create your Feature Branch (`git checkout -b feature/locations`) 115 | 3. Commit your Changes (`git commit -m 'Added pokemon location appearences'`) 116 | 4. Push to the Branch (`git push origin feature/locations`) 117 | 5. Open a Pull Request 118 | 119 | 120 | 121 | ## License 122 | 123 | Distributed under the MIT License. See `LICENSE` for more information. 124 | 125 | 126 | 127 | 128 | [contributors-shield]: https://img.shields.io/github/contributors/shinteimai/react-pokedex.svg?style=flat-square 129 | [contributors-url]: https://github.com/shinteimai/react-pokedex/graphs/contributors 130 | [forks-shield]: https://img.shields.io/github/forks/shinteimai/react-pokedex.svg?style=flat-square 131 | [forks-url]: https://github.com/shinteimai/react-pokedex/network/members 132 | [stars-shield]: https://img.shields.io/github/stars/shinteimai/react-pokedex.svg?style=flat-square 133 | [stars-url]: https://github.com/shinteimai/react-pokedex/stargazers 134 | [issues-shield]: https://img.shields.io/github/issues/shinteimai/react-pokedex.svg?style=flat-square 135 | [issues-url]: https://github.com/shinteimai/react-pokedex/issues 136 | [license-shield]: https://img.shields.io/github/license/shinteimai/react-pokedex.svg?style=flat-square 137 | [license-url]: https://github.com/shinteimai/react-pokedex/blob/master/LICENSE.txt 138 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const { POSTCSS_MODES } = require("@craco/craco"); 2 | 3 | module.exports = { 4 | style: { 5 | postcss: { 6 | mode: POSTCSS_MODES.file, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /images/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/images/screenshot.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-tailwind-boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.4.0", 7 | "@types/fast-levenshtein": "^0.0.1", 8 | "axios": "^0.21.1", 9 | "fast-levenshtein": "^3.0.0", 10 | "lodash-humps-ts": "^0.0.6", 11 | "react": "^16.14.0", 12 | "react-dom": "^16.14.0", 13 | "react-helmet-async": "^1.0.7", 14 | "react-icons": "^3.11.0", 15 | "react-progressive-image-loading": "^3.0.3", 16 | "react-redux": "^7.2.1", 17 | "react-responsive": "^8.1.0", 18 | "react-router-dom": "^5.2.0", 19 | "react-scripts": "3.4.3", 20 | "react-scroll": "^1.8.1", 21 | "react-spinners": "^0.9.0", 22 | "react-spring": "^8.0.27", 23 | "react-waypoint": "^9.0.3", 24 | "typescript": "~4.0.5" 25 | }, 26 | "scripts": { 27 | "start": "craco start", 28 | "build": "craco build", 29 | "test": "craco test" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@craco/craco": "^5.7.0", 48 | "@testing-library/dom": "^7.26.5", 49 | "@testing-library/jest-dom": "^5.11.5", 50 | "@testing-library/react": "^11.1.1", 51 | "@testing-library/user-event": "^12.2.0", 52 | "@types/axios": "^0.14.0", 53 | "@types/jest": "^26.0.15", 54 | "@types/node": "^14.14.7", 55 | "@types/react": "^16.9.56", 56 | "@types/react-dom": "^16.9.9", 57 | "@types/react-helmet": "^6.1.0", 58 | "@types/react-redux": "^7.1.11", 59 | "@types/react-responsive": "^8.0.2", 60 | "@types/react-router-dom": "^5.1.6", 61 | "@types/react-scroll": "^1.8.2", 62 | "autoprefixer": "^10.0.1", 63 | "postcss-nested": "^5.0.1", 64 | "tailwindcss": "^1.9.6" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("tailwindcss"), 4 | require("postcss-flexbugs-fixes"), 5 | require("postcss-preset-env")({ 6 | autoprefixer: { 7 | flexbox: "no-2009", 8 | }, 9 | stage: 3, 10 | features: { 11 | "nesting-rules": true, 12 | }, 13 | }), 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-Black.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-BlackItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-BoldItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-ExtraBold.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-ExtraLight.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-Italic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-Light.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-LightItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-MediumItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-Thin.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/Montserrat-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/fonts/montserrat/Montserrat-ThinItalic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/montserrat/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /public/assets/images/female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/images/female.png -------------------------------------------------------------------------------- /public/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/images/logo.png -------------------------------------------------------------------------------- /public/assets/images/male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/images/male.png -------------------------------------------------------------------------------- /public/assets/images/pokeball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/images/pokeball.png -------------------------------------------------------------------------------- /public/assets/pokemons/aerodactyl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/aerodactyl.png -------------------------------------------------------------------------------- /public/assets/pokemons/blastoise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/blastoise.png -------------------------------------------------------------------------------- /public/assets/pokemons/bulbasaur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/bulbasaur.png -------------------------------------------------------------------------------- /public/assets/pokemons/butterfree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/butterfree.png -------------------------------------------------------------------------------- /public/assets/pokemons/charizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/charizard.png -------------------------------------------------------------------------------- /public/assets/pokemons/charmander.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/charmander.png -------------------------------------------------------------------------------- /public/assets/pokemons/chespin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/chespin.png -------------------------------------------------------------------------------- /public/assets/pokemons/chikorita.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/chikorita.png -------------------------------------------------------------------------------- /public/assets/pokemons/chimcar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/chimcar.png -------------------------------------------------------------------------------- /public/assets/pokemons/clefable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/clefable.png -------------------------------------------------------------------------------- /public/assets/pokemons/cyndaquil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/cyndaquil.png -------------------------------------------------------------------------------- /public/assets/pokemons/diglett.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/diglett.png -------------------------------------------------------------------------------- /public/assets/pokemons/dragonite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/dragonite.png -------------------------------------------------------------------------------- /public/assets/pokemons/fennekin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/fennekin.png -------------------------------------------------------------------------------- /public/assets/pokemons/froakie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/froakie.png -------------------------------------------------------------------------------- /public/assets/pokemons/ganger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/ganger.png -------------------------------------------------------------------------------- /public/assets/pokemons/klinklang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/klinklang.png -------------------------------------------------------------------------------- /public/assets/pokemons/litten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/litten.png -------------------------------------------------------------------------------- /public/assets/pokemons/lucario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/lucario.png -------------------------------------------------------------------------------- /public/assets/pokemons/mew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/mew.png -------------------------------------------------------------------------------- /public/assets/pokemons/mudkip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/mudkip.png -------------------------------------------------------------------------------- /public/assets/pokemons/onix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/onix.png -------------------------------------------------------------------------------- /public/assets/pokemons/oshawott.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/oshawott.png -------------------------------------------------------------------------------- /public/assets/pokemons/pikachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/pikachu.png -------------------------------------------------------------------------------- /public/assets/pokemons/piplup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/piplup.png -------------------------------------------------------------------------------- /public/assets/pokemons/popplio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/popplio.png -------------------------------------------------------------------------------- /public/assets/pokemons/regice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/regice.png -------------------------------------------------------------------------------- /public/assets/pokemons/rowlet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/rowlet.png -------------------------------------------------------------------------------- /public/assets/pokemons/seviper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/seviper.png -------------------------------------------------------------------------------- /public/assets/pokemons/snivy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/snivy.png -------------------------------------------------------------------------------- /public/assets/pokemons/squirtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/squirtle.png -------------------------------------------------------------------------------- /public/assets/pokemons/tepig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/tepig.png -------------------------------------------------------------------------------- /public/assets/pokemons/togepi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/togepi.png -------------------------------------------------------------------------------- /public/assets/pokemons/torchic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/torchic.png -------------------------------------------------------------------------------- /public/assets/pokemons/totodile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/totodile.png -------------------------------------------------------------------------------- /public/assets/pokemons/treecko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/treecko.png -------------------------------------------------------------------------------- /public/assets/pokemons/turtwig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/turtwig.png -------------------------------------------------------------------------------- /public/assets/pokemons/unown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/unown.png -------------------------------------------------------------------------------- /public/assets/pokemons/venusaur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/venusaur.png -------------------------------------------------------------------------------- /public/assets/pokemons/weavile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/assets/pokemons/weavile.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React Pokédex 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenhansel/react-pokedex/c90b6a001b9c7adf5db615972a8754b69e7f5d94/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Pokédex", 3 | "name": "React Pokédex", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import SplashScreen from "./components/SplashScreen"; 5 | import { 6 | cachedPokemonsSelector, 7 | getCachedPokemons, 8 | } from "./features/cachedPokemonsSlice"; 9 | 10 | import { SliceStatus } from "./globals"; 11 | import Routes from "./Routes"; 12 | 13 | const App: React.FC = () => { 14 | const dispatch = useDispatch(); 15 | const cachedPokemons = useSelector(cachedPokemonsSelector); 16 | 17 | useEffect(() => { 18 | dispatch(getCachedPokemons()); 19 | //eslint-disable-next-line 20 | }, []); 21 | 22 | return ( 23 | <> 24 | {cachedPokemons.status.state === SliceStatus.LOADING || 25 | cachedPokemons.status.state === SliceStatus.IDLE ? ( 26 | 27 | ) : ( 28 | 29 | 30 | 31 | )} 32 | 33 | ); 34 | }; 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Switch, Route, useLocation } from "react-router-dom"; 4 | import { useTransition, animated } from "react-spring"; 5 | import SplashScreen from "./components/SplashScreen"; 6 | import PokemonDetailsPage from "./pages/PokemonDetailsPage"; 7 | const PokemonsPage = React.lazy(() => import("./pages/PokemonsPage")); 8 | 9 | const Routes: React.FC = () => { 10 | const location = useLocation(); 11 | const transitions = useTransition(location, (location) => location.pathname, { 12 | config: { 13 | duration: 250, 14 | }, 15 | from: { 16 | opacity: 0.25, 17 | }, 18 | enter: { 19 | opacity: 1, 20 | }, 21 | leave: { 22 | opacity: 0.25, 23 | }, 24 | }); 25 | 26 | return ( 27 | }> 28 | {transitions.map(({ item: location, props, key }) => ( 29 | 37 | 38 | 39 | 40 | 41 | 42 | ))} 43 | 44 | ); 45 | }; 46 | export default Routes; 47 | -------------------------------------------------------------------------------- /src/api/axios.ts: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | import { HTTP_METHODS } from "../globals"; 3 | 4 | const axios = Axios.create({ 5 | baseURL: "https://pokeapi.co/api/v2", 6 | }); 7 | 8 | export const createApiRequest = async ( 9 | url: string, 10 | method: HTTP_METHODS, 11 | data: any 12 | ) => { 13 | try { 14 | const response = await axios({ 15 | url, 16 | method, 17 | headers: { 18 | "Content-Type": "application/json", 19 | Accept: "application/json", 20 | }, 21 | data, 22 | }); 23 | return response.data; 24 | } catch (err) { 25 | console.error(err); 26 | throw new Error(err); 27 | // const statusCode = err.response.status; 28 | // const messages = err.response.data.data[0].messages; 29 | // throw new Error(JSON.stringify({ statusCode, messages })); 30 | } 31 | }; 32 | export const baseImageUrl = 33 | "https://raw.githubusercontent.com/HybridShivam/Pokemon/master/assets/images/"; 34 | 35 | export default axios; 36 | -------------------------------------------------------------------------------- /src/api/fromApi.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_METHODS } from "../globals"; 2 | import { createApiRequest } from "./axios"; 3 | 4 | class ApiCallCreator { 5 | getPokemons(limit: number, offset?: number) { 6 | return createApiRequest( 7 | `/pokemon/?limit=${limit}&offset=${offset}`, 8 | HTTP_METHODS.GET, 9 | {} 10 | ); 11 | } 12 | getPokemonByNameOrId(id: number | string) { 13 | return createApiRequest(`/pokemon/${id}/`, HTTP_METHODS.GET, {}); 14 | } 15 | getSpeciesByNameOrId(id: number | string) { 16 | return createApiRequest(`/pokemon-species/${id}/`, HTTP_METHODS.GET, {}); 17 | } 18 | getEvolutionChainByNameOrId(id: string | number) { 19 | return createApiRequest(`/evolution-chain/${id}/`, HTTP_METHODS.GET, {}); 20 | } 21 | } 22 | 23 | const fromApi = new ApiCallCreator(); 24 | export default fromApi; 25 | -------------------------------------------------------------------------------- /src/components/InfiniteScroll.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, createContext, useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { getPokemons, PAGINATE_SIZE } from "../features/pokemonSlice"; 4 | import LoadButton from "./LoadButton"; 5 | import { Waypoint as ReactWaypoint } from "react-waypoint"; 6 | 7 | type ContextType = { 8 | page: number; 9 | setPage: React.Dispatch>; 10 | isLoading: boolean; 11 | paginationHandler: ( 12 | page: number 13 | ) => (dispatch: React.Dispatch) => Promise; 14 | data: any[]; 15 | }; 16 | const InfiniteScrollContext = createContext({ 17 | page: 0, 18 | setPage: () => {}, 19 | isLoading: true, 20 | paginationHandler: getPokemons, 21 | data: [], 22 | }); 23 | 24 | const Waypoint = () => { 25 | const { isLoading, setPage, page, paginationHandler, data } = useContext( 26 | InfiniteScrollContext 27 | ); 28 | const dispatch = useDispatch(); 29 | 30 | useEffect(() => { 31 | setPage(data.length - (data.length % 6)); 32 | //eslint-disable-next-line 33 | }, []); 34 | 35 | return ( 36 |
37 | {!isLoading && ( 38 | { 40 | const dispatchPage = page + (data.length > page ? 6 : 0); 41 | setPage(dispatchPage); 42 | dispatch(paginationHandler(dispatchPage)); 43 | }} 44 | /> 45 | )} 46 |
47 | ); 48 | }; 49 | 50 | const Button = () => { 51 | const { isLoading, setPage, page } = useContext(InfiniteScrollContext); 52 | return ( 53 |
54 | {isLoading ? null : ( 55 |
56 | { 58 | setPage(page + PAGINATE_SIZE); 59 | }} 60 | /> 61 |
62 | )} 63 |
64 | ); 65 | }; 66 | 67 | type ContainerProps = { 68 | children: React.ReactNode; 69 | }; 70 | const Container = ({ children }: ContainerProps) => { 71 | return ( 72 |
73 | {children} 74 |
75 | ); 76 | }; 77 | 78 | type InfiniteScrollProps = { 79 | children: ({ 80 | mutatePage: resetPage, 81 | }: { 82 | mutatePage: React.Dispatch>; 83 | }) => React.ReactNode; 84 | paginationHandler: ( 85 | page: number 86 | ) => (dispatch: React.Dispatch) => Promise; 87 | isLoading: boolean; 88 | data: any[]; 89 | }; 90 | 91 | const InfiniteScroll = ({ 92 | children, 93 | paginationHandler, 94 | isLoading, 95 | data, 96 | }: InfiniteScrollProps) => { 97 | const [page, setPage] = useState(0); 98 | 99 | return ( 100 | 109 | {children({ mutatePage: setPage })} 110 | 111 | ); 112 | }; 113 | 114 | InfiniteScroll.Container = Container; 115 | InfiniteScroll.Button = Button; 116 | InfiniteScroll.Waypoint = Waypoint; 117 | export default InfiniteScroll; 118 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Helmet } from "react-helmet-async"; 3 | import Navbar from "./Navbar"; 4 | 5 | type Props = { 6 | children: React.ReactNode; 7 | title?: string; 8 | }; 9 | 10 | const Layout = ({ children, title }: Props) => { 11 | return ( 12 | <> 13 | 14 | 15 | React Pokédex {title && `| ${title}`} 16 | 20 | 21 | 22 |
{children}
23 | 24 | ); 25 | }; 26 | export default Layout; 27 | -------------------------------------------------------------------------------- /src/components/LoadButton.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import LoadButton from "./LoadButton"; 4 | 5 | it("renders LoadButton successfully", () => { 6 | const { container, getByText } = render( 7 | {}} /> 8 | ); 9 | 10 | expect(getByText("Load More")).toBeTruthy(); 11 | expect(container).toMatchSnapshot(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/LoadButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactComponent as PokeballIcon } from "../svg/pokeball.svg"; 3 | 4 | type Props = { 5 | clickHandler: () => void; 6 | }; 7 | 8 | const LoadButton = ({ clickHandler }: Props) => { 9 | return ( 10 | 19 | ); 20 | }; 21 | 22 | export default LoadButton; 23 | -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, createContext } from "react"; 2 | 3 | type ModalContextType = { 4 | showModal: boolean; 5 | setShowModal: React.Dispatch>; 6 | }; 7 | const ModalContext = createContext({ 8 | showModal: false, 9 | setShowModal: () => {}, 10 | }); 11 | 12 | type ButtonProps = { 13 | children: React.ReactNode; 14 | className?: string; 15 | disabled?: boolean; 16 | }; 17 | const Button = ({ children, className, disabled }: ButtonProps) => { 18 | const { setShowModal } = useContext(ModalContext); 19 | 20 | return ( 21 | 31 | ); 32 | }; 33 | 34 | type ContentProps = { 35 | title?: string; 36 | children?: React.ReactNode; 37 | handleSaveModal?: () => void; 38 | }; 39 | const Content = ({ children, title, handleSaveModal }: ContentProps) => { 40 | const { showModal, setShowModal } = useContext(ModalContext); 41 | return showModal ? ( 42 | <> 43 |
44 |
45 | {/* Content */} 46 |
47 | {/* Header */} 48 |
49 |

50 | {title || "Title"} 51 |

52 |
53 | {/* Body */} 54 |
55 | {children} 56 |
57 | {/* Footer */} 58 |
59 | 66 | 80 |
81 |
82 |
83 |
84 |
85 | 86 | ) : null; 87 | }; 88 | 89 | type ModalProps = { 90 | children: React.ReactNode; 91 | }; 92 | const Modal = ({ children }: ModalProps) => { 93 | const [showModal, setShowModal] = useState(false); 94 | 95 | return ( 96 | 97 | {children} 98 | 99 | ); 100 | }; 101 | 102 | Modal.Button = Button; 103 | Modal.Content = Content; 104 | 105 | export default Modal; 106 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { importImages } from "../globals"; 3 | import * as Scroll from "react-scroll"; 4 | import useScrollDirection from "../hooks/useScrollDirection"; 5 | 6 | const scroll = Scroll.animateScroll; 7 | 8 | const Navbar = () => { 9 | const scrollDirection = useScrollDirection({ initialDirection: "down" }); 10 | const [scrolledToTop, setScrolledToTop] = useState(true); 11 | 12 | const handleScroll = () => { 13 | setScrolledToTop(window.pageYOffset < 50); 14 | }; 15 | 16 | useEffect(() => { 17 | window.addEventListener("scroll", handleScroll); 18 | 19 | return () => { 20 | window.removeEventListener("scroll", handleScroll); 21 | }; 22 | }, []); 23 | 24 | return ( 25 |
34 | Pokeball scroll.scrollToTop()} 39 | /> 40 |
41 | ); 42 | }; 43 | export default Navbar; 44 | -------------------------------------------------------------------------------- /src/components/PokemonCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Pokemon } from "../features/pokemonSlice"; 3 | import { PokemonTypeColors, PokemonTypePlaceholders } from "../globals"; 4 | import { leftPad } from "../utils/leftPad"; 5 | import Trail from "./Trail"; 6 | import ProgressiveImage from "react-progressive-image-loading"; 7 | import { useHistory } from "react-router-dom"; 8 | 9 | const MaskStyling = { 10 | width: 130, 11 | height: 130, 12 | zIndex: -10, 13 | bottom: 8, 14 | left: 16, 15 | }; 16 | const ImageContainerStyling = { 17 | width: 175, 18 | height: 175, 19 | }; 20 | 21 | type Props = Pokemon; 22 | 23 | const PokemonCard = ({ id, name, sprites, types }: Props) => { 24 | const history = useHistory(); 25 | 26 | const backgroundColors = types.map(({ type }) => { 27 | const [[, backgroundColor]] = Object.entries(PokemonTypeColors).filter( 28 | ([key, _]) => key === type.name 29 | ); 30 | 31 | return backgroundColor; 32 | }); 33 | const imagePlaceholder = types.map(({ type }) => { 34 | const [[, image]] = Object.entries(PokemonTypePlaceholders).filter( 35 | ([key, _]) => key === type.name 36 | ); 37 | 38 | return image; 39 | }); 40 | 41 | return ( 42 | 43 |
history.push(`/pokemons/${id}`)} 49 | > 50 |
56 |

57 | #{leftPad(id, 3)} 58 |

59 | 60 |
64 |
71 | ( 75 | {name} 76 | )} 77 | /> 78 |
79 |
80 | 81 |
82 |

{name}

83 |
84 | {types.map(({ type }, index) => { 85 | return ( 86 |

94 | {type.name} 95 |

96 | ); 97 | })} 98 |
99 |
100 |
101 | 102 | ); 103 | }; 104 | 105 | export default PokemonCard; 106 | -------------------------------------------------------------------------------- /src/components/PokemonDetailsBiography.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Pokemon } from "../features/pokemonSlice"; 3 | import { Species } from "../features/speciesSlice"; 4 | import { importImages } from "../globals"; 5 | import { leftPad } from "../utils/leftPad"; 6 | import PokemonInformation from "./PokemonInformation"; 7 | 8 | type Props = { 9 | pokemon: Pokemon; 10 | species: Species; 11 | }; 12 | 13 | const PokemonDetailsBiography = ({ pokemon, species }: Props) => { 14 | const inches = (pokemon.height * 3.93701).toFixed(0); 15 | const feet = Math.floor(Number(inches) / 12); 16 | const genderPercentage = 17 | species.genderRate !== -1 ? (species.genderRate / 8) * 100 : -1; 18 | 19 | return ( 20 | <> 21 |
22 |

Pokémon Data

23 |

24 | { 25 | species.flavorTextEntries.find( 26 | (text) => text.language.name === "en" 27 | )?.flavorText 28 | } 29 |

30 |
    31 | gen.language.name === "en")?.genus 35 | } 36 | /> 37 | 43 | 47 | ( 50 |
  • 54 | {index + 1}. {ability.ability.name}{" "} 55 | {ability.isHidden && "(Hidden Ability)"} 56 |
  • 57 | ))} 58 | /> 59 | 63 | {genderPercentage === -1 ? ( 64 | Genderless 65 | ) : ( 66 | <> 67 |
    68 | male 73 | {100 - genderPercentage}% 74 |
    75 |
    76 | female 81 | {genderPercentage}% 82 |
    83 | 84 | )} 85 | 86 | } 87 | /> 88 |
89 |
90 |
91 |

Training

92 |
    93 | 97 | 101 | 105 | {species.growthRate.name} 109 | } 110 | /> 111 |
112 |
113 | 114 | ); 115 | }; 116 | 117 | export default PokemonDetailsBiography; 118 | -------------------------------------------------------------------------------- /src/components/PokemonDetailsEvolutions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { ChainLink } from "../features/evolutionChainSlice"; 4 | import { pokemonsSelector } from "../features/pokemonSlice"; 5 | 6 | import PokemonEvolution from "./PokemonEvolution"; 7 | 8 | type Props = { 9 | selectedIds: number[]; 10 | chainLinks: ChainLink[]; 11 | selectedBackgroundColor: { light: string; medium: string }; 12 | }; 13 | 14 | const PokemonDetailsEvolutions = ({ 15 | selectedBackgroundColor, 16 | selectedIds, 17 | chainLinks, 18 | }: Props) => { 19 | const pokemons = useSelector(pokemonsSelector); 20 | 21 | return ( 22 |
23 |
24 | {selectedIds.map((id) => { 25 | const pokemon = pokemons.data.find((p) => p !== null && id === p.id); 26 | const chain = chainLinks.find( 27 | ({ species }) => 28 | Number(species.url.split("/").splice(-2)[0]) === pokemon?.id 29 | ); 30 | 31 | return ( 32 | <> 33 | {pokemon && ( 34 | 39 | )} 40 | 41 | ); 42 | })} 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default PokemonDetailsEvolutions; 49 | -------------------------------------------------------------------------------- /src/components/PokemonDetailsHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import ProgressiveImage from "react-progressive-image-loading"; 3 | import { useSpring, animated } from "react-spring"; 4 | import { Pokemon } from "../features/pokemonSlice"; 5 | import { Species } from "../features/speciesSlice"; 6 | import { PokemonTypePlaceholders } from "../globals"; 7 | import { useResize } from "../hooks/useResize"; 8 | import { leftPad } from "../utils/leftPad"; 9 | 10 | const calc = (x: number, y: number, width: number, height: number) => [ 11 | x - width / 2, 12 | y - height / 2, 13 | ]; 14 | 15 | const trans1 = (x: number, y: number) => 16 | `translate3d(-${x / 30}px,-${y / 30}px,0)`; 17 | 18 | const trans2 = (x: number, y: number) => 19 | `translate3d(${x / 20}px,${y / 20}px,0)`; 20 | 21 | const MaskSize = 225; 22 | const ImageSize = 325; 23 | 24 | const MaskStyling = { 25 | width: MaskSize, 26 | height: MaskSize, 27 | bottom: 55, 28 | }; 29 | 30 | const PokemonImageStyling = { 31 | width: ImageSize, 32 | height: ImageSize, 33 | display: "block", 34 | left: 0, 35 | right: 0, 36 | bottom: 5, 37 | margin: "auto", 38 | }; 39 | 40 | type Props = { 41 | pokemon: Pokemon; 42 | species: Species; 43 | selectedBackgroundColor: { light: string; medium: string }; 44 | }; 45 | 46 | const PokemonDetailsHeader = ({ 47 | pokemon, 48 | species, 49 | selectedBackgroundColor, 50 | }: Props) => { 51 | const containerRef = useRef(null); 52 | const { width, height, top, left } = useResize(containerRef); 53 | const [props, set] = useSpring(() => ({ 54 | xy: [0, 0], 55 | config: { mass: 10, tension: 550, friction: 140 }, 56 | })); 57 | 58 | const kanjiName = species.names.find( 59 | (name) => name.language.name === "ja-Hrkt" 60 | ); 61 | const imagePlaceholder = pokemon.types.map(({ type }) => { 62 | const [[, image]] = Object.entries(PokemonTypePlaceholders).filter( 63 | ([key, _]) => key === type.name 64 | ); 65 | 66 | return image; 67 | }); 68 | 69 | return ( 70 | <> 71 |
75 | set({ 76 | xy: calc(clientX - left, clientY - top, width + left, height + top), 77 | }) 78 | } 79 | > 80 |
81 |

82 | #{leftPad(pokemon.id, 3)} 83 |

84 |

85 | {pokemon.name} 86 |

87 |
88 | 89 |
90 |

91 | {kanjiName && kanjiName.name} 92 |

93 | 94 | 103 | 104 | 112 | ( 116 | {pokemon.name} 117 | )} 118 | /> 119 | 120 |
121 |
122 |
123 | 124 | ); 125 | }; 126 | 127 | export default PokemonDetailsHeader; 128 | -------------------------------------------------------------------------------- /src/components/PokemonDetailsStats.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Pokemon } from "../features/pokemonSlice"; 3 | import PokemonStats from "./PokemonStats"; 4 | 5 | type Props = { 6 | pokemon: Pokemon; 7 | }; 8 | 9 | const transformStatNames = (statName: string) => { 10 | const map: string[][] = [ 11 | ["special-attack", "Sp. Atk"], 12 | ["special-defense", "Sp. Def"], 13 | ]; 14 | let transformed = statName; 15 | map.forEach(([a, b]) => { 16 | if (a === statName) { 17 | transformed = b; 18 | } 19 | }); 20 | 21 | return transformed; 22 | }; 23 | 24 | const PokemonDetailsStats = ({ pokemon }: Props) => { 25 | const stats = pokemon.stats.map((resource) => ({ 26 | name: transformStatNames(resource.stat.name), 27 | min: resource.baseStat, 28 | max: 29 | resource.stat.name === "hp" 30 | ? Number(resource.baseStat) * 2 + 204 31 | : (Number(resource.baseStat) * 2 + 99) * 1.1, 32 | })); 33 | 34 | return ( 35 | <> 36 |

Base Stats

37 |
    38 | {stats.map((st) => ( 39 | 45 | ))} 46 |
    47 | Total 48 | 49 | {stats.reduce((sum, { min }) => sum + min, 0)} 50 | 51 |
    52 | Max 53 |
    54 |
55 |

56 | Min & Max values are calculated for level 100 Pokemon. Minimum values 57 | are based on 0 EVs & 0 IVs, meanwhile Maximum values are based on 252 58 | EVs & 31 IVs. 59 |

60 | 61 | ); 62 | }; 63 | 64 | export default PokemonDetailsStats; 65 | -------------------------------------------------------------------------------- /src/components/PokemonEvolution.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ProgressiveImage from "react-progressive-image-loading"; 3 | import { Pokemon } from "../features/pokemonSlice"; 4 | import { PokemonTypePlaceholders } from "../globals"; 5 | import { leftPad } from "../utils/leftPad"; 6 | 7 | import { AiOutlineCaretDown, AiOutlineCaretRight } from "react-icons/ai"; 8 | import { ChainLink } from "../features/evolutionChainSlice"; 9 | import { useHistory } from "react-router-dom"; 10 | 11 | const MaskSize = 200; 12 | const ImageSize = 150; 13 | 14 | const MaskStyling = { 15 | width: MaskSize, 16 | height: MaskSize, 17 | bottom: 0, 18 | }; 19 | const ImageContainerStyling = { 20 | width: ImageSize, 21 | height: ImageSize, 22 | display: "block", 23 | left: 0, 24 | right: 0, 25 | bottom: 25, 26 | margin: "auto", 27 | }; 28 | 29 | type Props = { 30 | pokemon: Pokemon; 31 | chain: ChainLink | undefined; 32 | selectedBackgroundColor: { light: string; medium: string }; 33 | }; 34 | 35 | const PokemonEvolution = ({ 36 | pokemon, 37 | chain, 38 | selectedBackgroundColor, 39 | }: Props) => { 40 | const history = useHistory(); 41 | const imagePlaceholder = pokemon.types.map(({ type }) => { 42 | const [[, image]] = Object.entries(PokemonTypePlaceholders).filter( 43 | ([key, _]) => key === type.name 44 | ); 45 | 46 | return image; 47 | }); 48 | const minLevel = chain?.evolutionDetails[0]?.minLevel; 49 | 50 | return ( 51 |
52 |
53 |
54 |
61 |
history.push(`/pokemons/${pokemon.id}`)} 63 | className="cursor-pointer transform hover:-translate-y-2 transition-all duration-300" 64 | style={{ 65 | ...ImageContainerStyling, 66 | position: "absolute", 67 | }} 68 | > 69 | ( 73 | {pokemon.name} 74 | )} 75 | /> 76 |
77 |
78 |

79 | #{leftPad(pokemon.id, 3)} 80 |

81 |

{pokemon.name}

82 |

83 | {minLevel && `Level ${minLevel}`} 84 |

85 |
86 |

87 | {chain?.evolvesTo.length !== 0 && ( 88 | <> 89 | 93 | 97 | 98 | )} 99 |

100 |
101 | ); 102 | }; 103 | 104 | export default PokemonEvolution; 105 | -------------------------------------------------------------------------------- /src/components/PokemonForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { GoSearch } from "react-icons/go"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { 5 | PokemonGenerationsEnum, 6 | searchPokemonsByNameReducer, 7 | filterPokemonsByGenerationReducer, 8 | randomizePokemonsReducer, 9 | } from "../features/cachedPokemonsSlice"; 10 | import { 11 | pokemonsSelector, 12 | resetPokemonsReducer, 13 | } from "../features/pokemonSlice"; 14 | import { SliceStatus } from "../globals"; 15 | import PokemonGenerations from "./PokemonGenerations"; 16 | 17 | type Props = { 18 | mutatePage: React.Dispatch>; 19 | placeholder?: string; 20 | initialValue?: string; 21 | changeHandler?: () => void; 22 | }; 23 | 24 | const PokemonForm = ({ 25 | placeholder, 26 | initialValue = "", 27 | changeHandler, 28 | mutatePage, 29 | }: Props) => { 30 | const dispatch = useDispatch(); 31 | const pokemons = useSelector(pokemonsSelector); 32 | const [value, setValue] = useState(initialValue); 33 | const [ 34 | selectedGeneration, 35 | setSelectedGeneration, 36 | ] = useState(null); 37 | const inputRef = useRef(0); 38 | 39 | const isLoading = pokemons.status.state === SliceStatus.LOADING; 40 | 41 | useEffect(() => { 42 | if (changeHandler) { 43 | clearTimeout(inputRef.current); 44 | inputRef.current = window.setTimeout(() => { 45 | changeHandler(); 46 | }, 100); 47 | } 48 | }, [value, changeHandler]); 49 | 50 | const submitFormHandler = () => { 51 | if (!isLoading) { 52 | dispatch(resetPokemonsReducer({})); 53 | dispatch(searchPokemonsByNameReducer({ pokemonName: value })); 54 | mutatePage(0); 55 | } 56 | }; 57 | 58 | const changeGenerationHandler = () => { 59 | if (!isLoading) { 60 | dispatch(resetPokemonsReducer({})); 61 | dispatch(filterPokemonsByGenerationReducer({ selectedGeneration })); 62 | if (selectedGeneration === null) { 63 | dispatch(randomizePokemonsReducer({})); 64 | mutatePage(0); 65 | } else { 66 | mutatePage(0); 67 | } 68 | } 69 | }; 70 | 71 | return ( 72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 | 80 | { 85 | if (e.key === "Enter") { 86 | submitFormHandler(); 87 | } 88 | }} 89 | onChange={(e: React.FormEvent) => 90 | setValue(e.currentTarget.value) 91 | } 92 | /> 93 |
94 | 95 |
96 | 102 |
103 | 104 | 115 |
116 | ); 117 | }; 118 | 119 | export default PokemonForm; 120 | -------------------------------------------------------------------------------- /src/components/PokemonGenerationCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { romanize } from "../utils/romanize"; 3 | 4 | type Props = { 5 | images: string[]; 6 | generation: number; 7 | isSelected: boolean; 8 | handleClick: () => void; 9 | }; 10 | 11 | const PokemonGenerationCard = ({ 12 | images, 13 | generation, 14 | isSelected, 15 | handleClick, 16 | }: Props) => { 17 | return ( 18 |
handleClick()} 20 | // className="w-full tracking-wide text-center text-black bg-primaryGray mx-auto px-8 py-5 rounded-lg hover:bg-primarySecondary hover:text-white hover:font-medium transition-all duration-200 ease-in-out cursor-pointer" 21 | className={ 22 | "w-full tracking-wide text-center mx-auto px-8 py-5 rounded-lg hover:font-medium transition-all duration-200 ease-in-out cursor-pointer " + 23 | (isSelected 24 | ? "bg-primarySecondary text-white transform hover:-translate-y-2 hover:shadow-md" 25 | : "bg-primaryGray text-black hover:bg-primarySecondary hover:text-white") 26 | } 27 | > 28 |
29 | {images.map((image) => ( 30 | Pokemon 31 | ))} 32 |
33 |

Generation {romanize(generation)}

34 |
35 | ); 36 | }; 37 | export default PokemonGenerationCard; 38 | -------------------------------------------------------------------------------- /src/components/PokemonGenerations.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | 3 | import Modal from "./Modal"; 4 | import PokemonGenerationCard from "./PokemonGenerationCard"; 5 | import PokemonIcon from "./PokemonIcon"; 6 | 7 | import { importPokemonImage } from "../globals"; 8 | import { PokemonGenerationsEnum } from "../features/cachedPokemonsSlice"; 9 | 10 | const generations = [ 11 | [ 12 | importPokemonImage("bulbasaur"), 13 | importPokemonImage("charmander"), 14 | importPokemonImage("squirtle"), 15 | ], 16 | [ 17 | importPokemonImage("chikorita"), 18 | importPokemonImage("cyndaquil"), 19 | importPokemonImage("totodile"), 20 | ], 21 | [ 22 | importPokemonImage("treecko"), 23 | importPokemonImage("torchic"), 24 | importPokemonImage("mudkip"), 25 | ], 26 | [ 27 | importPokemonImage("turtwig"), 28 | importPokemonImage("chimcar"), 29 | importPokemonImage("piplup"), 30 | ], 31 | [ 32 | importPokemonImage("snivy"), 33 | importPokemonImage("tepig"), 34 | importPokemonImage("oshawott"), 35 | ], 36 | [ 37 | importPokemonImage("chespin"), 38 | importPokemonImage("fennekin"), 39 | importPokemonImage("froakie"), 40 | ], 41 | [ 42 | importPokemonImage("rowlet"), 43 | importPokemonImage("litten"), 44 | importPokemonImage("popplio"), 45 | ], 46 | ]; 47 | 48 | type Props = { 49 | selectedGeneration: PokemonGenerationsEnum | null; 50 | setSelectedGeneration: React.Dispatch< 51 | React.SetStateAction 52 | >; 53 | changeGenerationHandler: () => void; 54 | isLoading: boolean; 55 | }; 56 | 57 | const PokemonGenerations = ({ 58 | selectedGeneration, 59 | setSelectedGeneration, 60 | changeGenerationHandler, 61 | isLoading, 62 | }: Props) => { 63 | const indexToPokemonGenerations = useCallback( 64 | (realIndex: number): PokemonGenerationsEnum | null => { 65 | const pokemonGenerations = Object.entries(PokemonGenerationsEnum); 66 | let selectedEnum: PokemonGenerationsEnum | null = null; 67 | 68 | pokemonGenerations.forEach(([_, b], index) => { 69 | if (index === realIndex) { 70 | selectedEnum = b; 71 | } 72 | }); 73 | return selectedEnum; 74 | }, 75 | [] 76 | ); 77 | 78 | const pokemonGenerationsToIndex = useCallback( 79 | (selectedGeneration: PokemonGenerationsEnum): number => { 80 | const pokemonGenerations = Object.entries(PokemonGenerationsEnum); 81 | let selectedIndex: number = 0; 82 | 83 | pokemonGenerations.forEach(([_, b], index) => { 84 | if (b === selectedGeneration) { 85 | selectedIndex = index; 86 | } 87 | }); 88 | 89 | return selectedIndex; 90 | }, 91 | [] 92 | ); 93 | 94 | return ( 95 | 96 | 106 |
107 | {selectedGeneration !== null ? ( 108 | <> 109 | {generations[pokemonGenerationsToIndex(selectedGeneration)].map( 110 | (image, index) => ( 111 | 116 | ) 117 | )} 118 | 119 | ) : ( 120 | <> 121 | 125 | 129 | 133 | 134 | )} 135 |
136 |
137 | 141 |
142 | {generations.map((images, index) => ( 143 | { 152 | setSelectedGeneration((previousGeneration) => { 153 | const pickedGeneration = indexToPokemonGenerations(index); 154 | return previousGeneration === pickedGeneration 155 | ? null 156 | : pickedGeneration; 157 | }); 158 | }} 159 | /> 160 | ))} 161 |
162 |
163 |
164 | ); 165 | }; 166 | export default PokemonGenerations; 167 | -------------------------------------------------------------------------------- /src/components/PokemonIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | alt: string; 5 | src: string; 6 | }; 7 | 8 | const PokemonIcon = ({ src, alt }: Props) => { 9 | return ( 10 | {alt} 18 | ); 19 | }; 20 | 21 | export default PokemonIcon; 22 | -------------------------------------------------------------------------------- /src/components/PokemonInformation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | title: string; 5 | content: React.ReactNode; 6 | }; 7 | 8 | const PokemonInformation = ({ title, content }: Props) => { 9 | return ( 10 |
  • 11 | {title} 12 | {content} 13 |
  • 14 | ); 15 | }; 16 | export default PokemonInformation; 17 | -------------------------------------------------------------------------------- /src/components/PokemonSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PokemonSkeleton = () => { 4 | return ( 5 |
    11 |
    17 |

    18 | 19 |
    26 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 |
    45 |

    46 |
    47 |

    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 |
    55 | ); 56 | }; 57 | export default PokemonSkeleton; 58 | -------------------------------------------------------------------------------- /src/components/PokemonStats.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | type Props = { 3 | title: string; 4 | min: number; 5 | max: number; 6 | }; 7 | 8 | const PokemonStats = ({ title, min, max }: Props) => { 9 | return ( 10 |
    11 | {title} 12 | {min.toFixed(0)} 13 |
    14 |
    15 |
    22 |
    23 |
    24 | {max.toFixed(0)} 25 |
    26 | ); 27 | }; 28 | export default PokemonStats; 29 | -------------------------------------------------------------------------------- /src/components/SplashScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactComponent as PokeballIcon } from "../svg/pokeball.svg"; 3 | 4 | const SplashScreen = () => { 5 | return ( 6 |
    7 | 8 |
    9 | ); 10 | }; 11 | export default SplashScreen; 12 | -------------------------------------------------------------------------------- /src/components/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | handleSelect: () => void; 6 | isSelected: boolean; 7 | }; 8 | 9 | const Tab = ({ children, handleSelect, isSelected }: Props) => { 10 | return ( 11 | 20 | ); 21 | }; 22 | export default Tab; 23 | -------------------------------------------------------------------------------- /src/components/Trail.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTrail, animated } from "react-spring"; 3 | 4 | type Props = { 5 | children: React.ReactNode; 6 | open: boolean; 7 | className?: string; 8 | }; 9 | 10 | const Trail = ({ open, children, className, ...props }: Props) => { 11 | const items = React.Children.toArray(children); 12 | const trail = useTrail(items.length, { 13 | config: { mass: 5, tension: 2000, friction: 200 }, 14 | opacity: open ? 1 : 0, 15 | x: open ? 0 : 20, 16 | height: open ? 110 : 0, 17 | from: { opacity: 0, x: 20, height: 0 }, 18 | }); 19 | return ( 20 |
    21 |
    22 | {trail.map(({ x, height, ...rest }, index) => ( 23 | `translate3d(0,${x}px,0)`), 29 | }} 30 | > 31 | {items[index]} 32 | 33 | ))} 34 |
    35 |
    36 | ); 37 | }; 38 | 39 | export default Trail; 40 | -------------------------------------------------------------------------------- /src/components/__snapshots__/LoadButton.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders LoadButton successfully 1`] = ` 4 |
    5 | 23 |
    24 | `; 25 | -------------------------------------------------------------------------------- /src/features/cachedPokemonsSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import fromApi from "../api/fromApi"; 3 | import { SliceStatus } from "../globals"; 4 | import { RootState } from "./store"; 5 | import { NamedAPIResource } from "./types"; 6 | import { statusHandlerReducer, wrapReduxAsyncHandler } from "./utilities"; 7 | import Levenshtein from "fast-levenshtein"; 8 | import { shuffle } from "../utils/shuffle"; 9 | import { camelcaseObject } from "../utils/camelcaseObject"; 10 | 11 | export enum PokemonGenerationsEnum { 12 | GENERATION_1 = "151", 13 | GENERATION_2 = "251", 14 | GENERATION_3 = "386", 15 | GENERATION_4 = "494", 16 | GENERATION_5 = "649", 17 | GENERATION_6 = "721", 18 | GENERATION_7 = "809", 19 | } 20 | 21 | type SliceState = { 22 | cache: (NamedAPIResource & { distance: number })[]; 23 | data: (NamedAPIResource & { distance: number })[]; 24 | status: { 25 | state: SliceStatus; 26 | }; 27 | }; 28 | 29 | const initialState: SliceState = { 30 | cache: [], 31 | data: [], 32 | status: { 33 | state: SliceStatus.IDLE, 34 | }, 35 | }; 36 | 37 | const cachedPokemonsSlice = createSlice({ 38 | name: "cachedPokemons", 39 | initialState, 40 | reducers: { 41 | ...statusHandlerReducer, 42 | getCachedPokemonsReducer( 43 | state, 44 | action: PayloadAction<{ 45 | cachedPokemons: (NamedAPIResource & { distance: number })[]; 46 | }> 47 | ) { 48 | const { cachedPokemons } = action.payload; 49 | state.cache = cachedPokemons; 50 | state.data = shuffle([...cachedPokemons]); 51 | }, 52 | searchPokemonsByNameReducer( 53 | state, 54 | action: PayloadAction<{ 55 | pokemonName: string; 56 | }> 57 | ) { 58 | const { pokemonName } = action.payload; 59 | 60 | state.data = state.cache 61 | .map((pokemon) => { 62 | return { 63 | ...pokemon, 64 | distance: Levenshtein.get(pokemon.name, pokemonName), 65 | }; 66 | }) 67 | .sort((a, b) => a.distance - b.distance); 68 | 69 | console.log(state.data); 70 | }, 71 | filterPokemonsByGenerationReducer( 72 | state, 73 | action: PayloadAction<{ 74 | selectedGeneration: PokemonGenerationsEnum | null; 75 | }> 76 | ) { 77 | const { selectedGeneration } = action.payload; 78 | let cache: (NamedAPIResource & { distance: number })[] = state.cache; 79 | if (selectedGeneration) { 80 | const generations = Object.entries(PokemonGenerationsEnum); 81 | let startingIndex: number = 0; 82 | generations.forEach(([_, b], index) => { 83 | if (b === selectedGeneration) { 84 | startingIndex = index === 0 ? 0 : Number(generations[index - 1][1]); 85 | } 86 | }); 87 | cache = state.cache.slice(startingIndex, Number(selectedGeneration)); 88 | } 89 | state.data = cache; 90 | }, 91 | randomizePokemonsReducer(state, action) { 92 | state.data = shuffle([...state.cache]); 93 | }, 94 | }, 95 | }); 96 | 97 | export const cachedPokemonsReducer = cachedPokemonsSlice.reducer; 98 | export const { 99 | initialize, 100 | error, 101 | success, 102 | getCachedPokemonsReducer, 103 | searchPokemonsByNameReducer, 104 | filterPokemonsByGenerationReducer, 105 | randomizePokemonsReducer, 106 | } = cachedPokemonsSlice.actions; 107 | 108 | const statusHandler = { initialize, error, success }; 109 | 110 | export const cachedPokemonsSelector = (state: RootState) => 111 | state.cachedPokemons; 112 | 113 | export const getCachedPokemons = wrapReduxAsyncHandler( 114 | statusHandler, 115 | async (dispatch) => { 116 | const { 117 | results, 118 | }: { results: NamedAPIResource[] } = await fromApi.getPokemons( 119 | Number(PokemonGenerationsEnum.GENERATION_7) 120 | ); 121 | const transformedPokemons = results.map((res: NamedAPIResource) => ({ 122 | ...res, 123 | distance: 0, 124 | })); 125 | dispatch( 126 | getCachedPokemonsReducer({ 127 | cachedPokemons: camelcaseObject(transformedPokemons), 128 | }) 129 | ); 130 | } 131 | ); 132 | -------------------------------------------------------------------------------- /src/features/evolutionChainSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import fromApi from "../api/fromApi"; 3 | import { SliceStatus } from "../globals"; 4 | import { camelcaseObject } from "../utils/camelcaseObject"; 5 | import { RootState } from "./store"; 6 | import { NamedAPIResource } from "./types"; 7 | import { statusHandlerReducer, wrapReduxAsyncHandler } from "./utilities"; 8 | 9 | export type ChainLink = { 10 | isBaby: boolean; 11 | species: NamedAPIResource; 12 | evolutionDetails: { 13 | item: NamedAPIResource; 14 | trigger: NamedAPIResource; 15 | gender: number; 16 | heldItem: NamedAPIResource; 17 | knownMove: NamedAPIResource; 18 | knownMoveType: NamedAPIResource; 19 | location: NamedAPIResource; 20 | minLevel: NamedAPIResource; 21 | minHappiness: NamedAPIResource; 22 | minBeauty: NamedAPIResource; 23 | minAffection: NamedAPIResource; 24 | needsOverworldRain: boolean; 25 | partySpecies: NamedAPIResource; 26 | partyType: NamedAPIResource; 27 | relativePhysicalStats: number; 28 | timeOfDay: string; 29 | tradeSpecies: NamedAPIResource; 30 | turnUpsideDown: boolean; 31 | }[]; 32 | evolvesTo: ChainLink[]; 33 | }; 34 | 35 | export type EvolutionChain = { 36 | id: number; 37 | babyTriggerItem: NamedAPIResource; 38 | chain: ChainLink; 39 | }; 40 | 41 | type SliceState = { 42 | data: EvolutionChain[]; 43 | status: { 44 | state: SliceStatus; 45 | }; 46 | }; 47 | 48 | const initialState: SliceState = { 49 | data: [], 50 | status: { 51 | state: SliceStatus.IDLE, 52 | }, 53 | }; 54 | 55 | const evolutionChainSlice = createSlice({ 56 | name: "evolutionChains", 57 | initialState, 58 | reducers: { 59 | ...statusHandlerReducer, 60 | getEvolutionChainReducer( 61 | state, 62 | action: PayloadAction<{ evolutionChain: EvolutionChain }> 63 | ) { 64 | const { evolutionChain } = action.payload; 65 | const isExist = state.data.find((e) => e.id === evolutionChain.id); 66 | if (!isExist) { 67 | state.data.push(evolutionChain); 68 | } 69 | }, 70 | }, 71 | }); 72 | 73 | export const { 74 | initialize, 75 | error, 76 | success, 77 | getEvolutionChainReducer, 78 | } = evolutionChainSlice.actions; 79 | export const statusHandler = { initialize, error, success }; 80 | 81 | export const evolutionChainSelector = (state: RootState) => 82 | state.evolutionChain; 83 | export const evolutionChainReducer = evolutionChainSlice.reducer; 84 | 85 | export const getEvolutionChainByName = wrapReduxAsyncHandler( 86 | statusHandler, 87 | async (dispatch, { name }) => { 88 | const result = await fromApi.getEvolutionChainByNameOrId(name); 89 | dispatch( 90 | getEvolutionChainReducer({ evolutionChain: camelcaseObject(result) }) 91 | ); 92 | } 93 | ); 94 | export const getEvolutionChainById = wrapReduxAsyncHandler( 95 | statusHandler, 96 | async (dispatch, { id }) => { 97 | const result = await fromApi.getEvolutionChainByNameOrId(Number(id)); 98 | dispatch( 99 | getEvolutionChainReducer({ evolutionChain: camelcaseObject(result) }) 100 | ); 101 | } 102 | ); 103 | -------------------------------------------------------------------------------- /src/features/pokemonSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import fromApi from "../api/fromApi"; 3 | import { SliceStatus } from "../globals"; 4 | import { RootState } from "./store"; 5 | import { NamedAPIResource } from "./types"; 6 | import { camelcaseObject } from "../utils/camelcaseObject"; 7 | import { 8 | statusHandlerReducer, 9 | transformSpriteToBaseImage, 10 | wrapReduxAsyncHandler, 11 | } from "./utilities"; 12 | import { baseImageUrl } from "../api/axios"; 13 | 14 | export const PAGINATE_SIZE = 6; 15 | 16 | export type Pokemon = { 17 | id: number; 18 | name: string; 19 | baseExperience: number; 20 | height: number; 21 | isDefault: boolean; 22 | order: number; 23 | weight: number; 24 | abilities: { 25 | isHidden: boolean; 26 | slot: number; 27 | ability: NamedAPIResource; 28 | }[]; 29 | forms: NamedAPIResource[]; 30 | moves: { 31 | move: NamedAPIResource; 32 | }[]; 33 | sprites: { 34 | frontDefault: string; 35 | frontShiny: string; 36 | frontFemale: string; 37 | frontShinyFemale: string; 38 | backDefault: string; 39 | backShiny: string; 40 | backFemale: string; 41 | backShinyFemale: string; 42 | }; 43 | species: NamedAPIResource[]; 44 | stats: { 45 | baseStat: number; 46 | effort: number; 47 | stat: NamedAPIResource; 48 | }[]; 49 | types: { 50 | slot: number; 51 | type: NamedAPIResource; 52 | }[]; 53 | }; 54 | 55 | type SliceState = { 56 | data: (Pokemon | null)[]; 57 | status: { 58 | state: SliceStatus; 59 | }; 60 | }; 61 | 62 | const initialState: SliceState = { 63 | data: [], 64 | status: { 65 | state: SliceStatus.IDLE, 66 | }, 67 | }; 68 | 69 | const pokemonsSlice = createSlice({ 70 | name: "pokemons", 71 | initialState, 72 | reducers: { 73 | ...statusHandlerReducer, 74 | initializePokemonsReducer(state, action: PayloadAction<{ size: number }>) { 75 | const { size } = action.payload; 76 | const nullValues = new Array(size).fill(null); 77 | if (state.data.length === 0) { 78 | state.data = nullValues; 79 | } else { 80 | state.data = state.data.concat(nullValues); 81 | } 82 | }, 83 | getPokemonsReducer( 84 | state, 85 | action: PayloadAction<{ pokemon: Pokemon; index: number; size: number }> 86 | ) { 87 | const { pokemon, size, index } = action.payload; 88 | 89 | const isPokemonAlreadyExists = state.data.find( 90 | (existingPokemon) => 91 | existingPokemon !== null && existingPokemon.id === pokemon.id 92 | ); 93 | if (!isPokemonAlreadyExists) { 94 | state.data[state.data.length - (size - index)] = pokemon; 95 | } 96 | }, 97 | getSinglePokemonReducer( 98 | state, 99 | action: PayloadAction<{ pokemon: Pokemon }> 100 | ) { 101 | const { pokemon } = action.payload; 102 | const isPokemonAlreadyExists = state.data.find( 103 | (existingPokemon) => 104 | existingPokemon !== null && existingPokemon.id === pokemon.id 105 | ); 106 | if (!isPokemonAlreadyExists) { 107 | state.data.push(pokemon); 108 | } 109 | }, 110 | resetPokemonsReducer(state, action) { 111 | state.data = []; 112 | }, 113 | }, 114 | }); 115 | 116 | export const pokemonsReducer = pokemonsSlice.reducer; 117 | export const { 118 | initialize, 119 | error, 120 | success, 121 | initializePokemonsReducer, 122 | getPokemonsReducer, 123 | resetPokemonsReducer, 124 | getSinglePokemonReducer, 125 | } = pokemonsSlice.actions; 126 | 127 | export const pokemonsSelector = (state: RootState) => state.pokemons; 128 | 129 | const statusHandler = { initialize, error, success }; 130 | 131 | export const getPokemons = wrapReduxAsyncHandler( 132 | statusHandler, 133 | async (dispatch, { page, cachedPokemons, pokemons }) => { 134 | const size = PAGINATE_SIZE - (pokemons.length % PAGINATE_SIZE); 135 | const results = cachedPokemons.slice(page, page + size); 136 | dispatch(initializePokemonsReducer({ size })); 137 | 138 | for await (const [index, { url }] of results.entries()) { 139 | const pokemonId = Number(url.split("/").slice(-2)[0]); 140 | const pokemon = await fromApi.getPokemonByNameOrId(pokemonId); 141 | const pokemonImageUrl = transformSpriteToBaseImage( 142 | pokemon.id, 143 | baseImageUrl 144 | ); 145 | 146 | dispatch( 147 | getPokemonsReducer({ 148 | pokemon: { 149 | ...camelcaseObject(pokemon), 150 | sprites: { 151 | frontDefault: pokemonImageUrl, 152 | }, 153 | }, 154 | size, 155 | index, 156 | }) 157 | ); 158 | } 159 | } 160 | ); 161 | 162 | export const getPokemonById = wrapReduxAsyncHandler( 163 | statusHandler, 164 | async (dispatch, { pokemonId }) => { 165 | const pokemon = await fromApi.getPokemonByNameOrId(pokemonId); 166 | const pokemonImageUrl = transformSpriteToBaseImage( 167 | pokemon.id, 168 | baseImageUrl 169 | ); 170 | const transformedPokemon = { 171 | ...camelcaseObject(pokemon), 172 | sprites: { frontDefault: pokemonImageUrl }, 173 | }; 174 | dispatch(getSinglePokemonReducer({ pokemon: transformedPokemon })); 175 | } 176 | ); 177 | 178 | export const getPokemonsDynamically = wrapReduxAsyncHandler( 179 | statusHandler, 180 | async (dispatch, { pokemonIds }) => { 181 | for await (const id of pokemonIds) { 182 | const pokemon = await fromApi.getPokemonByNameOrId(id); 183 | const pokemonImageUrl = transformSpriteToBaseImage( 184 | pokemon.id, 185 | baseImageUrl 186 | ); 187 | const transformedPokemon = { 188 | ...camelcaseObject(pokemon), 189 | sprites: { frontDefault: pokemonImageUrl }, 190 | }; 191 | dispatch(getSinglePokemonReducer({ pokemon: transformedPokemon })); 192 | } 193 | } 194 | ); 195 | -------------------------------------------------------------------------------- /src/features/speciesSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import fromApi from "../api/fromApi"; 3 | import { SliceStatus } from "../globals"; 4 | import { camelcaseObject } from "../utils/camelcaseObject"; 5 | import { RootState } from "./store"; 6 | import { APIResource, NamedAPIResource } from "./types"; 7 | import { statusHandlerReducer, wrapReduxAsyncHandler } from "./utilities"; 8 | 9 | export type Species = { 10 | id: number; 11 | name: string; 12 | order: number; 13 | genderRate: number; 14 | captureRate: number; 15 | baseHappiness: number; 16 | isBaby: boolean; 17 | isLegendary: boolean; 18 | isMythical: boolean; 19 | hatchCounter: number; 20 | hasGenderDifferences: boolean; 21 | formsSwitchable: boolean; 22 | growthRate: NamedAPIResource; 23 | pokedexNumbers: { 24 | entryNumber: number; 25 | pokedex: NamedAPIResource; 26 | }[]; 27 | eggGroups: NamedAPIResource[]; 28 | color: NamedAPIResource; 29 | shape: NamedAPIResource; 30 | evolvesFromSpecies: NamedAPIResource; 31 | evolutionChain: APIResource; 32 | habitat: NamedAPIResource; 33 | generation: NamedAPIResource; 34 | names: { 35 | name: string; 36 | language: NamedAPIResource; 37 | }[]; 38 | palParkEncounters: { 39 | baseScore: number; 40 | rate: number; 41 | area: { 42 | name: string; 43 | url: string; 44 | }; 45 | }[]; 46 | flavorTextEntries: { 47 | flavorText: string; 48 | language: NamedAPIResource; 49 | version: NamedAPIResource; 50 | }[]; 51 | formDescriptions: { 52 | description: string; 53 | language: NamedAPIResource; 54 | }[]; 55 | genera: { 56 | genus: string; 57 | language: NamedAPIResource; 58 | }[]; 59 | varieties: { 60 | isDefault: boolean; 61 | pokemon: NamedAPIResource; 62 | }[]; 63 | }; 64 | 65 | type SliceState = { 66 | data: Species[]; 67 | status: { 68 | state: SliceStatus; 69 | }; 70 | }; 71 | 72 | const initialState: SliceState = { 73 | data: [], 74 | status: { 75 | state: SliceStatus.IDLE, 76 | }, 77 | }; 78 | 79 | const speciesSlice = createSlice({ 80 | name: "species", 81 | initialState, 82 | reducers: { 83 | ...statusHandlerReducer, 84 | getSpeciesReducer(state, action: PayloadAction<{ species: Species }>) { 85 | const { species } = action.payload; 86 | const isSpeciesAlreadyExists = state.data.find( 87 | (existingSpecies) => existingSpecies.id === species.id 88 | ); 89 | if (!isSpeciesAlreadyExists) { 90 | state.data.push(species); 91 | } 92 | }, 93 | }, 94 | }); 95 | 96 | export const speciesReducer = speciesSlice.reducer; 97 | export const { 98 | initialize, 99 | error, 100 | success, 101 | getSpeciesReducer, 102 | } = speciesSlice.actions; 103 | 104 | export const speciesSelector = (state: RootState) => state.species; 105 | 106 | const statusHandler = { initialize, error, success }; 107 | export const getSpeciesByName = wrapReduxAsyncHandler( 108 | statusHandler, 109 | async (dispatch, { pokemonName }) => { 110 | const pokemonSpecies = await fromApi.getSpeciesByNameOrId(pokemonName); 111 | dispatch(getSpeciesReducer({ species: camelcaseObject(pokemonSpecies) })); 112 | } 113 | ); 114 | 115 | export const getSpeciesById = wrapReduxAsyncHandler( 116 | statusHandler, 117 | async (dispatch, { pokemonId }) => { 118 | const pokemonSpecies = await fromApi.getSpeciesByNameOrId(pokemonId); 119 | 120 | dispatch(getSpeciesReducer({ species: camelcaseObject(pokemonSpecies) })); 121 | } 122 | ); 123 | -------------------------------------------------------------------------------- /src/features/store.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import { cachedPokemonsReducer } from "./cachedPokemonsSlice"; 3 | import { evolutionChainReducer } from "./evolutionChainSlice"; 4 | import { pokemonsReducer } from "./pokemonSlice"; 5 | import { speciesReducer } from "./speciesSlice"; 6 | 7 | export const rootReducer = combineReducers({ 8 | cachedPokemons: cachedPokemonsReducer, 9 | pokemons: pokemonsReducer, 10 | species: speciesReducer, 11 | evolutionChain: evolutionChainReducer, 12 | }); 13 | 14 | const store = configureStore({ 15 | reducer: rootReducer, 16 | }); 17 | 18 | export type RootState = ReturnType; 19 | export default store; 20 | -------------------------------------------------------------------------------- /src/features/types.ts: -------------------------------------------------------------------------------- 1 | export type NamedAPIResource = { 2 | name: string; 3 | url: string; 4 | }; 5 | 6 | export type APIResource = { 7 | url: string; 8 | }; 9 | -------------------------------------------------------------------------------- /src/features/utilities.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "react"; 2 | import { ActionCreatorWithPayload, PayloadAction } from "@reduxjs/toolkit"; 3 | import { SliceStatus } from "../globals"; 4 | import { leftPad } from "../utils/leftPad"; 5 | 6 | export const statusHandlerReducer = { 7 | initialize: (state: any, action: PayloadAction) => { 8 | state.status.state = SliceStatus.LOADING; 9 | }, 10 | error: (state: any, action: PayloadAction) => { 11 | state.status.state = SliceStatus.ERROR; 12 | }, 13 | success: (state: any, action: PayloadAction) => { 14 | state.status.state = SliceStatus.SUCCESS; 15 | }, 16 | }; 17 | 18 | type StatusHandler = { 19 | initialize: ActionCreatorWithPayload; 20 | success: ActionCreatorWithPayload; 21 | error: ActionCreatorWithPayload; 22 | }; 23 | 24 | export type WrapReduxAsyncHandlerType = ( 25 | args?: any 26 | ) => (dispatch: React.Dispatch) => Promise; 27 | 28 | export const wrapReduxAsyncHandler = ( 29 | statusHandler: StatusHandler, 30 | callback: (dispatch: Dispatch, args: any) => Promise 31 | ) => (args?: any) => async (dispatch: Dispatch) => { 32 | dispatch(statusHandler.initialize({})); 33 | 34 | callback(dispatch, args) 35 | .then(() => { 36 | dispatch(statusHandler.success({})); 37 | }) 38 | .catch((err) => { 39 | console.error(err); 40 | }); 41 | }; 42 | 43 | export const transformSpriteToBaseImage = ( 44 | pokemonId: number, 45 | baseUrl: string 46 | ): string => { 47 | return baseUrl + leftPad(pokemonId, 3) + ".png"; 48 | }; 49 | -------------------------------------------------------------------------------- /src/globals.ts: -------------------------------------------------------------------------------- 1 | export enum SliceStatus { 2 | IDLE = "IDLE", 3 | LOADING = "LOADING", 4 | SUCCESS = "SUCCESS", 5 | ERROR = "ERROR", 6 | } 7 | 8 | export enum HTTP_METHODS { 9 | GET = "GET", 10 | POST = "POST", 11 | PUT = "PUT", 12 | DELETE = "DELETE", 13 | PATCH = "PATCH", 14 | } 15 | 16 | export const PokemonTypeColors = { 17 | normal: { 18 | light: "#CDCDB9", 19 | medium: "#C4C4A4", 20 | }, 21 | fire: { 22 | light: "#F4934D", 23 | medium: "#F08030", 24 | }, 25 | fighting: { 26 | light: "#BA5852", 27 | medium: "#C03028", 28 | }, 29 | water: { 30 | light: "#85A5F0", 31 | medium: "#6890F0", 32 | }, 33 | flying: { 34 | light: "#B8A5F2", 35 | medium: "#A890F0", 36 | }, 37 | grass: { 38 | light: "#99D07D", 39 | medium: "#78C850", 40 | }, 41 | poison: { 42 | light: "#A768A7", 43 | medium: "#A040A0", 44 | }, 45 | electric: { 46 | light: "#F9DF78", 47 | medium: "#F8D030", 48 | }, 49 | ground: { 50 | light: "#EDD081", 51 | medium: "#E0C068", 52 | }, 53 | psychic: { 54 | light: "#F47DA1", 55 | medium: "#F85888", 56 | }, 57 | rock: { 58 | light: "#C5B059", 59 | medium: "#B8A038", 60 | }, 61 | ice: { 62 | light: "#B3E1E1", 63 | medium: "#98D8D8", 64 | }, 65 | bug: { 66 | light: "#B5C534", 67 | medium: "#A8B820", 68 | }, 69 | dragon: { 70 | light: "#8656FA", 71 | medium: "#7038F8", 72 | }, 73 | ghost: { 74 | light: "#7D6B9B", 75 | medium: "#705898", 76 | }, 77 | dark: { 78 | light: "#756459", 79 | medium: "#705848", 80 | }, 81 | steel: { 82 | light: "#C1C1D1", 83 | medium: "#B8B8D0", 84 | }, 85 | fairy: { 86 | light: "#EFA7B7", 87 | medium: "#EE99AC", 88 | }, 89 | }; 90 | 91 | export const importImages = (image: string, filetype?: string) => { 92 | return `${process.env.PUBLIC_URL}/assets/images/${image}.${ 93 | filetype || "png" 94 | }`; 95 | }; 96 | 97 | export const importPokemonImage = (image: string) => { 98 | return `${process.env.PUBLIC_URL}/assets/pokemons/${image}.png`; 99 | }; 100 | 101 | export const PokemonTypePlaceholders = { 102 | normal: importPokemonImage("togepi"), 103 | fire: importPokemonImage("charizard"), 104 | fighting: importPokemonImage("lucario"), 105 | water: importPokemonImage("blastoise"), 106 | flying: importPokemonImage("aerodactyl"), 107 | grass: importPokemonImage("venusaur"), 108 | poison: importPokemonImage("seviper"), 109 | electric: importPokemonImage("pikachu"), 110 | ground: importPokemonImage("diglett"), 111 | psychic: importPokemonImage("mew"), 112 | rock: importPokemonImage("onix"), 113 | ice: importPokemonImage("regice"), 114 | bug: importPokemonImage("butterfree"), 115 | dragon: importPokemonImage("dragonite"), 116 | ghost: importPokemonImage("ganger"), 117 | dark: importPokemonImage("weavile"), 118 | steel: importPokemonImage("klinklang"), 119 | fairy: importPokemonImage("clefable"), 120 | }; 121 | -------------------------------------------------------------------------------- /src/hooks/useResize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, MutableRefObject, useCallback } from "react"; 2 | 3 | export const useResize = (myRef: MutableRefObject) => { 4 | const getDimensions = useCallback( 5 | () => 6 | myRef.current 7 | ? { 8 | width: myRef.current.offsetWidth, 9 | height: myRef.current.offsetHeight, 10 | top: myRef.current.offsetTop, 11 | left: myRef.current.offsetLeft, 12 | } 13 | : { 14 | width: 0, 15 | height: 0, 16 | top: 0, 17 | left: 0, 18 | }, 19 | [myRef] 20 | ); 21 | 22 | const [dimensions, setDimensions] = useState({ 23 | width: 0, 24 | height: 0, 25 | left: 0, 26 | top: 0, 27 | }); 28 | 29 | useEffect(() => { 30 | const handleResize = () => { 31 | setDimensions(getDimensions()); 32 | }; 33 | 34 | if (myRef.current) { 35 | setDimensions(getDimensions()); 36 | } 37 | 38 | window.addEventListener("resize", handleResize); 39 | 40 | return () => { 41 | window.removeEventListener("resize", handleResize); 42 | }; 43 | }, [myRef, getDimensions]); 44 | 45 | return dimensions; 46 | }; 47 | -------------------------------------------------------------------------------- /src/hooks/useScrollDirection.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const SCROLL_UP = "up"; 4 | const SCROLL_DOWN = "down"; 5 | 6 | type Directions = "up" | "down"; 7 | 8 | type Props = { 9 | initialDirection?: Directions; 10 | thresholdPixels?: number; 11 | off?: boolean; 12 | }; 13 | 14 | const useScrollDirection = ({ 15 | initialDirection, 16 | thresholdPixels, 17 | off, 18 | }: Props = {}) => { 19 | const [scrollDir, setScrollDir] = useState(initialDirection); 20 | 21 | useEffect(() => { 22 | const threshold = thresholdPixels || 0; 23 | let lastScrollY = window.pageYOffset; 24 | let ticking = false; 25 | 26 | const updateScrollDir = () => { 27 | const scrollY = window.pageYOffset; 28 | 29 | if (Math.abs(scrollY - lastScrollY) < threshold) { 30 | // We haven't exceeded the threshold 31 | ticking = false; 32 | return; 33 | } 34 | 35 | setScrollDir(scrollY > lastScrollY ? SCROLL_DOWN : SCROLL_UP); 36 | lastScrollY = scrollY > 0 ? scrollY : 0; 37 | ticking = false; 38 | }; 39 | 40 | const onScroll = () => { 41 | if (!ticking) { 42 | window.requestAnimationFrame(updateScrollDir); 43 | ticking = true; 44 | } 45 | }; 46 | 47 | /** 48 | * Bind the scroll handler if `off` is set to false. 49 | * If `off` is set to true reset the scroll direction. 50 | */ 51 | !off 52 | ? window.addEventListener("scroll", onScroll) 53 | : setScrollDir(initialDirection); 54 | 55 | return () => window.removeEventListener("scroll", onScroll); 56 | }, [initialDirection, thresholdPixels, off]); 57 | 58 | return scrollDir; 59 | }; 60 | 61 | export default useScrollDirection; 62 | -------------------------------------------------------------------------------- /src/hooks/useTrigger.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | const useTrigger = () => { 4 | const [listener, setListener] = useState(0); 5 | 6 | const trigger = useCallback(() => setListener((l) => l + 1), []); 7 | 8 | return { listener, trigger }; 9 | }; 10 | 11 | export default useTrigger; 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | @font-face { 5 | font-family: "Montserrat"; 6 | font-weight: 400; 7 | src: url(/assets/fonts/montserrat/Montserrat-Regular.ttf); 8 | } 9 | 10 | @font-face { 11 | font-family: "Montserrat"; 12 | font-weight: 500; 13 | src: url(/assets/fonts/montserrat/Montserrat-Medium.ttf); 14 | } 15 | 16 | @font-face { 17 | font-family: "Montserrat"; 18 | font-weight: 600; 19 | src: url(/assets/fonts/montserrat/Montserrat-SemiBold.ttf); 20 | } 21 | 22 | @font-face { 23 | font-family: "Montserrat"; 24 | font-weight: 700; 25 | src: url(/assets/fonts/montserrat/Montserrat-Bold.ttf); 26 | } 27 | 28 | @font-face { 29 | font-family: "Montserrat"; 30 | font-weight: 800; 31 | src: url(/assets/fonts/montserrat/Montserrat-ExtraBold.ttf); 32 | } 33 | 34 | @font-face { 35 | font-family: "Montserrat"; 36 | font-weight: 900; 37 | src: url(/assets/fonts/montserrat/Montserrat-Black.ttf); 38 | } 39 | 40 | @tailwind utilities; 41 | 42 | html { 43 | margin: 0; 44 | padding: 0; 45 | } 46 | 47 | body { 48 | background-color: #f2f1f0; 49 | margin: 0; 50 | padding: 0; 51 | } 52 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import { HelmetProvider, Helmet } from "react-helmet-async"; 7 | import { Provider } from "react-redux"; 8 | import store from "./features/store"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | React Pokédex 16 | 20 | 21 | 22 | 23 | 24 | 25 | , 26 | document.getElementById("root") 27 | ); 28 | 29 | // If you want your app to work offline and load faster, you can change 30 | // unregister() to register() below. Note this comes with some pitfalls. 31 | // Learn more about service workers: https://bit.ly/CRA-PWA 32 | serviceWorker.unregister(); 33 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/PokemonDetailsPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { useHistory } from "react-router-dom"; 4 | import { RouteComponentProps } from "react-router"; 5 | import Layout from "../components/Layout"; 6 | import PokemonDetailsBiography from "../components/PokemonDetailsBiography"; 7 | import PokemonDetailsEvolutions from "../components/PokemonDetailsEvolutions"; 8 | import PokemonDetailsHeader from "../components/PokemonDetailsHeader"; 9 | import PokemonDetailsStats from "../components/PokemonDetailsStats"; 10 | import Tab from "../components/Tab"; 11 | import { 12 | getPokemonById, 13 | getPokemonsDynamically, 14 | pokemonsSelector, 15 | } from "../features/pokemonSlice"; 16 | import { getSpeciesById, speciesSelector } from "../features/speciesSlice"; 17 | import { PokemonTypeColors, SliceStatus } from "../globals"; 18 | import { ScaleLoader } from "react-spinners"; 19 | import { useTransition, animated } from "react-spring"; 20 | import { capitalize } from "../utils/capitalize"; 21 | import { 22 | ChainLink, 23 | evolutionChainSelector, 24 | getEvolutionChainById, 25 | } from "../features/evolutionChainSlice"; 26 | 27 | type PokemonTabs = "biography" | "stats" | "evolutions"; 28 | 29 | interface MatchParams { 30 | id: string; 31 | } 32 | 33 | const PokemonDetailsPage = ({ match }: RouteComponentProps) => { 34 | const { id } = match.params; 35 | const dispatch = useDispatch(); 36 | const history = useHistory(); 37 | const [activeTab, setActiveTab] = useState("biography"); 38 | const transitions = useTransition(activeTab, (p) => p, { 39 | from: { opacity: 0 }, 40 | enter: { opacity: 1 }, 41 | leave: { opacity: 0 }, 42 | config: { 43 | duration: 250, 44 | }, 45 | }); 46 | 47 | const pokemons = useSelector(pokemonsSelector); 48 | const species = useSelector(speciesSelector); 49 | const evolutionChain = useSelector(evolutionChainSelector); 50 | const [chainLinks, setChainLinks] = useState([]); 51 | const [ 52 | selectedEvolutionPokemonIds, 53 | setSelectedEvolutionPokemonIds, 54 | ] = useState([]); 55 | 56 | const selectedPokemon = pokemons.data.find( 57 | (pokemon) => pokemon !== null && pokemon.id === Number(id) 58 | ); 59 | const selectedSpecies = species.data.find((s) => s.id === Number(id)); 60 | const evolutionChainId = 61 | selectedSpecies?.evolutionChain?.url.split("/").slice(-2)[0] || null; 62 | const selectedEvolutionChain = 63 | evolutionChainId !== null 64 | ? evolutionChain.data.find((e) => e.id === Number(evolutionChainId)) 65 | : null; 66 | 67 | const getPokemonEvolution = useCallback( 68 | (chain: ChainLink | null): ChainLink[] => { 69 | if (!chain) { 70 | return []; 71 | } else { 72 | return [chain].concat(getPokemonEvolution(chain.evolvesTo[0])); 73 | } 74 | }, 75 | [] 76 | ); 77 | 78 | useEffect(() => { 79 | if (selectedEvolutionChain?.chain) { 80 | const pokemons: ChainLink[] = getPokemonEvolution( 81 | selectedEvolutionChain.chain 82 | ); 83 | const pokemonIds = pokemons.map(({ species }) => 84 | Number(species.url.split("/").slice(-2)[0]) 85 | ); 86 | dispatch(getPokemonsDynamically({ pokemonIds })); 87 | setSelectedEvolutionPokemonIds(pokemonIds); 88 | setChainLinks(pokemons); 89 | } 90 | //eslint-disable-next-line 91 | }, [selectedEvolutionChain]); 92 | 93 | useEffect(() => { 94 | if (pokemons.data.length === 0) { 95 | dispatch(getPokemonById({ pokemonId: id })); 96 | } 97 | dispatch(getSpeciesById({ pokemonId: id })); 98 | //eslint-disable-next-line 99 | }, [id, pokemons.data.length]); 100 | 101 | useEffect(() => { 102 | if (evolutionChainId) { 103 | dispatch(getEvolutionChainById({ id: Number(evolutionChainId) })); 104 | } 105 | //eslint-disable-next-line 106 | }, [selectedPokemon, evolutionChainId]); 107 | 108 | const backgroundColors = selectedPokemon?.types.map(({ type }) => { 109 | const [[, backgroundColor]] = Object.entries(PokemonTypeColors).filter( 110 | ([key, _]) => key === type.name 111 | ); 112 | 113 | return backgroundColor; 114 | }); 115 | 116 | const selectedBackgroundColor = backgroundColors && backgroundColors[0]; 117 | 118 | const isPageLoading = 119 | species.status.state === SliceStatus.IDLE || 120 | species.status.state === SliceStatus.LOADING || 121 | evolutionChain.status.state === SliceStatus.IDLE || 122 | evolutionChain.status.state === SliceStatus.LOADING || 123 | pokemons.status.state === SliceStatus.IDLE || 124 | pokemons.status.state === SliceStatus.LOADING; 125 | 126 | return ( 127 | 128 | {isPageLoading ? ( 129 |
    130 | 131 |
    132 | ) : ( 133 | <> 134 | <> 135 | {selectedPokemon && 136 | selectedSpecies && 137 | selectedBackgroundColor && 138 | selectedEvolutionChain && ( 139 |
    140 | 146 |
    154 | 159 |
    160 |
    161 | setActiveTab("biography")} 163 | isSelected={activeTab === "biography"} 164 | > 165 | Biography 166 | 167 | setActiveTab("stats")} 169 | isSelected={activeTab === "stats"} 170 | > 171 | Stats 172 | 173 | setActiveTab("evolutions")} 175 | isSelected={activeTab === "evolutions"} 176 | > 177 | Evolutions 178 | 179 |
    180 |
    181 | {transitions.map(({ item, key, props }) => { 182 | let page: JSX.Element = ( 183 | 187 | ); 188 | 189 | switch (item) { 190 | case "biography": 191 | page = ( 192 | 196 | ); 197 | break; 198 | case "stats": 199 | page = ( 200 | 203 | ); 204 | break; 205 | case "evolutions": 206 | page = ( 207 | 214 | ); 215 | break; 216 | default: 217 | break; 218 | } 219 | return ( 220 | 229 | {page} 230 | 231 | ); 232 | })} 233 |
    234 |
    235 |
    236 |
    237 | )} 238 | 239 | 240 | )} 241 |
    242 | ); 243 | }; 244 | export default PokemonDetailsPage; 245 | -------------------------------------------------------------------------------- /src/pages/PokemonsPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PokemonForm from "../components/PokemonForm"; 3 | 4 | import Layout from "../components/Layout"; 5 | import InfiniteScroll from "../components/InfiniteScroll"; 6 | import PokemonCard from "../components/PokemonCard"; 7 | import { useSelector } from "react-redux"; 8 | import { pokemonsSelector, getPokemons } from "../features/pokemonSlice"; 9 | import { SliceStatus } from "../globals"; 10 | import { cachedPokemonsSelector } from "../features/cachedPokemonsSlice"; 11 | import PokemonSkeleton from "../components/PokemonSkeleton"; 12 | import { AiFillGithub } from "react-icons/ai"; 13 | 14 | const PokemonsPage = () => { 15 | const pokemons = useSelector(pokemonsSelector); 16 | const cachedPokemons = useSelector(cachedPokemonsSelector); 17 | 18 | return ( 19 | 20 |
    21 |

    22 | React Pokédex 23 |

    24 | 30 | 31 | 32 |
    33 | 34 | 37 | getPokemons({ 38 | page, 39 | cachedPokemons: cachedPokemons.data, 40 | pokemons: pokemons.data, 41 | }) 42 | } 43 | isLoading={pokemons.status.state === SliceStatus.LOADING} 44 | > 45 | {({ mutatePage }) => ( 46 | <> 47 |
    48 | 52 |
    53 |
    54 | {!( 55 | cachedPokemons.status.state === SliceStatus.LOADING || 56 | cachedPokemons.status.state === SliceStatus.IDLE 57 | ) && ( 58 | <> 59 | 60 | {pokemons.data.map((pokemon, index) => 61 | pokemon === null ? ( 62 | 63 | ) : ( 64 | 65 | ) 66 | )} 67 | 68 | 69 | 70 | )} 71 |
    72 | 73 | )} 74 |
    75 |
    76 | ); 77 | }; 78 | export default PokemonsPage; 79 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/svg/pokeball.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/camelcaseObject.test.ts: -------------------------------------------------------------------------------- 1 | import { camelcaseObject } from "./camelcaseObject"; 2 | 3 | describe("camelcaseObject", () => { 4 | it("returns an empty object when received an empty object", () => { 5 | let object = camelcaseObject({}); 6 | 7 | expect(object).toEqual({}); 8 | }); 9 | 10 | it("returns a camelcase object when the object has a snake case attribute name", () => { 11 | let object = camelcaseObject({ first_name: "Jonh" }); 12 | 13 | expect(object).toEqual({ firstName: "Jonh" }); 14 | }); 15 | 16 | it("returns a camelcase object array when receive an array with a snake case object", () => { 17 | let array = camelcaseObject([ 18 | { first_name: "Jonh" }, 19 | { firstName: "Peter" }, 20 | { "first-name": "Jaz" }, 21 | ]); 22 | 23 | expect(array).toEqual([ 24 | { firstName: "Jonh" }, 25 | { firstName: "Peter" }, 26 | { firstName: "Jaz" }, 27 | ]); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/utils/camelcaseObject.ts: -------------------------------------------------------------------------------- 1 | import humps from "lodash-humps-ts"; 2 | 3 | export const camelcaseObject = (object: object) => humps(object); 4 | -------------------------------------------------------------------------------- /src/utils/capitalize.test.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from "./capitalize"; 2 | 3 | describe("capitalize", () => { 4 | it("returns an empty string when receives undefined", () => { 5 | let string = capitalize(undefined); 6 | 7 | expect(string).toEqual(""); 8 | }); 9 | 10 | it("returns an empty string when receives an empty string", () => { 11 | let string = capitalize(""); 12 | 13 | expect(string).toEqual(""); 14 | }); 15 | 16 | it("returns a Hello when receives a Hello", () => { 17 | let string = capitalize("Hello"); 18 | 19 | expect(string).toEqual("Hello"); 20 | }); 21 | 22 | it("returns a 'See you tomorrow' when receives 'see you tomorrow'", () => { 23 | let string = capitalize("see you tomorrow"); 24 | 25 | expect(string).toEqual("See you tomorrow"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/utils/capitalize.ts: -------------------------------------------------------------------------------- 1 | export const capitalize = (str: string | undefined): string => { 2 | if (!str) return ""; 3 | 4 | return str.charAt(0).toUpperCase() + str.slice(1); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/leftPad.test.ts: -------------------------------------------------------------------------------- 1 | import { leftPad } from "./leftPad"; 2 | 3 | describe("leftPad", () => { 4 | it("returns '001' when receives '1, -3'", () => { 5 | let output = leftPad(1, -3); 6 | 7 | expect(output).toEqual("001"); 8 | }); 9 | 10 | it("returns '001' when receives '-1, 3'", () => { 11 | let output = leftPad(-1, 3); 12 | 13 | expect(output).toEqual("001"); 14 | }); 15 | 16 | it("returns '0' when receives '0, 1'", () => { 17 | let output = leftPad(0, 1); 18 | 19 | expect(output).toEqual("0"); 20 | }); 21 | 22 | it("returns '00' when receives '0, 2'", () => { 23 | let output = leftPad(0, 2); 24 | 25 | expect(output).toEqual("00"); 26 | }); 27 | 28 | it("adds four 0's to the left when receives a 5 as length", () => { 29 | let output = leftPad(1, 5); 30 | 31 | expect(output).toEqual("00001"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/utils/leftPad.ts: -------------------------------------------------------------------------------- 1 | export const leftPad = (number: number, targetLength: number): string => { 2 | let output = Math.abs(number).toString(); 3 | while (output.length < Math.abs(targetLength)) { 4 | output = "0" + output; 5 | } 6 | return output; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/randomize.test.ts: -------------------------------------------------------------------------------- 1 | import { randomize } from "./randomize"; 2 | 3 | beforeEach(() => { 4 | jest.spyOn(global.Math, "random").mockReturnValue(0.123456789); 5 | }); 6 | 7 | afterEach(() => { 8 | jest.spyOn(global.Math, "random").mockReset(); 9 | }); 10 | 11 | describe("randomize", () => { 12 | it("returns 1 when receives 1 as minimum and 2 as maximum", () => { 13 | let number = randomize(1, 2); 14 | 15 | expect(number).toEqual(1); 16 | }); 17 | 18 | it("returns 21 when receives 20 as minimum and 30 as maximum", () => { 19 | let number = randomize(20, 30); 20 | 21 | expect(number).toEqual(21); 22 | }); 23 | 24 | it("returns 31 when receives 30 as minimum and 40 as maximum", () => { 25 | let number = randomize(30, 40); 26 | 27 | expect(number).toEqual(31); 28 | }); 29 | 30 | it("returns 91 when receives 90 as minimum and 100 as maximum", () => { 31 | let number = randomize(90, 100); 32 | 33 | expect(number).toEqual(91); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/utils/randomize.ts: -------------------------------------------------------------------------------- 1 | export const randomize = (minimum: number, maximum: number): number => 2 | Math.floor(Math.random() * (maximum - minimum + 1)) + minimum; 3 | -------------------------------------------------------------------------------- /src/utils/romanize.test.ts: -------------------------------------------------------------------------------- 1 | import { romanize } from "./romanize"; 2 | 3 | describe("romanize", () => { 4 | test.each([ 5 | [0, ""], 6 | [1, "I"], 7 | [5, "V"], 8 | [10, "X"], 9 | [15, "XV"], 10 | [100, "C"], 11 | [500, "D"], 12 | [900, "CM"], 13 | ])("returns %b when receives %a", (a, b) => { 14 | let numeral = romanize(a); 15 | 16 | expect(numeral).toEqual(b); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/utils/romanize.ts: -------------------------------------------------------------------------------- 1 | const numeralCodes = [ 2 | ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"], // Ones 3 | ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"], // Tens 4 | ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"], // Hundreds 5 | ]; 6 | 7 | export const romanize = (num: number): string => { 8 | let numeral = ""; 9 | const digits = num.toString().split("").reverse(); 10 | for (let i = 0; i < digits.length; i++) { 11 | numeral = numeralCodes[i][parseInt(digits[i])] + numeral; 12 | } 13 | return numeral; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/shuffle.ts: -------------------------------------------------------------------------------- 1 | export const shuffle = (array: any[]): any[] => { 2 | let counter = array.length; 3 | 4 | // While there are elements in the array 5 | while (counter > 0) { 6 | // Pick a random index 7 | let index = Math.floor(Math.random() * counter); 8 | 9 | // Decrease counter by 1 10 | counter--; 11 | 12 | // And swap the last element with it 13 | let temp = array[counter]; 14 | array[counter] = array[index]; 15 | array[index] = temp; 16 | } 17 | 18 | return array; 19 | }; 20 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.{js,ts,jsx,tsx}"], 3 | theme: { 4 | minHeight: { 5 | 0: "0", 6 | "1/4": "25%", 7 | "1/2": "50%", 8 | "3/4": "75%", 9 | full: "100%", 10 | 72: "18rem", 11 | 84: "21rem", 12 | 96: "24rem", 13 | 108: "27rem", 14 | 120: "30rem", 15 | 132: "33rem", 16 | 154: "36rem", 17 | }, 18 | extend: { 19 | fontFamily: { 20 | sans: ["Montserrat", "sans-serif"], 21 | }, 22 | letterSpacing: { 23 | xl: "0.3em", 24 | "2xl": "0.6em", 25 | "3xl": "0.9em", 26 | "4xl": "1.2em", 27 | }, 28 | inset: { 29 | "1/8": "12.5%", 30 | "1/5": "20%", 31 | "1/4": "25%", 32 | "1/2": "50%", 33 | "3/4": "75%", 34 | }, 35 | spacing: { 36 | 72: "18rem", 37 | 84: "21rem", 38 | 96: "24rem", 39 | 108: "27rem", 40 | 120: "30rem", 41 | 132: "33rem", 42 | 154: "36rem", 43 | 166: "39rem", 44 | 178: "42rem", 45 | }, 46 | colors: { 47 | primary: "#E3350D", 48 | primarySecondary: "#EA5D60", 49 | primaryGray: "#DEDEDE", 50 | secondaryGray: "#ACAAAA", 51 | tertiaryGray: "#8A8A8A", 52 | darkGray: "#8C8C8C", 53 | darkerGray: "#6D6D6D", 54 | }, 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "experimentalDecorators": true 18 | }, 19 | "include": ["src"] 20 | } 21 | --------------------------------------------------------------------------------