├── .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 | 
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 | You need to enable JavaScript to run this app.
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 |
14 |
17 | Load More
18 |
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 | {
23 | if (!disabled) {
24 | setShowModal(true);
25 | }
26 | }}
27 | className={className}
28 | >
29 | {children}
30 |
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 | setShowModal(false)}
63 | >
64 | Close
65 |
66 | {
71 | if (handleSaveModal) {
72 | handleSaveModal();
73 | }
74 |
75 | setShowModal(false);
76 | }}
77 | >
78 | Change
79 |
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 |
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 |
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 |
73 |
{100 - genderPercentage}%
74 |
75 |
76 |
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 |
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 |
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 |
103 |
104 |
113 | Search
114 |
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 |
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 |
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 |
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 |
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 |
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 |
18 | {children}
19 |
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 |
8 |
11 |
14 | pokeball.svg
15 |
16 |
17 |
20 | Load More
21 |
22 |
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 |
history.push("/")}
143 | >
144 | Go Back
145 |
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 |
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 |
--------------------------------------------------------------------------------