Mapping through Starwars API by planet Name in React - javascript

I am currently trying to map through every planet in the star wars api and print out the individual planet name but I am stuck. API LINK (https://swapi.dev/)
Thankyou for any help or advice
I tried using axios and Use Effect but am not sure where my errors may be. Below is what i have in my AllPlanets React Component.
`
import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
const AllPlanets = () => {
const [data, setData] = useState("");
useEffect(() => {
axios
.get(`https://swapi.dev/api/planets`)
.then((res) => setData(res.data))
.catch((error) => console.log(error));
});
return (
<div>
{data && (
<div className="flex">
{data.map((planet, idx) => (
<p>{planet.name}</p>
))}
</div>
)}
</div>
);
};
export default AllPlanets;
`

These are the needed fixes (see comments in code as well):
The API response contains the property results that has the actual list of planets
The useEffect needs an empty dependencies array to block it from running on each render, and creating an infinite loop
Each item in an rendered list needs a key with a unique value
const { useState, useEffect } = React;
const AllPlanets = () => {
const [data, setData] = useState([]);
useEffect(() => {
axios
.get(`https://swapi.dev/api/planets`)
.then(res => setData(res.data.results)) // get the results property from the res.data object
.catch(error => console.log(error));
}, []); // add a dependency array so you won't have an infinite loop
return (
<div>
{!!data.length && (
<div className="flex">
{data.map(({ name }) => (
// use the name as key
<p key={name}>{name}</p>
))}
</div>
)}
</div>
);
};
ReactDOM
.createRoot(root)
.render(<AllPlanets />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js" integrity="sha512-0qU9M9jfqPw6FKkPafM3gy2CBAvUWnYVOfNPDYKVuRTel1PrciTj+a9P3loJB+j0QmN2Y0JYQmkBBS8W+mbezg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="root"></div>

If you console.log the result you'll notice it's not an array of planets. It's an object with this structure:
{
count: 60
next: "https://swapi.dev/api/planets/?page=2"
previous: null
results: Array(10)
}
So replace
.then((res) => setData(res.data))
with
.then((res) => setData(res.data.results))
Alternatively, if you want to use next and previous provided links, for pagination, keep data in the returned format and map its results instead:
{data.results.map((planet, idx) => (
<p>{planet.name}</p>
))}
... and also make use of the previous and next links.
Here's an example:
const { useState, useEffect, useCallback, Fragment } = React;
const Button = ({ label, ...args }) => <button {...args}>{label}</button>;
const App = () => {
const [data, setData] = useState(null);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const load = useCallback((url) => {
setLoading(true);
axios
.get(url)
.then(({ data }) => {
setData(data);
setPage(Number(url[url.length - 1]) || 1);
setLoading(false);
})
.catch(console.log);
}, []);
useEffect(() => {
load("https://swapi.dev/api/planets");
}, []);
const mapButton = useCallback(
(type) => ({
label: type === "next" ? "→" : "←",
onClick: () => load(data[type]),
disabled: !data[type],
}),
[data]
);
return loading ? (
<div>Loading...</div>
) : (
(data && (
<Fragment>
<Button {...mapButton("previous")} />
{data.results.length && (
<small>
Page {page} of {Math.ceil(data.count / data.results.length)}
</small>
)}
<Button {...mapButton("next")} />
<div className="flex">
{data.results.map((planet, key) => (
<p {...{ key }}>{planet.name}</p>
))}
</div>
</Fragment>
)) ||
null
);
};
ReactDOM.createRoot(root).render(<App />);
#root {
font-family: sans-serif;
text-align: center
}
button:not(:disabled) {
cursor: pointer
}
small {
padding: 0 1rem
}
<script src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/axios#1.1.3/dist/axios.min.js"></script>
<div id="root"></div>

Related

How can I truncate text of a specific element inside .map function in React?

const { useEffect, useState } = React;
function App() {
const [data, setData] = useState([]);
const [show, setShow] = useState(false)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((res) => setData(res))
.catch((err) => console.log("error", err.message))
}, [])
let showNames = (users, index) => {
return (
<h3 key={index} onMouseOver={() => setShow(true)} onMouseLeave={() => setShow(false)}>
{show ? users.name : `${users.name.substring(0, 5)}...`}
</h3>
)
}
return (
<div className="App">
<header className="App-header">
{
data && data.map((users, index) => {
return (
showNames(users, index)
)
})
}
</header>
</div>
);
}
ReactDOM.createRoot(document.body).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
I'm currently learning ReactJS, and I am facing an issue while trying to hide/show truncated text. The following code is working, but when I hover over the text it truncates every name from the list. I would instead like it to show/hide the name only where I hover.
Your problem is that you only have one show state that controls the visibility of all your text items. There are a few options to fix this, one way would be to .map() the data you get from your API to include a show property, which you can update for particular objects within data when you hover over the associated item. This option is good if you need multiple options expanded at the same time, however, with your current set up you can only have one item expanded (by the nature of your mouse enter/leave to toggle the expanded state). The other option to deal with this is to store the id of the currently expanded item, and then within your .map() only expand the item if the item id matches the stored id within your state.
See working example below:
const { useEffect, useState } = React;
function App() {
const [data, setData] = useState([]);
const [showId, setShowId] = useState(-1); // invalid id that matches no users
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((res) => setData(res))
.catch((err) => console.log("error", err.message))
}, [])
let showNames = user => {
return (
<h3 key={user.id} onMouseOver={() => setShowId(user.id)} onMouseLeave={() => setShowId(-1)}>
{showId === user.id ? user.name : `${user.name.substring(0, 5)}...`}
</h3>
)
}
return (
<div className="App">
<header className="App-header">
{
data && data.map(user => showNames(user))
}
</header>
</div>
);
}
ReactDOM.createRoot(document.body).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
I've also updated your component to use the user id as the key, without relying on the index (using the index as the key can cause rendering bugs if your users are removed from the list)
you can simply set show status pre user.name and check if users.name is true or not
here is what I did :
const { useEffect, useState } = React;
function App() {
const [data, setData] = useState([]);
const [show, setShow] = useState({});
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((res) => setData(res))
.catch((err) => console.log("error", err.message));
}, []);
let showNames = (users, index) => {
return (
<h3
key={index}
onMouseOver={() =>
setShow((prevState) => ({ ...prevState, [users.name]: true }))
}
onMouseLeave={() =>
setShow((prevState) => ({ ...prevState, [users.name]: false }))
}
>
{show[users.name] ? users.name : `${users.name.substring(0, 5)}...`}
</h3>
);
};
return (
<div className="App">
<header className="App-header">
{data &&
data.map((users, index) => {
return showNames(users, index);
})}
</header>
</div>
);
}
ReactDOM.createRoot(document.body).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

Material UI Pagination

I don't understand why my page can't recognize other pages when I click (for example on page 2, the same page appears again and again)
This is in MealNew.js component:
import React, {useEffect, useState } from "react";
import './MealNew.css';
import Card from "../UI/Card";
import AppPagination from "./AppPagination";
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query,setQuery] = useState('');
const[page,setPage] = useState(9);
const[numberOfPages,setNumberOfPages]= useState(10);
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=991fbfc719c743a5896bebbd98dfe996&page=${page}`;
fetch (link)
.then ((response)=> response.json())
.then ((data) => {
setData(data.results)
setNumberOfPages(data.total_pages)
const elementFood = data?.map((meal,key) => {
return (<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image}
alt='e-meal'/>
</div> )
})
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
}
useEffect(()=> {
handleClick();
},[page])
return (
<Card className="meal">
<form onSubmit={handleSubmit}>
<input
className="search"
placeholder="Search..."
value={query}
onChange={(e)=>setQuery(e.target.value)}/>
<input type='submit' value='Search'/>
</form>
<li className="meal">
<div className = 'meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination
setPage={setPage}
pageNumber={numberOfPages}
/>
</div>
</li>
</Card>
) }
export default MealNew;
This is in AppPagination.js component:
import React from "react";
import { Pagination } from "#mui/material";
const AppPagination = ({setPage,pageNumber}) => {
const handleChange = (page)=> {
setPage(page)
window.scroll(0,0)
console.log (page)
}
return (
<div >
<div >
<Pagination
onChange={(e)=>handleChange(e.target.textContent)}
variant="outlined"
count={pageNumber}/>
</div>
</div>
)
}
export default AppPagination;
Thanks in advance, I would appreciate it a lot
The only error I am getting in Console is this:
Line 64:3: React Hook useEffect has a missing dependency: 'handleClick'. Either include it or remove the dependency array react-hooks/exhaustive-deps
You are not following the spoonacular api.
Your link looks like this:
https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&page=${page}
I checked the spoonacular Search Recipes Api and there's no page parameter you can pass. You have to used number instead of page.
When you receive response from the api, it returns the following keys: offset, number, results and totalResults.
You are storing totalResults as totalNumberOfPages in state which is wrong. MUI Pagination count takes total number of pages not the total number of records. You can calculate the total number of pages by:
Math.ceil(totalRecords / recordsPerPage). Let say you want to display 10 records per page and you have total 105 records.
Total No. of Pages = Math.ceil(105/10)= 11
Also i pass page as prop to AppPagination component to make it as controlled component.
Follow the documentation:
Search Recipes
Pagination API
Complete Code
import { useEffect, useState } from "react";
import { Card, Pagination } from "#mui/material";
const RECORDS_PER_PAGE = 10;
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [numberOfPages, setNumberOfPages] = useState();
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&number=${page}`;
fetch(link)
.then((response) => response.json())
.then((data) => {
setData(data.results);
const totalPages = Math.ceil(data.totalResults / RECORDS_PER_PAGE);
setNumberOfPages(totalPages);
});
};
const elementFood = data?.map((meal, key) => {
return (
<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image} alt='e-meal' />
</div>
);
});
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
};
useEffect(() => {
handleClick();
console.log("first");
}, [page]);
return (
<Card className='meal'>
<form onSubmit={handleSubmit}>
<input className='search' placeholder='Search...' value={query} onChange={(e) => setQuery(e.target.value)} />
<input type='submit' value='Search' />
</form>
<li className='meal'>
<div className='meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination setPage={setPage} pageNumber={numberOfPages} page={page} />
</div>
</li>
</Card>
);
};
const AppPagination = ({ setPage, pageNumber, page }) => {
const handleChange = (page) => {
setPage(page);
window.scroll(0, 0);
console.log(page);
};
console.log("numberOfPages", pageNumber);
return (
<div>
<div>
<Pagination
page={page}
onChange={(e) => handleChange(e.target.textContent)}
variant='outlined'
count={pageNumber}
/>
</div>
</div>
);
};
export default MealNew;

How to add a "show more" button to each card on React?

I have React component:
Main.jsx
import { useState, useEffect } from "react";
import { Preloader } from "../Preloader";
import { Pokemons } from "../Pokemons";
import { LoadMore } from "../LoadMore";
function Main() {
const [pokemons, setPokemons] = useState([]);
const [loading, setLoading] = useState(true);
const [pokemonsPerPage] = useState(20);
const [page, setPage] = useState(1);
function getPokemons(pokemonOffset) {
fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${pokemonsPerPage}&offset=${pokemonOffset}`
)
.then((responce) => responce.json())
.then((data) => {
data.results && setPokemons((p) => [...p, ...data.results]);
setLoading(false);
});
}
useEffect(() => {
const offset = page * pokemonsPerPage - pokemonsPerPage;
getPokemons(offset);
}, [page]);
return (
<main className="container content">
{loading ? <Preloader /> : <Pokemons pokemons={pokemons} />}
<LoadMore next={() => setPage((p) => p + 1)} />
</main>
);
}
export { Main };
Pokemon.jsx
import { useState, useEffect } from "react";
function Pokemon({ name, url }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then(setData);
}, [url]);
return (
<div>
{data ? (
<div className="card animate__animated animate__fadeIn">
<div className="card-image">
<img src={data.sprites.front_default} />
<span className="card-title">{name}</span>
</div>
<div className="card-content">
{data.abilities.map((n, index) => (
<p key={index}>{n.ability.name}</p>
))}
</div>
</div>
) : (
<div>loading...</div>
)}
</div>
);
}
export { Pokemon };
I need each card (Pokemon) to have a "Details" button, which, when clicked, displays additional (unique) information from the fetch request in the "url" for the selected card
I think I need to do this in Pokemon.jsx but I just started learning React and haven't come across a similar challenge
If you just need a button for each card I would assume this
{data.map((item, index) =>
<div key={index}>
....
<button onClick={()=> { do something }}>
</div>
)}
and then create a function that fetches data and add it to your array where you keep
the data and might have to mess with the useEffect when you want to see the change.

I want the "show" and "hide button to only target 1 image at a time

I've almost completed my small project, which is a react app which calls on a API that displays dog images. However, at the moment, the show and hide buttons are targeting all of the images on the page (12 in total). I simply want to target one image at a time, so when I click hide or show, it will only do so for one of the images.
App.js
import './App.css';
import './Dog.js';
import './index.css';
import FetchAPI from './FetchAPI';
function DogApp() {
return (
<div className="dogApp">
<FetchAPI />
</div>
);
}
export default DogApp;
FetchAPI.js
import React, { useState, useEffect } from 'react'
// hide the div then display it with onclick
const FetchAPI = () => {
const [show, setShow] = useState(false);
const [data, setData] = useState([]);
const apiGet = () => {
const API_KEY = "";
fetch(`https://api.thedogapi.com/v1/images/search?limit=12&page=10&order=Desc?API_KEY=${API_KEY}`)
.then((response) => response.json())
.then((json) => {
console.log(json);
setData([...data, ...json]);
});
};
useEffect(() => { //call data when pagee refreshes/initially loads
apiGet();
}, []);
return (
<div>
{data.map((item) => (
<div class="dog">
<img alt ="dog photos" class="flexMate" src={item.url}></img>
{show ? <p>{JSON.stringify(item.breeds)}</p> : null}
<button onClick={() => setShow(true)}>Show</button>
<button onClick={() => setShow(false)}>Hide</button>
</div>
))}
</div>
)
}
export default FetchAPI;
import React, { useState, useEffect } from 'react'
// hide the div then display it with onclick
const FetchAPI = () => {
const [show, setShow] = useState({});
const [data, setData] = useState([]);
const apiGet = () => {
const API_KEY = "";
fetch(`https://api.thedogapi.com/v1/images/search?limit=12&page=10&order=Desc?API_KEY=${API_KEY}`)
.then((response) => response.json())
.then((json) => {
console.log(json);
setData([...data, ...json]);
});
};
useEffect(() => { //call data when pagee refreshes/initially loads
apiGet();
}, []);
return (
<div>
{data.map((item, id) => (
<div class="dog">
<img alt ="dog photos" class="flexMate" src={item.url}></img>
{show[id] !== false ? <p>{JSON.stringify(item.breeds)}</p> : null}
<button onClick={() => setShow((prev) => ({ ...prev, [id]: true }))}>Show</button>
<button onClick={() => setShow((prev) => ({ ...prev, [id]: false }))}>Hide</button>
</div>
))}
</div>
)
}
export default FetchAPI;

Scrolling the list Issues in Reactjs

I was implementing an Autocomplete feature while learning REACT by watching this Youtube Tutorial and here is the github repo of this project (incase you need to clone and run). I implemented it and it worked as expected. But there is a small functionality that it doesn't provide, i.e., I can't scroll down the Autocomplete list. Refer to this image for output. When I click the Side Scroll Bar The list Vanishes. How to activate that? Do I need to make another useEffect for Scroll Bar too?
Here is the App.js Code (Same as Github though)
import React, { useEffect, useState, useRef } from "react";
import logo from "./logo.svg";
import "./App.css";
const Auto = () => {
const [display, setDisplay] = useState(false);
const [options, setOptions] = useState([]);
const [search, setSearch] = useState("");
const wrapperRef = useRef(null);
useEffect(() => {
const pokemon = [];
const promises = new Array(20)
.fill()
.map((v, i) => fetch(`https://pokeapi.co/api/v2/pokemon-form/${i + 1}`));
Promise.all(promises).then(pokemonArr => {
return pokemonArr.map(value =>
value
.json()
.then(({ name, sprites: { front_default: sprite } }) =>
pokemon.push({ name, sprite })
)
);
});
setOptions(pokemon);
}, []);
useEffect(() => {
window.addEventListener("mousedown", handleClickOutside);
return () => {
window.removeEventListener("mousedown", handleClickOutside);
};
});
const handleClickOutside = event => {
const { current: wrap } = wrapperRef;
if (wrap && !wrap.contains(event.target)) {
setDisplay(false);
}
};
const updatePokeDex = poke => {
setSearch(poke);
setDisplay(false);
};
return (
<div ref={wrapperRef} className="flex-container flex-column pos-rel">
<input
id="auto"
onClick={() => setDisplay(!display)}
placeholder="Type to search"
value={search}
onChange={event => setSearch(event.target.value)}
/>
{display && (
<div className="autoContainer">
{options
.filter(({ name }) => name.indexOf(search.toLowerCase()) > -1)
.map((value, i) => {
return (
<div
onClick={() => updatePokeDex(value.name)}
className="option"
key={i}
tabIndex="0"
>
<span>{value.name}</span>
<img src={value.sprite} alt="pokemon" />
</div>
);
})}
</div>
)}
</div>
);
};
function App() {
return (
<div className="App">
<h1>Custom AutoComplete React</h1>
<div className="logo"></div>
<div className="auto-container">
<Auto />
</div>
</div>
);
}
export default App;
Please Help me to figure out this.
Sounds more like a CSS problem if I'm understnading it correctly. Try adding this to your ./App.css file:
.autoContainer{
max-height: 350px; overflow-y: auto; overflow-x: hidden;
}
Here is the idea: https://codesandbox.io/s/green-flower-zeued?file=/src/App.js:1509-1522

Categories