Skip to content

Tienda con carrito en JS ​

Una tienda en línea sin servidor ni base de datos: catálogo con categorías, búsqueda y orden; carrito lateral con cantidades, cupones, envío gratis a partir de cierto monto, total en pesos y persistencia en localStorage. Al finalizar, el pedido completo se envía por WhatsApp al vendedor.

Es perfecta para emprendedores que venden por redes sociales y todavía no necesitan pagos en línea: se publica gratis en Netlify o GitHub Pages y se actualiza editando una lista de productos.

Vista previa ​

Barro y Grano

Prueba: agrega productos, cambia cantidades, aplica el cupón BIENVENIDA10, recarga la página (el carrito se conserva) y pulsa "Finalizar pedido".

Funciones incluidas ​

  • Catálogo generado desde un arreglo PRODUCTOS.
  • Filtro por categoría, buscador que ignora acentos y orden por precio, nombre o más vendidos.
  • Etiquetas de oferta, precio anterior tachado, calificación y productos agotados.
  • Carrito lateral con capa oscura, cierre con Esc, bloqueo del scroll de fondo.
  • Sumar, restar y eliminar, respetando el stock disponible.
  • Barra de progreso hacia el envío gratis.
  • Cupones de porcentaje o monto fijo con compra mínima.
  • Persistencia en localStorage con try/catch y sincronización entre pestañas.
  • Total formateado con Intl.NumberFormat('es-MX').
  • Pedido por WhatsApp con detalle, descuento, envío y total.

Configuración ​

Todo lo editable está al inicio del script:

js
const WHATSAPP = '5215512345678';      // número del vendedor (52 + 1 + 10 dígitos o 52 + 10)
const ENVIO_GRATIS_DESDE = 899;        // monto para envío gratis
const COSTO_ENVIO = 129;
const CLAVE_STORAGE = 'barroygrano_carrito_v1';
const CUPONES = {
  BIENVENIDA10: { tipo: 'porcentaje', valor: 10 },
  CAFE50: { tipo: 'monto', valor: 50, minimo: 500 },
};

Productos ​

js
const PRODUCTOS = [
  {
    id: 1,                       // único y numérico
    nombre: 'Café Chiapas Altura 500 g',
    cat: 'Café',                 // las categorías se generan solas
    precio: 229,
    antes: null,                 // precio anterior (para mostrar oferta) o null
    img: 'cafe-chiapas',         // en la demo: semilla de picsum.photos
    rating: 4.9, resenas: 214,
    stock: 30,                   // 0 = agotado
    etiqueta: 'Más vendido',     // texto del distintivo o null
  },
];

Para usar fotos reales, cambia la función urlImg:

js
const urlImg = (p) => `img/productos/${p.img}.webp`;

Cómo funciona el carrito ​

Estado mínimo ​

El carrito guarda sólo los ids y cantidades, nunca precios. Así, si cambias un precio en PRODUCTOS, el carrito guardado del cliente muestra el precio nuevo:

js
// carrito = { items: { "1": 2, "5": 1 }, cupon: "BIENVENIDA10" }
function lineas() {
  return Object.entries(carrito.items).map(([id, cant]) => {
    const p = PRODUCTOS.find((x) => x.id === Number(id));
    return { ...p, cant, subtotal: p.precio * cant };
  });
}

Persistencia segura ​

localStorage puede fallar (modo privado, almacenamiento bloqueado) o contener datos viejos. Por eso la carga valida todo y la tienda funciona aunque no se pueda guardar:

js
function cargarCarrito() {
  try {
    const datos = JSON.parse(localStorage.getItem(CLAVE_STORAGE));
    if (datos && typeof datos.items === 'object') {
      // Limpia productos que ya no existen y ajusta al stock actual
      for (const id of Object.keys(datos.items)) {
        const p = PRODUCTOS.find((x) => x.id === Number(id));
        if (!p || !(datos.items[id] > 0)) delete datos.items[id];
        else datos.items[id] = Math.min(datos.items[id], p.stock);
      }
      return datos;
    }
  } catch (e) { /* bloqueado o corrupto */ }
  return { items: {}, cupon: null };
}

Cambiaste la estructura del carrito

Si en una versión futura cambias el formato guardado, sube la versión de la clave (_v2). Así los clientes empiezan con un carrito limpio en lugar de uno incompatible.

Cálculo de totales ​

js
function totales() {
  const ls = lineas();
  const subtotal = ls.reduce((a, l) => a + l.subtotal, 0);
  // descuento según el cupón, envío gratis si (subtotal - descuento) >= ENVIO_GRATIS_DESDE
  // ...
  return { ls, unidades, subtotal, descuento, envio, total };
}

Cada acción (agregar, cambiarCantidad, eliminar) modifica el estado, llama a guardarCarrito() y vuelve a pintar con pintarCarrito(). Es el mismo patrón "estado → render" que usan React o Vue, pero en JavaScript puro.

Eventos delegados ​

En lugar de poner un addEventListener en cada botón (que se recrean al repintar), se escucha en el contenedor y se identifica el botón con atributos data-:

js
$('#items').addEventListener('click', (e) => {
  const b = e.target.closest('button');
  if (!b) return;
  if (b.dataset.mas) cambiarCantidad(Number(b.dataset.mas), 1);
  if (b.dataset.menos) cambiarCantidad(Number(b.dataset.menos), -1);
  if (b.dataset.eliminar) eliminar(Number(b.dataset.eliminar));
});

Personalización visual ​

css
:root {
  --primario: #7c2d12;   /* marrón café: logo, botón del carrito, precios */
  --acento: #ea580c;     /* etiquetas de oferta, contador, barra de envío */
  --verde: #15803d;      /* "Nuevo", cupón aplicado */
  --fondo: #faf7f2;
}

Siguientes pasos (cuando el negocio crezca) ​

NecesidadSolución
El dueño quiere editar productos sin tocar códigoMueve PRODUCTOS a un productos.json o a una hoja de Google publicada como CSV
Pagos con tarjetaStripe Payment Links o Mercado Pago Checkout Pro (botón por producto o carrito)
Inventario real y pedidos guardadosBackend con el esquema e-commerce y una API en Node o PHP
Muchos productos y SEO por productoWooCommerce, Shopify o una tienda con Next.js

Los precios del navegador no son confiables

En esta tienda el pedido llega por WhatsApp y el vendedor confirma el total, así que no hay riesgo. Pero si conectas pagos en línea, el servidor debe recalcular el total con los precios de la base de datos: cualquiera puede modificar el JavaScript y comprar a $1.

Academia Web — aprende, copia y construye.