I have these cart items where there could be the same products with multiple colors in the cart. Like this:
How can I display it where it will display the product name, size, and category once, then below it are the different colors along with their quantity. Something like this:
Product ID
Tumbler 500 ML
Green : 4 (button to add and remove product) and the total
Pink : 5 (button to add and remove product) and the total
Black: 6 (button to add and remove product) and the total
Any help would be appreciated. Thank you.
Codesandbox: https://codesandbox.io/s/add-to-cart-sampled-2-efqrhd?file=/src/Cart.js:67-1764
const Cart = ({ cartItems, handleCartClearance, handleRemove, handleAdd }) => {
console.log(cartItems, "items");
const totalAmount = cartItems.reduce(
(price, item) => price + item.quantity * item.price,
0
);
const handleSubmit = (e) => {
e.preventDefault();
console.log(cartItems, "order");
};
return (
<div>
<form onSubmit={handleSubmit}>
Order page
{cartItems.length >= 1 && (
<Button onClick={handleCartClearance}>Clear Orders</Button>
)}
{cartItems.length === 0 && <div>No Items in the cart</div>}
<div>
{cartItems.map((item) => (
<div key={item.id + item.color}>
<li>{item.id}</li>
<li>{item.name + " " + item.size + "" + item.cat}</li>
<li>{item.color}</li>
<li>{item.quantity}</li>
<li>Total: {Number(item.quantity) * Number(item.price)}</li>
<button
onClick={(e) =>
handleAdd(
item.id,
item.prodName,
item.price,
item.size,
item.cat,
item.color
)
}
>
+
</button>
<button onClick={() => handleRemove(item)}>- </button>
</div>
))}
-----------------------------------------------------
<div>
<b>Total Amount :{totalAmount}</b>
</div>
{cartItems.length >= 1 && <Button type="submit">Save Order</Button>}
</div>
</form>
</div>
);
};
export default Cart;
Try this
codesandbox: https://codesandbox.io/s/add-to-cart-sampled-2-forked-zzcnpf
{Object.entries(
cartItems.reduce((prev, item) => {
if (!prev[item.id]) prev[item.id] = { ...item, nest: [] };
prev[item.id].nest.push(item);
return prev;
}, {})
).map(([id, obj], idx) => (
<div key={id + obj.color}>
<li>{obj.id}</li>
<li>{obj.name + " " + obj.size + "" + obj.cat}</li>
{obj.nest.map((nest, idx) => (
<React.Fragment key={idx}>
<li>{nest.color}</li>
<li>{nest.quantity}</li>
<li>Total: {Number(nest.quantity) * Number(nest.price)}</li>
<button
onClick={(e) =>
handleAdd(
nest.id,
nest.prodName,
nest.price,
nest.size,
nest.cat,
nest.color
)
}
>
+
</button>
<button onClick={() => handleRemove(nest)}>- </button>
</React.Fragment>
))}
</div>
))}
Related
I have a button that trigger decreaseQuantity function and I have a state,checkoutList comes from context API. When I click that button, its directly decrease the quantity to 1, and what I want is only decrease by one. Why it's not decreasing one by one?
const { checkoutList, setCheckoutList } = useContext(AppContext);
const [uniqueCheckoutList, setUniqueCheckoutList] = useState([]);
const decreaseQuantity = (item) => {
let updatedList = [...uniqueCheckoutList];
let productIndex = updatedList.findIndex((p) => p.id === item.id);
updatedList[productIndex].quantity -= 1;
if (updatedList[productIndex].quantity === 0) {
updatedList = updatedList.filter((p) => p.id !== item.id);
}
setUniqueCheckoutList(updatedList);
setCheckoutList(updatedList);
localStorage.setItem("checkoutList", JSON.stringify(updatedList));
};
Edit: I have a useEffect hook that updates uniqueCheckoutList if the product has the same id. Add quantity property to it so it will just display once. This way I can show the quantity of that product next to the buttons in a span.
useEffect(() => {
let updatedList = [];
checkoutList.forEach((item) => {
let productIndex = updatedList.findIndex((p) => p.id === item.id);
if (productIndex === -1) {
updatedList.push({ ...item, quantity: 1 });
} else {
updatedList[productIndex].quantity += 1;
}
});
setUniqueCheckoutList(updatedList);
}, [checkoutList]);
Here I call the decreaseQuantity function.
<div className="checkout">
{uniqueCheckoutList.length === 0 ? (
<div>THERE IS NO ITEM IN YOUR BAG!</div>
) : (
uniqueCheckoutList.map((item) => {
return (
<div className="container" key={item.id}>
<div>
<img src={item.images[0]} alt={item.title} />
</div>
<div className="texts">
<h1>{item.title} </h1>
<p>{item.description} </p>
<button onClick={() => increaseQuantity(item)}>+</button>
<span>{item.quantity}</span>
<button onClick={() => decreaseQuantity(item)}>-</button>
</div>
</div>
);
})
)}
</div>
I am trying to make a simple e-commerce app. When a user goes to cart section and try to increase or decrease quantity, it changes the state but remains same on the page. I need to go back and go cart again to update. How can it change dynamically?
function CardItem() {
const {cart, setCart} = useContext(ProductContext)
const addQuantity = (cartItem) => {
return cart.map((item) => (
cartItem.id === item.id ? item.quantity = item.quantity + 1 : item
))
}
const removeQuantity = (cartItem) => {
cart.map((item) => (
cartItem.id === item.id ? item.quantity = item.quantity - 1 : item
))
}
return (
{
cart.map((cartItem) => (
<tr key={cartItem.id}>
<td class="quantity__item">
<div class="quantity">
<div class="pro-qty-2 d-flex align-items-center justify-content-center text-center">
<button className='increase' onClick={() => removeQuantity(cartItem)}>
-</button>
{cartItem.quantity}
<button className='increase' onClick={() => addQuantity(cartItem)}>+</button>
</div>
</div>
</td>
</tr>
))
})}
Issue
The code in your snippet isn't calling setCart to update any state.
The add/remove quantity handlers are just mutating the cart state object. Don't mutate React state.
Solution
Use the setCart state updater function to enqueue a state update to trigger a rerender to view the updated state. Use a functional state update to correctly update from the previous state and remember that you should shallow copy any state that is being updated.
Example:
function CardItem() {
const { cart, setCart } = useContext(ProductContext);
const addQuantity = (cartItem) => {
setCart(cart => cart.map(item => cartItem.id === item.id
? { // <-- new item object reference
...item, // <-- shallow copy item
quantity: item.quantity + 1, // <-- update property
}
: item
));
};
const removeQuantity = (cartItem) => {
setCart(cart => cart.map(item => cartItem.id === item.id
? {
...item,
quantity: item.quantity - 1,
}
: item
));
};
return (
{cart.map((cartItem) => (
<tr key={cartItem.id}>
<td class="quantity__item">
<div class="quantity">
<div class=" .... ">
<button className='increase' onClick={() => removeQuantity(cartItem)}>
-
</button>
{cartItem.quantity}
<button className='increase' onClick={() => addQuantity(cartItem)}>
+
</button>
</div>
</div>
</td>
</tr>
))
});
}
Since adding/removing is essentially the same action, it's common to use a single function to handle both and pass in the quantity you want to adjust by.
function CardItem() {
const { cart, setCart } = useContext(ProductContext);
const addQuantity = (id, amount) => () => {
setCart(cart => cart.map(item => cartItem.id === id
? {
...item,
quantity: item.quantity + amount,
}
: item
));
};
return (
{cart.map((cartItem) => (
<tr key={cartItem.id}>
<td class="quantity__item">
<div class="quantity">
<div class=" .... ">
<button
className='increase'
onClick={addQuantity(cartItem.id, -1)}
>
-
</button>
{cartItem.quantity}
<button
className='increase'
onClick={addQuantity(cartItem.id, 1)}
>
+
</button>
</div>
</div>
</td>
</tr>
))
});
}
I will recommend using reducers with context to manage state. Something like below a new CartReducer with ADD and REMOVE item etc. actions
const [state, dispatch] = useReducer(CartReducer, initalState);
const addToCart = (id) => {
dispatch({ type: ADD, payload: id});
};
const showHideCart = () => {
dispatch({ type: SHOW, payload:'' });
};
const removeItem = (id) => {
dispatch({ type: REMOVE, payload: id });
};
You can refer this project if it helps
shopping-cart-with-context-api
I have these products with product variations of colors. The problem is that it does not record the product’s color that I have added.
For example, the shirt has 2 colors red and green, and I’ll choose red, only the red will be saved and the quantity will be updated for that product as a whole but not on the color.
This is what happens:
Codesandbox: https://codesandbox.io/s/add-to-cart-jsd9fd?file=/src/App.js:0-2599
Codes:
export default function App() {
const [cartItems, setCartItems] = useState([]);
const handleAdd = (id, name, size, cat, color) => {
console.log("add", id, name, size, cat, color);
const productExist = cartItems.find((item) => item.id === id);
if (productExist) {
setCartItems(
cartItems.map((item) =>
item.id === id
? { ...productExist, quantity: productExist.quantity + 1 }
: item
)
);
} else {
setCartItems([...cartItems, { id, name, size, cat, color, quantity: 1 }]);
}
};
// const handleAdd = (item) => {
// console.log("add", item);
// };
console.log(cartItems);
return (
<div className="App">
{products.map((index) => (
<ul key={index.id}>
<li>
<b>{index.prodName}</b>
</li>
<li>Category: {index.cat}</li>
<li>Size: {index.size}</li>
{Object.entries(index.colorMap).map((color) => (
<>
{color[1] !== 0 ? (
<>
{color[0]}
<button
onClick={(e) =>
handleAdd(
index.id,
index.prodName,
index.size,
index.cat,
color[0]
)
}
>
Add to Cart
</button>
{/* <button onClick={(e) => handleAdd(index)}>Add to Cart</button> */}
</>
) : (
<>2</>
)}
<br />
</>
))}
Php {index.price}.00
</ul>
))}
<Cart cartItems={cartItems} />
</div>
);
}
When you check if the product is already in the cart, you only check by its id:
const productExist = cartItems.find((item) => item.id === id);
But if you want to separate the products by id and color then you need to check for that too:
const productExist = cartItems.find((item) => item.id === id && item.color === color);
And of course update the same comparison logic when updating the cart:
setCartItems(
cartItems.map((item) =>
(item.id === id && item.color === color) // <--- here
? { ...productExist, quantity: productExist.quantity + 1 }
: item
)
);
I've been scratching my head over this for ages now.
I am trying to change the background colour of a specific button that is in a nested array.
I have an array of names in pairs that I loop over twice using a map, once to get the pair and once again to get the value. I output and assign the values to a button for each and am displaying the pairs together (E.g. each pair is indexed 0 and 1).
When I click on the button I wish to change only the background colour of the selected button. Currently all the buttons change colour. The issue being is that the state of the buttons effects all of them when I use a boolean to define the selection.
The handler I am using to do this also adds the value of the button to an array to be passed into global state later on as well.
Any help with this would be greatly greatly appreciated as I can't seem to find a way past it. Thanks!
import React, { Component } from "react";
import "../../App.scss";
import { Link } from "react-router-dom";
import Button from "../Button/Button";
class Matches extends Component {
constructor(props) {
super(props);
this.state = {
champ: [],
winningPlayers: [],
selected: false,
};
this.handleAddWinners = this.handleAddWinners.bind(this);
this.handleRound = this.handleRound.bind(this);
}
// Adds winners to a local array which is then sent
// to the global array using the handleNextRound action.
handleAddWinners = (e) => {
const winner = e.target.value;
const { champ } = this.state;
const { round } = this.props;
if (round !== 3) {
this.setState({
selected: !false,
winningPlayers: [...this.state.winningPlayers, winner],
});
} else {
this.setState({ champ: [...champ, winner] });
}
};
handleRound = () => {
const { round, handleNextRound, handleChampion } = this.props;
round !== 3 ? handleNextRound(this.state) : handleChampion(this.state);
this.setState({ winningPlayers: [] });
};
render() {
const { pairs, round, handleClear, roundWinners, champion } = this.props;
const { winningPlayers, selected, champ } = this.state;
const semi = roundWinners[0];
const final = roundWinners[1];
const champName = champion.map((item) => item);
const reset =
round !== 4 ? "block__reset__tournament" : "block__reset__new-game";
const newGame = `${round !== 4 ? "Reset" : "New Game?"}`;
const buttonClick = `${selected ? "selected" : "block__player"}`;
return (
<>
<div classname="container__wrapper">
<div className="container__tournament">
{round === 1 ? (
<section className="block__round ">
{pairs.map((item, index) => (
<div className="pairs" key={index}>
{item.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e)}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 2 ? (
<section className="block__round ">
{semi.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 3 ? (
<section className="block__round ">
{final.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : (
<section className="block__champion">
<p className="champion__greeting">
Congratulations
<br />
<span className="champion__name">{champName}!</span>
<br /> You've won the whole shebang!
</p>
</section>
)}
<Button
buttonClass={`${
round !== 4 ? "block__next-round" : "button__notActive"
}`}
label={`${round !== 3 ? "Next Round" : "See Winner"}`}
handleClick={this.handleRound}
disabled={disabled}
/>
<Link to={"/"} className={reset}>
<Button
buttonClass={reset}
handleClick={handleClear}
label={newGame}
/>
</Link>
</div>
</div>
</>
);
}
}
export default Matches;
This is the component that is handling most of this.
First I would like to say that you should always avoid from using array's index as keys. That is, unless your array is always at the same size and order.
Having said that - what you want to do is to know which button was selected - right?
So you need to store the last button that was selected. Because you don't use any ids anywhere, you can use the index of the pair and the index of the button to know which button was clicked.
Here's an example - I've changed only the round1 and the state code.
import React, { Component } from "react";
import "../../App.scss";
import { Link } from "react-router-dom";
import Button from "../Button/Button";
class Matches extends Component {
constructor(props) {
super(props);
this.state = {
champ: [],
winningPlayers: [],
selected: null,
};
this.handleAddWinners = this.handleAddWinners.bind(this);
this.handleRound = this.handleRound.bind(this);
}
// Adds winners to a local array which is then sent
// to the global array using the handleNextRound action.
handleAddWinners = (e, pairIndex, itemIndex) => {
const winner = e.target.value;
const { champ } = this.state;
const { round } = this.props;
if (round !== 3) {
this.setState({
selected: `${pairIndex}-${itemIndex}`,
winningPlayers: [...this.state.winningPlayers, winner],
});
} else {
this.setState({ champ: [...champ, winner] });
}
};
handleRound = () => {
const { round, handleNextRound, handleChampion } = this.props;
round !== 3 ? handleNextRound(this.state) : handleChampion(this.state);
this.setState({ winningPlayers: [] });
};
render() {
const { pairs, round, handleClear, roundWinners, champion } = this.props;
const { winningPlayers, selected, champ } = this.state;
const semi = roundWinners[0];
const final = roundWinners[1];
const champName = champion.map((item) => item);
const reset =
round !== 4 ? "block__reset__tournament" : "block__reset__new-game";
const newGame = `${round !== 4 ? "Reset" : "New Game?"}`;
const buttonClick = `${selected ? "selected" : "block__player"}`;
return (
<>
<div classname="container__wrapper">
<div className="container__tournament">
{round === 1 ? (
<section className="block__round ">
{pairs.map((item, pairIndex) => (
<div className="pairs" key={pairIndex}>
{item.map((names, itemIndex) => (
<Button
key={itemIndex}
handleClick={(e) => this.handleAddWinners(e, pairIndex, itemIndex)}
label={names}
buttonClass={`${pairIndex}-${itemIndex}` === selected ? '<enterYourBackgroundClass' : buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 2 ? (
<section className="block__round ">
{semi.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 3 ? (
<section className="block__round ">
{final.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : (
<section className="block__champion">
<p className="champion__greeting">
Congratulations
<br />
<span className="champion__name">{champName}!</span>
<br /> You've won the whole shebang!
</p>
</section>
)}
<Button
buttonClass={`${
round !== 4 ? "block__next-round" : "button__notActive"
}`}
label={`${round !== 3 ? "Next Round" : "See Winner"}`}
handleClick={this.handleRound}
disabled={disabled}
/>
<Link to={"/"} className={reset}>
<Button
buttonClass={reset}
handleClick={handleClear}
label={newGame}
/>
</Link>
</div>
</div>
</>
);
}
}
export default Matches;
I wasn't sure how to title this correctly. I have an array of items, where I am mapping through them and creating a button for each item. when each button (which represents a category) is clicked, it loads the posts in that category. I am adding an extra item (which will also be a button) to the end of the array that will be a "view all" button, but it will call a different function. So far this component is like:
const Posts = ({ state }) => {
const [categories, setCategories] = useState([]);
const [categoryId, setCategoryId] = useState();
const [page, setPage] = useState(1);
const [posts, setPosts] = useState([]);
const [allPosts, setAllPosts] = useState([]);
useEffect(() => {
fetch(state.source.api + "/wp/v2/categories")
.then(response => response.json())
.then(data => {
setCategories(data);
})
}, []);
console.log(categories);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
useEffect(() => {
if (!categoryId) {
return;
}
let url = state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setPosts([...posts, ...data]);
});
}, [categoryId, page]);
useEffect(() => {
let url = state.source.api + "/wp/v2/posts?per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setAllPosts([...allPosts, ...data]);
});
}, [page]);
const allCategories = categories.map((category, i) => (category))
allCategories.push("View All");
console.log(allCategories);
return (
<>
{allCategories.length > 0 ? (
allCategories.map((category, i) => {
return (
<>
<button className="btn" key={i} onClick={() => {
setPage(1);
setPosts([]);
setCategoryId(category.id);
}}>{category.name}</button>
{(category === "View All") && (<button>View all</button>)}
</>
)
})
) : (
<p>Loading...</p>
)
}
<div>
{posts.length === 0 ? (
<>
{allPosts.map((generalPost, i) => {
return (
<li key={i}>{generalPost.title.rendered}</li>
)
})}
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
) : (
<>
<ol>
{posts.map((post, i) => {
// console.log(post.id);
return (
<li key={i}>{post.title.rendered}</li>
)
})}
</ol>
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
)}
</div>
</>
)
}
I was able to get the "view all" button to be added to the end, but there seems to be an extra empty button before the "view all" button. I am not sure how that is getting in there. It's displaying like:
[Books] [Movies] [Songs] [ ] [View all]
Is there something wrong with the way I am adding the "view all" button to the array here?
In your original code, you are always rendering a <button class="btn">...</button> + conditional check to render <button>View all</button>:
allCategories.map((category, i) => {
return (
<>
<button className="btn" key={i} onClick={() => {
setPage(1);
setPosts([]);
setCategoryId(category.id);
}}>{category.name}</button>
{(category === "View All") && (<button>View all</button>)}
</>
)
})
Therefore, when category === "View All" is true, it also renders a <button class="btn"> element with empty content because in that case, category.name is undefined.
What you need to do is to make a if-else statement or ternary expression to render only one of them:
allCategories.map((category, i) => {
return (
{(category === "View All") ? (
<button>View all</button>
) : (
<button className="btn" key={i} onClick={() => {
setPage(1);
setPosts([]);
setCategoryId(category.id);
}}>{category.name}</button>
)
)
})