Carrito de compras rápidas

En este artículo vamos a ver como agregar el carrito compras rápidas en tu diseño, el cual se muestra en un popup sin tener que refrescar la página a la hora de agregar un producto al carrito:

Esta funcionalidad incluye el carrito de compras con:

  • Una notificación que se muestra al agregar un producto al carrito
  • La posibilidad de agregar productos al carrito, alterar sus cantidades y borrarlos; sin refrescar o redireccionar la página
  • Un botón para seguir comprando que cierra el carrito

Este tutorial no incluye:

  • Calculador de envíos. Si bien es clave para el carrito, es un tutorial aparte que pueden ver acá
  • Promociones dentro del carrito. Este es otra funcionalidad que pueden agregar luego siguiendo este tutorial

HTML

Lo primero que vamos a hacer es crear los tpls necesarios para la funcionalidad.

1. Empezamos creando los tpls cart-panel.tpl, cart-item-ajax.tpl y cart-totals.tpl dentro de la carpeta snipplets. Es muy importante que no modifiquen los IDs o las clases “js-..”

cart-panel.tpl

Representa al modal o popup dentro del cual se muestra el carrito, incluye el for que lista los productos y los mensajes para un carrito vacío si no hay stock de un producto. 

<div class="js-ajax-cart-list cart-row">
    {# Cart panel items #}
    {% if cart.items %}
      {% for item in cart.items %}
        {% include "snipplets/cart-item-ajax.tpl" %}
      {% endfor %}
    {% endif %}
</div>
<div class="js-empty-ajax-cart cart-row" {% if cart.items_count > 0 %}style="display:none;"{% endif %}>
     {# Cart panel empty #}
    <div class="alert alert-info">{{ "El carrito de compras está vacío." | translate }}</div>
</div>
<div id="error-ajax-stock" style="display: none;">
    <div class="alert alert-warning">
         {{ "¡Uy! No tenemos más stock de este producto para agregarlo al carrito. Si querés podés" | translate }}<a href="{{ store.products_url }}" class="btn-link ml-1">{{ "ver otros acá" | translate }}</a>
    </div>
</div>
<div class="cart-row">
    {% include "snipplets/cart-totals.tpl" %}
</div>

cart-item-ajax.tpl

Este es el item de cada producto agregado al carrito. Como usamos PHP para su funcionamiento, es importante que no cambiemos su nombre ni posición dentro de las carpetas del theme. 

Este archivo muestra del producto agregado:

  • El nombre
  • La variante
  • Los controles para modificar cantidad
  • El botón para borrar el producto
  • El subtotal de la sumatoria de las cantidades de ese producto 
<div class="js-cart-item cart-item form-row" data-item-id="{{ item.id }}">


  {# Cart item image #}
  <div class="col-2 {% if cart_page %}col-md-1{% endif %}">
    <img src="{{ item.featured_image | product_image_url('medium') }}" class="img-fluid" />
  </div>
  <div class="col-10 {% if cart_page %}col-md-11{% endif %}">


    {# Cart item name #}
    <div class="cart-item-name">
      <a href="{{ item.url }}">
        {{ item.short_name }}
      </a>
      <small>{{ item.short_variant_name }}</small>
    </div>


    {# Cart item quantity controls #}
    <span class="pull-left">
      <button type="button" class="js-cart-quantity-btn cart-item-btn btn" onclick="LS.minusQuantity({{ item.id }}{% if not cart_page %}, true{% endif %})">
        {% include "snipplets/svg/minus.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
      </button>
      <span>
        <input type="number" name="quantity[{{ item.id }}]" data-item-id="{{ item.id }}" value="{{ item.quantity }}" class="js-cart-quantity-input cart-item-input form-control"/>
      </span>
      <span class="js-cart-input-spinner cart-item-spinner" style="display: none;">
        {% include "snipplets/svg/sync-alt.tpl" with {svg_custom_class: "icon-inline icon-spin svg-icon-text"} %}
      </span>
      <button type="button" class="js-cart-quantity-btn cart-item-btn btn" onclick="LS.plusQuantity({{ item.id }}{% if not cart_page %}, true{% endif %})">
        {% include "snipplets/svg/plus.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
      </button>
    </span>


    {# Cart item subtotal mobile #}
    <h6 class="js-cart-item-subtotal cart-item-subtotal" data-line-item-id="{{ item.id }}">{{ item.subtotal | money }}</h6>
  </div>


  {# Cart item delete #}
  <div class="col-1 cart-item-delete text-right">
    <button type="button" class="btn" onclick="LS.removeItem({{ item.id }}{% if not cart_page %}, true{% endif %})">
      {% include "snipplets/svg/trash-alt.tpl" with {svg_custom_class: "icon-inline icon-lg svg-icon-text"} %}
    </button>
  </div>
</div>

cart-totals.tpl

Acá mostramos los totales y subtotales del carrito. Si a futuro quieren agregar las promociones o el calculador de envíos, pueden hacerlo en este archivo.

En este archivo también incluimos un link para continuar comprando que cierra el carrito. En caso de querer modificar este link para que lleve al usuario a la página de inicio podemos usar {{ store.url }} o si queremos que vaya al listado de productos podemos usar {{ store.products_url }}

En este snipplet usamos la condición cart_page para que pueda ser reutilizado en templates/cart.tpl donde se encuentra el carrito en su versión “página” y no en el popup que explicamos en este tutorial.

{# IMPORTANT Do not remove this hidden subtotal, it is used by JS to calculate cart total #}
<div class="subtotal-price hidden" data-priceraw="{{ cart.subtotal }}"></div>


{# Used to assign currency to total #}
<div id="store-curr" class="hidden">{{ cart.currency }}</div>
    
{# Cart panel subtotal #}
<h5 class="js-visible-on-cart-filled {% if not cart_page %}row{% else %}text-right{% endif %} mb-1 {% if cart_page %}text-center-xs{% endif %}" {% if cart.items_count == 0 %}style="display:none;"{% endif %}>
  <span {% if not cart_page %}class="col"{% endif %}>
    {{ "Subtotal" | translate }}
    {% if settings.shipping_calculator_cart_page %}
      <small>{{ " (sin envío)" | translate }}</small>
    {% endif %}
    :
  </span>
  <strong class="js-ajax-cart-total js-cart-subtotal {% if not cart_page %}col{% endif %} text-right" data-priceraw="{{ cart.subtotal }}">{{ cart.subtotal | money }}</strong>
</h5>


{# Cart panel total #}


<div class="js-cart-total-container js-visible-on-cart-filled mb-3" {% if cart.items_count == 0 %}style="display:none;"{% endif %}>
  <h2 class="{% if not cart_page %}row{% else %}text-right{% endif %} text-primary mb-0">
    <span {% if not cart_page %}class="col"{% endif %}>{{ "Total" | translate }}:</span>
    <span class="js-cart-total {% if cart.shipping_data.selected %}js-cart-saved-shipping{% endif %} {% if not cart_page %}col{% endif %} text-right">{{ cart.total | money }}</span>
  </h2>


  {# IMPORTANT Do not remove this hidden total, it is used by JS to calculate cart total #}
  <div class='total-price hidden'>
    {{ "Total" | translate }}: {{ cart.total | money }}
  </div>
</div>


<div class="js-visible-on-cart-filled container-fluid" {% if cart.items_count == 0 %}style="display:none;"{% endif %}>


  {# No stock alert #}


  <div id="error-ajax-stock" class='alert alert-warning' role='alert' style="display:none;">
     {{ "¡Uy! No tenemos más stock de este producto para agregar este producto al carrito. Si querés podés" | translate }}<a href="{{ store.products_url }}" class="btn-link">{{ "ver otros acá" | translate }}</a>
  </div>
  <div>
    {% if cart_page %}
    <div class="row justify-content-end">
      <div class="col col-md-3">
    {% endif %}


    {# Cart panel CTA #}
    
    {% set cart_total = (settings.cart_minimum_value * 100) %}


    <div class="js-ajax-cart-submit row mb-3" {{ cart.total < cart_total ? 'style="display:none"' }} id="ajax-cart-submit-div">
      <input class="btn btn-primary btn-block" type="submit" name="go_to_checkout" value="{{ 'Iniciar Compra' | translate }}"/>
    </div>


    {# Cart panel continue buying link #}


    {% if settings.continue_buying %}
      <div class="row mb-2">
        <div class="text-center w-100">
          <a href="#" class="js-modal-close btn btn-link">{{ 'Seguir comprando' | translate }}</a>
        </div>
      </div>
    {% endif %}


    {# Cart minium alert #}


    <div class="js-ajax-cart-minimum alert alert-warning mt-4" {{ cart.total >= cart_total ? 'style="display:none"' }} id="ajax-cart-minumum-div">
      {{ "El monto mínimo de compra (subtotal) es de" | translate }} {{ cart_total | money }}
    </div>
    <input type="hidden" id="ajax-cart-minimum-value" value="{{ cart_total }}"/>
    {% if cart_page %}
    </div>
    </div>
    {% endif %}
  </div>
</div>

2. En este paso necesitamos agregarle al botón de “Agregar al carrito” en el formulario de producto, las clases js-addtocart js-prod-submit-form. En el caso del theme Base se encuentra en el snipplet product-form.tpl pero quizás en tu diseño esté dentro del template product.tpl

Queda como el siguiente ejemplo:

{% set state = store.is_catalog ? 'catalog' : (product.available ? product.display_price ? 'cart' : 'contact' : 'nostock') %}
{% set texts = {'cart': "Agregar al carrito", 'contact': "Consultar precio", 'nostock': "Sin stock", 'catalog': "Consultar"} %}
<input type="submit" class="js-addtocart js-prod-submit-form btn btn-primary btn-block mb-4 {{ state }}" value="{{ texts[state] | translate }}" {% if state == 'nostock' %}disabled{% endif %} />

También tenemos que agregar dentro del template product.tpl en el div padre de todo el detalle de producto el ID “single-product” y las clases js-product-detail js-product-container. Si ya hiciste el tutorial del Calculador de envíos, probablemente ya tengas esto hecho quedando de la siguiente forma:

<div id="single-product" class=”js-product-detail js-product-container" data-variants="{{product.variants_object | json_encode }}" itemscope itemtype="http://schema.org/Product">

HTML del detalle de producto

</div>

3. En el archivo snipplets/header-utilities.tpl o donde tengas el icono del carrito de compras en la navegación de tu diseño, vamos a agregar lo siguiente:

<div id="ajax-cart" class="cart-summary">
    <a href="#" class="js-modal-open js-toggle-cart" data-toggle="#modal-cart">
        {% include "snipplets/svg/shopping-bag.tpl" with {svg_custom_class: "icon-inline icon-w-14 svg-icon-text"} %}
        <span class="js-cart-widget-amount cart-widget-amount">{{ "{1}" | translate(cart.items_count ) }}</span>
    </a>
</div>

Este código muestra un icono pero podés usar el que quieras, lo importante está en las clases “js-..” que sirven para abrir el carrito y para actualizar la cantidad de productos en el icono dentro de la navegación. 

4. Ahora necesitamos crear los snipplets de componentes base que usamos en el carrito:

  • La notificación visible al agregar un producto con el archivo notification.tpl
  • El componente para el modal o popup
  • Los iconos SVG dentro de la carpeta snipplets/SVG

notification.tpl

Este archivo tiene la notificación donde se le muestra al usuario que el producto se agregó al carrito con éxito.

Creamos el archivo notification.tpl dentro de la carpeta snipplets con el siguiente código:

{# Add to cart notification #}

{% if add_to_cart %}
    <div class="js-alert-added-to-cart notification-floating notification-hidden" style="display: none;">
        <div class="js-toggle-cart notification notification-primary"> 
            {% include "snipplets/svg/shopping-bag.tpl" with {svg_custom_class: "icon-inline svg-icon-primary mr-1"} %}
            <span>{{ '¡Excelente! Ya agregamos tu producto al carrito.' | translate }}</span>
        </div>
    </div>
{% endif %}

modal.tpl

Precisamos agregar este archivo dentro de la carpeta snipplets para poder usar el embed dentro del cual se muestra el carrito.

{# /*============================================================================
  #Modal
==============================================================================*/

#Properties
    // ID
    // Position - Top, Right, Bottom, Left
    // Transition - Slide and Fade
    // Width - Full and Box
    // modal_form_action - For modals that has a form


#Head
    // Block - modal_head
#Body
    // Block - modal_body
#Footer
    // Block - modal_footer

#}

{% set modal_overlay = modal_overlay | default(true) %}


<div id="{{ modal_id }}" class="js-modal modal modal-{{ modal_class }} modal-{{modal_position}} transition-{{modal_transition}} modal-{{modal_width}} transition-soft" style="display: none;">
    {% if modal_form_action %}
    <form action="{{ modal_form_action }}" method="post" class="{{ modal_form_class }}">
    {% endif %}
    <div class="js-modal-close modal-header">
        <span class="modal-close">
            {% include "snipplets/svg/times.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
        </span>
        {% block modal_head %}{% endblock %}
    </div>
    <div class="modal-body">
        {% block modal_body %}{% endblock %}
    </div>
    {% if modal_footer %}
        <div class="modal-footer d-md-block">
            {% block modal_foot %}{% endblock %}
        </div>
    {% endif %}
    {% if modal_form_action %}
    </form>
    {% endif %}
</div>

5. En este paso tenemos que incluir el modal junto con el carrito, en el caso del theme Base lo hacemos dentro del snipplet header.tpl que es donde esta el head. En tu diseño podés incluirlo en cualquier parte pero recomendamos que sea fuera de cualquier elemento tenga un position: fixed;

{% if not store.is_catalog %}           

    {# Cart Ajax #}

    {% embed "snipplets/modal.tpl" with{modal_id: 'modal-cart', modal_position: 'right', modal_transition: 'slide', modal_width: 'docked-sm', modal_form_action: store.cart_url, modal_form_class: 'js-ajax-cart-panel' } %}
        {% block modal_head %}
            {% block page_header_text %}{{ "Carrito de Compras" | translate }}{% endblock page_header_text %}
        {% endblock %}
        {% block modal_body %}
            {% snipplet "cart-panel.tpl" %}
        {% endblock %}
    {% endembed %}

{% endif %}

6. Por último para la parte de HTML, necesitamos agregar una carpeta SVG dentro de la carpeta snipplets. Acá vamos sumar los SVGs que usamos para los iconos en el carrito.

minus.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M368 224H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h352c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16z"/></svg>

plus.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M368 224H224V80c0-8.84-7.16-16-16-16h-32c-8.84 0-16 7.16-16 16v144H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h144v144c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16V288h144c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16z"/></svg>

trash-alt.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"/></svg>

sync-alt.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M483.515 28.485L431.35 80.65C386.475 35.767 324.485 8 256 8 123.228 8 14.824 112.338 8.31 243.493 7.971 250.311 13.475 256 20.301 256h28.045c6.353 0 11.613-4.952 11.973-11.294C66.161 141.649 151.453 60 256 60c54.163 0 103.157 21.923 138.614 57.386l-54.128 54.129c-7.56 7.56-2.206 20.485 8.485 20.485H492c6.627 0 12-5.373 12-12V36.971c0-10.691-12.926-16.045-20.485-8.486zM491.699 256h-28.045c-6.353 0-11.613 4.952-11.973 11.294C445.839 370.351 360.547 452 256 452c-54.163 0-103.157-21.923-138.614-57.386l54.128-54.129c7.56-7.56 2.206-20.485-8.485-20.485H20c-6.627 0-12 5.373-12 12v143.029c0 10.691 12.926 16.045 20.485 8.485L80.65 431.35C125.525 476.233 187.516 504 256 504c132.773 0 241.176-104.338 247.69-235.493.339-6.818-5.165-12.507-11.991-12.507z"/></svg>

shopping-bag.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M352 128C352 57.42 294.579 0 224 0 153.42 0 96 57.42 96 128H0v304c0 44.183 35.817 80 80 80h288c44.183 0 80-35.817 80-80V128h-96zM224 48c44.112 0 80 35.888 80 80H144c0-44.112 35.888-80 80-80zm176 384c0 17.645-14.355 32-32 32H80c-17.645 0-32-14.355-32-32V176h48v40c0 13.255 10.745 24 24 24s24-10.745 24-24v-40h160v40c0 13.255 10.745 24 24 24s24-10.745 24-24v-40h48v256z"/></svg>

times.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>

CSS

Requisito:

Tener agregados en tu diseño las clases helpers. Podés seguir este este pequeño tutorial para hacerlo (simplemente es copiar y pegar algunas clases, no toma más de 1 minuto).

1. Agregamos el siguiente SASS de colores en static/style-colors.scss.tpl (o la hoja de tu diseño que tenga los colores y tipografías de la tienda). Recordá que las variables de colores y tipografías pueden variar respecto a tu diseño:

{# This mixin adds browser prefixes to a CSS property #}


@mixin prefix($property, $value, $prefixes: ()) {
    @each $prefix in $prefixes {
        #{'-' + $prefix + '-' + $property}: $value;
    }
       #{$property}: $value;
}


{# /* // Wrappers */ #}


%body-font {
  font-size: 12px;
}


{# /* // Buttons */ #}


.btn{
  text-decoration: none;
  text-align: center;
  border: 0;
  cursor: pointer;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  text-transform: uppercase;
  background: none;
  @include prefix(transition, all 0.4s ease, webkit ms moz o);
  &:hover,
  &:focus{
    outline: 0;
    opacity: 0.8;
  }
  &[disabled],
  &[disabled]:hover{
    opacity: 0.5;
    cursor: not-allowed;
    outline: 0;
  }
  &-default{
    padding: 10px 15px; 
    background-color: rgba($main-foreground, .2);
    color: $main-foreground;
    fill: $main-foreground;
    font-weight: bold;
  }
  &-primary{
    padding: 15px;
    background-color: $primary-color;
    color: $main-background;
    fill: $main-background;
    letter-spacing: 4px;
    @extend %body-font;
    &:hover{
      color: $main-background;
      fill: $main-background;
    }
  }
  &-secondary{
    padding: 10px 15px; 
    background-color: $main-background;
    color: $main-foreground;
    fill: $main-foreground;
    border: 1px solid $main-foreground;
  }
  &-block{
    float: left;
    width: 100%;
  }
  &-small{
    display: inline-block;
    padding: 10px;
    font-size: 10px;
    letter-spacing: 2px;
  }
}


button{
  @extend %body-font;
  cursor: pointer;
  &:focus{
    outline: 0;
    opacity: 0.8;
  }
}


.btn-link{
  color: $primary-color;
  fill: $primary-color;
  text-transform: uppercase;
  border-bottom: 1px solid;
  font-weight: bold;
  cursor: pointer;
  &:hover,
  &:focus{
    color: rgba($primary-color, .5);
    fill: rgba($primary-color, .5);
  }
}


{# /* // Modals */ #}


.modal{
  color: $main-foreground;
  background-color:$main-background;
}


{# /* // Alerts and notifications */ #}


.notification-primary{
  color: $primary-color;
  fill: $primary-color;
  border-color: rgba($primary-color, .2);
  background-color: rgba($primary-color, .1);
}


.notification-floating .notification-primary{
  background-color: $main-background;
  border-color: rgba($primary-color, .2);
}


.notification-secondary {
  padding: 5px 0;
  background: darken($main-background, 3%);
  color: rgba($main-foreground, .8);
  border-bottom: 1px solid rgba($main-foreground, .1);
}

2. Agregar los estilos dentro del archivo static/style-critical.tpl 

Si en tu diseño usas una hoja de estilos para el CSS crítico, vamos a necesitar agregar el código debajo dentro de la misma, pero si no es el caso podés unificar el CSS de los pasos 2 y 3 en un solo archivo.

{# /* // Notifications */ #}

.notification{
  padding: 10px;
  opacity: 0.98;
  text-align: center;
}
.notification-floating {
  position: absolute;
  left: 0;
  width: 100%;
  z-index: 2000;
}
.notification-floating .notification{
  margin: 10px;
}
.notification-close {
  padding: 0 5px;
}
.notification-floating .notification {
  box-shadow: 0 0 5px 0 rgba(0, 0, 0, .1), 0 2px 3px 0 rgba(0, 0, 0, .06);
}

3. Agregar los estilos dentro del archivo static/style-async.tpl 

Si en tu diseño usas una hoja de estilos para CSS asíncrono, vamos a necesitar agregar el código debajo dentro de la misma, pero si no es el caso podés unificar el CSS de los pasos 2 y 3 en un solo archivo.

/* // Modals */


.modal {
  position: fixed;
  top: 0;
  display: block;
  width: 80%;
  height: 100%;
  padding: 10px;
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
  transition: all .2s cubic-bezier(.16,.68,.43,.99);
  z-index: 20000;
  &-header{
    width: calc(100% + 20px);
    margin: -10px 0 10px -10px;
    padding: 10px 15px;
    font-size: 20px;
  }
  &-footer{
    padding: 10px;
    clear: both;
  }
  &-full {
    width: 100%;
  }
  &-docked-sm{
    width: 100%;
  }
  &-docked-small{
    width: 80%;
  }
  &-top{
    top: -100%;
    left: 0;
  }
  &-bottom{
    top: 100%;
    left: 0;
  }
  &-left{
    left: -100%;
  }
  &-right{
    right: -100%;
  }
  &-centered{
    height: 100%;
    width: 100%;
  }
  &-top.modal-show,
  &-bottom.modal-show {
    top: 0;
  }
  &-left.modal-show {
    left: 0;
  }
  &-right.modal-show {
    right: 0;
  }
  &-close { 
    display: inline-block;
    padding: 1px 5px 5px 0;
    margin-right: 5px;
    vertical-align: middle;
    cursor: pointer;
  }
  .tab-group{
    margin:  0 -10px 20px -10px;
  }
}


.modal-overlay{
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #00000047;
  z-index: 10000;
}




/* Cart */


/* Table */


.cart-table-row{
  padding: 10px 0;
}


.cart-item{
  position: relative;
  @extend %element-margin;
  &-name{
    float: left;
    width: 100%;
    padding: 0 40px 10px 0;
  }
  &-subtotal{
    float: right;
    margin: 10px 0;
    text-align: right;
    font-weight: normal;
  }
  &-btn{
    padding: 6px;
    display: inline-block;
    background: transparent;
    font-size: 16px;
    opacity: 0.8;
    &:hover{
      opacity: 0.6;
    }
  }
  &-input{
    display: inline-block;
    width: 40px;
    height: 30px;
    font-size: 16px;
    text-align: center;
    -moz-appearance:textfield;
    &::-webkit-outer-spin-button,
    &::-webkit-inner-spin-button{
      -webkit-appearance: none;
    }
  }
  .fa-cog{
    display: none;
  }
  &-spinner{
    display: inline-block;
    width: 40px;
    text-align: center;
  }
  &-delete{
    position: absolute;
    right: 0;
    .btn{
      padding-right:0; 
    }
  }
}


.cart-quantity-input-container svg{
  padding: 6px 14px;
}


.cart-unit-price{
  float: left;
  width: 100%;
  margin: 5px 0 2px 0;
}


.cart-promotion-detail{
  float: left;
  width: 65%;
  text-align: left;
} 
.cart-promotion-number{
  position: absolute;
  right: 0;
  bottom: 0;
  float: right;
  text-align: right;
  font-weight: bold;
} 




/* // Totals */ 


.cart-subtotal{
  float: right;
  clear: both;
  margin: 0 0 10px 0;
}
.total-promotions-row{
  float: right;
  width: 100%;
  margin-bottom: 5px;
  position: relative;
  .cart-promotion-number{
    margin-left: 5px;
  }
}
.cart-total{
  clear: both;
  margin: 10px 0;
  font-weight: bold;
}


/* Totals */ 


.cart-promotion-detail{
  width: 65%;
  float: left;
}
.cart-promotion-number{
  position: absolute;
  right: 0;
  bottom: 0;
  width: 35%;
  float: right;
  margin: 0;
  text-align: right;
}


/* // Min width 768px */ 


@media (min-width: 768px) { 


 /* //// Components */


/* Modals */ 


  .modal{
    &-centered{
      height: 80%;
      width: 80%;
      left: 10%;
      margin: 5% auto;
    }
    &-docked-sm{
      width: 500px;
    }
    &-docked-small{
      width: 350px;
    }
  }
}

JS

⚠️ A partir del día 30 de enero de 2023, la librería jQuery será removida del código de nuestras tiendas, por lo tanto la función "$" no podrá ser utilizada.

1. El JavaScript necesitamos agregarlo en el archivo store.js.tpl (o donde tengas tus funciones de JS). El código que necesitamos para el carrito es el siguiente:

jQueryNuvem(document).on("click", ".js-addtocart:not(.js-addtocart-placeholder)", function (e) {

    if (!jQueryNuvem(this).hasClass('contact')) {

        e.preventDefault();

        var callback_add_to_cart = function(){
            if (window.innerWidth < 768) {
                jQueryNuvem(".js-toggle-cart").click();
            }else{
               jQueryNuvem(".js-alert-added-to-cart").show().toggleClass("notification-visible notification-hidden");
                setTimeout(function(){
                    jQueryNuvem(".js-alert-added-to-cart").toggleClass("notification-visible notification-hidden");
                },7000);
            }
            jQueryNuvem(".js-shipping-filled-cart").show();
        }
        $prod_form = jQueryNuvem(this).closest("form");
        LS.addToCartEnhanced(
            $prod_form,
            '{{ "Agregar al carrito" | translate }}',
            '{{ "Agregando..." | translate }}',
            '{{ "¡Uy! No tenemos más stock de este producto para agregarlo al carrito." | translate }}',
            {{ store.editable_ajax_cart_enabled ? 'true' : 'false' }},
                callback_add_to_cart
        );
    }
});

{# /* // Cart quantitiy changes */ #}

jQueryNuvem(document).on("keypress", ".js-cart-quantity-input", function (e) {
    if (e.which != 8 && e.which != 0 && (e.which < 48 || e.which > 57)) {
        return false;
    }
});

jQueryNuvem(document).on("focusout", ".js-cart-quantity-input", function (e) {
    var itemID = jQueryNuvem(this).attr("data-item-id");
    var itemVAL = jQueryNuvem(this).val();
    if (itemVAL == 0) {
        var r = confirm("{{ '¿Seguro que quieres borrar este artículo?' | translate }}");
        if (r == true) {
            LS.removeItem(itemID, true);
        } else {
            jQueryNuvem(this).val(1);
        }
    } else {
        LS.changeQuantity(itemID, itemVAL, true);
    }
});

{# /* // Empty cart alert */ #}

jQueryNuvem(".js-trigger-empty-cart-alert").on("click", function (e) {
    e.preventDefault();
    let emptyCartAlert = jQueryNuvem(".js-mobile-nav-empty-cart-alert").fadeIn(100);
    setTimeout(() => emptyCartAlert.fadeOut(500), 1500);
});

2. Pero también necesitamos agregar el JS que hace funcionar al componente del modal en general


{#/*============================================================================
  #Modals
==============================================================================*/ #}
 
{# Full screen mobile modals back events #}

if (window.innerWidth < 768) {

    {# Clean url hash function #}

    cleanURLHash = function(){
        const uri = window.location.toString();
        const clean_uri = uri.substring(0, uri.indexOf("#"));
        window.history.replaceState({}, document.title, clean_uri);
    };

    {# Go back 1 step on browser history #}

    goBackBrowser = function(){
        cleanURLHash();
        history.back();
    };

    {# Clean url hash on page load: All modals should be closed on load #}

    if(window.location.href.indexOf("modal-fullscreen") > -1) {
        cleanURLHash();
    }

    {# Open full screen modal and url hash #}

    jQueryNuvem(document).on("click", ".js-fullscreen-modal-open", function(e) {
        e.preventDefault();
        var modal_url_hash = jQueryNuvem(this).data("modalUrl");
        window.location.hash = modal_url_hash;
    });

    {# Close full screen modal: Remove url hash #}

    jQueryNuvem(document).on("click", ".js-fullscreen-modal-close", function(e) {
        e.preventDefault();
        goBackBrowser();
    });

    {# Hide panels or modals on browser backbutton #}

    window.onhashchange = function() {
        if(window.location.href.indexOf("modal-fullscreen") <= -1) {

            {# Close opened modal #}

            if(jQueryNuvem(".js-fullscreen-modal").hasClass("modal-show")){

                {# Remove body lock only if a single modal is visible on screen #}

                if(jQueryNuvem(".js-modal.modal-show").length == 1){
                    jQueryNuvem("body").removeClass("overflow-none");
                }

                var $opened_modal = jQueryNuvem(".js-fullscreen-modal.modal-show");
                var $opened_modal_overlay = $opened_modal.prev();

                $opened_modal.removeClass("modal-show");
                setTimeout(() => $opened_modal.hide(), 500);
                $opened_modal_overlay.fadeOut(500);


            }
        }
    }
}

jQueryNuvem(document).on("click", ".js-modal-open", function(e) {
    e.preventDefault(); 
    var modal_id = jQueryNuvem(this).data('toggle');
    var $overlay_id = jQueryNuvem('.js-modal-overlay[data-modal-id="' + modal_id + '"]');
    if (jQueryNuvem(modal_id).hasClass("modal-show")) {
        let modal = jQueryNuvem(modal_id).removeClass("modal-show");
        setTimeout(() => modal.hide(), 500);
    } else {

        {# Lock body scroll if there is no modal visible on screen #}
        
        if(!jQueryNuvem(".js-modal.modal-show").length){
            jQueryNuvem("body").addClass("overflow-none");
        }
        $overlay_id.fadeIn(400);
        jQueryNuvem(modal_id).detach().appendTo("body");
        $overlay_id.detach().insertBefore(modal_id);
        jQueryNuvem(modal_id).show().addClass("modal-show");
    }             
});

jQueryNuvem(document).on("click", ".js-modal-close", function(e) {
    e.preventDefault();  
    {# Remove body lock only if a single modal is visible on screen #}

    if(jQueryNuvem(".js-modal.modal-show").length == 1){
        jQueryNuvem("body").removeClass("overflow-none");
    }
    var $modal = jQueryNuvem(this).closest(".js-modal");
    var modal_id = $modal.attr('id');
    var $overlay_id = jQueryNuvem('.js-modal-overlay[data-modal-id="#' + modal_id + '"]');
    $modal.removeClass("modal-show");
    setTimeout(() => $modal.hide(), 500);
    $overlay_id.fadeOut(500);

    {# Close full screen modal: Remove url hash #}

    if ((window.innerWidth < 768) && (jQueryNuvem(this).hasClass(".js-fullscreen-modal-close"))) {
        goBackBrowser();
    }    
});

jQueryNuvem(document).on("click", ".js-modal-overlay", function(e) {
    e.preventDefault();
    {# Remove body lock only if a single modal is visible on screen #}

    if(jQueryNuvem(".js-modal.modal-show").length == 1){
        jQueryNuvem("body").removeClass("overflow-none");
    }
    var modal_id = jQueryNuvem(this).data('modalId');
    let modal = jQueryNuvem(modal_id).removeClass("modal-show");
    setTimeout(() => modal.hide(), 500); 
    jQueryNuvem(this).fadeOut(500);   

    if (jQueryNuvem(this).hasClass("js-fullscreen-overlay") && (window.innerWidth < 768)) {
        cleanURLHash();
    }
});

Traducciones

Para terminar agregamos los textos para las traducciones en el archivo config/translations.txt

--- Cart ---


es "Carrito"
pt "Carrinho"
en "Cart"
es_mx "Carrito"


es "Carrito de Compras"
pt "Carrinho de Compras"
en "Shopping Cart"
es_mx "Carrito de compras"


es "Carrito de compras"
pt "Carrinho de compras"
en "Shopping Cart"
es_mx "Carrito de compras"


es "Editar carrito"
pt "Editar carrinho"
en "Edit my cart"
es_mx "Editar carrito"


es "No podemos ofrecerte {1} unidades de {2}. Solamente tenemos {3} unidades."
pt "Não podemos te oferecer {1} unidades do produto {2}. No momento, somente possuímos {3} unidades"
en "We cannot offer you {1} units of {2}. We only have {3} left in stock."
es_mx "Sólo tenemos {3} unidades de {2}."


es "El monto mínimo de compra (subtotal) es de"
pt "O valor mínimo de compra (subtotal) é de"
en "The minimum subtotal value is"
es_mx "El monto mínimo de compra (subtotal) es de"


es "El monto mínimo de compra es de"
pt "O valor mínimo de compra é de"
en "The minimum purchase value is"
es_mx "El monto mínimo de compra es de"


es "¿Seguro que quieres borrar este artículo?"
pt "Tem certeza de que deseja excluir este item?"
en "Are you sure you want to delete this item?"
es_mx "¿Realmente quieres borrar este producto?"


es "Elige un monto mínimo de compra"
pt "Escolha um valor mínimo de compra"
en "Set a minimum purchase value"
es_mx "Elige un monto mínimo de compra"


es "Resumen del carrito de compras"
pt "Resumo do carrinho de compras"
en "Shopping cart summary"
es_mx "Resumen del carrito de compras"


es "Edición del carrito de compras"
pt "Edição do carrinho de compras"
en "Shopping cart editing"
es_mx "Edición del carrito de compras"


es "El producto fue agregado exitosamente al carrito"
pt "O produto foi adicionado ao carrinho com sucesso"
en "The product has been added succesfully to the cart"
es_mx "Hemos agregado tu producto al carrito"


es "¡Uy! No tenemos más stock de este producto para agregarlo al carrito. Si querés podés"
pt "Oops! Não temos mais estoque para incluir este produto ao carrinho. Se você quiser, pode"
en "Oops! We don&#39;t have enough stock left to add this product to the shopping cart. If you wish, you can"
es_mx "¡Ups! No tenemos más stock de este producto para agregarlo al carrito. Si quieres"


es "¡Uy! No tenemos más stock de este producto para agregarlo al carrito."
pt "Oops! Não temos mais estoque para incluir este produto ao carrinho."
en "Oops! We don&#39;t have enough stock left to add this product to the shopping cart."
es_mx "¡Ups! No tenemos más stock de este producto para agregarlo al carrito."


es "ver otros acá"
pt "ver outros aquí"
en "see others here"
es_mx "ver otros aquí"


es "¡Excelente! Ya agregamos tu producto al carrito."
pt "Excelente! Já adicionamos o produto ao carrinho."
en "Excellent! your product was added to the cart."
es_mx "¡Excelente! Hemos agregado el producto al carrito."


--- --- Headers


es "Producto"
pt "Produto"
en "Product"
es_mx "Producto"


es "Cantidad"
pt "Quantidade"
en "Quantity"
es_mx "Cantidad"


es "Precio"
pt "Preço"
en "Price"
es_mx "Precio"


es "Stock"
pt "Estoque"
en "Stock"
es_mx "Stock"


es "Subtotal"
pt "Subtotal"
en "Subtotal"
es_mx "Subtotal"


es "eliminar"
pt "remover"
en "remove"
es_mx "quitar"


es "Total"
pt "Total"
en "Total"
es_mx "Total"


es "(opcional)"
pt "(opcional)"
en "(optional)"
es_mx "(opcional)"


--- --- Empty


es "Carrito vacío"
pt "Carrinho vazio"
en "Empty cart"
es_mx "Carrito vacío"


es "No hay suficiente stock para agregar este producto al carrito."
pt "Desculpe, mas não há estoque suficiente para incluir este produto ao carrinho."
en "There&#39;s not enough stock left to add this product to the shopping cart."
es_mx "No tenemos suficiente stock para agregar este producto al carrito."


es "El carrito de compras está vacío."
pt "O carrinho de compras está vazio."
en "The shopping cart is empty."
es_mx "El carrito de compras está vacío."


--- --- CTA


es "Cambiar Cantidades"
pt "Alterar Quantidades"
en "Update quantities"
es_mx "Cambiar cantidades"


es "Iniciar Compra"
pt "Finalizar Compra"
en "Go to Checkout"
es_mx "Comprar"


es "Seguir comprando"
pt "Continuar comprando"
en "Continue shopping"
es_mx "Seguir comprando"

Listo, ya tenés en tu diseño la funcionalidad aplicada ¡Excelente!