DEV Community

Cover image for Build a Pokédex with React and PokéAPI 🔍
Axel
Axel

Posted on • Edited on

Build a Pokédex with React and PokéAPI 🔍

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.

PokemonFinder main page

Prerequisites

Before getting started, make sure you have the following prerequisites:

  • Basic knowledge of React and JavaScript
  • Node.js and npm installed on your machine

This project is ideal for developing your React skills, learning to use an API, and experimenting with Material-UI components.

Project Setup and Installation

Initialize the Project

First, create a new React application using Create React App. This will create a basic skeleton for our React application:

npx create-react-app pokemon-finder
cd pokemon-finder
Enter fullscreen mode Exit fullscreen mode

This command creates a new folder containing all the necessary files for a React application. Once installed, navigate into the folder to get started.

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
Enter fullscreen mode Exit fullscreen mode
  • Axios: This library is essential for interacting with the PokéAPI.
  • @mui/material: Provides pre-built UI components that help us quickly create a modern and functional interface.
  • Framer Motion: To add smooth animations to your components, enhancing the user experience.

Adding Custom Fonts

To give the application a unique look, we can use custom fonts. Here's how to add fonts to your project:

  • Create a folder named fonts inside the src/assets directory.
  • Place your font files inside this folder.
  • Create a CSS file named fonts.css inside the src/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;
}
Enter fullscreen mode Exit fullscreen mode
  • Import the fonts.css file in your App.js file:
import './assets/fonts/fonts.css';
Enter fullscreen mode Exit fullscreen mode

This allows you to use your custom fonts throughout the application.

Creating the Components

To structure our application, we will create several React components. This will make code management easier and allow us to reuse elements.

Create a Components Folder

Before starting to build components, create a folder named components in the src directory. We will place all our component files in this folder.

Header

The Header component is a simple navigation bar at the top of the application. Here is the code for the Header:

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;
Enter fullscreen mode Exit fullscreen mode

This component uses Material-UI to create a clean and responsive app bar. It includes a logo and the name of the application.

PokemonCard

The PokemonCard component is used to display detailed information about a 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

This component allows the user to initiate a search by clicking a button or pressing the Enter key.

Main application file

The App component is the heart of the application, managing state and handling API requests. Here is the full 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;
Enter fullscreen mode Exit fullscreen mode

This component also manages the "shiny" state to allow the user to view the shiny version of the Pokémon, which adds a fun touch to the application.

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.

I hope this guide was helpful and that you had fun while improving your React skills! If you have any questions or suggestions, feel free to leave them in the comments.

Top comments (1)

Collapse
 
karlmantle profile image
Karl Mantle

thanks for posting this, got me interested in and helped me create my first React app. I've learnt a tonne from it since!