Evitando productos duplicados en el carrito
Clase 18 de 31 • Curso de React.js con Vite.js y TailwindCSS
Contenido del curso
Clase 18 de 31 • Curso de React.js con Vite.js y TailwindCSS
Contenido del curso
Andres Eduardo Maneiro Antunez
Martin Migueles
Andres Eduardo Maneiro Antunez
Andrés Julian Caro Restrepo
Jose Ever Muñoz Muñoz
Miguel Prada
Sebastian Rojas Vargas
Jordy Tirado Torres
Alexander Moreno
Alan Garcia
Rubén Ernesto Aragón Gil
Roberto Bravo
Jannez Milson Urrego Quiroz
David Pacco
Daniel Merchán Cáceres
Alan Dell Oso
Alvaro Arturo
Jacobo Hernández Isaza
Wilberth Willy Mollinedo Candia
Daniel Morales
Gilbert Ardila
Jean Bustamante
Andres Burbano
Lucia Nishimiya
Lucia Nishimiya
Guillermo Prituluk
Guillermo Prituluk
Roberth Salazar
Miguel Angel Reyes Moreno
danilo.ascanio161
por si alguien le sirve, yo desde antes habia creado es una funcion que agrega los productos al carrito y si se vuelve a dar al mismo producto lo que hace es sumar la cantidad y el precio
//Agrega un producto al carrito, y si ya existe aumenta la cantidad y suma los productos const addProduct = payload => { const productIndex = cart.findIndex(product => product.id === payload.id) let newCart = [] if (productIndex >= 0) { newCart = [...cart] newCart[productIndex].quantity++ newCart[productIndex].price = payload.price + newCart[productIndex].price } else { newCart = [...cart, { ...payload, quantity: 1 }] } setCart(newCart) getTotalInfo(newCart) openCheckoutSideMenu() }
donde iria este codigo ? en que archivo ?
Buenas noches Martin
en su momento cree un Hook que se encargaban de toda la logica del carrito
import { useState } from 'react' export const useShoppingCart = () => { const [cart, setCart] = useState([]) const [totalQuantity, setTotalQuantity] = useState(0) const [totalPrice, setTotalPrice] = useState(0) const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false) // Funciones que se encargan de abrir y cerrar el menu laterial de las ordenes const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true) const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false) const toggleCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(!isCheckoutSideMenuOpen) // Agrega un producto al carrito, y si ya existe aumenta la cantidad y suma los productos const addProduct = payload => { const productIndex = cart.findIndex(product => product.id === payload.id) let newCart = [] if (productIndex >= 0) { newCart = [...cart] newCart[productIndex].quantity++ newCart[productIndex].price = payload.price + newCart[productIndex].price } else { newCart = [...cart, { ...payload, quantity: 1 }] } setCart(newCart) getTotalInfo(newCart) } const increaseQuantity = (id) => { const productIndex = cart.findIndex(product => product.id === id) const newCart = [...cart] newCart[productIndex].quantity++ setCart(newCart) getTotalInfo(newCart) } const decreaseQuantity = (id) => { const productIndex = cart.findIndex(product => product.id === id) const newCart = [...cart] if (newCart[productIndex].quantity <= 1) { return 'El elemento no puede ser menor 1' } newCart[productIndex].quantity-- setCart(newCart) getTotalInfo(newCart) } // Elimina un producto del carrito const deleteProduct = (id) => { const newCart = cart.filter(product => product.id !== id) setCart(newCart) getTotalInfo(newCart) } // Suma la cantidad total de productos en el carrito const getTotalQuantity = (data) => { const quantity = data.reduce((total, product) => total + product.quantity, 0) setTotalQuantity(quantity) } // Suma el precio total de todos los productos en el carrito const getTotalPrice = (data) => { const price = data.reduce((total, product) => total + product.price, 0) setTotalPrice(price) } // Funcion que se encarga de llamar tanto a la cantidad total y el precio total const getTotalInfo = (data) => { getTotalQuantity(data) getTotalPrice(data) } const cleanCart = () => { setCart([]) setTotalQuantity(0) setTotalPrice(0) } return { cart, addProduct, deleteProduct, increaseQuantity, decreaseQuantity, cleanCart, totalQuantity, totalPrice, isCheckoutSideMenuOpen, openCheckoutSideMenu, closeCheckoutSideMenu, toggleCheckoutSideMenu } }
Les comparto como lo realicé haciendo uso del método some, el cual directamente nos devuelve el booleano de si hay al menos un elemento que cumpla con la condición que le damos:
excelente, no conocia la funcion
Me parece que esa función auxiliar renderIcon que estamos definiendo dentro del mismo componente debería hacerse por fuera del mismo, incluso como un componente aparte. Así evitamos que react redefina la función en cada re-render del componente padre, y dejamos que react decida cuándo re-renderizar el componente hijo, así:
import { useContext } from "react" import { PlusIcon, CheckIcon } from '@heroicons/react/24/solid' import { ShoppingCartContext } from "../../context" const AddItemButton = ({ isInCart, onItemAdded }) => { if ( isInCart ) { return ( <div className="absolute top-0 right-0 flex justify-center items-center bg-black w-6 h-6 rounded-full m-2 p-1"> <CheckIcon className="w-6 h-6 text-white" /> </div> ) } return ( <div className="absolute top-0 right-0 flex justify-center items-center bg-white w-6 h-6 rounded-full m-2 p-1" onClick={onItemAdded} > <PlusIcon className="w-6 h-6 text-black" /> </div> ) } export default function Card({ item }) { const context = useContext(ShoppingCartContext) const showProduct = () => { context.setSelectedProduct(item) context.openProductDetails() context.closeCheckoutMenu() } const addToCart = (event) => { event.stopPropagation(); const newCartProducts = [...context.cartProducts, item] context.setCartProducts(newCartProducts) context.setCount(context.count + 1) context.openCheckoutMenu() context.closeProductDetails() } const isInCart = !!context.cartProducts.find(product => product.id === item.id) return ( <div className="bg-white cursor-pointer w-56 h-60" onClick={showProduct} > <figure className="relative mb-2 w-full h-4/5"> <span className="absolute bottom-0 left-0 bg-white/60 rounded-full text-black text-xs px-3 py-1 m-2"> { item.category.name } </span> <img className="w-full h-full object-cover rounded-lg" src={item.images[0]} alt={item.title} /> <AddItemButton isInCart={isInCart} onItemAdded={addToCart} /> </figure> <p className="flex justify-between items-center"> <span className="text-sm font-light"> { item.title } </span> <span className="text-lg font-medium"> ${ item.price } </span> </p> </div> ) }
En este caso tal vez no haga gran diferencia, pero en otros escenarios ese patrón de definir componentes dentro de componentes puede causar bugs raros, por ejemplo esto:
Muchas gracias! Tenía el problema que cuando agregaba un producto no cambiaba el ícono, con su aporte pude solucionar el problema!
muy cierto, y si trabajas con sonarQube se va a quejar
En mi caso trate de no sobre cargar el componente card y cree un componente aparte para del boton de AddToCart sin repetir mucho codigo
Que buena forma de hacerlo mas legible para no saturarlo :0 Una duda como se llama el theme que usas?
Yo agregué al bg una transparencia:
className='absolute top-0 right-0 flex justify-center items-center bg-black/50 w-6 h-6 rounded-full m-2 p-1'>
Asi va mi Cart
Buen día
Estoy siguiendo el curso y voy bien.
Estoy revisando los comentarios de los compañeros , es mas interesante escoger del mismo producto varias veces con un contador. hasta el momento lo llevo igual a los videos , mientras aprendo mas de react. saludos a todos
Si estan en Windows pueden agregar la siguientes clases de TailwindCSS para que el Scroll se vea mas estilizado.
[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300
Definitivamente voy por cartProducts.some(...) que ya devuleve un booleano, pero como siempre, lo importante primero es encontrar una solución eficaz y después mejorar la eficiencia si es requerido.
Por si a alguien le suena, no hice función:
<figure className='relative mb-5 w-full h-4/5'> <span className='absolute bottom-0 left-0 bg-white/80 rounded-lg text-black text-xs m-2 p-1'> Categorie {data?.volumeInfo?.categories} </span> <img className='w-full h-full object-fit rounded-lg' src={data?.data?.volumeInfo?.imageLinks?.thumbnail} alt='book' /> {context.cartProducts.filter((product) => product.id === data?.data?.id) .length > 0 ? ( <div className='absolute top-0 right-0 flex justify-center items-center text-xs bg-white w-6 h-6 rounded-full m-2'> <AiOutlineCheck className='h-5 w-5 text-green-600' /> </div> ) : ( <div className='absolute top-0 right-0 flex justify-center items-center text-xs bg-white/80 w-6 h-6 rounded-full m-2' onClick={(event) => addBookToCart(event, data?.data)} > <HiOutlinePlus /> </div> )}
genial!
Por que quitaron la fecha de los comentarios del curso? me era util para poder visualizar el ultimo comentario, al menos asi me entero lo desactualizado que es esta el curso en algunos comentarios se podia ver como que le falta al curso y si esta desactualizado con que lo actualizaron
Pienso lo mismo, me imagino que los quitaron para que la gente no diga que está desactualizado o haga comentarios negativos...
lo resumí un poco más sencillo: cree un estado isClicked el cual se cambia cuando se hace click en el más y dependiendo de ello se muestra o un icono u otro  => { event.stopPropagation(); context.setCart([...context.cart, data.data]); context.setCount(context.count + 1); context.handleOpenCheckout(); setIsClicked(true);
<button className={${isClicked ? 'hover:bg-red-600' :'hover:bg-blue-500'} absolute top-0 right-0 flex justify-centetr items-center bg-white w-6 h-6 rounded-full m-2 p-1}>
{isClicked ? (
<CheckIcon className="h-6 w-6 text-green-600 " />
) : (
<PlusCircleIcon
className="h-6 w-6 text-black hover:text-white "
onClick={(event) => handleAddToCart(event)}
/>
)}
</button>
};
Alguien sabe como alinear a la izquierda las cards usando Tailwind de preferencia? pero al mismo tiempo mantener centrado el contenido? o sea como al final hay 2 cards en lugar de 3, se centran esas de la última fila, quisiera que se alinearan a la izquierda :(
Si le doy justify-start todo el contenido se va a la izquierda es el problema.

const isInCart = context.cartProducts.includes(product)
Tiene algún side effect. los objetos los compara por referencia si la base de datos no tiene objetos duplicados no debe generar ningún bug.
hay algo que no tuve en cuenta ?
Yo lo hice agregando una propiedad quantity que inicia en 1 y cuando se vuelve a agregar un producto que ya está en el carrito se incrementa quantity
const addToCart = (product) => { // Busca el índice del producto en el carrito const productInCart = cart.findIndex(item => item.id === product.id) // Si el producto ya está en el carrito (índice mayor o igual a 0) if (productInCart >= 0) { // Clona el carrito existente para no mutar el estado directamente const newCart = structuredClone(cart) // Incrementa la cantidad del producto existente en el carrito newCart[productInCart].quantity += 1 // Actualiza el estado del carrito con el nuevo carrito clonado setCart(newCart) } else { // Si el producto no está en el carrito, crea un nuevo carrito // usando el estado anterior y agrega el nuevo producto con cantidad 1 setCart(prevState => ([ ...prevState, { ...product, quantity: 1 } ])) } }
Función para decrementar cantidad de productos
const removeOneFromCart = (product) => { const productInCart = cart.findIndex(item => item.id === product.id) if (productInCart >= 0) { const newCart = structuredClone(cart) if (newCart[productInCart].quantity !== 1) { newCart[productInCart].quantity -= 1 setCart(newCart) } } }
Función para remover productos del carrito
const removeFromCart = (product) => { setCart(prevState => prevState.filter(item => item.id !== product.id)) }
yo lo hice con un operador ternario y utilice también la propiedad 'stroke' de Tailwind que permite establecer color a los trazos del svg , en éste caso 'stroke-current' ya que no lo vi muy lindo el check con un bg-black.
const renderIcon = (id) => { const isInCart = cartProducts.filter(product => product.id === id).length > 0 return( <div className='absolute top-0 right-0 flex justify-center items-center bg-white/60 w-6 h-6 rounded-full m-2 p-1'> {isInCart ? <CheckIcon className='w-6 h-6 text-green-700 stroke-current' /> : <PlusIcon className='w-6 h-6 text-black stroke-current' onClick={(event) => addProductsToCart(event, data.data)}/> } </div> ) }
Una mejor manera es usar el método some de los arrays para encontrar si existe algún producto en el carro "isInCart", es mas eficiente por que se detiene hasta encontrar uno, retorna un booleano y es menos código que escribir
ejemplo
const isInCart = context.cartProducts.some(``p`` => p.id === product.id)
Con SVG sin necesidad de instalar dependencias:
const renderIcon = (id) => { const isInCard = context.CartProducts.filter((product) => product.id === id).length > 0 if (isInCard) { return ( <button className="absolute top-0 right-0 flex justify-center items-center bg-black w-6 h-6 rounded-full m-2 p-1 cursor-pointer"> {/* check icon */} <svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-white"> <path fillRule="evenodd" d="M19.916 4.626a.75.75 0 01.208 1.04l-9 13.5a.75.75 0 01-1.154.114l-6-6a.75.75 0 011.06-1.06l5.353 5.353 8.493-12.739a.75.75 0 011.04-.208z" clipRule="evenodd" /> </svg> </button> ) } else { return ( <button onClick={(event)=> addProductsToCart(event, data.data)} className="absolute top-0 right-0 flex justify-center items-center bg-white w-6 h-6 rounded-full m-2 p-1 cursor-pointer" > {/* + plus icon */} <svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6"> <path fillRule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clipRule="evenodd" /> </svg> </button> ) } }