tl;dr new to coding. 1 card = 1 pokémon name. Mapped out 898+ cards, listed in increments of 20. How can I set a modal to pop up when a card is clicked, and have it display the data in Pokemon.js?
Currently a developer in training. I have individual pokémon names mapped out onto 898+ cards (paginated 20 at a time). When a specific card is clicked, it loads that pokémon's information and wipes out the list of cards. When you go back to the main page, the names reset back to the first 20. How do I implement modals to load the pokémon's information instead when a card is clicked? I've watched a bunch of tutorials but am a bit confused as to where to even place the modal in the build (I have components on top of components). I’m also trying to avoid adding buttons on the cards for designating an onClick
. I'm assuming this process isn't as complex as I'm making it, but any instruction on how to do it based on the code I've provided would be immensely appreciated. I'll include the individual components and my App.js file.
App.js
import React from 'react';
import { HashRouter as Router, Route, Switch } from 'react-router-dom';
import NavBar from '../src/components/layout/NavBar';
import Dashboard from './components/layout/Dashboard';
import Pokemon from './components/pokemon/Pokemon';
import backgroundImage from './pokeball.png';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
function App() {
return (
<Router>
<div
className='App'
style={{
background: `url(${backgroundImage})`
}}
>
<NavBar />
<div className='container'>
<Switch>
<Route
exact path='/'
component={Dashboard}
/>
<Route
exact path='/pokemon/:indexNum'
render={(props) => <Pokemon {...props} />}
/>
</Switch>
</div>
</div>
</Router>
);
}
export default App;
Dashboard.js
import React from 'react';
import PokemonList from '../pokemon/PokemonList';
const Dashboard = () => {
return (
<div className='row'>
<div
className='col'
style={{
marginTop: '25px'
}}
>
<PokemonList />
</div>
</div>
)
}
export default Dashboard
PokemonList.js
import React, { useState, useEffect } from 'react';
import PokemonCard from './PokemonCard';
import Pagination from '../layout/Pagination';
import axios from 'axios';
const PokemonList = () => {
const [pokemon, setPokemon] = useState([])
const [currentPageUrl, setCurrentPageUrl] = useState('https://pokeapi.co/api/v2/pokemon')
const [loading, setLoading] = useState(true)
const [nextPageUrl, setNextPageUrl] = useState()
const [prevPageUrl, setPrevPageUrl] = useState()
useEffect(() => {
setLoading(true)
let cancel
axios.get(currentPageUrl, {
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(res => {
setLoading(false)
// console.log(res.data.results)
setPokemon(res.data.results)
setNextPageUrl(res.data.next)
setPrevPageUrl(res.data.previous)
})
return () => cancel()
}, [currentPageUrl])
const goToNextPage = () => {
setCurrentPageUrl(nextPageUrl)
}
const goToPrevPage = () => {
setCurrentPageUrl(prevPageUrl)
}
if (loading) return "Loading Your Pokémon..."
return (
<>
{pokemon ? (
<div className='row'>
{pokemon.map((i) => (
<PokemonCard
key={i.name}
pokemon={i.name}
url={i.url}
/>
))}
</div>
) : (
<h1>Loading</h1>
)}
<div className='row'>
<Pagination
goToNextPage={nextPageUrl ? goToNextPage : null}
goToPrevPage={prevPageUrl ? goToPrevPage : null}
/>
</div>
</>
)
}
export default PokemonList
Pagination.js
import React from 'react';
import { ChevronDoubleLeft, ChevronDoubleRight } from 'react-bootstrap-icons';
const Pagination = ({ goToNextPage, goToPrevPage }) => {
return (
<div className='mx-auto'>
{
goToPrevPage &&
<button
className='badge badge-pill badge-danger'
type='button'
onClick={goToPrevPage}
style={{
marginBottom: '20px',
padding: '10px'
}}
>
<ChevronDoubleLeft />
</button>
}
<span>
</span>
{
goToNextPage &&
<button
className='badge badge-pill badge-danger'
type='button'
onClick={goToNextPage}
style={{
marginBottom: '20px',
padding: '10px'
}}
>
<ChevronDoubleRight />
</button>
}
</div>
)
}
export default Pagination
PokemonCard.js
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import styled from 'styled-components';
import gif from '../../loading-wheel.gif';
const Sprite = styled.img`
width: 5em;
height: 5em;
`;
const Card = styled.div`
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
&:hover {
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
-moz-user-select: none;
-website-user-select: none;
user-select: none;
-o-user-select: none;
`;
const StyledLink = styled(Link)`
text-decoration: none;
color: black;
&:focus,
&:hover,
&:visited,
&:link,
&:active {
text-decoration: none;
}
`;
const PokemonCard = ({ pokemon, url }) => {
const [sprite, setSprite] = useState('')
const [loadImage, setLoadImage] = useState(true)
const [tooManyRequests, setTooManyRequests] = useState(false)
const [didMount, setDidMount] = useState(false)
const [indexNum] = useState(url.split('/')[url.split('/').length - 2])
useEffect(() => {
axios.get('https://pokeapi.co/api/v2/pokemon/' + pokemon)
.then (res => {
setSprite(res.data.sprites.front_default)
})
setDidMount(true);
return () => setDidMount(false)
}, [pokemon])
if (!didMount) {
return null;
}
return (
<div className='col-md-3 col-sm-6 mb-5'>
<StyledLink to={`pokemon/${indexNum}`}>
<Card className='card'>
<p className='card-header'>
<img
src='https://icon-library.com/images/small-pokeball-icon/small-pokeball-icon-4.jpg'
alt='Pokéball Icon'
width='40px'
height='31.75px'
/>
{indexNum}
</p>
{loadImage ? (
<img
className='card-img-top rounded mx-auto d-block mt-2'
src={gif}
alt='Loading'
style={{
width: '5em',
height: '5em'
}}
/>
) : null}
<div className='card-body'>
<center>
<Sprite
className='card-img-top rounded mx-auto mt-2'
src={sprite}
alt='Pokémon Sprite'
onLoad={() => setLoadImage(false)}
onError={() => setTooManyRequests(true)}
style={
tooManyRequests ? { display: 'block'} :
loadImage ? null : { display: 'block'}
}
/>
<h6>
{pokemon
.toLowerCase()
.split('-')
.map(letter => letter.charAt(0).toUpperCase() + letter.substring(1))
.join(' ')
}
</h6>
</center>
</div>
</Card>
</StyledLink>
</div>
)
}
export default PokemonCard
Pokemon.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import gif from '../../loading-wheel.gif';
const type_colors = {
bug: 'B1C12E',
dark: '4F3A2D',
dragon: '755EDF',
electric: 'FCBC17',
fairy: 'F4B1F4',
fighting: '823551D',
fire: 'E73B0C',
flying: 'A3B3F7',
ghost: '6060B2',
grass: '74C236',
ground: 'D3B357',
ice: 'A3E7FD',
normal: 'C8C4BC',
poison: '934594',
psychic: 'ED4882',
rock: 'B9A156',
steel: 'B5B5C3',
water: '3295F6'
};
const Pokemon = (props) => {
const { match } = props
const { params } = match
const { indexNum } = params
const [pokemon, setPokemon] = useState(undefined)
const [bio, setBio] = useState('')
const [genderRatioMale, setGenderRatioMale] = useState(0)
const [genderRatioFemale, setGenderRatioFemale] = useState(0)
const [catchRate, setCatchRate] = useState(0)
const [growthRate, setGrowthRate] = useState('')
const [hatchSteps, setHatchSteps] = useState(0)
useEffect(() => {
axios.get(`https://pokeapi.co/api/v2/pokemon/${indexNum}/`)
.then (res => {
const { data } = res
// console.log(data)
setPokemon(data)
})
.catch(err => {
setPokemon(false)
})
}, [indexNum])
useEffect(() => {
axios.get(`https://pokeapi.co/api/v2/pokemon-species/${indexNum}/`)
.then(res => {
const { data } = res
let description = '';
// console.log(data)
data.flavor_text_entries
.some(flavor => {
const { flavor_text, language } = flavor
const { name } = language
if (name === 'en') {
description = flavor_text
// console.log(description)
setBio(description)
}
const affirm1 = console.log('Description set as state for bio')
return affirm1
})
const femaleRate = data.gender_rate
setGenderRatioFemale(12.5 * femaleRate)
setGenderRatioMale(12.5 * (8 - femaleRate))
setCatchRate(data.capture_rate)
setGrowthRate(data.growth_rate.name)
setHatchSteps(255 * (data.hatch_counter) + 1)
})
}, [indexNum])
const loadPokemonJsx = () => {
const {
name,
id,
abilities,
height,
weight,
types,
stats,
sprites
} = pokemon
const {front_default} = sprites
const fullImageUrl = `https://pokeres.bastionbot.org/images/pokemon/${id}.png`
let [
hp,
attack,
defense,
speed,
specialAttack,
specialDefense
] = ''
stats.map(stat => {
switch (stat.stat.name) {
case 'hp':
hp = stat['base_stat'];
// console.log(`HP: ${hp}`)
break;
case 'attack':
attack = stat['base_stat'];
// console.log(`ATK: ${attack}`)
break;
case 'defense':
defense = stat['base_stat'];
// console.log(`DEF: ${defense}`)
break;
case 'speed':
speed = stat['base_stat'];
// console.log(`SPEED: ${speed}`)
break;
case 'special-attack':
specialAttack = stat['base_stat'];
// console.log(`SP. ATK: ${specialAttack}`)
break;
case 'special-defense':
specialDefense = stat['base_stat'];
// console.log(`SP. DEF: ${specialDefense}`)
break;
default:
break;
}
const affirm2 = console.log('Variables from switch case are set')
return affirm2
})
return (
<>
<div
className='col'
style={{
marginTop: '25px'
}}
>
<div className='card'>
<div className='card-header'>
<div className='row'>
<div className='col-5'>
<h5>
<img
src='https://icon-library.com/images/small-pokeball-icon/small-pokeball-icon-4.jpg'
alt='Pokéball Icon'
width='45.714px'
height='36.285px'
/>
{indexNum}
</h5>
</div>
<div className='col-7'>
<div
style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{ types.map(typeInfo => {
const { type } = typeInfo;
const { name } = type;
return <span
key={name}
className='badge badge-primary badge-pill mr-1'
style={{
backgroundColor: `#${type_colors[name]}`,
color: 'white'
}}
>
{`${name
.toLowerCase()
.split(' ')
.map(letter => letter.charAt(0).toUpperCase() + letter.substring(1))
.join(' ')
}`
}
</span>
}) }
</div>
</div>
</div>
</div>
<div className='card-body'>
<div className='row align-items-center'>
<div className='col-md-3'>
<img
src={fullImageUrl}
alt='Pokémon Large Pic'
className='card-img-top rounded mx-auto mt-2'
/>
</div>
<div className='col-md-9'>
<h5 className='mx-auto'>
{ name
.toLowerCase()
.split('-')
.map(letter => letter.charAt(0).toUpperCase() + letter.substring(1))
.join(' ')
}
<img
src={front_default}
alt='Pokémon Sprite'
/>
</h5>
<div className='row align-items-center'>
<div className='col-12 col-md-3'>HP</div>
<div className='col-12 col-md-9'>
<div className='progress'>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${hp}%`
}}
aria-valuenow='25'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{hp}</small>
</div>
</div>
</div>
</div>
<div className='row align-items-center'>
<div className='col-12 col-md-3'>Attack</div>
<div className='col-12 col-md-9'>
<div className='progress'>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${attack}%`
}}
aria-valuenow='25'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{attack}</small>
</div>
</div>
</div>
</div>
<div className='row align-items-center'>
<div className='col-12 col-md-3'>Sp. Attack</div>
<div className='col-12 col-md-9'>
<div className='progress'>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${specialAttack}%`
}}
aria-valuenow='25'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{specialAttack}</small>
</div>
</div>
</div>
</div>
<div className='row align-items-center'>
<div className='col-12 col-md-3'>Defense</div>
<div className='col-12 col-md-9'>
<div className='progress'>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${defense}%`
}}
aria-valuenow='25'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{defense}</small>
</div>
</div>
</div>
</div>
<div className='row align-items-center'>
<div className='col-12 col-md-3'>Sp. Defense</div>
<div className='col-12 col-md-9'>
<div className='progress'>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${specialDefense}%`
}}
aria-valuenow='25'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{specialDefense}</small>
</div>
</div>
</div>
</div>
<div className='row align-items-center'>
<div className='col-12 col-md-3'>Speed</div>
<div className='col-12 col-md-9'>
<div className='progress'>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${speed}%`
}}
aria-valuenow='25'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{speed}</small>
</div>
</div>
</div>
</div>
</div>
<div className='row mt-2'>
<div className='col'>
<p className='p-2'>{bio}</p>
</div>
</div>
</div>
</div>
<hr />
<div className='card-body'>
<h5 className='card-title text-center'>Profile</h5>
<div className='row'>
<div className='col-md-6'>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Approx. Height:</h6>
</div>
<div className='col-md-6'>
<h6 style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{
(Math.round((height * 0.328084 + 0.0001) * 100) / 100) + ' ft / ' +
(Math.round((height * 10 + 0.0001) * 100) / 100) + ' cm'
}
</h6>
</div>
</div>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Approx. Weight:</h6>
</div>
<div className='col-md-6'>
<h6 style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{
(Math.round((weight * 0.220462 + 0.0001) * 100) / 100) + ' lbs / ' +
(Math.round((weight * 0.1 + 0.0001) * 100) / 100) + ' kg'
}
</h6>
</div>
</div>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Catch Rate:</h6>
</div>
<div className='col-md-6'>
<h6 style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{ `${catchRate}%` }
</h6>
</div>
</div>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Gender Ratio:</h6>
</div>
<div className='col-md-6'>
<div className='progress'>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${genderRatioFemale}%`,
backgroundColor: '#C2185B',
}}
aria-valuenow='15'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{`${genderRatioFemale}%`}</small>
</div>
<div
className='progress-bar'
role='progressbar'
style={{
width: `${genderRatioMale}%`,
backgroundColor: '#1976D2'
}}
aria-valuenow='30'
aria-valuemin='0'
aria-valuemax='100'
>
<small>{`${genderRatioMale}%`}</small>
</div>
</div>
</div>
</div>
</div>
<div className='col-md-6'>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Growth Rate:</h6>
</div>
<div className='col-md-6'>
<h6 style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{growthRate
.toLowerCase()
.split('-')
.map(letter => letter.charAt(0).toUpperCase() + letter.substring(1))
.join(' ')
}
</h6>
</div>
</div>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Hatch Steps:</h6>
</div>
<div className='col-md-6'>
<h6 style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{hatchSteps}
</h6>
</div>
</div>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Effort Values:</h6>
</div>
<div className='col-md-6'>
<h6 style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{
stats.filter(stat => {
if (stat.effort > 0) {
return true;
}
return false;
})
.map(statInfo => {
const { effort, stat } = statInfo
const { name } = stat
return `${effort} ${name
.toLowerCase()
.split('-')
.map(letter => letter.charAt(0).toUpperCase() + letter.substring(1))
.join(' ')
}`
}).join(', ')
}
</h6>
</div>
</div>
<div className='row flex-container'>
<div className='col-md-6'>
<h6 style={{display: 'flex'}}>Abilities:</h6>
</div>
<div className='col-md-6'>
<h6 style={{
display: 'flex',
flexDirection: 'row-reverse'
}}
>
{
abilities.map(abilityInfo => {
const { ability } = abilityInfo;
const { name } = ability;
return `${name
.toLowerCase()
.split('-')
.map(letter => letter.charAt(0).toUpperCase() + letter.substring(1))
.join(' ')
}`
}).join(', ')
}
</h6>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</>
)
}
return (
<>
{pokemon === undefined &&
<img
src={gif}
alt='Loading'
/>
}
{pokemon !== undefined && pokemon && loadPokemonJsx(pokemon)}
{pokemon === false && <h1> Pokémon Not Found</h1>}
</>
)
}
export default Pokemon
Sorry this is such a long post. Thanks again!