Carrinho de compras rápidas

Neste artigo, veremos como adicionar o carrinho de compras ao seu layout, que é mostrado em um pop-up sem ter que atualizar a página ao adicionar um produto ao carrinho:

Essa funcionalidade inclui o carrinho de compras com:

  • Uma notificação exibida ao adicionar um produto ao carrinho
  • A capacidade de adicionar produtos ao carrinho, alterar suas quantidades e excluí-los; sem atualizar ou redirecionar a página
  • Um botão para continuar comprando que fecha o carrinho

Este tutorial não inclui:

  • Calculadora de frete. Embora seja a chave para o carrinho, é um tutorial separado que você pode ver aqui
  • Promoções dentro do carrinho. Esta é outra funcionalidade que você pode adicionar mais tarde seguindo esse tutorial.

HTML

A primeira coisa que vamos fazer é criar os tpls necessários para a funcionalidade.

1. Começamos criando o tpls cart-panel.tpl, cart-item-ajax.tpl e cart-totals.tpl dentro da pasta snipplets. É muito importante que você não modifique os IDs ou classes "js- .."

cart-panel.tpl

Representa o modal ou pop-up em que o carrinho é exibido, inclui o for que lista os produtos e as mensagens para um carrinho vazio se não houver estoque de um produto.

<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 é o item de cada produto adicionado ao carrinho. Como usamos o PHP para seu funcionamento, é importante que não mudemos seu nome ou posição dentro das pastas do layout.

Este arquivo mostra o seguindo do produto adicionado:

  • O nome
  • A variante
  • Os controles para alterar a quantidade
  • O botão para excluir o produto
  • O subtotal da soma das quantidades desse produto
<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

Aqui mostramos os totais e subtotais do carrinho. Se você quiser adicionar promoções ou a calculadora de envio no futuro, pode fazê-lo neste arquivo.

Neste arquivo também incluímos um link para continuar comprando que fecha o carrinho. Caso você queira modificar este link para levar o usuário à página inicial, podemos usar {{store.url}} ou, se quisermos ir para a lista de produtos, podemos usar {{store.products_url}}.

Neste snipplet, usamos a condição cart_page para que ela possa ser reutilizada em templates/cart.tpl, onde o carrinho está em sua versão "página" e não na do pop-up explicado neste 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. Nesta etapa, precisamos adicionar ao botão "Adicionar ao carrinho" no formulário do produto, as classes js-addtocart js-prod-submit-form. No caso do layout Base, ele é encontrado no snipplet product-form.tpl, mas talvez em seu layout ele esteja dentro do template product.tpl

Similar ao seguinte exemplo:

{% 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 %} />

Também temos que adicionar dentro do template product.tpl no div pai de todo o detalhe do produto, o ID “single-product” e as classes js-product-detail js-product-container. Se você já fez o tutorial da ‘Calculadora de frete’, provavelmente já fez isso da seguinte maneira:

<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 do detalhe do produto

</div>

3. No arquivo snipplets/header-utilities.tpl ou onde você tem o ícone do carrinho de compras na navegação do seu layout, adicionaremos o seguinte:

<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 mostra um ícone mas você pode usar o que você quiser, o importante é nas classes "js- .." que servem para abrir o carrinho e atualizar a quantidade de produtos no ícone dentro da navegação.

4. Agora precisamos criar os snipplets do componente base que usamos no carrinho:

  • A notificação visível ao adicionar um produto com o arquivo notification.tpl
  • O componente para modal ou pop-up
  • Ícones SVG dentro da pasta snipplets/SVG

notification.tpl

Este arquivo tem a notificação onde o usuário é mostrado que o produto foi adicionado ao carrinho com sucesso.

Criamos o arquivo notification.tpl dentro da pasta snipplets com o seguinte 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 adicionar esse arquivo dentro da pasta snipplets para usar a incorporação na qual o carrinho é mostrado.

{# /*============================================================================
  #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. Nesta etapa, temos que incluir o modal junto com o carrinho, no caso do layout Base, fazemos isso dentro do snipplet header.tpl que é onde está o head. Em seu layout você pode incluí-lo em qualquer lugar, mas recomendamos que seja fora de qualquer elemento e que tenha uma posição: 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 a parte do HTML, precisamos adicionar uma pasta SVG dentro da pasta snipplets. Aqui vamos adicionar os SVGs que usamos para os ícones no carrinho.

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:

Ter adicionado helper classes em seu layout. Você pode seguir este pequeno tutorial para fazer isso (é só copiar e colar algumas classes, não leva mais que 1 minuto).

1. Adicionamos o seguinte SASS de cores em style-colors.scss.tpl (ou na stylesheet do seu layout que possui as cores e fontes da loja). Lembre-se de que as variáveis de cores e fontes podem variar em relação ao seu layout:

{# 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. Adicione os estilos no arquivo static/style-critical.tpl

Se em seu layout você usar um stylesheet para o CSS crítico, precisaremos do seguinte código dentro dele, do contrário, você pode unificar o CSS dos passos 2 e 3 em um único arquivo.

{# /* // 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. Adicione os estilos no arquivo static/style-async.tpl

Se em seu layout você usar um stylesheet CSS assíncrono, precisaremos do seguinte código dentro dela, do contrário, você pode unificar o CSS dos passos 2 e 3 em um único arquivo.

/* // 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 do dia 30 de janeiro de 2023, a biblioteca jQuery será removida do código de nossas lojas, portanto, a função "$" não poderá ser utilizada.

1. JavaScript precisam ser adicionados no arquivo store.js.tpl (ou onde você tem suas funções JS). O código que precisamos é o seguinte:

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. Mas também precisamos adicionar o JS que faz o componente modal funcionar em geral

{#/*============================================================================
  #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();
    }
});

Traduções

Nesta etapa, adicionamos os textos para as traduções no arquivo 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"

Pronto, você já tem em seu layout esta funcionalidade aplicada.