Saltar al contenido
Curso de React
Todos los cursos

Ejercicio: Proyecto: carrito de compras

  • ⏱ 120 minutos
  • 📶 Nivel: principiante

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.

⚛️ Tu editor de React (empieza desde aquí)
Consola

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.

    App.jsx
    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 %).