Ejercicio: Proyecto: carrito de compras
Objetivo
Construir la tienda completa. Las pruebas usan tu aplicación como lo haría una persona: buscan, agregan, suman, restan, quitan y recargan.
Cómo se trabaja
El código inicial ya incluye los datos (PRODUCTOS) y la función moneda: no los modifiques. Escribe el resto: el reductor, el contexto, los componentes y App. Prueba en el editor y, cuando todo funcione, pega todo el archivo en el comprobador.
Pasos
Marca cada paso a medida que lo completes. Tu avance se guarda en este navegador.
💡 Pista 1: agregar un producto repetido
En el caso "agregar", busca con find si ya existe la línea. Si existe, usa map para sumar 1; si no, [...lineas, { ...producto, cantidad: 1 }].
💡 Pista 2: cálculos
lineas.reduce((suma, l) => suma + l.precio * l.cantidad, 0) te da el total; con l.cantidad en lugar del producto, obtienes las unidades.
💡 Pista 3: formato
Escribe una función moneda = (n) => "$" + n.toFixed(2) y úsala en el catálogo y en el total.
🔎 Comprueba tu trabajo
Pega tu código y pulsa Comprobar. La revisión ocurre en tu navegador; nada se envía a ningún sitio. Cada punto en rojo incluye una pista.
✅ Ver una solución completa de ejemplo
Intenta resolverlo antes de mirar. Tu versión puede ser distinta y también correcta.
import { createContext, useContext, useEffect, useReducer, useState } from "react";
const PRODUCTOS = [
{ id: 1, nombre: "Teclado", precio: 25, categoria: "Periféricos" },
{ id: 2, nombre: "Mouse", precio: 12.5, categoria: "Periféricos" },
{ id: 3, nombre: "Monitor", precio: 180, categoria: "Pantallas" },
{ id: 4, nombre: "Webcam", precio: 40, categoria: "Pantallas" },
];
const moneda = (n) => "$" + n.toFixed(2);
// ----- Reductor: todas las formas de cambiar el carrito -----
function reductor(lineas, accion) {
switch (accion.tipo) {
case "agregar": {
const existe = lineas.find((l) => l.id === accion.producto.id);
if (existe) {
return lineas.map((l) => (l.id === existe.id ? { ...l, cantidad: l.cantidad + 1 } : l));
}
return [...lineas, { ...accion.producto, cantidad: 1 }];
}
case "sumar":
return lineas.map((l) => (l.id === accion.id ? { ...l, cantidad: l.cantidad + 1 } : l));
case "restar":
return lineas
.map((l) => (l.id === accion.id ? { ...l, cantidad: l.cantidad - 1 } : l))
.filter((l) => l.cantidad > 0);
case "quitar":
return lineas.filter((l) => l.id !== accion.id);
default:
return lineas;
}
}
// ----- Contexto y proveedor (con persistencia) -----
const CarritoContext = createContext(null);
function leerGuardado() {
try {
return JSON.parse(localStorage.getItem("carrito")) ?? [];
} catch {
return [];
}
}
function CarritoProvider({ children }) {
const [lineas, dispatch] = useReducer(reductor, [], leerGuardado);
useEffect(() => {
localStorage.setItem("carrito", JSON.stringify(lineas));
}, [lineas]);
const total = lineas.reduce((suma, l) => suma + l.precio * l.cantidad, 0);
const unidades = lineas.reduce((suma, l) => suma + l.cantidad, 0);
return (
<CarritoContext.Provider value={{ lineas, dispatch, total, unidades }}>
{children}
</CarritoContext.Provider>
);
}
function useCarrito() {
return useContext(CarritoContext);
}
// ----- Componentes -----
function Catalogo() {
const { dispatch } = useCarrito();
const [busqueda, setBusqueda] = useState("");
const visibles = PRODUCTOS.filter((p) =>
p.nombre.toLowerCase().includes(busqueda.trim().toLowerCase())
);
return (
<section aria-label="Catálogo">
<input
type="search"
aria-label="Buscar producto"
placeholder="Buscar…"
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
/>
{visibles.map((p) => (
<article key={p.id} className="producto">
<h3>{p.nombre}</h3>
<p className="precio">{moneda(p.precio)}</p>
<button onClick={() => dispatch({ tipo: "agregar", producto: p })}>Agregar</button>
</article>
))}
</section>
);
}
function Carrito() {
const { lineas, dispatch, total } = useCarrito();
return (
<aside aria-label="Carrito">
<h2>Tu carrito</h2>
{lineas.length === 0 ? (
<p className="vacio">Tu carrito está vacío</p>
) : (
<ul className="lineas">
{lineas.map((l) => (
<li key={l.id} className="linea">
<span className="nombre">{l.nombre}</span>
<button aria-label="Restar" onClick={() => dispatch({ tipo: "restar", id: l.id })}>−</button>
<span className="cantidad">{l.cantidad}</span>
<button aria-label="Sumar" onClick={() => dispatch({ tipo: "sumar", id: l.id })}>+</button>
<button aria-label="Quitar" onClick={() => dispatch({ tipo: "quitar", id: l.id })}>Quitar</button>
</li>
))}
</ul>
)}
<p>Total: <strong id="total">{moneda(total)}</strong></p>
</aside>
);
}
function Cabecera() {
const { unidades } = useCarrito();
return (
<header>
<h1>Tienda</h1>
<p>Artículos: <span id="contador">{unidades}</span></p>
</header>
);
}
export default function App() {
return (
<CarritoProvider>
<Cabecera />
<Catalogo />
<Carrito />
</CarritoProvider>
);
}
💡 🏁 Reto extra (opcional)
Añade un filtro por categoría y un campo de cupón de descuento con el código REACT10 (10 %).