Jul 19, 2024

PokeNext.js - A Simple Pokédex Page

PokeNext.js - A Simple Pokédex Page

A next.js web application that leverages the RESTful Pokémon API, efficient data fetching & caching, serving as a Pokédex page.

  • Jorge Perez Avatar
    Jorge Perez
    14 min read
  • What is PokeNext.js?

    PokeNext.js is a web application built using Next.js, TypeScript & Tailwind CSS that serves as a Pokédex. This project integrates with the RESTful Pokémon API PokéAPI to fetch and display detailed information about the first 151 Pokémon, including their sprites, stats, types, abilities, evolution chains and even playing the Pokémon’s cry sound.

    The PokeAPI documentation page states that you need to "Locally cache resources whenever you request them". So I wanted to learn and focus on Next.js capabilities in handling server-side rendering, static site generation, and client-side rendering, along with effective data fetching and caching strategies.

    Live Demo:

    You can see it up and running here: https://poke-nextjs-opal.vercel.app/

    Clone, Deploy & GitHub Repo:

    First, execute create-next-app with npm to bootstrap the example:

    npm create next-app --example https://github.com/JPerez00/PokeNext.js/tree/main your-project-name-here
    

    Then run the development server:

    npm run dev

    Or just go to the project repo on Github and download it there, also don't forget to star! 😁

    https://github.com/JPerez00/PokeNext.js
    

    Main Features:

    • Responsive design (desktop & mobile).
    • Search and sort functionality.
    • Full Pokemon cards & detail page for the first 151 Pokémon.
    • Interactive features: Hover a button to see the shiny sprite and playing the Pokémon’s cry sound.
    • Optimized image handling (loading gifs).
    • Dark mode support, with toggle (Next Themes).
    • Efficient data fetching & local caching with SWR and axios, as requested in the PokéAPI doc page, more info below.

    Documentation Used:

    Structure Of The Project:

    Here's a map of what the final project looks like. Libs are mostly constants, like pokemon names or the colour of the weaknesses. Except for usePokemonData.ts, more on that later.

    poke-nextjs/
    ├── app/
       ├── components/
          ├── BackToTop.tsx
          ├── Footer.tsx
          ├── Navbar.tsx
          ├── PokemonCard.tsx
          ├── SearchBar.tsx
          ├── Dropdown.tsx
          └── Icons.tsx
       ├── pokemon/
          ├── [name]/
             ├── page.tsx
             └── metadata.ts
       ├── api/
          ├── pokemon/
             └── [name]/
                 └── route.ts
       ├── globals.css
       ├── layout.tsx
       └── page.tsx
    ├── lib/
       ├── typeColors.ts
       ├── pokemonNames.ts
       ├── typeWeaknesses.ts
       └── usePokemonData.ts
    ├── public/
       ├── pikachu-sprint-animation.gif
       └── images/
           └── twitter-card.png
    ├── .gitignore
    ├── package.json
    ├── tsconfig.json
    └── README.md

    Main Implementation Steps:

    I'll be focusing on the main important pages & component files, I'll assume that you already have, or know how to create a Navbar, Footer, BackToTop, Dropdown, etc.

    1. Create the project.

    npx create-next-app@latest pokedex --ts
    cd pokedex
    npm install axios

    2. Building the Home Page:

    Create the home page to display a list of Pokémon using static generation, with search and pagination functionality using Axios. This page will showcase the pokemon card component created in step #2.

    'use client';
    
    import { useState, useEffect } from 'react';
    import axios from 'axios';
    import PokemonCard from './components/PokemonCard';
    import SearchBar from './components/SearchBar';
    import Dropdown from './components/Dropdown';
    import Link from 'next/link';
    import heroImage from '@/public/PokeNextjs.png';
    import Image from 'next/image';
    import loadingGif from '@/public/pikachu-sprint-animation.gif';
    
    interface Pokemon {
      name: string;
      url: string;
      id: number;
      image: string;
      types: string[];
      abilities: string[];
    }
    
    export default function HomePage() {
      const [pokemonList, setPokemonList] = useState<Pokemon[]>([]);
      const [searchQuery, setSearchQuery] = useState('');
      const [visiblePokemonCount, setVisiblePokemonCount] = useState(16);
      const [sortOrder, setSortOrder] = useState('Lowest Number (First)');
      const [loading, setLoading] = useState(true); // State for loading status
    
      useEffect(() => {
        const fetchPokemon = async () => {
          try {
            const response = await axios.get('https://pokeapi.co/api/v2/pokemon?limit=151');
            const pokemonData = await Promise.all(response.data.results.map(async (pokemon: any) => {
              const pokemonDetails = await axios.get(pokemon.url);
              return {
                name: pokemonDetails.data.name,
                url: pokemon.url,
                id: pokemonDetails.data.id,
                image: pokemonDetails.data.sprites.other['official-artwork'].front_default,
                types: pokemonDetails.data.types.map((typeInfo: any) => typeInfo.type.name),
                abilities: pokemonDetails.data.abilities.map((abilityInfo: any) => abilityInfo.ability.name),
              };
            }));
            setPokemonList(pokemonData);
          } catch (error) {
            console.error('Error fetching Pokémon data:', error);
          } finally {
            setLoading(false); // Set loading to false after data is fetched
          }
        };
        fetchPokemon();
      }, []);
    
      // Handle the sorting.
      const handleSort = (list: Pokemon[], order: string) => {
        switch (order) {
          case 'Lowest Number (First)':
            return list.sort((a, b) => a.id - b.id);
          case 'Highest Number (First)':
            return list.sort((a, b) => b.id - a.id);
          case 'A-Z':
            return list.sort((a, b) => a.name.localeCompare(b.name));
          case 'Z-A':
            return list.sort((a, b) => b.name.localeCompare(a.name));
          default:
            return list;
        }
      };
    
      const sortedPokemon = handleSort([...pokemonList], sortOrder);
    
      const filteredPokemon = sortedPokemon.filter(pokemon =>
        pokemon.name.toLowerCase().includes(searchQuery.toLowerCase())
      );
    
      const handleLoadMore = () => {
        setVisiblePokemonCount(prevCount => Math.min(prevCount + 16, filteredPokemon.length));
      };
    
      return (
        <div className="pb-16 pt-20 text-center lg:pt-24 px-2">
          <div className='mt-4 flex gap-2 flex-row align-center items-center justify-center'>
            <div className="shadow inline-flex rounded-full px-3 py-1 text-sm leading-6 text-zinc-500 dark:text-zinc-400 hover:ring-gray-900/20 bg-white dark:bg-zinc-800 ring-1 ring-zinc-900/10 backdrop-blur dark:ring-white/20 dark:hover:ring-white/30">
              Next.js
            </div>
            <div className="shadow inline-flex rounded-full px-3 py-1 text-sm leading-6 text-zinc-500 dark:text-zinc-400 hover:ring-gray-900/20 bg-white dark:bg-zinc-800 ring-1 ring-zinc-900/10 backdrop-blur dark:ring-white/20 dark:hover:ring-white/30">
              Typescript
            </div>
            <div className="shadow inline-flex rounded-full px-3 py-1 text-sm leading-6 text-zinc-500 dark:text-zinc-400 hover:ring-gray-900/20 bg-white dark:bg-zinc-800 ring-1 ring-zinc-900/10 backdrop-blur dark:ring-white/20 dark:hover:ring-white/30">
              Tailwind CSS
            </div>
          </div>
          <div className="flex justify-center items-center mx-auto max-w-2xl">
            <Image 
              src={heroImage} 
              alt='Hero Image'
              width={560}
              height={180} 
              className="mt-6 drop-shadow-lg" 
              priority
              style={{ width: 'auto', height: 'auto' }}
            />
          </div>
          <p className="mx-auto mt-6 max-w-2xl text-lg tracking-tight text-zinc-500 dark:text-zinc-400">
            A simple Pokédex page using the RESTful Pokémon API from{' '}
            <Link href="https://pokeapi.co/" aria-label="Poke API">
              <span className='dark:text-blue-400 text-blue-500 font-bold underline'>PokéAPI</span>
            </Link>
          </p>
          <div className='mt-8 mx-auto max-w-2xl'>
            <SearchBar setSearchQuery={setSearchQuery} />
            <Dropdown setSortOrder={setSortOrder} />
          </div>
          {loading ? (
            <div className='mt-16 mb-10 text-center font-bold text-2xl tracking-tighter text-zinc-600 dark:text-zinc-300 animate-bounce transition-all'>
              <Image 
                src={loadingGif} 
                alt='Loading Gif'
                width={100}
                height={50}
                className='justify-center align-center text-center flex items-center mx-auto mb-4'
                priority
                style={{ height: 'auto' }}
                unoptimized
              />
              Loading Pokémon...
            </div>
          ) : (
            <>
              <div className="mt-16 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
                {filteredPokemon.slice(0, visiblePokemonCount).map(pokemon => (
                  <PokemonCard key={pokemon.id} pokemon={pokemon} />
                ))}
              </div>
              {visiblePokemonCount < filteredPokemon.length && (
                <div className="mt-20">
                  <button
                    onClick={handleLoadMore}
                    className="px-6 py-2 bg-blue-500 text-white rounded-full"
                  >
                    Load More Pokémon
                  </button>
                </div>
              )}
            </>
          )}
        </div>
      );
    }

    3. Building a reusable PokemonCard component.

    Create the "PokemonCard" component in "app/components/PokemonCard.tsx". This component will be used to display the basic pokemon information in the home page.

    import Link from 'next/link';
    import Image from 'next/image';
    import { getTypeColor } from '@/lib/typeColors';
    
    interface PokemonCardProps {
      pokemon: {
        name: string;
        id: number;
        image: string;
        types: string[];
        abilities: string[];
      };
    }
    
    export default function PokemonCard({ pokemon }: PokemonCardProps) {
      if (!pokemon || !pokemon.name) {
        return null; // Render nothing if the pokemon object is undefined or incomplete
      }
    
      const formattedId = pokemon.id.toString().padStart(3, '0');
    
      return (
        <Link href={`/pokemon/${pokemon.name}`}>
          <div className="min-w-56 bg-white dark:bg-zinc-800 rounded-3xl shadow-md hover:shadow-lg overflow-hidden transform transition-transform hover:scale-105 cursor-pointer ring-1 ring-zinc-900/5 backdrop-blur dark:ring-white/30 dark:hover:ring-white/40">
            <div className="relative w-full pb-full bg-gray-200 dark:bg-zinc-700" style={{ paddingBottom: '100%' }}>
              <Image 
                src={pokemon.image} 
                alt={pokemon.name} 
                fill
                sizes="(max-width: 768px) 100vw, (min-width: 769px) 25vw"
                className="p-2 absolute top-0 left-0 w-full h-full object-cover" 
                priority
              />
            </div>
            <div className="px-6 py-4 text-left">
              <h2 className="text-2xl font-bold capitalize text-gray-900 dark:text-white">{pokemon.name}</h2>
              <p className="mt-2 text-zinc-500 dark:text-zinc-400">#{formattedId}</p>
              <div className="mt-2">
                <h3 className="text-sm font-medium text-zinc-900 dark:text-zinc-50">Types:</h3>
                <div className="flex space-x-2 mt-3 mb-1">
                  {pokemon.types.map((type) => (
                    <span 
                      key={type} 
                      className={`capitalize px-5 py-2 text-xs font-semibold rounded-full ${getTypeColor(type)}`}
                    >
                      {type}
                    </span>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </Link>
      );
    }

    4. Creating the Pokémon Detail Page:

    Now that the home page and the pokemonCard components are done, we need to create the Pokémon detail page (app/pokemon/[name]/page.tsx) this page will load when the user clicks on the pokemonCard component and it will display comprehensive information about each Pokémon. Use the custom hook usePokemonData for efficient data fetching and caching.

    'use client';
    
    import { useEffect, useRef, useState } from 'react';
    import { useParams, useRouter } from 'next/navigation';
    import Image from 'next/image';
    import { getTypeColor } from '@/lib/typeColors';
    import { pokemonNames } from '@/lib/pokemonNames';
    import PokemonCard from '@/app/components/PokemonCard';
    import loadingGif from '@/public/pikachu-sprint-animation.gif';
    import { PlayIcon } from '@/app/components/Icons';
    import usePokemonData from '@/lib/usePokemonData';
    
    interface Pokemon {
      name: string;
      id: number;
      image: string;
      shinyImage: string;
      types: string[];
      abilities: string[];
      weight: number;
      height: number;
      stats: { name: string, base_stat: number }[];
      weaknesses: string[];
      category: string;
      species: {
        habitat: string;
        shape: string;
        color: string;
        generation: string;
      };
      evolutionChain: {
        name: string;
        id: number;
        image: string;
        types: string[];
        abilities: string[];
      }[];
      flavorTextEntries: { version: string, text: string }[];
    }
    
    export default function PokemonDetail() {
      const { name } = useParams();
      const router = useRouter();
      const [pokemon, setPokemon] = useState<Pokemon | null>(null);
      const [hover, setHover] = useState(false);
      const [selectedVersion, setSelectedVersion] = useState<string>('');
      const audioRef = useRef<HTMLAudioElement>(null);
    
      const { data: pokemonData, loading } = usePokemonData(name as string);
    
      useEffect(() => {
        if (pokemonData) {
          setPokemon(pokemonData);
          setSelectedVersion(pokemonData.flavorTextEntries[0]?.version || '');
        }
      }, [pokemonData]);
    
      const handleNavigation = (direction: 'prev' | 'next') => {
        const currentIndex = pokemonNames.indexOf(name as string);
        const newIndex = direction === 'prev' ? currentIndex - 1 : currentIndex + 1;
        const newName = pokemonNames[newIndex];
        if (newName) {
          router.push(`/pokemon/${newName}`);
        }
      };
    
      if (loading || !pokemon)
        return (
          <div className='mt-10 mb-10 text-center font-bold text-2xl tracking-tighter text-zinc-700 dark:text-zinc-300 animate-bounce transition-all'>
            <Image 
              src={loadingGif} 
              alt='Loading Gif'
              width={100}
              height={100}
              className='justify-center align-center text-center flex items-center mx-auto mb-4'
              priority
              unoptimized
            />
            Loading Pokémon...
          </div>
        );
    
      const formattedId = pokemon.id.toString().padStart(3, '0');
    
      const playCry = () => {
        if (audioRef.current) {
          audioRef.current.play();
        }
      };
    
      const handleVersionChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
        setSelectedVersion(event.target.value);
      };
    
      const selectedFlavorText = pokemon.flavorTextEntries.find((entry) => entry.version === selectedVersion)?.text;
    
      return (
        <div className="min-h-screen">
          <div className="max-w-4xl mx-auto md:bg-zinc-50 md:dark:bg-zinc-800 rounded-lg md:shadow-lg overflow-hidden mb-10">
            <div className="flex justify-between p-6">
              <button
                onClick={()=> handleNavigation('prev')}
                className="bg-sky-500 hover:bg-sky-600 text-white px-4 py-2 rounded-xl text-sm transition-all shadow"
                disabled={pokemonNames.indexOf(name as string)= 0}
              >
                Previous Pokémon
              </button>
              <button
                onClick={()=> handleNavigation('next')}
                className="bg-sky-500 hover:bg-sky-600 text-white px-4 py-2 rounded-xl text-sm transition-all shadow"
                disabled={pokemonNames.indexOf(name as string)= pokemonNames.length - 1}
              >
                Next Pokémon
              </button>
            </div>
            <div className="p-2 md:px-10 flex flex-col md:flex-row items-center">
              <div className="w-full md:w-2/4 flex justify-center md:justify-start">
                <div className="relative w-full pb-full md:pb-0 md:h-0" style={{ paddingBottom: '100%' }}>
                  {pokemon.image && (
                    <Image 
                      src={hover ? pokemon.shinyImage : pokemon.image} 
                      alt={pokemon.name} 
                      fill
                      sizes="(max-width: 768px) 100vw, (min-width: 769px) 50vw"
                      className="absolute top-0 left-0 w-full h-full object-cover" 
                      priority
                    />
                  )}
                </div>
              </div>
              <div className="w-full md:w-2/4 md:pl-6 mt-6 md:mt-0 mb-6">
                <h1 className="text-4xl md:text-5xl font-bold capitalize text-gray-900 dark:text-white">{pokemon.name}</h1>
                <p className="text-xl mt-3 text-zinc-600 dark:text-zinc-300">#{formattedId}</p>
                <div className="flex space-x-2 mt-3">
                  <button
                    aria-hidden="true"
                    onClick={playCry}
                    className="flex items-center bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-700 hover:dark:bg-zinc-600 text-zinc-800 dark:text-zinc-200 px-4 py-2 text-xs font-semibold rounded-full"
                  >
                    Play Cry
                    <PlayIcon className="ml-1 h-3 w-3 stroke-none fill-zinc-900 dark:fill-zinc-50" />
                  </button>
                  <button
                    onMouseEnter={()=> setHover(true)}
                    onMouseLeave={()=> setHover(false)}
                    className="bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-700 hover:dark:bg-zinc-600 text-zinc-800 dark:text-zinc-200 px-4 py-2 text-xs font-semibold rounded-full"
                  >
                    Shiny Version
                  </button>
                </div>
                <div className="mt-4">
                  <h2 className="text-xl font-semibold text-zinc-800 dark:text-zinc-200">Types:</h2>
                  <div className="flex space-x-2 mt-2">
                    {pokemon.types.map((type) => (
                      <span 
                        key={type} 
                        className={`capitalize px-4 py-2 text-xs font-semibold rounded-full ${getTypeColor(type)}`}
                      >
                        {type}
                      </span>
                    ))}
                  </div>
                </div>
                <div className="mt-2">
                  <h2 className="text-xl font-semibold text-zinc-800 dark:text-zinc-200">Abilities:</h2>
                  <div className="flex space-x-2 mt-2">
                    {pokemon.abilities.map((ability) => (
                      <span key={ability} className="capitalize font-semibold bg-zinc-200 dark:bg-zinc-700 text-zinc-800 dark:text-zinc-200 rounded-full px-4 py-2 text-xs">
                        {ability}
                      </span>
                    ))}
                  </div>
                </div>
                <div className="mt-4">
                  <h2 className="text-xl font-semibold text-zinc-800 dark:text-zinc-200">Weaknesses:</h2>
                  <div className="flex flex-wrap gap-2 mt-2">
                    {pokemon.weaknesses.map((weakness) => (
                      <span key={weakness} className={`capitalize text-xs font-semibold rounded-full ${getTypeColor(weakness)} px-4 py-2 text-xs`}>
                        {weakness}
                      </span>
                    ))}
                  </div>
                </div>
              </div>
            </div>
            <div className="py-10 md:px-6 grid grid-cols-1 md:grid-cols-2 gap-8">
              <div>
                <h2 className="text-2xl font-semibold text-zinc-800 dark:text-zinc-200">Pokémon Details:</h2>
                <div className="mt-2">
                  <div className="container mx-auto flex">
                    <label htmlFor="sort" className="block tracking-tight text-lg text-zinc-800 dark:text-zinc-200 mt-3 mr-5">
                      Game Version:
                    </label>
                    <select
                      className="capitalize mt-2 bg-zinc-200 dark:bg-zinc-700 text-zinc-800 dark:text-zinc-200 px-4 py-2 rounded-md"
                      value={selectedVersion}
                      onChange={handleVersionChange}
                    >
                      {pokemon.flavorTextEntries.map((entry) => (
                        <option key={entry.version} value={entry.version}>
                        {entry.version}
                      </option>
                      ))}
                    </select>
                  </div>
                  <p className="text-lg text-zinc-800 dark:text-zinc-200 bg-zinc-200 dark:bg-zinc-700 px-4 py-2 rounded-xl mt-4">{selectedFlavorText}</p>
                  <p className="text-lg text-zinc-700 dark:text-zinc-300 mt-3">Category: {pokemon.category}</p>
                  <p className="text-lg text-zinc-700 dark:text-zinc-300">Height: {pokemon.height / 10} m</p>
                  <p className="text-lg text-zinc-700 dark:text-zinc-300">Weight: {pokemon.weight / 10} kg</p>
                </div>
              </div>
              <div>
                <h2 className="text-2xl font-semibold text-zinc-800 dark:text-zinc-200">Species Information:</h2>
                <div className="mt-4">
                  <p className="capitalize text-lg text-zinc-700 dark:text-zinc-300">Habitat: {pokemon.species.habitat}.</p>
                  <p className="capitalize text-lg text-zinc-700 dark:text-zinc-300">Shape: {pokemon.species.shape}.</p>
                  <p className="capitalize text-lg text-zinc-700 dark:text-zinc-300">Color: {pokemon.species.color}.</p>
                  <p className="capitalize text-lg text-zinc-700 dark:text-zinc-300">Generation: {pokemon.species.generation}.</p>
                </div>
              </div>
            </div>
            <div className="py-6 md:px-6">
              <h2 className="text-2xl font-semibold text-zinc-800 dark:text-zinc-200">Pokémon Stats:</h2>
              <div className="grid grid-cols-2 gap-8 mt-4">
                {pokemon.stats.map((stat) => (
                  <div key={stat.name} className="bg-gray-200 dark:bg-gray-700 px-6 py-4 rounded-xl hover:scale-105 overflow-hidden transform transition-transform shadow hover:shadow-md">
                    <h3 className="text-lg font-semibold text-zinc-800 dark:text-zinc-200 capitalize">{stat.name}:</h3>
                    <p className="text-2xl font-bold text-zinc-800 dark:text-zinc-100">{stat.base_stat}</p>
                  </div>
                ))}
              </div>
            </div>
            <div className="py-6 md:px-6 mb-6">
              <h2 className="text-2xl font-semibold text-zinc-800 dark:text-zinc-200">Pokémon Evolution Chain:</h2>
              <div className="group flex max-md:flex-col justify-center gap-10 mt-10 px-4 md:px-0">
                {pokemon.evolutionChain.map((evolution) => (
                  <PokemonCard key={evolution.id} pokemon={evolution} />
                ))}
              </div>
            </div>
            <audio ref={audioRef} src={`https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/latest/${pokemon.id}.ogg`} />
          </div>
        </div>
      );
    }

    5. Custom Hook for Data Fetching

    We must implement efficient local data caching to enhance performance and reduce unnecessary API requests.

    We will use the swr package for data fetching and caching.

    npm install swr
    

    Now, let's create the file "lib/usePokemonData.ts".

    // lib/usePokemonData.ts
    // Custom Hook for Data Fetching and Caching
    
    import useSWR from 'swr';
    import axios from 'axios';
    import { typeWeaknesses } from '@/lib/typeWeaknesses';
    
    const fetcher = async (url: string) => {
      // Uncomment below if you want to see the data caching in action - Part I
      // console.log(`Fetching data from: ${url}`);
      const response = await axios.get(url);
      return response.data;
    };
    
    const getEvolutionChain = async (evolutionChainUrl: string) => {
      const response = await axios.get(evolutionChainUrl);
      const evolutionData = response.data;
      const chain: { name: string; id: number; image: string; types: string[]; abilities: string[]; }[] = [];
      const fetchChainData = async (chainLink: any) => {
        const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${chainLink.species.name}`);
        const data = response.data;
        chain.push({
          name: data.name,
          id: data.id,
          image: data.sprites.other['official-artwork'].front_default,
          types: data.types.map((typeInfo: any) => typeInfo.type.name),
          abilities: data.abilities.map((abilityInfo: any) => abilityInfo.ability.name),
        });
        if (chainLink.evolves_to.length > 0) {
          await fetchChainData(chainLink.evolves_to[0]);
        }
      };
      await fetchChainData(evolutionData.chain);
      return chain;
    };
    
    const getPokemonData = async (name: string) => {
      const response = await fetcher(`https://pokeapi.co/api/v2/pokemon/${name}`);
      const speciesResponse = await fetcher(response.species.url);
      const speciesData = speciesResponse;
      const evolutionChain = await getEvolutionChain(speciesData.evolution_chain.url);
      const types = response.types.map((typeInfo: any) => typeInfo.type.name);
      const weaknesses = Array.from(new Set(types.flatMap((type: string) => typeWeaknesses[type]))) as string[];
      const flavorTextEntries = speciesData.flavor_text_entries
        .filter((entry: any) => entry.language.name === 'en')
        .map((entry: any) => ({ version: entry.version.name, text: entry.flavor_text }));
    
      return {
        name: response.name,
        id: response.id,
        image: response.sprites.other['official-artwork'].front_default,
        shinyImage: response.sprites.other['official-artwork'].front_shiny,
        types,
        abilities: response.abilities.map((abilityInfo: any) => abilityInfo.ability.name),
        weight: response.weight,
        height: response.height,
        stats: response.stats.map((statInfo: any) => ({
          name: statInfo.stat.name,
          base_stat: statInfo.base_stat,
        })),
        weaknesses,
        category: speciesData.genera.find((genus: any) => genus.language.name === 'en').genus,
        species: {
          habitat: speciesData.habitat?.name || 'Unknown',
          shape: speciesData.shape?.name || 'Unknown',
          color: speciesData.color?.name || 'Unknown',
          generation: speciesData.generation?.name || 'Unknown',
        },
        evolutionChain,
        flavorTextEntries,
      };
    };
    
    // Uncomment below if you want to see the data caching in action - Part II
    // export default function usePokemonData(name: string) {
    //   const { data, error } = useSWR(
    //     name ? `pokemon/${name}` : null, 
    //     () => getPokemonData(name),
    //     {
    //       revalidateOnFocus: false,  // Do not revalidate when window gains focus
    //       dedupingInterval: 60000,   // Deduplicate requests within 1 minute
    //       onSuccess: (data) => {
    //         console.log('Data fetched successfully:', data);
    //       },
    //       onError: (error) => {
    //         console.error('Error fetching data:', error);
    //       }
    //     }
    //   );
    
    //   console.log('Data:', data);
    //   console.log('Error:', error);
    
    //   return {
    //     data,
    //     loading: !data && !error,
    //     error,
    //   };
    // }
    
    export default function usePokemonData(name: string) {
      const { data, error } = useSWR(
        name ? `pokemon/${name}` : null,
        () => getPokemonData(name),
        {
          revalidateOnFocus: false,
          dedupingInterval: 60000,
        }
      );
    
      return {
        data,
        loading: !data && !error,
        error,
      };
    }

    Thoughts on the project and the data caching process.

    Alright, this was a bit more complicated than I initially thought, but we made it, friends!

    • We implemented efficient local data caching to enhance performance and reduce redundant API requests.
    • We created a custom hook, usePokemonData.ts, to handle data fetching and caching.
    • This hook uses useEffect to fetch data from the PokeAPI and leverages localStorage to store fetched data, ensuring that subsequent requests check for existing data before making API calls.
    • This approach encapsulates the fetching logic, allowing for code reusability and cleaner component code.

    By fetching all necessary data in a single hook and using localStorage for caching, we significantly improve performance and reduce API calls.

    This method ensures quick data retrieval, even across page reloads and sessions, providing a faster and more responsive user experience. Implementing local caching in PokeNext.js demonstrates how modern web development practices can lead to optimized data management and a more robust application.