I recently leveled up my React skills by building a Pokédex! This was such a fun project that I wanted to share the process with you all. The app allows users to search for Pokémon by name or ID, fetching detailed information from the PokéAPI, including their type, abilities, and game appearances.
You can check out the live version of the Pokédex app here.
Prerequisites
- Basic knowledge of React and JavaScript
- Node.js and npm installed on your machine
Project Setup and Installation
Initialize the Project
First, create a new React application using Create React App:
npx create-react-app pokemon-finder
cd pokemon-finder
Install Dependencies
Next, install the necessary libraries, including axios for making HTTP requests and @mui/material for UI components:
npm install axios @mui/material @emotion/react @emotion/styled framer-motion
Adding Custom Fonts
If you want to add custom fonts to your project, you can include them in your project directory and import them into your CSS. Here are the steps to follow:
- Create a folder named fonts inside the
src/assets
directory. - Place your font files inside the fonts folder.
- Create a CSS file named
fonts.css
inside thesrc/assets
directory and import your fonts like this:
@font-face {
font-family: 'GeneralSans';
src: url('./fonts/GeneralSans-Regular.woff2') format('woff2'),
url('./fonts/GeneralSans-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'PokemonPixel';
src: url('./fonts/PokemonPixel.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
- Import the fonts.css file in your App.js file:
import './assets/fonts/fonts.css';
Creating the Components
Before diving into the components, create a new folder named components in the src directory. We will place all our component files in this folder.
Header
The Header component provides a simple top navigation bar for the app:
import React, { Component } from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Icon from '../assets/images/logo.png';
import '../assets/fonts/fonts.css';
class Header extends Component {
render() {
return (
<AppBar position="static" sx={{ backgroundColor: '#ef233c' }}>
<Toolbar>
<Box sx={{ display: 'flex', alignItems: 'center', flexGrow: 1 }}>
<img src={Icon} alt="logo" style={{ marginRight: 10, width: 40, height: 40 }} />
<Typography variant="h6" component="div" sx={{fontFamily: 'GeneralSans', fontSize: '1.5rem'}}>
POKEMON FINDER
</Typography>
</Box>
</Toolbar>
</AppBar>
);
}
}
export default Header;
PokemonCard
The PokemonCard component displays detailed information about the Pokémon, including its name, type, abilities, and generation:
import React from 'react';
import { Card, CardContent, Typography, CardMedia, Divider, Stack, CircularProgress } from '@mui/material';
import '../assets/fonts/fonts.css';
import { motion, AnimatePresence } from 'framer-motion';
const variants = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 50, damping: 10 } },
exit: { opacity: 0, y: -20, transition: { duration: 0.3 } },
};
const cardStyles = {
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
margin: '20px auto',
width: '90%',
minWidth: 300,
maxWidth: 600,
overflow: 'hidden',
backgroundColor: '#f6f6f6',
boxShadow: '0 0 10px 0 rgba(0,0,0,0.2)',
transition: 'transform 0.3s, box-shadow 0.3s',
'&:hover': {
transform: 'scale(1.02)',
boxShadow: '0 0 20px 0 rgba(0,0,0,0.3)',
},
};
const contentStyles = {
flex: '1',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: 2,
fontFamily: 'Arial, PokemonPixel',
};
const PokemonCard = ({ pokemon, loading, isShiny }) => {
if (loading) {
return (
<AnimatePresence>
<motion.div
key="loading"
variants={variants}
initial="initial"
animate="animate"
exit="exit"
>
<Card sx={cardStyles}>
<CardContent sx={contentStyles}>
<CircularProgress />
</CardContent>
</Card>
</motion.div>
</AnimatePresence>
);
}
if (!pokemon) return null;
const types = pokemon.types.map(typeInfo => typeInfo.type.name).join(', ');
const abilities = pokemon.abilities.map(abilityInfo => abilityInfo.ability.name).join(', ');
const { generation, description, id } = pokemon;
const imageUrl = isShiny ? pokemon.sprites.front_shiny : pokemon.sprites.front_default;
return (
<AnimatePresence>
<motion.div
key={id}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
>
<Card sx={cardStyles}>
<CardMedia
component="img"
sx={{
width: { xs: '100%', sm: 170 },
height: { xs: 170, sm: 'auto' },
objectFit: 'contain',
}}
image={imageUrl}
alt={pokemon.name}
/>
<CardContent sx={contentStyles}>
<Stack spacing={2} alignItems="left">
<Typography gutterBottom variant="h5" component="div" sx={{ fontFamily: 'PokemonPixel', textAlign: 'left', fontSize: '2rem' }}>
{pokemon.name} (#{id})
</Typography>
<Divider variant="middle" sx={{ bgcolor: '#ef233c', width: '100%' }} />
<Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
<b>Description:</b> {description}
</Typography>
<Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
<b>Type:</b> <span style={{ color: '#4A90E2' }}>{types}</span>
</Typography>
<Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
<b>Generation:</b> {generation.replace('generation-', '').toUpperCase()}
</Typography>
<Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
<b>Abilities:</b> {abilities}
</Typography>
</Stack>
</CardContent>
</Card>
</motion.div>
</AnimatePresence>
);
};
export default PokemonCard;
SearchBar
The SearchBar component provides the input field and search button for the user to enter the Pokémon name or ID:
import React from 'react';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
const SearchBar = ({ onSearch }) => {
return (
<form onSubmit={onSearch} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', margin: '20px' }}>
<TextField
name="pokemonName"
label="Enter a Pokémon name or ID"
variant="outlined"
fullWidth
style={{ maxWidth: '500px' }}
/>
<Button type="submit" variant="contained" color="primary">
Search
</Button>
</form>
);
};
export default SearchBar;
Main application file
The App component is the heart of the application, managing the state and handling API requests. Here’s the code for the App component:
import React, { useState } from 'react';
import axios from 'axios';
import { CircularProgress, FormControlLabel, Switch, Box } from '@mui/material';
import SearchBar from './components/SearchBar';
import PokemonCard from './components/PokemonCard';
import Header from './components/Header';
import './assets/fonts/fonts.css';
import './App.css';
function App() {
const [pokemonData, setPokemonData] = useState(null);
const [loading, setLoading] = useState(false);
const [isShiny, setIsShiny] = useState(false);
const cleanDescription = (description) => description.replace(/\f/g, ' ');
const fetchPokemonData = async (pokemonNameOrId) => {
setPokemonData(null);
setLoading(true);
try {
const sanitizedInput = pokemonNameOrId.toLowerCase().replace(/^0+/, '');
const baseResponse = await axios.get(`https://pokeapi.co/api/v2/pokemon/${sanitizedInput}`);
const speciesResponse = await axios.get(baseResponse.data.species.url);
const generation = speciesResponse.data.generation.name;
const flavorTextEntries = speciesResponse.data.flavor_text_entries.filter(entry => entry.language.name === 'en');
let description = flavorTextEntries.length > 0 ? flavorTextEntries[0].flavor_text : 'No description available.';
description = cleanDescription(description);
setPokemonData({
...baseResponse.data,
generation,
description
});
} catch (error) {
window.alert('Pokémon not found. Please try a different name or ID.');
} finally {
setLoading(false);
}
};
return (
<div className="App">
<Header />
<SearchBar onSearch={(e) => {
e.preventDefault();
const pokemonName = e.target.elements.pokemonName.value.trim();
if (pokemonName) fetchPokemonData(pokemonName);
}} />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
<FormControlLabel
control={
<Switch checked={isShiny} onChange={(e) => setIsShiny(e.target.checked)} color="primary" />
}
label="Show Shiny"
/>
</Box>
{loading && (
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '20%' }}>
<CircularProgress />
</div>
)}
{pokemonData && <PokemonCard pokemon={pokemonData} isShiny={isShiny} />}
</div>
);
}
export default App;
Conclusion
Congratulations, you have successfully built a Pokédex using React and the PokéAPI! You can now search for any Pokémon by name or ID and view detailed information about them.
Feel free to explore and add more features to your app, such as displaying Pokémon stats or comparing multiple Pokémon. For more details and to contribute to this project, check out the Pokemon Finder repository on GitHub.