I am trying to achieve cart functionality, but I want to save the cart on the refresh of the page. Anyway, the problem is this I have Context Api and cart logic (add to cart, removeitem, amount, setCartItem) so when I try to add an item to the cart, nothing happens, and when I check the cart nothing is in there. I know this question has a lot of code, and the mistake could be anywhere, if someone could help I would be really grateful.
Cart Context
import React from "react";
function getCartFromLocalStorage() {
return localStorage.getItem("cart")
? JSON.parse(localStorage.getItem("cart"))
: [];
}
const CartContext = React.createContext();
function CartProvider({ children }) {
const [cart, setCart] = React.useState(getCartFromLocalStorage());
const [total, setTotal] = React.useState(0);
const [cartItems, setCartItems] = React.useState(0);
React.useEffect(() => {
localStorage.setItem("cart", JSON.stringify(cart));
let newTotal = cart.reduce((total, cartItem) => {
return (total += cartItem.amount * cartItem.price);
}, 0);
newTotal = parseFloat(newTotal.toFixed(2));
setTotal(newTotal);
let newCartItems = cart.reduce((total, cartItem) => {
return (total += cartItem.amount);
}, 0);
setCartItems(newCartItems);
}, [cart]);
const removeItem = key => {
setCart([...cart].filter(item => item.key !== key));
};
const addToCart = book => {
const { key, image, bookName, by } = book;
const item = [...cart].find(item => item.key === key);
}
const clearCart = () => {
setCart([]);
};
return (
<CartContext.Provider
value={{
cart,
cartItems,
total,
removeItem,
addToCart,
clearCart
}}
>
{children}
</CartContext.Provider>
);
}
export { CartContext, CartProvider };
Books
import React,{useContext} from 'react'
import { CartContext } from '../../context/cart';
import HoverBooks from './HoverBooks';
import { useHistory } from "react-router-dom";
const Books = ({category}) => {
const {addToCart }= useContext(CartContext)
return (
<div className='books__main'>
{category.slice(0, 5).map((book) => {
return(
<>
<HoverBooks
key={book.key}
{...book}
/>
<div className='book__content'>
<li>{book.bookName}</li>
<h4>By{book.by}</h4>
<h4>Narreted by:{book.Narreted}</h4>
<h4>Length: {book.length}</h4>
<h4>Release Date: {book.ReleaseDate}</h4>
<h4>Language: {book.Language}</h4>
<h4>{book.rating}</h4>
</div>
<div>
<span>Regular Price: {book.RegularPrice}</span>
<button onClick={() => {
addToCart(book);
}}
>Add to cart</button>
</div>
</>
)})}
</div>
)
}
export default Books
Cart
import React from "react";
import { Link } from "react-router-dom";
import EmptyCart from "./EmptyCart";
import CartItem from './CartItem'
import { UserContext } from "../../context/user";
import { CartContext } from "../../context/cart";
import './Cart.css'
export default function Cart() {
const { cart, total } = React.useContext(CartContext);
const { user } = React.useContext(UserContext);
if (cart.length === 0) {
return <EmptyCart />;
}
return (
<div className="cart__items">
<h2>your cart</h2>
{cart.map(item => {
return <CartItem key={item.key} {...item} />;
})}
<h2>total : ${total}</h2>
{user.token ? (
<Link to="/cart" className="btn">
</Link>
) : (
<Link to="/login" className="btn">
login
</Link>
)}
</div>
);
}
Cart Item
import React, { useContext } from "react";
import { CartContext } from "../../context/cart";
export default function CartItem({ key, image,bookName, amount }) {
const {removeItem} = useContext(CartContext)
return (
<div className="cart__item">
<img src={image} />
<div>
<h4>{bookName}</h4>
<h5>${by}</h5>
<button
className="cart__removebtn"
onClick={() => {
removeItem(key);
}}
>
remove
</button>
</div>
<p className="item__amount">{amount}</p>
</div>
);
}
Cart Link
import React from "react";
import { Link } from "react-router-dom";
import {FiShoppingCart} from 'react-icons/fi'
import { CartContext } from "../../context/cart";
export default function CartLink() {
const { cartItems } = React.useContext(CartContext);
return (
<div className="cartlink__container">
<Link to="/cart">
<FiShoppingCart />
</Link>
<span className="cartlink__total">{cartItems}</span>
</div>
);
}
Home
import React,{useState, useEffect, useContext} from 'react'
import './Home.css'
import Books from './Books'
import { BookContext } from "../../context/books";
const Home = () => {
const {data, handleSelectCategory, currentSelectedCategory }
=useContext(BookContext)
return (
<div className='books__container' >
<h1 className='categories'>Categories</h1>
{Object.keys(data).map((key, index)=>{
let books = data[key];
return (
<>
<span key={key} onClick={() => handleSelectCategory(key)}
className='books__list' >
{books[0].category}
</span>
</>
);})}
<Books category={currentSelectedCategory} />
</div>
)
}
export default Home
book
import React, {useState, useEffect} from 'react'
import URL from '../utilis/URL';
const BookContext = React.createContext();
export default function BooksProvider({ children }) {
const [data, setData] = useState([])
const [currentSelectedCategory, setCurrentSelectedCategory] = useState([]);
const handleSelectCategory = (category) => {
setCurrentSelectedCategory(data[category]);
};
const fetchData = async () => {
const response = await fetch(URL);
const result = await response.json();
console.log(result)
setCurrentSelectedCategory(result[Object.keys(result)[0]]);
setData(result);
};
useEffect(()=>{
fetchData();
},[])
return (
<BookContext.Provider value={{ data, handleSelectCategory, setCurrentSelectedCategory, currentSelectedCategory }}>
{children}
</BookContext.Provider>
);
}
export {BookContext, BooksProvider}
function onUpdate() {
localStorage.setItem("cart", JSON.stringify(cart));
let newTotal = cart.reduce((total, cartItem) => {
return (total += cartItem.amount * cartItem.price);
}, 0);
newTotal = parseFloat(newTotal.toFixed(2));
setTotal(newTotal);
let newCartItems = cart.reduce((total, cartItem) => {
return (total += cartItem.amount);
}, 0);
setCartItems(newCartItems);
}
React.useEffect(onUpdate, [cart]);
const addToCart = (book) => {
const { key, image, bookName, by } = book;
let item = cart.find((item) => item.key === key);
if (item) {
item.amount++;
onUpdate();
} else {
setCart(
cart.concat({
amount: 1,
price: book.RegularPrice,
// add other cartItem attributes.
...book
})
);
}
};
Related
I am developing an app with react and redux toolkit. I am using an API. the part that I would like help with is this:
I have a favorite Icon. this icon takes a copy of the movie and displays it in the favorite section. There is another icon which is for watched movies. so the idea is that if I click on watched it should disable only the favorite icon of the movie card I clicked. however, it disables all favorite icons for all movie cards. The watched icon is referred to as Eye the favorite icon is referred to as star
This is the eye component (Watched)
import { useState } from "react";
import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import {
add_watched,
remove_watched,
add_occupied,
remove_occupied,
} from "../features/animeSlice";
const Eye = ({ anime, type }) => {
const [active_eye, setActive_eye] = useState(false);
const dispatch = useDispatch();
const toggle_eye = () => {
if (!active_eye) {
setActive_eye((prev) => {
return !prev;
});
dispatch(add_watched(anime));
dispatch(add_occupied(type));
} else {
setActive_eye((prev) => {
return !prev;
});
dispatch(remove_watched(anime?.mal_id));
dispatch(remove_occupied());
}
};
return (
<div>
{!active_eye ? (
<BsEyeSlash
className="text-xl text-green-500 icons_1"
onClick={toggle_eye}
/>
) : (
<BsEyeFill
className={"text-xl text-red-500 icons_1"}
onClick={toggle_eye}
/>
)}
</div>
);
};
export default Eye;
This is the Star Component (Favorite)
import { useState } from "react";
import { AiOutlineStar, AiFillStar } from "react-icons/ai";
import { add_favourite, remove_favourite } from "../features/animeSlice";
import { useDispatch, useSelector } from "react-redux";
const Star = ({ anime }) => {
const { value} = useSelector((state) => state.anime);
const [active_star, setActive_star] = useState(false);
const dispatch = useDispatch();
const toggle_star = () => {
if (!active_star) {
setActive_star((prev) => {
return !prev;
});
dispatch(add_favourite(anime));
} else {
setActive_star((prev) => {
return !prev;
});
dispatch(remove_favourite(anime?.mal_id));
}
};
return (
<div>
{!active_star ? (
<AiOutlineStar
className={
value === "occupied"
? "text-xl text-gray-300 pointer-events-none"
: "text-xl text-yellow-500 icon_1"
}
onClick={toggle_star}
/>
) : (
<AiFillStar
className={
value === "occupied"
? "text-xl text-gray-300 pointer-events-none"
: "text-xl text-yellow-500 icon_1"
}
onClick={toggle_star}
/>
)}
</div>
);
};
export default Star;
this is the redux toolkit slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
favourite: [],
watched: [],
value: "",
page:""
};
const animeSlice = createSlice({
name: "anime",
initialState,
reducers: {
add_favourite(state, { payload }) {
state.favourite.push(payload);
},
remove_favourite(state, { payload }) {
state.favourite = state.favourite.filter(
(anime) => anime?.mal_id !== payload
);
},
add_watched(state, { payload }) {
state.watched.push(payload);
},
remove_watched(state, { payload }) {
state.watched = state.watched.filter((anime) => anime?.mal_id !== payload);
},
add_occupied(state, { payload }) {
state.value = payload;
},
remove_occupied(state) {
state.value = "";
},
pageNumber(state, { payload }) {
state.page = payload
}
},
});
export const {
add_favourite,
remove_favourite,
add_watched,
remove_watched,
add_occupied,
remove_occupied,
pageNumber
} = animeSlice.actions;
export default animeSlice.reducer;
This is the component that holds the component of eye and star
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import Eye from "./Eye";
import Star from "./Star";
const TopAnime = ({ anime }) => {
//pagination
//
//
let colorYear = (year) => {
if (year === "N/A") {
return "text-violet-500";
} else {
return "";
}
};
return (
<section className="grand px-4 pt-2 pb-5 w-64 bg-white/5 bg-opacity-80 backdrop-blur-sm rounded-lg cursor-pointer font-poppins animate-slideup">
<div className="pb-1 wrapper_icons">
<div className="wrapper_hover">
<Eye anime={anime} type="occupied" />
<Star anime={anime} />
</div>
</div>
<Link to={`/anime/${anime?.mal_id}`}>
<div className="wrapper_1 flex flex-col items-center justify-center">
<div className="h-[313px] w-[219px]">
<img
src={anime?.images?.jpg?.large_image_url}
alt={anime?.title}
className="h-full w-full"
/>
</div>
</div>
<div className="flex flex-col mt-3">
<p className="text-sm text-white truncate mx-1">
{anime?.title_english ? anime?.title_english : anime?.title}
</p>
<div className="flex justify-between items-center text-sm text-yellow-500 mx-1">
<p className={colorYear(anime?.year ? anime?.year : "N/A")}>
{anime?.year ? anime?.year : "N/A"}
</p>
<p
className={
anime?.score <= 7
? "text-cyan-500"
: anime?.score <= 5
? "text-red-600"
: "text-green-500"
}
>
{anime?.score}
</p>
</div>
</div>
</Link>
</section>
);
};
export default TopAnime;
This is where TopAnime is rendered
import { useGetAnimeQuery } from "../features/API";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import TopAnime from "../components/TopAnime";
import Spinner from "../components/Spinner";
import { NavLink, Link } from "react-router-dom";
import Paginator from "../components/Paginator";
//import ReactPaginate from "react-paginate";
import Pagination from "#mui/material/Pagination";
const Home = () => {
const [page, setPage] = useState(1);
const {
data: manga = [],
isLoading,
isFetching,
error,
} = useGetAnimeQuery(page);
const { data, pagination } = manga;
//destructuring objects
//const { data, pagination } = manga;
//const top_anime = data;
const total = Math.ceil(pagination?.items?.total / 24)
//const current_page = pagination?.current_page;
//const per_page = pagination?.items?.per_page;
//const { items: pages } = total;
/* let fetchData = async (page = 1) => {
let res = await fetch(
`https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
);
let query = await res.json();
const { data, pagination } = query;
let totalPages = Math.ceil(pagination?.items.total / 24);
setPageCount(totalPages);
setData(data);
};
useEffect(() => {
fetchData();
}, []);
*/
import { useGetAnimeQuery } from "../features/API";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import TopAnime from "../components/TopAnime";
import Spinner from "../components/Spinner";
import { NavLink, Link } from "react-router-dom";
import Paginator from "../components/Paginator";
//import ReactPaginate from "react-paginate";
import Pagination from "#mui/material/Pagination";
const Home = () => {
const [page, setPage] = useState(1);
const {
data: manga = [],
isLoading,
isFetching,
error,
} = useGetAnimeQuery(page);
const { data, pagination } = manga;
//destructuring objects
//const { data, pagination } = manga;
//const top_anime = data;
const total = Math.ceil(pagination?.items?.total / 24)
//const current_page = pagination?.current_page;
//const per_page = pagination?.items?.per_page;
//const { items: pages } = total;
/* let fetchData = async (page = 1) => {
let res = await fetch(
`https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
);
let query = await res.json();
const { data, pagination } = query;
let totalPages = Math.ceil(pagination?.items.total / 24);
setPageCount(totalPages);
setData(data);
};
useEffect(() => {
fetchData();
}, []);
*/
const handleChange = (event, value) => {
setPage(value);
};
const display = data?.map((anime) => (
<TopAnime anime={anime} key={anime.mal_id} />
));
//const pageCount = Math.ceil(pagination?.items?.total / 24);
if (isLoading) {
return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
} else if (isFetching) {
return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
} else if (error) {
console.log("ERROR =>", error.message);
}
return (
<section className="bg-gradient-to-r from-[#141e30] to-[#243b55]">
<div className="container font-poppins">
<div className="grid grid-cols-4 gap-3 place-items-center px-20">
{/* {top_anime &&
top_anime?.map((anime) => (
<TopAnime anime={anime} key={anime.mal_id} />
))} */}
{display}
</div>
<div className="button text-yellow-500 flex items-center justify-center mt-2 pb-2 cursor-pointer">
{/* <Paginator paginated={paginated} NumP={pagination?.last_visible_page} /> */}
{/* <ReactPaginate
previousLabel={"Previous"}
nextLabel={"Next"}
onPageChange={(page) => fetchData(page.selected + 1)}
pageCount={pageCount}
className="flex space-x-2"
activeClassName="active"
/> */}
<Pagination count={total} page={page} onChange={handleChange} defaultPage={1} boundaryCount={3} color="secondary" sx={{button:{color:'#ffffff'}}} />
</div>
</div>
</section>
);
};
export default Home;
Issue
The issue with the code/current implementation is that there is only one state.anime.value and one "occupied" value. Each time a movie's "watched" status is toggled the Redux state sets/unsets the single state.anime.value state. In other words, you can toggle N movies "watched" and the state.anime.value value is "occupied", but then toggling just 1 movie back to "unwatched" and state.anime.value is reset back to "". All the Star components read this single state value and this is why the stars all toggle together.
Solution
If you want a specific movie to toggle the Star component when the Eye icon is toggled then I think the solution is a bit simpler than you are making it out to be in your code.
The "eye" and "star" components effectively duplicate your Redux state and don't synchronize well with it. It's often considered a React anti-pattern to duplicate state. The state.anime.favourite and state.anime.watched arrays are all the app needs to know what has been marked "watched" and what has been favourited.
Update the animeSlice to store the anime.mal_id properties in objects instead of arrays to provide O(1) lookup in the UI. Remove the anime.value state as it's unnecessary.
animeSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
favourite: {},
watched: {},
page: ""
};
const animeSlice = createSlice({
name: "anime",
initialState,
reducers: {
add_favourite(state, { payload }) {
state.favourite[payload] = true;
},
remove_favourite(state, { payload }) {
delete state.favourite[payload];
},
add_watched(state, { payload }) {
state.watched[payload] = true;
delete state.favourite[payload]; // un-star when adding as watched
},
remove_watched(state, { payload }) {
delete state.watched[payload];
},
pageNumber(state, { payload }) {
state.page = payload;
}
}
});
export const {
add_favourite,
remove_favourite,
add_watched,
remove_watched,
pageNumber
} = animeSlice.actions;
export default animeSlice.reducer;
Update the Eye and Star components to read the true state from the store. These components should pass anime.mal_id to the dispatched actions and use anime.mal_id to check the watched/favourites map objects.
Eye
import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import { add_watched, remove_watched } from "../features/animeSlice";
const Eye = ({ anime }) => {
const dispatch = useDispatch();
const { watched } = useSelector((state) => state.anime);
const isWatched = watched[anime.mal_id];
const toggle_eye = () => {
dispatch((isWatched ? remove_watched : add_watched)(anime.mal_id));
};
const Eye = isWatched ? BsEyeFill : BsEyeSlash;
const className = [
"text-xl icons_1",
isWatched ? "text-green-500" : "text-red-500"
].join(" ");
return (
<div>
<Eye className={className} onClick={toggle_eye} />
</div>
);
};
export default Eye;
Star
import { AiOutlineStar, AiFillStar } from "react-icons/ai";
import { add_favourite, remove_favourite } from "../features/animeSlice";
import { useDispatch, useSelector } from "react-redux";
const Star = ({ anime }) => {
const { favourite, watched } = useSelector((state) => state.anime);
const dispatch = useDispatch();
const isFavorited = favourite[anime.mal_id];
const isWatched = watched[anime.mal_id];
const toggle_star = () => {
dispatch((isFavorited ? remove_favourite : add_favourite)(anime.mal_id));
};
const Star = isFavorited ? AiFillStar : AiOutlineStar;
const className = [
"text-xl",
isWatched ? "text-gray-300 pointer-events-none" : "text-yellow-500 icon_1"
].join(" ");
return (
<div>
<Star className={className} onClick={toggle_star} />
</div>
);
};
export default Star;
sorry for the long post, but I'm really stuck in this part of my project. I'm using React to build a notes app, and I can't get my note to update when I alter the text inside of my modal popup...the commented out parts are where I'm having trouble (I wrote what I thought of doing, but don't know how. I've been using React for less than a week). Thank you in advance!
App.js
import React, { useState } from "react";
import { nanoid } from "nanoid";
import NotesList from "./Components/NotesList";
import AddNote from "./Components/AddNote";
const App = () => {
const [notes, setNotes] = useState([
/*empty array of notes*/
]);
const addNote = (title, text) => {
const date = new Date();
const newNote = {
id: nanoid(),
title: title,
text: text,
date: date.toDateString(),
};
const newNotes = [...notes, newNote];
setNotes(newNotes);
};
const deleteNote = (id) => {
const newNotes = notes.filter((note) => note.id !== id);
setNotes(newNotes);
};
const editNote = (text, title, id) => {
const editDate = new Date();
const editedNote = {
id: nanoid(),
title: title,
text: text,
date: editDate.toDateString(),
};
const editedNotes = notes.map((note) => {
if (note.id == id) {
return editedNote;
}
return note;
});
setNotes(editedNotes);
};
return (
<div className="container">
<AddNote handleAddNote={addNote} />
<NotesList
notes={notes}
handleAddNote={addNote}
handleDeleteNote={deleteNote}
editNote={editNote}
updateNote={updateNote}
/>
</div>
);
};
export default App;
NotesList.js
import Note from "./Note";
import React, { useState } from "react";
import MyModal from "./Modal";
const NotesList = ({ notes, handleDeleteNote, updateNote })=> {
const [open, setOpen] = useState(false);
const [note, setNote] = useState({});
const handleOpen = (id) => {
setNote(notes.filter((note) => note.id === id)[0]);
setOpen(true);
};
const handleClose = () => {
setNote({});
setOpen(false);
};
const clickedTodo = useState({});
return (
<div className="notes-list">
<div className="blank-note"></div>
<div className="existing-notes">
<MyModal
note={note}
clickedTodo={clickedTodo}
handleClose={handleClose}
open={open}
updateNote={updateNote}
/>
{notes.map((note) => (
<Note
id={note.id}
title={note.title}
text={note.text}
date={note.date}
handleOpen={handleOpen}
handleDeleteNote={handleDeleteNote}
/>
))}
</div>
</div>
);
};
export default NotesList;
AddNote.js
import { useState } from "react";
import React from "react";
const AddNote = ({ handleAddNote, isUpdate, note, updateNote }) => {
const [noteTitle, setNoteTitle] = useState("");
const [noteText, setNoteText] = useState("");
const characterLimit = 300;
const handleChange = (event) => {
if (characterLimit - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
};
const handleChange2 = (event) => {
if (characterLimit - event.target.value.length >= 0) {
setNoteTitle(event.target.value);
}
};
const handleSaveClick = () => {
// if (!isUpdate) then do this
// if (!isUpdate) {
(noteText.trim().length > 0)
handleAddNote(noteTitle, noteText);
setNoteText("");
setNoteTitle("");
// } else updateNote(note.id)
// else we use all of the info we have gotten
// we call update note with note.id
};
return (
<div className="note new">
<h2>Add note</h2>
<textarea className="new-text-title"
placeholder={!isUpdate?"Add a title...":note.title}
value={noteTitle}
onChange={handleChange2}
></textarea>
<textarea className="new-text-body"
cols="8"
rows="10"
placeholder={!isUpdate?"Type your note here...":note.text}
value={noteText}
onChange={handleChange}
></textarea>
<div className="note-footer">
<small>Characters remaining: {characterLimit - noteText.length}</small>
<button className="save" onClick={handleSaveClick}>
<strong>Save</strong>
</button>
</div>
</div>
);
};
export default AddNote;
Note.js
import { MdDeleteForever } from "react-icons/md";
const Note = (props) => {
const { id, title, text, date, handleOpen, handleDeleteNote } = props;
const handleOpenModal = (id) => {
handleOpen(id);
};
return (
<div className="note">
<div className="note-upper" onClick={() => handleOpenModal(id)}>
<p className="title">
<textarea className="text-title">{title}</textarea>
</p>
<textarea className="text-body">{text}</textarea>
</div>
<div className="note-footer">
<div className="footer-left" onClick={() => handleOpenModal(id)}>
<small>{date}</small>
</div>
<div className="footer-right">
<MdDeleteForever
onClick={() => {
if (window.confirm("Delete this note?")) {
handleDeleteNote(id);
}
}}
className="delete-icon"
size="1.3em"
/>
</div>
</div>
</div>
);
};
export default Note;
Modal.js
import React, { useState } from "react";
import { Modal, Box } from "#mui/material";
import Note from "./Note";
export default function MyModal(props) {
const { open, handleClose, note, updateNote } = props;
return (
<div onClose={console.log("closing")}>
<Modal open={open} onClose={handleClose}>
<Box
sx={{
position: "relative",
top: "50%",
left: "32%",
outline: "none",
}}
>
<Note
updateNote={updateNote}
note={note}
id={note.id}
title={note.title}
text={note.text}
date={note.date}
/>
</Box>
</Modal>
</div>
);
}
Your Modal.js requires updateNote prop, but in App.js you don't supply it to NotesList.js, thus the parent prop of MyModal.js has no such prop:
<NotesList
notes={notes}
handleAddNote={addNote}
handleDeleteNote={deleteNote}
editNote={editNote}
/>
And later in NotesList.js:
<MyModal
note={note}
clickedTodo={clickedTodo}
handleClose={handleClose}
open={open}
updateNote={updateNote} // this is missing because you are not
// passing this prop from the App component
/>
I am trying to toggle view between list of meals and meal details. I have placed a button in the child component Meal.js to the Meals.js which is meant to be the list and the details view.
Can you please help me fix this issue. Seems like its not working even with the conditional rendering method I've used in the code below.
Meal.js
import { useState } from 'react'
import './Meal.css'
const Meal = (props) => {
const [isToggled, setIsToggled] = useState(false);
const sendIdHandler = () => {
if (isToggled === true) {
setIsToggled(false);
}
else {
setIsToggled(true);
}
props.onSaveIdHandler(props.id, isToggled)
}
return (
<div
className='meal'
onClick={sendIdHandler}
>
{props.label}
</div>
);
}
export default Meal;
Meals.js
import Meal from './Meal/Meal'
const Meals = (props) => {
let toggleCondition = false;
const saveIdHandler = (data, isToggled) => {
toggleCondition = isToggled;
const mealDetails = props.mealsMenuData.findIndex(i =>
i.id === data
)
console.log(mealDetails, toggleCondition)
}
return (
<div>
{toggleCondition === false &&
props.mealsMenuData.map(item =>
<Meal
key={item.id}
id={item.id}
label={item.label}
onSaveIdHandler={saveIdHandler}
/>
)
}
{toggleCondition === true &&
<div>Horray!</div>
}
</div>
);
}
export default Meals;
UPDATE
Finally figured how to do this properly. I put the condition true/false useState in the parent instead and have Meal.js only send the id I need to view the item
Code is below..
Meals.js
import { useState } from 'react'
import Meal from './Meal/Meal'
import MealDetails from './MealDetails/MealDetails'
const Meals = (props) => {
const [show, setShow] = useState(false);
const [mealId, setMealId] = useState(0);
const saveIdHandler = (data) => {
setShow(true);
setMealId(props.mealsMenuData.findIndex(i =>
i.id === data)
)
console.log(props.mealsMenuData[mealId].ingridients)
}
const backHandler = () => {
setShow(false)
}
return (
<div>
{show === false &&
props.mealsMenuData.map(item =>
<Meal
key={item.id}
id={item.id}
label={item.label}
onSaveIdHandler={saveIdHandler}
/>
)
}
{show === true &&
<div>
<MealDetails data={props.mealsMenuData[mealId]} />
<button onClick={backHandler}>Back</button>
</div>
}
</div>
);
}
export default Meals;
Meal.js
import './Meal.css'
const Meal = (props) => {
const sendIdHandler = () => {
props.onSaveIdHandler(props.id)
}
return (
<div
className='meal'
onClick={sendIdHandler}
>
{props.label}
</div>
);
}
export default Meal;
Your problem in sendIdHandler: You can update like this:
const sendIdHandler = () => {
const newIsToggled = !isToggled;
setIsToggled(newIsToggled)
props.onSaveIdHandler(props.id, newIsToggled)
}
So the problem I am currently facing is this. I have a Cart logic located in the CartContext. Everything works except the total number of prices it is displaying NAN. Here is the link to the CodeSandbox for a better understanding https://codesandbox.io/s/frosty-sound-5y7pg?file=/src/CartItem.js:1486-1494.Please comment if something is wrong with sandbox
import React from "react";
function getCartFromLocalStorage() {
return localStorage.getItem("cart")
? JSON.parse(localStorage.getItem("cart"))
: [];
}
const CartContext = React.createContext();
function CartProvider({ children }) {
const [cart, setCart] = React.useState(getCartFromLocalStorage());
const [total, setTotal] = React.useState(0);
const [cartItems, setCartItems] = React.useState(0);
React.useEffect(() => {
localStorage.setItem("cart", JSON.stringify(cart));
let newTotal = cart.reduce((total, cartItem) => {
return (total += cartItem.amount * cartItem.price);
}, 0);
newTotal = parseFloat(newTotal.toFixed(2));
setTotal(newTotal);
// cart items
let newCartItems = cart.reduce((total, cartItem) => {
return (total += cartItem.amount);
}, 0);
setCartItems(newCartItems);
}, [cart]);
// global functions
const removeItem = id => {
setCart([...cart].filter(item => item.id !== id));
};
const increaseAmount = id => {
const newCart = [...cart].map(item => {
return item.id === id
? { ...item, amount: item.amount + 1 }
: { ...item };
});
setCart(newCart);
};
const decreaseAmount = (id, amount) => {
if (amount === 1) {
removeItem(id);
return;
} else {
const newCart = [...cart].map(item => {
return item.id === id
? { ...item, amount: item.amount - 1 }
: { ...item };
});
setCart(newCart);
}
};
const addToCart = book => {
const { id, image, by, bookName,RegularPrice } = book;
const item = [...cart].find(item => item.id === id);
if (item) {
increaseAmount(id);
return;
} else {
const newItem = { id, image, by, bookName, RegularPrice, amount: 1 };
const newCart = [...cart, newItem];
setCart(newCart);
}
};
const clearCart = () => {
setCart([]);
};
return (
<CartContext.Provider
value={{
cart,
cartItems,
total,
removeItem,
increaseAmount,
decreaseAmount,
addToCart,
clearCart
}}
>
{children}
</CartContext.Provider>
);
}
export { CartContext, CartProvider };
Cart Item
import React, { useContext } from "react";
import {Link, useHistory } from 'react-router-dom'
import { CartContext } from "../../context/cart";
import { FaAngleDown, FaAngleUp } from "react-icons/fa";
import Checkout from "./Checkout";
export default function CartItem({ id, image,bookName, RegularPrice, by, amount }) {
const { removeItem, increaseAmount, decreaseAmount } = React.useContext(
CartContext
);
return (
<div id={id} className="cart__item">
<img className='cart__image' src={image} />
<div className='cart__itemdesc'>
<h4>{bookName}</h4>
<h6 className='cart__by'>By: {by}</h6>
<button
className="cart__removebtn"
onClick={() => {
removeItem(id);
}}
>
Remove
</button>
<div>
<button
className="cart-btn amount-btn"
onClick={() => {
increaseAmount(id);
}}
>
<FaAngleUp />
</button>
<p className="item-amount">{amount}</p>
<button
className="cart-btn amount-btn"
onClick={() => {
decreaseAmount(id, amount);
}}
>
<FaAngleDown />
</button>
</div>
</div>
<span className='circle'><span className='circleone'></span></span>
<span className='cart__regular'>{RegularPrice}</span>
<div>
<Checkout />
</div>
</div>
);
}
Checkout
import React,{useContext} from 'react'
import { CartContext } from '../../context/cart'
import {Link, useHistory } from 'react-router-dom'
import EmptyCart from './EmptyCart';
const Checkout = () => {
const history = useHistory()
const {cart, total} = useContext(CartContext)
if (cart.length === 0) {
return <EmptyCart />;
}
return (
<div className='checkout'>
<h2>Summary</h2>
<h2>Subtotal : ${total}</h2>
<Link to='/stripecontainer' className='checkout__btnOne'>Proceed to Checkout</Link>
</div>
)
}
export default Checkout
to watch the problem you can vivsit the test site http://u100525.test-handyhost.ru/products
the problem appears if to click many times on category items, images of products start to bug becouse react loads image of one item over and over again, on every change of category - on every filter of products, so how to make one load and save somehow the loaded images?
so if i click on categories my code is filtering products array and update statement - visibleProducts then im doing visibleProducts.map((product)=>{});
and i`m getting bug problem, because every time when react renders my the component does request to the server for getting image by id and waits while the image will load, but if i click on an other category react(ProductItem) starts other request for new images then it is starting to bug they start blinking and changing ;c
im new in react and just stated to practice what i have to do guys?
is my code correct ?
here is my ProductItem component ->
import React, { useState, useEffect, memo, useCallback } from "react";
import { Link } from "react-router-dom";
import { connect } from "react-redux";
import { setModalShow, onQuickViewed, addedToCart } from "../../actions";
import Checked from "../checked";
import "./product-item.css";
import Spinner from "../spinner";
const ProductItem = ({
product,
wpApi,
addedToCart,
onQuickViewed,
setModalShow,
}) => {
const [prodImg, setProdImg] = useState("");
const [animated, setAnimated] = useState(false);
const [checked, setChecked] = useState(false);
const [itemLoading, setItemLoading] = useState(true);
const checkedFn = useCallback(() => {
setChecked(true);
setTimeout(() => {
setChecked(false);
}, 800);
},[product]);
const onModalOpen = useCallback((e, id) => {
onQuickViewed(e, id);
setModalShow(true);
}, product);
const addHandle = useCallback((e, id) => {
e.preventDefault();
addedToCart(id);
checkedFn();
},[product]);
useEffect(()=>{
setItemLoading(false);
}, [prodImg]);
useEffect(() => {
wpApi.getImageUrl(product.imageId).then((res) => {
setProdImg(res);
});
});
return (
<div className="product foo">
<div
className='product__inner'}
>
{!itemLoading? <div
className="pro__thumb"
style={{
backgroundImage:prodImg
? `url(${prodImg})`
: "assets/images/product/6.png",
}}
>
<Link
to={`/product-details/${product.id}`}
style={{ display: `block`, width: `100%`, paddingBottom: `100%` }}
>
</Link>
</div>: <Spinner/>}
<div className="product__hover__info">
<ul className="product__action">
<li>
<a
onClick={(e) => {
onModalOpen(e, product.id);
}}
title="Quick View"
className="quick-view modal-view detail-link"
href="#"
>
<span ><i class="zmdi zmdi-eye"></i></span>
</a>
</li>
<li>
<a
title="Add TO Cart"
href="#"
onClick={(e) => {
addHandle(e, product.id);
}}
>
{checked ? (
<Checked />
) : (
<span className="ti-shopping-cart"></span>
)}
</a>
</li>
</ul>
</div>
</div>
<div className="product__details">
<h2>
<Link to={`/product-details/${product.id}`}>{product.title}</Link>
</h2>
<ul className="product__price">
<li className="old__price">${product.price}</li>
</ul>
</div>
</div>
);
};
const mapStateToProps = ({ options, cart, total, showModal }) => {
return {};
};
const mapDispatchToProps = {
onQuickViewed,
setModalShow,
addedToCart,
};
export default connect(mapStateToProps, mapDispatchToProps)(memo(ProductItem));
here is my parent component Products ->
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import ProductItem from "../product-item";
import { withWpApiService } from "../hoc";
import { onQuickViewed, addedToCart, categoriesLoaded } from "../../actions";
import CategoryFilter from "../category-filter";
import Spinner from "../spinner";
import "./products.css";
const Products = ({
maxProducts,
WpApiService,
categoriesLoaded,
addedToCart,
onQuickViewed,
products,
categories,
loading,
}) => {
const [activeIndex, setActiveIndex] = useState(0);
const [activeCategory, setActiveCategory] = useState(0);
const [visibleProducts, setVisibleProducts] = useState([]);
const wpApi = new WpApiService();
useEffect(() => {
updateVisibleProducts(activeCategory, products);
}, [products]);
useEffect(() => {
wpApi.getCategories().then((res) => {
categoriesLoaded(res);
});
}, []);
const getCatId = (cat) => {
setActiveCategory(cat);
updateVisibleProducts(cat, products);
setActiveIndex(cat);
};
const updateVisibleProducts = (category, products) => {
let updatedProducts = [];
switch (category) {
case 0:
updatedProducts = products;
setVisibleProducts(updatedProducts);
break;
default:
updatedProducts = products.filter(
(product) => product.categories.indexOf(category) >= 0
);
setVisibleProducts(updatedProducts);
}
};
let currentLocation = window.location.href.split("/");
if (!loading) {
return (
<section className="htc__product__area shop__page mb--60 mt--130 bg__white">
<div className={currentLocation[3] == "" ? `container` : ""}>
<div className="htc__product__container">
<CategoryFilter
activeIndex={activeIndex}
categories={categories}
getCatId={getCatId}
/>
<div
className="product__list another-product-style"
style={{ height: "auto" }}
>
{visibleProducts
.slice(0, maxProducts ? maxProducts : products.length)
.map((prod, id) => {
return (
<ProductItem
wpApi={wpApi}
key={id}
onQuickViewed={onQuickViewed}
addedToCart={addedToCart}
product={prod}
/>
);
})}
</div>
</div>
</div>
</section>
);
} else {
return <Spinner />;
}
};
const mapStateToProps = ({ products, loading, activeCategory, categories }) => {
return {
products,
activeCategory,
categories,
loading,
};
};
const mapDispatchToProps = {
addedToCart,
categoriesLoaded,
onQuickViewed,
};
export default withWpApiService()(
connect(mapStateToProps, mapDispatchToProps)(Products)
);
and if you need, here is my CategoryFilter component ->
import React from 'react'
const CategoryFilter = ({categories, getCatId, activeIndex}) => {
return (
<div className="row mb--60">
<div className="col-md-12">
<div className="filter__menu__container">
<div className="product__menu">
{categories.map((cat) => {
return (
<button key={cat.id}
className={activeIndex === cat.id? 'is-checked' : null}
onClick={() => getCatId(cat.id)}
data-filter=".cat--4"
>
{cat.name}
</button>
);
})}
</div>
</div>
</div>
</div>
)
}
export default CategoryFilter