I have created a scoreboard that updates when a user hovers over each team's card, hovering over a team's card also changes some CSS attributes to make it stand out.
The current default state value is "0", I need the default value to be equal to the top team's score.
Below is the TeamCard component that updates TopGroups with onMouseEnter:
const TeamCard = ({
data,
setDisplayGoals,
setDisplayMilestones,
setDisplayPoints,
}) => {
return (
<TeamCardStyle>
<Row className="d-flex justify-content-between">
{!data && <Spinner />}
{data &&
data.getGroupScores &&
data.getGroupScores.slice(0, 4).map((group, index) => {
return (
<Col
key={guid()}
className="teamCard mt-2 mb-2 mx-1"
onMouseEnter={() => [
setDisplayGoals(group.goalsDone),
setDisplayPoints(group.totalScore),
setDisplayMilestones(group.milestonesDone),
]}
>
<Row>
{/* <div className="arrow-down" /> */}
<p key={guid}>{seed[index]}</p>
</Row>
<Row>
<Col className="hideSmall">
<img className="mouseOn" src="../images/group.png" />
<img
className="mouseOff"
src="../images/groupSelected.png"
/>
</Col>
</Row>
<p key={guid}>{group.name.slice(0, 14)}</p>
</Col>
);
})}
</Row>
</TeamCardStyle>
);
};
This is a snippet of the TopGroups component:
const TopGroups = () => {
const currentQTR = `Q${moment().quarter()} ${moment().year()}`;
const { loading, error, data } = useQuery(GET_GROUP_SCORES, {
variables: { quarter: currentQTR },
});
if (data) {
const sortedGroups = data.getGroupScores.sort((a, b) => {
if (a.totalScore > b.totalScore) {
return -1;
}
if (a.totalScore < b.totalScore) {
return 1;
} else {
return 0;
}
});
const test = data.GoalsDone;
}
if (error) {
return <p>An error has occured</p>;
}
if (loading) {
<Spinner />;
}
const [displayGoals, setDisplayGoals] = useState("0");
const [displayPoints, setDisplayPoints] = useState("0");
const [displayMilestones, setDisplayMilestones] = useState("0");
return (
<div className="row-12">
<TeamCard
data={data}
setDisplayGoals={setDisplayGoals}
setDisplayPoints={setDisplayPoints}
setDisplayMilestones={setDisplayMilestones}
/>
I have included a picture of the app when it first loads, I would like those zeros to be the #1 Team's score, in this case it should represent Team Clown Car.
Maybe you can use something like this when you get the data
useEffect(()=>{
setDisplayGoals("first group's goals"),
setDisplayPoints("first group's points"),
setDisplayMilestones("first group's Milestones")
},[])
you get the idea. Empty brackets will cause useEffect only run on the first render.
Related
I am trying to filter an array of cars whenever any of my 2 filters changes. I run my own refilter function where I check all filters (not just the changed one), as you can see below. Any idea why it does not work? I am expecting to see cars satisfying the filters whenever I click a filter OR all the cars if no filters are applied. Currently, whenever I select a fuel type or seat capacity, all the cars disappear.
all my initializations:
const [timeRange, setTimeRange] = useState([]);
const [fuelTypes, setFuelTypes] = useState([]); // all present fuel types in my DB (initialized in first useEffect)
const [seatNumbers, setSeatNumbers] = useState([]); // all present seat capacity numbers in my DB (initialized in first useEffect)
const [selectedTimeRange, setSelectedTimeRange] = useState([]);
const [selectedFuelTypes, setSelectedFuelTypes] = useState([]); // selected fuel types (selected by the user from the antd Select.Option component)
const [selectedSeatNumbers, setSelectedSeatNumbers] = useState([]); // selected seat numbers (selected by the user from the antd Select.Option component)
const { loading } = useSelector((store) => store.alertsReducer);
const [totalCars, setTotalCars] = useState([]);
const dispatch = useDispatch();
the components whose callbacks change my states (I am using antd)
<Select
allowClear
mode="multiple"
placeholder="Fuel"
style={{ width: "10%" }}
onChange={(values) => {
setSelectedFuelTypes(values);
}}
>
{fuelTypes.map((fuelType, index) => {
return (
<Select.Option key={index} value={fuelType}>
{fuelType}
</Select.Option>
);
})}
</Select>
<Select
onChange={(values) => {
setSelectedSeatNumbers(values);
}}
allowClear
mode="multiple"
placeholder="Seats"
style={{ width: "10%" }}
>
{seatNumbers.map((seatNumber, index) => {
return (
<Select.Option key={index} value={seatNumber}>
{seatNumber}
</Select.Option>
);
})}
</Select>
all my useEffects:
useEffect(() => {
dispatch(getAllCars());
}, []);
useEffect(() => {
setFuelTypes(
[...new Set(cars.map((car) => car.fuelType.toLowerCase()))].sort()
); // all possible fuel types
setSeatNumbers(
[...new Set(cars.map((car) => car.capacity))].sort((a, b) => a - b)
); // all possible seat numbers
setTotalCars(cars); // all cars
}, [cars]);
useEffect(() => {
refilter();
}, [selectedTimeRange, selectedFuelTypes, selectedSeatNumbers]);
my refilter function
function refilter() {
setTotalCars(
cars.filter(
(car) =>
selectedFuelTypes.includes(car.fuelType) &&
selectedSeatNumbers.includes(car.capacity)
)
);
}
on startup:
on selecting a filter (no car shown):
I don't see "cars" defined anywhere and can't test it because of that. I see totalCars but not cars.
refilter has no parameters passed in, and it's using cars. Is that a global?
The problem was with the filtering logic, not the useEffects or anything else strictly related to React. Here is the full working code (could be improved, but this is a working solution):
import moment from "moment";
import { Link } from "react-router-dom";
import Spinner from "../components/Spinner";
import { Col, Row, DatePicker, Select } from "antd";
import React, { useState, useEffect, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import DefaultLayout from "../components/DefaultLayout";
import { getAllCars } from "../redux/actions/carsActions";
const { RangePicker } = DatePicker;
function Home() {
const { cars } = useSelector((store) => {
console.log("in callback:", store.carsReducer);
return store.carsReducer;
});
const [timeRange, setTimeRange] = useState([]);
const [fuelTypes, setFuelTypes] = useState([]); // all present fuel types in my DB (initialized in first useEffect)
const [seatNumbers, setSeatNumbers] = useState([]); // all present seat capacity numbers in my DB (initialized in first useEffect)
const [selectedTimeRange, setSelectedTimeRange] = useState([]);
const [selectedFuelTypes, setSelectedFuelTypes] = useState([]); // selected fuel types (selected by the user from the antd Select.Option component)
const [selectedSeatNumbers, setSelectedSeatNumbers] = useState([]); // selected seat numbers (selected by the user from the antd Select.Option component)
const { loading } = useSelector((store) => store.alertsReducer);
const [totalCars, setTotalCars] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getAllCars());
}, []);
useEffect(() => {
setFuelTypes(
[...new Set(cars.map((car) => car.fuelType.toLowerCase()))].sort()
); // all possible fuel types
setSeatNumbers(
[...new Set(cars.map((car) => car.capacity))].sort((a, b) => a - b)
); // all possible seat numbers
setTotalCars(cars); // all cars
}, [cars]);
useEffect(() => {
refilter();
}, [selectedTimeRange, selectedFuelTypes, selectedSeatNumbers]);
function refilter() {
// no need for 3 separate functions (that would mean setting the state of the totalCars variable 3 times
// using the `setTotalCars` function - which -if I am not mistaken- might trigger unexpected behavior
// with the re-rendering of the component)
// temporary array; eventually, totalCars will be assigned `temp` value using the `setTotalCars` method!
var temp = [];
// logica timeRange
if (selectedTimeRange.length === 0) {
console.log("user selected no time range");
temp = [...cars];
} else {
console.log('user selected timeRange: ', selectedTimeRange);
var selectedFrom = moment(selectedTimeRange[0], "MMM DD yyyy HH");
var selectedTo = moment(selectedTimeRange[1], "MMM DD yyyy HH");
for (var car of cars) {
if (car.bookedTimeSlots.length == 0) {
temp.push(car);
} else {
var toBeAdded = true;
for (var booking of car.bookedTimeSlots) {
if (selectedFrom.isBetween(booking.from, booking.to) ||
selectedTo.isBetween(booking.from, booking.to) ||
moment(booking.from).isBetween(selectedFrom, selectedTo) ||
moment(booking.to).isBetween(selectedFrom, selectedTo)) {
toBeAdded = false; // if time range of booking overlaps with selected time range, don't add car to temp!
break;
}
}
if (toBeAdded) {
temp.push(car);
}
}
}
}
// fuelTypes logic
if (selectedFuelTypes.length !== 0){
console.log('User selected fuel types: ', selectedFuelTypes);
temp = temp.filter((car) => selectedFuelTypes.includes(car.fuelType));
console.log(`filtered by fuelTypes: ${temp}`);
}
// seatsNumber logic
if (selectedSeatNumbers.length !== 0){
console.log('User selected seat numbers: ', selectedSeatNumbers);
temp = temp.filter((car) => selectedSeatNumbers.includes(car.capacity));
console.log(`filtered by seatsNumber: ${temp.length}`);
}
// finally, assign filtered values to totalCars!
console.log("temp:", temp);
setTotalCars(temp);
}
return (
<DefaultLayout>
<Row className="mt-3" justify="center">
<Col lg={20} sm={24} className="d-flex justify-content-left">
<RangePicker
showTime={{ format: "HH:mm" }}
format="MMM DD yyyy HH:mm"
onChange={(values) => {
setSelectedTimeRange(values);
}}
/>
<Select
allowClear
mode="multiple"
placeholder="Fuel"
style={{ width: "10%" }}
onChange={(values) => {
setSelectedFuelTypes(values);
}}
>
{fuelTypes.map((fuelType, index) => {
return (
<Select.Option key={index} value={fuelType}>
{fuelType}
</Select.Option>
);
})}
</Select>
<Select
onChange={(values) => {
setSelectedSeatNumbers(values);
}}
allowClear
mode="multiple"
placeholder="Seats"
style={{ width: "10%" }}
>
{seatNumbers.map((seatNumber, index) => {
return (
<Select.Option key={index} value={seatNumber}>
{seatNumber}
</Select.Option>
);
})}
</Select>
</Col>
</Row>
{loading === true && <Spinner />}
<Row justify="center" gutter={16}>
{totalCars.map((car) => {
return (
<Col lg={5} sm={24} xs={24}>
<div className="car p-1 bs1">
<img src={car.image} className="carimg" />
<div className="car-content d-flex align-items-center justify-content-between">
<div className="text-left pl-2">
<p>{car.name}</p>
<p>
<sup>{car.rentPerHour} eur</sup>/<sub>Hour</sub>
</p>
</div>
<div className="text-left pl-2">
<p>Seats: {car.capacity}</p>
</div>
<div className="text-left pl-2">
<p>Fuel: {car.fuelType}</p>
</div>
<div>
<button className="btn1 mr-2">
<Link to={`/booking/${car._id}`}>Book Now</Link>
</button>
</div>
</div>
</div>
</Col>
);
})}
</Row>
</DefaultLayout>
);
}
export default Home;
I am trying to filter an array of cars whenever any of my 3 filters changes. The way I see fit -for the moment- with React is to: check all filters whenever any of them changes, in this order: handleOnChangeDateFilter (where I set all my cars to the initial state if this filter is not set), handleOnChangeFuelFilter, handleOnChangeSeats. I displayed some console.logs but for some reason (I think it might be something related to rendering and when useEffect runs. It runs after every re-render OR after any of the values in the dependency array changes, right?) (PS: I know I could somehow check to compare the new values with the old values by somehow capturing the prevState, so that I do not run a specific filter again with no reason, but forgot how to capture prevState. Will do that later, as it should work fine without that now, too)
So, any idea why the below code does not work?
import moment from "moment";
import { Link } from "react-router-dom";
import Spinner from "../components/Spinner";
import { Col, Row, DatePicker, Select } from "antd";
import React, { useState, useEffect, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import DefaultLayout from "../components/DefaultLayout";
import { getAllCars } from "../redux/actions/carsActions";
import cron from "node-cron";
const { RangePicker } = DatePicker;
function Home() {
const { cars } = useSelector((state) => state.carsReducer); // state aici e de fapt store-ul nostru!
const [timeRange, setTimeRange] = useState([]);
const [fuelTypes, setFuelTypes] = useState([]);
const [seatNumbers, setSeatNumbers] = useState([]);
const { loading } = useSelector((state) => state.alertsReducer);
const [totalCars, setTotalcars] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
console.log("merge!");
dispatch(getAllCars());
}, []);
useEffect(() => {
console.log("cars bro", cars);
setFuelTypes(
[...new Set(cars.map((car) => car.fuelType.toLowerCase()))].sort()
);
setSeatNumbers(
[...new Set(cars.map((car) => car.capacity))].sort((a, b) => a - b)
);
setTotalcars(cars);
}, [cars]);
useEffect(() => {
console.log("refilter is triggered!");
refilter();
}, [timeRange, fuelTypes, seatNumbers]);
function handleOnChangeDateFilter() {
console.log("intra in handleOnChangeDateFilter", timeRange);
console.log("totalCars:", totalCars);
if (timeRange.length === 0) {
console.log(
"No date range filter specified! Will reinitialize and then proceed with other filters!"
);
setTotalcars(cars);
return;
}
var selectedFrom = moment(timeRange[0], "MMM DD yyyy HH:mm");
var selectedTo = moment(timeRange[1], "MMM DD yyyy HH:mm");
console.log("selectedFrom", selectedFrom, "selectedTo", selectedTo);
var temp = [];
for (var car of cars) {
if (car.bookedTimeSlots.length === 0) {
temp.push(car);
} else {
var toAdd = true;
for (var booking of car.bookedTimeSlots) {
if (
selectedFrom.isBetween(booking.from, booking.to) ||
selectedTo.isBetween(booking.from, booking.to) ||
moment(booking.from).isBetween(selectedFrom, selectedTo) ||
moment(booking.to).isBetween(selectedFrom, selectedTo)
) {
console.log(
`${car.name} is booked from ${booking.from} to ${booking.to}! Will NOT be added!`
);
toAdd = false;
break; // we should not add this car to the displayed cars if we found a minimum of one booking that
// intersects non-available time range
}
}
if (toAdd) temp.push(car);
}
}
setTotalcars(temp);
}
function handleOnChangeFuelFilter() {
console.log(`intra in handleOnChangeFuelFilter:`, totalCars);
if (fuelTypes === []) {
console.log("no fuel filter specified! Will leave function!");
return;
}
var temp = [];
for (var car of totalCars) {
if (fuelTypes.includes(car.fuelType.toLowerCase())) {
// console.log(`${car.name} is of type ${car.fuelType}! Will be added!`);
temp.push(car);
}
}
setTotalcars(temp);
}
function handleOnChangeSeatsFilter() {
console.log(`intra in handleOnChangeSeatsFilter:`, totalCars);
if (seatNumbers === []) {
console.log("No seat filter specified! Will leave function!");
return;
}
var temp = [];
for (var car of totalCars) {
if (seatNumbers.includes(car.capacity)) {
// console.log(`${car.name} has ${car.capacity}! Will be added!`);
temp.push(car);
}
}
setTotalcars(temp);
}
function onRangePickerFilterChange(values) {
console.log("============STARTED============");
console.log("onRangePickerFilterChange ->", values);
setTimeRange(values);
}
function onSeatsFilterChange(values) {
console.log("============STARTED============");
console.log("onSeatsFilterChange ->", values);
setSeatNumbers(values);
}
function onFuelFilterChange(values) {
console.log("============STARTED============");
console.log("onFuelFilterChange ->", values);
setFuelTypes(values);
}
function refilter() {
// console.log('values refilter:', values);
// console.log('============STARTED============');
handleOnChangeDateFilter();
console.log("AFTER DATE FILTER:", totalCars);
handleOnChangeFuelFilter();
console.log("AFTER FUEL FILTER:", totalCars);
handleOnChangeSeatsFilter();
console.log("AFTER SEATS FILTER(final):", totalCars);
console.log("============FINISHED============");
}
return (
<DefaultLayout>
<Row className="mt-3" justify="center">
<Col lg={20} sm={24} className="d-flex justify-content-left">
<RangePicker
// ref={refRangePicker}
showTime={{ format: "HH:mm" }}
format="MMM DD yyyy HH:mm"
// onChange={handleOnChangeDateFilter}
// onChange={refilter}
onChange={onRangePickerFilterChange}
/>
<Select
// onChange={handleFuelFilterChange}
// onChange={refilter}
onChange={onFuelFilterChange}
allowClear
mode="multiple"
placeholder="Fuel type"
style={{ width: "10%" }}
>
{fuelTypes.map((fuelType, index) => {
return (
<Select.Option key={index} value={fuelType}>
{fuelType}
</Select.Option>
);
})}
</Select>
<Select
// onChange={refilter}
onChange={onSeatsFilterChange}
allowClear
mode="multiple"
placeholder="Seats"
style={{ width: "10%" }}
>
{seatNumbers.map((seatNumber, index) => {
return (
<Select.Option key={index} value={seatNumber}>
{seatNumber}
</Select.Option>
);
})}
</Select>
</Col>
</Row>
{loading === true && <Spinner />}
<Row justify="center" gutter={16}>
{totalCars.map((car) => {
return (
<Col lg={5} sm={24} xs={24}>
<div className="car p-1 bs1">
<img src={car.image} className="carimg" />
<div className="car-content d-flex align-items-center justify-content-between">
<div className="text-left pl-2">
<p>{car.name}</p>
<p>
<sup>{car.rentPerHour} eur</sup>/<sub>Hour</sub>
</p>
</div>
<div className="text-left pl-2">
<p>Seats: {car.capacity}</p>
</div>
<div className="text-left pl-2">
<p>Fuel: {car.fuelType}</p>
</div>
<div>
<button className="btn1 mr-2">
<Link to={`/booking/${car._id}`}>Book Now</Link>
</button>
</div>
</div>
</div>
</Col>
);
})}
</Row>
</DefaultLayout>
);
}
export default Home;
The problem was with the filtering logic, not the useEffects or anything else strictly related to React. Here is the full working code (could be improved, but this is a working solution):
import moment from "moment";
import { Link } from "react-router-dom";
import Spinner from "../components/Spinner";
import { Col, Row, DatePicker, Select } from "antd";
import React, { useState, useEffect, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import DefaultLayout from "../components/DefaultLayout";
import { getAllCars } from "../redux/actions/carsActions";
const { RangePicker } = DatePicker;
function Home() {
const { cars } = useSelector((store) => {
console.log("in callback:", store.carsReducer);
return store.carsReducer;
});
const [timeRange, setTimeRange] = useState([]);
const [fuelTypes, setFuelTypes] = useState([]); // all present fuel types in my DB (initialized in first useEffect)
const [seatNumbers, setSeatNumbers] = useState([]); // all present seat capacity numbers in my DB (initialized in first useEffect)
const [selectedTimeRange, setSelectedTimeRange] = useState([]);
const [selectedFuelTypes, setSelectedFuelTypes] = useState([]); // selected fuel types (selected by the user from the antd Select.Option component)
const [selectedSeatNumbers, setSelectedSeatNumbers] = useState([]); // selected seat numbers (selected by the user from the antd Select.Option component)
const { loading } = useSelector((store) => store.alertsReducer);
const [totalCars, setTotalCars] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getAllCars());
}, []);
useEffect(() => {
setFuelTypes(
[...new Set(cars.map((car) => car.fuelType.toLowerCase()))].sort()
); // all possible fuel types
setSeatNumbers(
[...new Set(cars.map((car) => car.capacity))].sort((a, b) => a - b)
); // all possible seat numbers
setTotalCars(cars); // all cars
}, [cars]);
useEffect(() => {
refilter();
}, [selectedTimeRange, selectedFuelTypes, selectedSeatNumbers]);
function refilter() {
// no need for 3 separate functions (that would mean setting the state of the totalCars variable 3 times
// using the `setTotalCars` function - which -if I am not mistaken- might trigger unexpected behavior
// with the re-rendering of the component)
// temporary array; eventually, totalCars will be assigned `temp` value using the `setTotalCars` method!
var temp = [];
// logica timeRange
if (selectedTimeRange.length === 0) {
console.log("user selected no time range");
temp = [...cars];
} else {
console.log('user selected timeRange: ', selectedTimeRange);
var selectedFrom = moment(selectedTimeRange[0], "MMM DD yyyy HH");
var selectedTo = moment(selectedTimeRange[1], "MMM DD yyyy HH");
for (var car of cars) {
if (car.bookedTimeSlots.length == 0) {
temp.push(car);
} else {
var toBeAdded = true;
for (var booking of car.bookedTimeSlots) {
if (selectedFrom.isBetween(booking.from, booking.to) ||
selectedTo.isBetween(booking.from, booking.to) ||
moment(booking.from).isBetween(selectedFrom, selectedTo) ||
moment(booking.to).isBetween(selectedFrom, selectedTo)) {
toBeAdded = false; // if time range of booking overlaps with selected time range, don't add car to temp!
break;
}
}
if (toBeAdded) {
temp.push(car);
}
}
}
}
// fuelTypes logic
if (selectedFuelTypes.length !== 0){
console.log('User selected fuel types: ', selectedFuelTypes);
temp = temp.filter((car) => selectedFuelTypes.includes(car.fuelType));
console.log(`filtered by fuelTypes: ${temp}`);
}
// seatsNumber logic
if (selectedSeatNumbers.length !== 0){
console.log('User selected seat numbers: ', selectedSeatNumbers);
temp = temp.filter((car) => selectedSeatNumbers.includes(car.capacity));
console.log(`filtered by seatsNumber: ${temp.length}`);
}
// finally, assign filtered values to totalCars!
console.log("temp:", temp);
setTotalCars(temp);
}
return (
<DefaultLayout>
<Row className="mt-3" justify="center">
<Col lg={20} sm={24} className="d-flex justify-content-left">
<RangePicker
showTime={{ format: "HH:mm" }}
format="MMM DD yyyy HH:mm"
onChange={(values) => {
setSelectedTimeRange(values);
}}
/>
<Select
allowClear
mode="multiple"
placeholder="Fuel"
style={{ width: "10%" }}
onChange={(values) => {
setSelectedFuelTypes(values);
}}
>
{fuelTypes.map((fuelType, index) => {
return (
<Select.Option key={index} value={fuelType}>
{fuelType}
</Select.Option>
);
})}
</Select>
<Select
onChange={(values) => {
setSelectedSeatNumbers(values);
}}
allowClear
mode="multiple"
placeholder="Seats"
style={{ width: "10%" }}
>
{seatNumbers.map((seatNumber, index) => {
return (
<Select.Option key={index} value={seatNumber}>
{seatNumber}
</Select.Option>
);
})}
</Select>
</Col>
</Row>
{loading === true && <Spinner />}
<Row justify="center" gutter={16}>
{totalCars.map((car) => {
return (
<Col lg={5} sm={24} xs={24}>
<div className="car p-1 bs1">
<img src={car.image} className="carimg" />
<div className="car-content d-flex align-items-center justify-content-between">
<div className="text-left pl-2">
<p>{car.name}</p>
<p>
<sup>{car.rentPerHour} eur</sup>/<sub>Hour</sub>
</p>
</div>
<div className="text-left pl-2">
<p>Seats: {car.capacity}</p>
</div>
<div className="text-left pl-2">
<p>Fuel: {car.fuelType}</p>
</div>
<div>
<button className="btn1 mr-2">
<Link to={`/booking/${car._id}`}>Book Now</Link>
</button>
</div>
</div>
</div>
</Col>
);
})}
</Row>
</DefaultLayout>
);
}
export default Home;
When clicking on a specific pokemon, the user can show details of the pokemon. When the user clicks the back button to see all pokemon, I want them to continue in the exact same scroll position/pokemon, where they first clicked. How to achieve this?
Here is the pokedex component, where the user can click on each pokemon:
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { ScrollArrow } from './utils/scrollArrow';
import { Container, Card, Col, Row, Spinner } from 'react-bootstrap';
const Pokedex = () => {
const [pokemon, setPokemon] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getPokedex();
}, []);
const getPokedex = async () => {
try {
const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=151');
const { results } = await res.json();
const pokedex = results.map((pokemon: any, index: number) => {
const paddedId = ('00' + (index + 1)).slice(-3);
const image = `https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${paddedId}.png`;
return { ...pokemon, image };
});
setPokemon(pokedex);
setLoading(false);
} catch (err) {
console.error(err);
}
};
return (
<Container fluid className='pokedex'>
{loading ? (
<Spinner animation='border' role='status'>
<span className='visually-hidden'>Fetching Pokemon...</span>
</Spinner>
) : (
<Row>
{pokemon.map((pokemon: any, index: number) => (
<Col key={index} xs={12} sm={6} lg={4} xl={2} className='col'>
<Card>
<Link to={`/pokemon/${index + 1}`}>
<Card.Img src={pokemon.image} alt={pokemon.name} />
<Card.Body>
<Card.Text>
#{(index + 1).toString().padStart(3, '0')}
</Card.Text>
<Card.Title>{pokemon.name}</Card.Title>
</Card.Body>
</Link>
</Card>
</Col>
))}
</Row>
)}
<ScrollArrow />
</Container>
);
};
export default Pokedex;
Here is the pokemon component, where the user can go back to the pokedex to see all pokemon:
import { useEffect, useState } from 'react';
import { colors } from './utils/bgColor';
import {
Button,
Col,
Container,
Image,
Row,
Spinner,
ListGroup,
ProgressBar,
Tab,
Tabs,
TabContainer,
} from 'react-bootstrap';
import { useNavigate, useParams } from 'react-router-dom';
const Pokemon = () => {
const [pokemonDetails, setPokemonDetails] = useState<any>([]);
const [loading, setLoading] = useState(true);
const { id } = useParams();
const getPokemon = async (id: string | undefined) => {
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const pokemon = await res.json();
const paddedId = ('00' + id).slice(-3);
pokemon.image = `https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${paddedId}.png`;
setPokemonDetails(pokemon);
setLoading(false);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
getPokemon(id);
}, [id]);
let navigate = useNavigate();
const handleClick = () => {
navigate('/');
};
let typeName = pokemonDetails.types && pokemonDetails.types[0].type.name;
const bgColor: string = colors[typeName];
return (
<Container fluid className='pokemon' style={{ backgroundColor: bgColor }}>
{loading ? (
<Spinner animation='border' role='status'>
<span className='visually-hidden'>Fetching Pokemon...</span>
</Spinner>
) : (
<div className='details' style={{ position: 'relative' }}>
<Row>
<Col className='header'>
<h1>{pokemonDetails.name}</h1>
<h3>#{pokemonDetails.id.toString().padStart(3, '0')}</h3>
</Col>
</Row>
<Row>
<Col>
<ListGroup className='type'>
{pokemonDetails.types.map((type: any, index: number) => (
<ListGroup.Item key={index}>{type.type.name}</ListGroup.Item>
))}
</ListGroup>
</Col>
</Row>
<Row>
<Image
src={pokemonDetails.image}
alt={pokemonDetails.name}
className='pokemon-img'
/>
</Row>
<TabContainer>
<Row className='clearfix'>
<Col sm={12} className='box'>
<Tabs defaultActiveKey='stats'>
<Tab eventKey='abilities' title='Abilities'>
<ListGroup>
{pokemonDetails.abilities.map(
(ability: any, index: number) => (
<ListGroup.Item key={index}>
{ability.ability.name}
</ListGroup.Item>
)
)}
</ListGroup>
</Tab>
<Tab eventKey='stats' title='Stats'>
<ListGroup>
{pokemonDetails.stats.map((stat: any, index: number) => (
<ListGroup.Item key={index}>
{stat.stat.name}
<ProgressBar
now={stat.base_stat}
label={stat.base_stat}
/>
</ListGroup.Item>
))}
</ListGroup>
</Tab>
<Tab eventKey='moves' title='Moves'>
<ListGroup className='moves'>
{pokemonDetails.moves
.slice(0, 62)
.map((move: any, index: number) => (
<ListGroup.Item key={index}>
{move.move.name}
</ListGroup.Item>
))}
</ListGroup>
</Tab>
<Tab eventKey='evolutions' title='Evolutions' disabled>
{/* <p className='possible evolution'>
{pokemonDetails.stats.map((type: any, index: number) => (
<p key={index}>{type.type.name}</p>
))}
</p> */}
</Tab>
</Tabs>
</Col>
</Row>
</TabContainer>
<Button variant='dark' onClick={handleClick}>
Catch another Pokémon
</Button>
</div>
)}
</Container>
);
};
export default Pokemon;
I found an answer in this thread:
How do people handle scroll restoration with react-router v4?
this is in v4 and TS but still have a good solution for this
Hopefully it will help you
I have built a component CreatePost which is used for creating or editing posts,
the problem is if I render this component twice even if I upload a file from the second component they are changed in the first one, why? Here is the code:
import FileUpload from "#components/form/FileUpload";
import { Attachment, Camera, Video, Writing } from "public/static/icons";
import styles from "#styles/components/Post/CreatePost.module.scss";
import { useSelector } from "react-redux";
import { useInput, useToggle } from "hooks";
import { useRef, useState } from "react";
import StyledButton from "#components/buttons/StyledButton";
import Modal from "#components/Modal";
import { post as postType } from "types/Post";
import Removeable from "#components/Removeable";
interface createPostProps {
submitHandler: (...args) => void;
post?: postType;
isEdit?: boolean;
}
const CreatePost: React.FC<createPostProps> = ({ submitHandler, post = null, isEdit = false }) => {
console.log(post);
const maxFiles = 10;
const [showModal, setShowModal, ref] = useToggle();
const [description, setDescription] = useInput(post?.description || "");
const user = useSelector((state) => state.user);
const [files, setFiles] = useState<any[]>(post?.files || []);
const handleFileUpload = (e) => {
const fileList = Array.from(e.target.files);
if (fileList.length > maxFiles || files.length + fileList.length > maxFiles) {
setShowModal(true);
} else {
const clonedFiles = [...files, ...fileList];
setFiles(clonedFiles);
}
e.target.value = "";
};
const removeHandler = (id) => {
const filtered = files.filter((file) => file.name !== id);
setFiles(filtered);
};
return (
<div className={styles.createPost}>
<div className={styles.top}>
<span>
<img src="/static/images/person1.jpg" />
</span>
<textarea
onChange={setDescription}
className="primaryScrollbar"
aria-multiline={true}
value={description}
placeholder={`What's on your mind ${user?.name?.split(" ")[0]}`}
></textarea>
{description || files.length ? (
<StyledButton
background="bgPrimary"
size="md"
className={styles.submitButton}
onClick={() => {
if (!isEdit)
submitHandler({
files: files,
author: { name: user.name, username: user.username },
postedTime: 52345,
id: Math.random() * Math.random() * 123456789101112,
comments: [],
likes: [],
description,
});
else {
submitHandler({
...post,
description,
files,
});
}
setDescription("");
setFiles([]);
}}
>
{isEdit ? "Edit" : "Post"}
</StyledButton>
) : null}
</div>
<div className={styles.middle}>
<div className={styles.row}>
{files.map((file) => {
return (
<Removeable
key={file.name + Math.random() * 100000}
removeHandler={() => {
removeHandler(file.name);
}}
>
{file.type.includes("image") ? (
<img src={URL.createObjectURL(file)} width={150} height={150} />
) : (
<video>
<source src={URL.createObjectURL(file)} type={file.type} />
</video>
)}
</Removeable>
);
})}
</div>
</div>
<div className={styles.bottom}>
<FileUpload
id="uploadPhoto"
label="upload photo"
icon={
<span>
<Camera /> Photo
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
accept="image/*"
/>
<FileUpload
id="uploadVideo"
label="upload video"
icon={
<span>
<Video /> Video
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
accept="video/*"
/>
<FileUpload
id="writeArticle"
label="write article"
icon={
<span>
<Writing /> Article
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
/>
</div>
{showModal && (
<Modal size="sm" backdrop="transparent" ref={ref} closeModal={setShowModal.bind(null, false)} yPosition="top">
<p>Please choose a maximum of {maxFiles} files</p>
<StyledButton size="md" background="bgPrimary" onClick={setShowModal.bind(null, false)}>
Ok
</StyledButton>
</Modal>
)}
</div>
);
};
export default CreatePost;
Now on my main file I have:
const Main = () => {
const [posts, setPosts] = useState<postType[]>([]);
const addPost = (post: postType) => {
setPosts([post, ...posts]);
};
const editPost = (post: postType) => {
const updated = posts.map((p) => {
if (post.id === post.id) {
p = post;
}
return p;
});
setPosts(updated);
};
const deletePost = (id) => {
const filtered = posts.filter((post) => post.id !== id);
setPosts(filtered);
};
return (
<>
<CreatePost submitHandler={addPost} key="0" />
<CreatePost submitHandler={addPost} key="1"/>
{posts.map((post) => {
return <PostItem {...post} editHandler={editPost} key={post.id} deleteHandler={deletePost.bind(null, post.id)} />;
})}
</>
);
};
export default Main;
I tried to add/remove the key but doesn't change anything, also tried to recreate this problem in a simpler way in sandbox but I can't it works fine there. And the problem is only when I upload files not when I write text inside the <textarea/>
Note: The second in reality is shown dynamically inside a modal when clicked edit in a post, but I just showed it here for simplicity because the same problem occurs in both cases.
Okay after some hours of debugging I finally found the problem.
Because my <FileUpload/> uses id to target the input inside the <CreatePost/> the <FileUpload/> always had same it, so when I used <CreatePost/> more than 1 time it would target the first element that found with that id that's why the first component was being updated
I am new to react, I am working on a project. I need little help regarding the filtering of the data. There are two filters one-: State and Location. The location is dependent on State, which is working fine, but after that, I want to filter my card component i.e VideoCategory card basis on above-selected value. There are three levels of filtering and for the third level, the filtering is not working.
Please, anyone, help me out, as I am trying to fix this issue from last two days.
import React, { useState, useEffect } from "react";
import Menu from "../common/Menu";
import { Form, Container, Row, Col } from "react-bootstrap";
import {
getVideosBasedOnCategories,
getAllStates,
} from "./helper/userApiCalls";
import VideoCard from "../common/VideoCard";
const CategoryVideos = ({ match }) => {
const [videoCategory, setVideoCategory] = useState([]);
const [selectState, setSelectState] = useState([]);
const [selectLocation, setSelectLocation] = useState([]);
let location="";
const preload = (categoryId) => {
getVideosBasedOnCategories(categoryId).then((data) => {
if (data.error || !data) {
console.log(data.error);
return <h1>No Data to Show Now</h1>;
} else {
setVideoCategory(...videoCategory, data);
}
});
};
//Intial Loading
useEffect(() => {
preload(match.params.categoryId);
getAllStateForSelect();
}, []);
//getting data for first Select Component from API
const getAllStateForSelect = () => {
getAllStates().then((data) => {
console.log(data);
if (data.error) {
return console.log(data.error);
} else {
setSelectState(...selectState, data);
}
});
};
const handleChange = (event) => {
console.log(event.target.value);
setSelectLocation(event.target.value);
};
const onLocationChange=(event)=>{
location = event
console.log(location)
}
//Storing Location into Option for Second Select
const onSplit = (x) => {
var arr = [];
for (let i = 0; i < x.citynames.length; i++) {
arr.push(
<option key={i} value={x.citynames[i]}>
{x.citynames[i]}
</option>
);
}
return arr;
};
return (
<div>
<Menu />
<Container style={{ marginTop: "200px" }}>
<Form>
<Form.Group controlId="exampleForm.SelectCustom">
<Form.Label>Select State</Form.Label>
<Form.Control
as="select"
onChange={handleChange.bind(selectState[0])}
custom
>
{selectState.map((data, index) => (
<option name="setmap" key={index} value={data._id}>
{data.statename}
</option>
))}
</Form.Control>
</Form.Group>
</Form>
<Form>
<Form.Group controlId="exampleForm.SelectCustom">
<Form.Label>Select Location</Form.Label>
<Form.Control
as="select"
onChange={(e) => onLocationChange(e.target.value)}
custom
>
{selectState
.filter((selectState) => selectState._id.includes(selectLocation))
.map((e) => onSplit(e))}
</Form.Control>
</Form.Group>
</Form>
<Row>
{videoCategory.filter(videocard=>videocard.location.includes(location.toString()))
.map((videocard) => {
return (
<Col lg={4} xs={12} md={12} className="py-3 px-3">
<VideoCard videoCategory={videocard}
/>
</Col>
);
})}
</Row>
</Container>
</div>
);
};
export default CategoryVideos;
VideoCategory File
import React, { Fragment,useEffect,useState } from "react";
import { Card, Button, Container, Row, Col } from "react-bootstrap";
import {getStateById} from '../user/helper/userApiCalls'
const VideoCard = ({ videoCategory }) => {
const [state,setState] = useState("");
const [city,setCity] = useState("");
//const { name, link, description } = videoCategory;
const getStateByFromVideoId = (stateId)=>{
getStateById(stateId).then((data)=>{
console.log(data)
if(data.error){
return console.log(data.error)
}
else{
setState(data.statename)
}
})
}
useEffect(() => {
getStateByFromVideoId(videoCategory.state);
}, []);
return (
<Container fluid>
<iframe
src={videoCategory.link}
width="300px"
height="300px"
id="myId"
className="myClassname"
display="initial"
position="relative"
allowfullscreen="allowfullscreen"
></iframe>
<Card style={{ width: "300px", }}>
<Card.Body>
<Card.Title>{videoCategory.name}</Card.Title>
<Card.Text>{videoCategory.description}</Card.Text>
<Card.Text>{state}</Card.Text>
<Card.Text>{videoCategory.location}</Card.Text>
<Button variant="primary">Go somewhere</Button>
</Card.Body>
</Card>
</Container>
);
};
export default VideoCard;
**UPDATE**
The API structure for State, which will reflect in card, the location is hardcoded, it doesn't have any id.
{
"_id": "5eb59d177693b6f99404f4c6",
"link": "https://www.youtube.com/embed/NEIwl93Yr8o",
"description": "LifeStyle",
"name": "Testing",
"state": "5eb564ec7693b6f99404f4c5",
"category": "5ead7555fb5c440f458e625b",
"location": "Kondapur",
"createdAt": "2020-05-08T17:55:35.731Z",
"updatedAt": "2020-05-08T18:28:43.635Z",
"__v": 0
},
** The Filter of State and location(citynames) is :**
{
"citynames": [
"ABC",
"EFG"
],
"_id": "5eb2ad8b554215be68773cf1",
"statename": "Madras",
"createdAt": "2020-05-06T12:28:59.149Z",
"updatedAt": "2020-05-06T12:28:59.149Z",
"__v": 0
},
Here's i change some code, but there's also some notes i need to confirm first
import React, { useState, useEffect } from "react";
import Menu from "../common/Menu";
import { Form, Container, Row, Col } from "react-bootstrap";
import {
getVideosBasedOnCategories,
getAllStates,
} from "./helper/userApiCalls";
import VideoCard from "../common/VideoCard";
const CategoryVideos = ({ match }) => {
const [videoCategory, setVideoCategory] = useState([]);
const [apiStates, setApiStates] = useState([])
const [selectedState, setSelectedState] = useState(null);
// Change default value to '' i think
const [selectedLocation, setSelectedLocation] = useState(null);
const preload = (categoryId) => {
getVideosBasedOnCategories(categoryId).then((data) => {
if (data.error || !data) {
console.log(data.error);
return <h1>No Data to Show Now</h1>;
} else {
setVideoCategory(...videoCategory, data);
}
});
};
//Intial Loading
useEffect(() => {
preload(match.params.categoryId);
getAllStateForSelect();
}, []);
//getting data for first Select Component from API
const getAllStateForSelect = () => {
getAllStates().then((data) => {
console.log(data);
if (data.error) {
return console.log(data.error);
} else {
setApiStates(...apiStates, data);
// remove this line
// setSelectedLocation(data[0])
}
});
};
const onStateChange = event => {
setSelectedState(event.target.value)
}
const onLocationChange= event => {
setSelectedLocation(event.target.value);
}
//Storing Location into Option for Second Select
const onSplit = (x) => {
var arr = [];
for (let i = 0; i < x.citynames.length; i++) {
arr.push(
<option key={i} value={x.citynames[i]}>
{x.citynames[i]}
</option>
);
}
return arr;
};
return (
<div>
<Menu />
<Container style={{ marginTop: "200px" }}>
<Form>
<Form.Group controlId="exampleForm.SelectCustom">
<Form.Label>Select State</Form.Label>
<Form.Control
as="select"
onChange={onStateChange}
custom
>
{apiStates.map((data, index) => (
<option name="setmap" key={index} value={data._id}>
{data.statename}
</option>
))}
</Form.Control>
</Form.Group>
</Form>
<Form>
<Form.Group controlId="exampleForm.SelectCustom">
<Form.Label>Select Location</Form.Label>
<Form.Control
as="select"
onChange={onLocationChange}
custom
>
{apiStates
.filter(apiState => apiState._id === selectedState)
.map(value => onSplit(value))}
</Form.Control>
</Form.Group>
</Form>
<Row>
// I'm curious about the location, is it array of string ?
{videoCategory.filter(videocard => videocard.location === selectedLocation))
.map((videocard) => {
return (
<Col lg={4} xs={12} md={12} className="py-3 px-3">
<VideoCard videoCategory={videocard} />
</Col>
);
})}
</Row>
</Container>
</div>
);
};
export default CategoryVideos;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>