DEV Community

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

Posted 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

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

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

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 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

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

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;
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

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

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.

Top comments (0)