Skip to content
En esta página

Creando un carrito de compras

Hacer un carrito de compras no es una idea muy original pero sirve para entender cómo funciona la lógica interna de una aplicación cuando hay varios componentes interconectados.

Como para tener una idea de cómo funciona un carrito de compras en una aplicación real sería bueno googlear distintos sitios de e-commerce y ver cómo manejan esa lógica en términos de UI y UX.

Ejemplos reales

Acá pueden ver algunos ejemplos de e-commerce:

Le Blé

Quijote Lunch

Atelier Douceur

Nucha Bakery

Tea Connection

Si los prueban van a ver que el funcionamiento es bastante parecido:

1. Una página principal donde se muestran cards de los productos, casi siempre con la opción de agregar al carrito al menos 1 unidad desde la card, o clickear sobre la imagen para ir a una nueva página con el detalle del producto, y agregarlo al carrito desde esa página, seleccionando la cantidad con un contador: ➖️ 1 ➕️.

2. Un ícono de carrito 🛒️ generalmente arriba a la derecha, con un número indicando la cantidad de productos que tenemos agregados. A veces este número es el total de todas las unidades de cada producto (por ejemplo, si tenemos dos productos y dos unidades de cada uno 🍰️🍰️ 🧁️🧁️ el contador dice 4) o el total de productos individuales (si tenemos varias unidades de un sólo producto 🍰️🍰️🍰️ el contador dice 1). Y al clickear en el ícono aparece una sidebar con el carrito (Quijote Lunch) o nos redirecciona a la página del carrito (Le Blé).

3. Cuando se agrega el producto al carrito aparece algún mensaje que indica que el producto fue agregado. En algunos casos el botón de agregar al carrito en la card se transforma en un botón para cambiar la cantidad (Le Blé) ➖️ 1 ➕️, o en un botón para ir al carrito (Atelier) 🛒️, o directamente aparece una sidebar del lado derecho con el detalle del carrito (Quijote Lunch).

4. Cuando clickeamos en Finalizar compra nos aparece un cartel diciendo que primero tenemos que ingresar con nuestro usuario, o directamente nos redirecciona a una página de Login (Nucha).

Y tambien pueden ver que tienen algunas diferencias:

En algunos casos (Le Blé) el estado del carrito está sincronizado con el estado de las tarjetas (cards) de los productos de manera que si agregamos un producto, vamos al carrito, y luego volvemos a la página principal, en la card del producto que agregamos se indica que el producto ya está en el carrito (e incluso la cantidad de unidades agregadas) para que no volvamos a agregarlo accidentalmente 👍️

La página de Le Blé está hecha con Angular, el framework de frontend desarrollado por Google, y Angular tiene una muy buena herramienta para manejar el estado global llamada NgRx que es muy paredida a Vuex (la de Vue) y a Redux (la de React).

En cambio, las otras páginas están hechas con Tienda Nube 😐️ o WooCommerce 😖️ el plugin de e-commerce para WordPress 😬️

Y esa diferencia se nota. Si se fijan, salvo en el caso de Le Blé, en las otras páginas, cuando luego de agregar un producto e ir al carrito volvemos a la página principal o a la página del producto, no hay nada que nos indique que el producto ya fue agregado.

Y en el caso de Nucha (WooCommerce) ni siquiera está sincronizada la cantidad que aparece en el carrito con la cantidad que aparece en el detalle del producto 🤦‍♂️️

Otra diferencia muy notable es la velocidad. La de Le Blé es por lejos la más rápida (y eso que Angular es considerado un framework lento en comparación con Vue, React o Svelte) mientras que la de Nucha (WordPress) es extremadamente lenta 🥱️

La de Quijote Lunch (Tienda Nube) tampoco es muy rápida que digamos. Internamente Tienda Nube usa Symfony, un framework para Server Side Rendering con PHP. A diferencia del SSR hidratado de los meta-frameworks como Nuxt (para Vue), Next (para React) o SvelteKit, el SSR tradicional suele ser bastante lento. Esto es por la cantidad de llamadas al servidor que son ejectuadas cada vez que navegamos de una página a otra de la aplicación, o cada vez que el estado del frontend cambia.

Ejemplos con Vue

En el caso de que quieren comparar con sitios hechos con Vue, en la página de Nuxt hay varios ejemplos.

Simulando una API REST

Primero que nada, dentro de App.vue tenemos que mostrar las cards de los productos.

Para ver más claramente la lógica no voy a mostrar las imágenes de los productos, sólo el nombre y el precio. Y los estilos de CSS van a ser super simples. Tampoco voy a usar íconos svg.

Recuerden que la data de los productos debe venir de una fuente externa, generalmente de una API REST. Mientras no tengamos una API real configurada podemos usar un archivo index.json y subirlo a GitHub Pages y luego copiamos la URL:

https://dav-leda.github.io/api/products/

Obviamente, esto sólo va a servir para peticiones de tipo GET (o sea para obtener información), no POST, PUT o DELETE. Y tampoco les va servir para hacer queries. Pero para obtener nuestro array de productos es suficiente.

Variables de entorno

Es considerado una buena práctica no hardcodear las URLs de las APIs que estemos usando dentro de la app sino declararlas como variables de entorno dentro de un archivo .env. Esto es para que no queden expuestas públicamente en el repositorio de GitHub.

Para eso, en la raíz del proyecto tenemos que crear un archivo llamado .env, y dentro de ese archivo incluimos la variable. La convención es que el nombre de las variables env sea en mayúsculas 🤷‍♂️️ No es es necesario poner comillas en el valor de la variable:

VITE_API_URL=https://dav-leda.github.io/api

Si instalaron Vue con Vite todas las variables env deben comenzar con el prefijo VITE_, y luego son accedidas desde cualquier componente o archivo de la aplicación de esta forma:

js
const baseUrl = import.meta.env.VITE_API_URL

O usando destructuring:

js
const { VITE_API_URL: baseUrl } = import.meta.env

.gitignore

No olviden agregar .env dentro de .gitignore para que el archivo .env no sea incluido en los commits a GitHub.

No olviden tampoco incluir un archivo .env.example que contenga los nombres de las variables, pero no sus valores:

VITE_API_URL=

Este archivo no debe ser incluido en el .gitignore ya que sirve como guía para otro desarrollador que esté trabajando sobre el mismo repositorio, para saber cuáles son las variables de entorno que deben ser declaradas. Y también para ustedes, si quieren clonar su propio repo en otra máquina. Lo único que tienen que hacer es agregar el valor de la variable y luego cambiar el nombre del archivo de .env.example a .env, y de esa manera ya tienen las variables listas para ser usadas 👍️

Recuerden también que esto es sólo para no exponer esta información en el repositorio, pero una vez que la app está online cualquiera que tenga cierto conocimiento de JavaScript podría llegar a encontrar estas variables usando las Dev Tools del browser. Si la información que contienen las variables es muy confidencial (por ejemplo, API Keys) la única forma de ocultarlas es en el backend, ya sea usando serverless functions, un framework para SSR con Vue como Nuxt o creando nuestro propio backend (por ejemplo, con Node.js).

Obtener la data de los productos con fetch

Entonces en App.vue hacemos una llamada a la API con fetch para obtener la data de los productos. Esta llamada va a ser invocada en el lifecycle hook created(), es decir, apenas el componente es creado pero antes de que el template sea renderizado.

Recuerden que el método que usa fetch deber ser asincrónico, es decir, usando async/await dentro de un bloque try/catch:

Por ahora vamos a poner el fetch dentro de los methods del componente, pero más adelante tendríamos que pasar la lógica del fetch a un archivo .js aparte, para poder reutilizar el método en cualquier componente donde lo necesitemos (por ejemplo, para hacer un fetch a una API de autenticación en el componente del Login).

js
import ProductCard from '@/components/ProductCard.vue'

const { VITE_API_URL: baseUrl } = import.meta.env
const endpoint = baseUrl + '/products'

export default {

  components: {
    ProductCard
  },

  data: () => ({ 
    products: [],
    fetchError: ''
  }),

  created() {
    this.fetchData(endpoint)
  },

  methods: {
    async fetchData(endpoint) {
      try {
        this.products = await (await fetch(endpoint)).json()
      } catch (err) { 
        this.fetchError = 'Error de conexión.'
        console.log(err)
      }
    }
  }
}

Si la llamada falla vamos a ver el mensaje de error en la consola. Pero también es necesario que el usuario se entere de que algo salió mal. Para eso usamos this.fetchError con un mensaje que el usuario pueda entender (Error de conexión) y lo mostramos en la vista:

html
<template>
    <div>
      
      <p v-if="fetchError" class="error">{{ fetchError }}</p>

      <div v-else>
        <ProductCard
          v-for="product in products" :key="product.id"
          :product="product"
        />
      </div>
    </div>
</template>

Entonces, si se produce un error, se muestra el mensaje de error al usuario, y si salió bien, se muestran las cards de los productos.

Store para el carrito

En una aplicación real la forma de manejar el estado global del carrito sería con Vuex o Pinia. Pero por ahora vamos a usar una store simple.

Para eso lo único que tenemos que hacer es crear un archivo .js (no .vue) con un objeto exportable de JavaScript dentro del directorio /src en un directorio que podemos llamar /stores:

js
// src/stores/cartStore.js

export const cartStore = {
  // STATE
  cart: [],

  // ACTIONS
  addToCart(product) {
    this.cart.push({
      ...product,
      qty: 1,
      subtotal: product.price
    })    
  }
}

En ese array vacío van a ir guardándose los productos. El método addToCart hace un push al array, tomando como parámetro el producto a agregar e incluyendo en el objeto del producto una propiedad para la cantidad (qty) y otra para el precio total para ese producto (o sea, el subtotal de la compra) que se va a ir incrementando a medida que agreguemos más unidades del mismo.

Agregar productos al carrito

En la mayoría de los e-commerce cuando uno clickea en el botón de agregar al carrito el producto se agrega una sola vez y nos muestra el carrito en una sidebar, o el botón de agregar es reemplazado por un contador (Le Blé):

Esto tiene sentido, porque si el botón de agregar no cambia el usuario no sabe si el producto se agregó o no.

Aunque también puede ocurrir que el botón de agregar sólo muestre un mensaje mientras el producto se está agregando y luego vuelve al estado original 🤔️ (como en Tienda Nube):

En el caso de que la cantidad de unidades se maneje con un contador (y no clickeando varias veces en el botón de agregar 😒️) podemos usar dos métodos nuevos (decrementQty(), incrementQty()) en el store:

js
export const cartStore = {
  // STATE
  cart: [],

  // GETTERS
  findById(id) {
    return this.cart.find(item => item.id === id)
  },
  productQty(id) {
    const inCart = this.findById(id)
    return inCart ? inCart.qty : 0
  },

  // ACTIONS
  addToCart(product) {
    this.cart.push({
      ...product,
      qty: 1,
      subtotal: product.price
    })    
  },
  decrementQty(id) {
    const inCart = this.findById(id)
    if (inCart && inCart.qty > 1) {
      inCart.qty--
      inCart.subtotal = inCart.price * inCart.qty
    }
  },
  incrementQty(id) {
    const inCart = this.findById(id)
    if (inCart) { 
      inCart.qty++
      inCart.subtotal = inCart.price * inCart.qty
    }
  }
}

Primero agregamos dos getters (o sea, métodos que obtienen un valor a partir del estado pero que no producen cambios en el estado). Uno para encontrar el producto en el carrito por su id: findById(id). Si no encuentra nada el método retorna undefined, que en un condicional if JavaScript lo toma como si fuese un false.

Y otro para obtener la cantidad de unidades de ese producto (si es que está en el carrito): productQty(id). Qty es una abreviatura de Quantity, o sea, cantidad. Usamos un operador ternario para chequear si el producto ya está en el carrito. Si ya está retorna la cantidad de unidades, y si no está retorna 0.

Luego agregamos las dos actions (o sea, métodos que mutan el estado): decrementQty(id) e incrementQty(id).

Estos dos métodos reciben el id del producto por parámetro y luego buscan el producto en el carrito usando el getter findById y guardando el resultado en inCart. Si no encuentran nada inCart va a ser undefined y el método no hace nada más. Y si encuentra algo inCart va a ser el objeto del producto.

Como los objetos de JavaScript funcionan por referencia, al mutar la propiedad inCart.qty automáticamente cambia la propiedad qty en el objeto del producto que está en el carrito. Y lo mismo con inCart.subtotal, que nos va a dar el costo total para ese producto (multiplicando precio * cantidad).

Estos dos métodos van a ser usados en el componente ProductCounter con los botones de sumar y restar. Y ProductCounter va a estar incluído dentro del componente del carrito y también dentro del componente del producto:

html
<template>
  <div>
    <button @click="decrement"> ➖️ </button>
    <span>{{ productQty }}</span>
    <button @click="increment"> ➕️ </button>
  </div>
</template>

<script>

import { cartStore } from '@/stores/cartStore'

export default {

  name: 'ProductCounter',

  props: {
    productId: Number
  },

  data: () => ({
    cartStore
  }),

  computed: {
    productQty() {
      return this.cartStore.productQty(this.productId)
    }
  },

  methods: {
    decrement() {
      this.cartStore.decrementQty(this.productId)
    },
    increment() {
      this.cartStore.incrementQty(this.productId)
    }
  }
}
</script>

El componente recibe por props el id del producto. También estoy importando la store del carrito que acabamos de crear y la declaro en data para que se vuelva reactivo. Y así puedo usar los métodos de la store dentro del componente: los getters siempre dentro de computed y las actions siempre dentro de methods ya que getters y computed son equivalentes (y lo mismo actions y methods).

En este caso, una computed para obtener la cantidad de unidades de ese producto en el carrito (productQty) y los methods para incrementar o decrementar esa cantidad.

Fíjense que productQty no es modificada en forma directa por estos methods, las mutaciones ocurren únicamente dentro de la store. Ésta es una de las reglas de patrón Flux: la store debe ser la única fuente de verdad y el único lugar en donde ocurren las mutaciones.

También fíjense que los methods increment y decrement primero chequean si el producto está en el carrito. Si no está en el carrito no hacen nada, y si lo está, recién ahí modifican la cantidad. Con lo cual el usuario primero debe agregar el producto al carrito y recién después puede modificar la cantidad.

No es obligatorio hacerlo de esta forma, en algunos e-commerce el usuario puede seleccionar la cantidad de unidades antes de agregar el producto al carrito. Pero tengan en cuenta que si quieren que la cantidad sea seleccionada antes de agregar al carrito, la lógica de la store debe ser modificada para que el método addToCart tome como parámetro la cantidad de unidades pre-seleccionadas por el usuario.

Para que quede más claro para el usuario que primero tiene que agregar el producto al carrito y luego puede modificar la cantidad de unidades lo que podemos hacer es deshabilitar los botones de ➕️ y ➖️ en función de la existencia o no de ese producto dentro del array del carrito. Y luego podemos usar el atributo nativo de HTML disabled para deshabilitar los botones, haciendo que Vue lo maneje en forma dinámica con v-bind, o sea, :disabled:

html
<template>
  <div>
    <button
      :disabled="notInCart" 
      @click="decrement"
    > ➖️ </button>
    
    <span>{{ productQty }}</span>
    
    <button 
      :disabled="notInCart"
      @click="increment"
    > ➕️ </button>
  </div>
</template>

Y en el script agregamos la computed notInCart que retorna true si el producto no está en el carrito y false si lo está:

js
computed: {
  notInCart() {
    return !this.cartStore.findById(this.productId)
  },
  productQty() {
    return this.cartStore.productQty(this.productId)
  }
}

De esta forma, si el producto no está en el carrito la computed retorna true y el botón queda deshabilitado.

Reutilizar el botón de agregar

Otra opción es no deshabilitar el botón de agregar al carrito y usarlo para sumar más unidades del producto, como en el ejemplo de Tienda Nube. En mi opinión esta segunda opción no tiene mucho sentido, pero suele usarse 🤷‍♂️️

En este caso el método AddToCart de la store sería distinto:

js
export const cartStore = {

  // STATE
  cart: [],
  
  // GETTERS
  findById(id) {
    return this.cart.find(item => item.id === id)
  },

  // ACTIONS
  addToCart(product) {
    const inCart = this.findById(product.id)
    if (!inCart) {
      this.cart.push({
        ...product,
        qty: 1,
        subtotal: product.price
      })    
    } else {
      inCart.qty++
      inCart.subtotal = inCart.price * inCart.qty
    }
  }
}

En este caso el método addToCart(product) primero chequea si el producto ya está en el carrito. Si no está (o sea, !inCart) lo agrega, pero si ya está en el carrito le suma una unidad (inCart.qty++).

Y así, cada vez que volvemos a clickear en Agregar al carrito se van sumando unidades de ese producto.

Cantidad total de productos

Además de obtener la cantidad de unidades de cada producto, podemos usar el store del carrito para obtener la cantidad total de productos.

Esto se puede hacer de dos maneras: en algunos e-commerce el número que se muestra es el total de todas las unidades de cada producto (por ejemplo, si tenemos dos productos y dos unidades de cada uno 🍰️🍰️ 🧁️🧁️ el contador dice 4), y en otros es el total de productos individuales: si tenemos varias unidades de un sólo producto 🍰️🍰️🍰️ el contador dice 1 (Perdón que me repita, es sólo para que quede más claro 😬️ )

Creo que la primea opción tiene más sentido, así que voy a usar esa:

js
export const cartStore = {
  
  // STATE
  cart: [],
  
  // GETTERS
  findById(id) {
    return this.cart.find(item => item.id === id)
  },
  productQty(id) {
    const inCart = this.findById(id)
    return inCart ? inCart.qty : 0
  },
  cartQty() {
    return this.cart.reduce((total, item) => total + item.qty, 0)
  },

  // ACTIONS etc, etc...
}

En el getter cartQty usamos el método array.reduce() para sumar todas las unidades de todos los productos.

Card para el producto

Estos métodos los podemos usar en el componente que muestra las tarjetas de productos (ProductCard).

En este caso el botón de Agregar al carrito se va a deshabilitar luego de agregar un producto y si luego el usuario quiere agregar más unidades puede usar el contador (ProductCounter) que toma el id del producto como prop.

Para deshabilitar el botón hacemos que el atributo disabled sea dinámico, y lo mismo para la clase que cambia el color del botón (btnColor).

Y el texto dentro del botón va a ser condicional, usando un operador ternario. Si added es true (o sea que ya fue agregado) el texto del botón es 'Agregado', y si es false, 'Agregar':

html
<template>
  <div>

    <header>
      <h2>ProductCard</h2>
      <h4>{{ product.title }}</h4>
      <p>$ {{ product.price }}</p>
    </header>

    <footer>  
   
      <ProductCounter :productId="product.id"/>

      <button 
        @click="addToCart"
        :disabled="added"
        :class="btnColor"
      >
        {{ added ? 'Agregado' : 'Agregar al carrito' }}
      </button>
    </footer>
    
  </div>
</template>

<script>

import { cartStore } from '@/stores/cartStore'
import ProductCounter from '@/components/cart/ProductCounter.vue'

export default {

  name: 'ProductCard',

  components: {
    ProductCounter
  },

  props: {
    product: {
      id: Number,
      title: String,
      price: Number
    }
  },

  data: () => ({ 
    cartStore
  }),

  computed: {
    added() {
      return this.cartStore.findById(this.product.id)
    },
    btnColor() {
      return this.added ? 'btn-secondary' : 'btn-primary'
    }
  },

  methods: {
    addToCart() {
      this.cartStore.addToCart(this.product)
    }
  }
}
</script>

Entonces, la computed added() usa el método del store para buscar el producto en el carrito. Si encuentra algo va a retornar truthy y si no va a retornar falsy.

Y la computed para el color del botón va a usar a su vez la computed para determinar la clase de CSS que le aplica.

Entonces, las cards de los productos nos quedarían así:

Contador del carrito

En la NavBar, del lado derecho, vamos a incluir un número que muestre la cantidad de productos en el carrito. Lo más usual en estos casos es usar un ícono svg de un carrito con un número arriba, pero para simplificarlo voy a poner solamente un círculo con el número:

html
<template>
  <div>
    <div class="cart-counter">{{ cartQty }}</div>
  </div>
</template>

<script>
import { cartStore } from '@/stores/cartStore'

export default {

  data: () => ({ 
    cartStore
  }),

  computed: {
    cartQty() {
      return this.cartStore.cartQty()
    }
  }
}
</script>

<style>
.cart-counter {
  /* Crear un círculo en CSS con border-radius 50% */
  border-radius: 50%;
  width: 2.2rem;
  height: 2.2rem;
  font-size: 1.5rem;
  cursor: pointer;
}
</style>

Entonces la NavBar nos quedaría así, y al agregar un producto, o sumar más unidades con el contador, el número del total en el círculo cambia:

Mostral el detalle del carrito

Al clickear en ese número debería mostrarnos un detalle con todos los productos en el carrito, sus precios, el precio total de la compra, y botones para sumar o restar unidades, o eliminar un producto.

Para el precio total de la compra vamos a necesitar agregar un getter más a la store del carrito. Y para eliminar un producto vamos a necesitar una action porque al eliminar estamos haciendo una mutación (o sea, un cambio en el estado del carrito):

js
export const cartStore = {
  
  // STATE
  cart: [],
  
  // GETTERS
  findById(id) {
    return this.cart.find(item => item.id === id)
  },
  productQty(id) {
    const inCart = this.findById(id)
    return inCart ? inCart.qty : 0
  },
  cartQty() {
    return this.cart.reduce((total, item) => total + item.qty, 0)
  },
  cartTotalPrice() {
    return this.cart.reduce((total, item) => total + item.subtotal, 0)
  },

  // ACTIONS
  addToCart(product) {
    this.cart.push({
      ...product,
      qty: 1,
      subtotal: product.price
    })    
  },

  removeFromCart(id) {
    const index = this.cart.findIndex(item => item.id === id)
    this.cart.splice(index, 1)
  },
  // etc, etc...
}

El getter cartTotalPrice() también usa un reduce sumando el subtotal de cada producto para obtener el costo total.

Y la action removeFromCart(id) busca el producto por su índice en el array del carrito y lo elimina con un splice.

Componente de la tabla

Para mostrar toda esta información del carrito lo mejor es usar una tabla. Esta tabla puede estar en un side panel que aparezca desde la derecha (como en el ejemplo de Tienda Nube) o puede estar en una página aparte como en el ejemplo de Le Blé.

Para mostrar el carrito en una nueva página deben usar Vue Router.

Si nunca usaron Vue Router lo que pueden hacer es mostrar el componente de la tabla dentro de una ventana modal importando la tabla dentro de la modal. Otra opción mejor es usar slots en el componente de la modal, y pasarle la tabla como un slot, y de esta forma el componente de la modal se puede reutilizar para otras cosas (por ejemplo, para mostrar una ventana de Login).

Importando la store del carrito en la tabla podemos saber si está vacío o si ya tiene algún producto. Si está vacío mostramos un mensaje que diga: 'Aún no agregaste nada', y si ya tiene algo mostramos los productos en la tabla.

Como el carrito es un array, para chequear si está vacío usamos .length.

Y para mostrar los productos usamos un v-for para iterar sobre el array:

html
<template>
  <div>
    <table v-if="products.length">
      <thead>
        <tr>
          <th>Producto</th>
          <th>Precio</th>
          <th>Cantidad</th>
          <th>Total</th>
          <th></th>
        </tr>
      </thead>

      <tbody>
        <tr v-for="product in products" :key="product.id">
          <td>{{ product.title }}</td>
          <td>$ {{ product.price }}</td>
          <td>
            <ProductCounter :productId="product.id"/>
          </td>
          <td>$ {{ product.subtotal }}</td>
          <td @click="remove(product.id)"> 🗑️ </td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <td>Total:</td>
          <td></td>
          <td></td>
          <td>$ {{ total }}</td>
          <td></td>
        </tr>
      </tfoot>
    </table>

    <p v-else>Aún no has agregado productos en el carrito.</p>

  </div>
</template>

<script>

import { cartStore } from '@/stores/cartStore'
import ProductCounter from '@/components/cart/ProductCounter.vue'

export default {

  name: 'CartTable',

  components: {
    ProductCounter
  },

  data: () => ({ 
    cartStore
  }),

  computed: {
    products() {
      return this.cartStore.cart
    },
    total() {
      return this.cartStore.cartTotalPrice()
    }
  },

  methods: {
    remove(id) {
      this.cartStore.removeFromCart(id)
    }
  }
}
</script>

Entonces, luego de agregar productos, al cliquear en el número vemos la modal con la tabla del carrito dentro:

Confirmar la compra

Como pueden ver la ventana modal tiene abajo un botón para cerrarla y otro para confirmar la compra. Estos dos botones los podemos tener en componentes aparte e importarlos en el componente de la modal. Otra opción mejor es pasárselos a la modal mediante slots y así podemos reutilizar la modal para otra cosa.

Como vimos en el ejemplo de la modal, ésta puede tener un slot para el header y otro para el footer:

html
<template>
  <transition>
    <dialog v-if="showModal">
      <article>
        <header>
          <slot name="header"/>
        </header>
        <footer>
          <slot name="footer"/>
        </footer>
      </article>
    </dialog>
  </transition>
</template>

<script>
export default {

  name: 'ModalWindow',

  props: {
    showModal: Boolean
  }
}
</script>

Entonces, en el componente que use la modal (puede ser la NavBar o el componente CartCounter) le podemos pasar al componente de la modal dentro de esos slots la tabla del carrito en el header y los botones en el footer:

html
<template>
  <ModalWindow :showModal="showModal">

    <template #header>

      <CartTable v-if="!showGracias"/>
      <h3 v-else>¡Gracias por tu compra!</h3>

    </template>

    <template #footer>
      <div>
        <button @click="closeModal">Cerrar</button>

        <button
          v-if="!showGracias" 
          @click="confirmOrder"
        >Confirmar compra</button>

      </div>
    </template>

  </ModalWindow>
</template>
<script>

import ModalWindow from '@/components/ModalWindow.vue'
import CartTable from '@/components/cart/CartTable.vue'

export default {

  components: {
    ModalWindow, CartTable
  },

  data: () => ({ 
    showModal: false,
    showGracias: false
  }),

  methods: {
    confirmOrder() {
      this.showGracias = true
    },
    closeModal() {
      this.showModal = this.showGracias = false
    }
  }
}
</script>

Entonces, al clickear en el botón de confirmar la compra el booleano showGracias (perdón por el spanglish 🤷‍♂️️) pasa de false a true y desaparece la tabla (y el botón de confirmar) y aparece un mensaje diciendo 'Gracias':

Por ahora lo vamos a dejar así, pero más adelante vamos a agregar la funcionalidad del Login, y hacer que antes de confirmar la compra haya un chequeo para ver si el usuario ya hizo el Login, y de lo contrario que muestre un mensaje diciendo que antes de confirmar la compra primero debe ingresar.

Y más adelante vamos a hacer que al confirmar la compra los datos de la compra se guarden en una base de datos, referenciando el id del usuario a quien pertenece esa compra. Y que en otra sección el usuario pueda ver un historial de todas sus compras ordenadas por fecha.

Y también podríamos enviar un mail al usuario con los datos de su compra, pero para eso deberíamos armar un backend o usar serverless functions. Como ése es un tema bastante largo quedará para otro post 📝️