Cross selling

Neste tutorial, vamos implementar cross selling ao adicionar um produto ao carrinho.

HTML

1. Vamos criar um novo snipplet com o nome snipplets/cross-selling.tpl dentro da pasta snipplets. O código para este é o seguinte:

{# Cross selling promotion form #}
{% if promotion %}
    {{ component(
        'promotions/cross-selling-form', {
            css_classes: {
                main_container: 'm-auto',
                image_container: 'position-relative',
                discount_percentage_label: 'label label-accent position-absolute label-top-left',
                image: 'img-fluid w-100 lazyload product-image-limited',
                form_container: 'px-4 py-3',
                product_name: 'font-big text-center mb-2',
                prices_container: 'price-container text-center mb-3',
                price_wrapper: 'd-inline-block',
                original_price: 'price-compare font-weight-normal mb-0',
                promo_price: 'text-primary mb-0',
                variant_selection_group: 'form-group px-2 mb-2',
                variant_selection_label: 'form-label',
                variant_select: 'form-select',
                variant_select_icon_container: 'form-select-icon',
                add_to_cart_button: 'btn btn-primary btn-block mt-3 mb-1 cart'
            },
            icon_config: {
                use_svg_icon: false,
                use_custom_icon: true,
                custom_icon_markup: include("snipplets/svg/chevron-down.tpl", { svg_custom_class: "icon-inline icon-w-12 icon-md mr-3" })
            }
        })
    }}
{% endif %}

É importante mencionar que estamos usando o componente privado de "cross-selling-form".

2. Perto do final do arquivo snipplets/header/header.tpl, é necessário adicionar o seguinte trecho de código:

{# Cross selling promotion notification on add to cart #}
{% embed "snipplets/modal.tpl" with {
    modal_id: 'js-cross-selling-modal',
    modal_class: 'bottom modal-bottom-sheet h-auto overflow-none modal-body-scrollable-auto',
    modal_header: true,
    modal_header_class: 'm-0 w-100',
    modal_position: 'bottom',
    modal_transition: 'slide',
    modal_footer: true,
    modal_width: 'centered-md m-0 p-0 modal-full-width modal-md-width-400px'
} %}
    {% block modal_head %}
        {{ '¡Descuento exclusivo!' | translate }}
    {% endblock %}


    {% block modal_body %}
        {# Promotion info and actions #}


        <div class="js-cross-selling-modal-body" style="display: none"></div>
    {% endblock %}
{% endembed %}

3. Agora bem, para que o modal possa ser aberto e mostrar a promoção corretamente, é necessário que em snipplets/modal.tpl contemos com o parâmetro {{ modal_header_class }}. Caso não o tenha, é necessário modificar o componente. Para isso, devemos procurar a seguinte linha nesse arquivo:

<div class="js-modal-close {% if modal_mobile_full_screen %}js-fullscreen-modal-close{% endif %} modal-header">

E substituí-la por esta:

<div class="js-modal-close {% if modal_mobile_full_screen %}js-fullscreen-modal-close{% endif %} modal-header {{ modal_header_class }}">

4. É necessário ajustar o resumo do carrinho para mostrar o desconto aplicado por cross selling. Para isso, em snipplets/cart-totals.tpl, devemos procurar a seguinte linha, que deveria aparecer duas vezes:

<span class="js-promo-in" style="display:none;">{{ "en" | translate }}</span>

E logo acima dessa linha, adicione o seguinte:

<span class="js-promo-discount" style="display:none;"> {{ "Descuento" | translate }}</span>

Posteriormente, devemos procurar:

    {% elseif promotion.isBuyXPayY %}
       {{ promotion.buy }}x{{ promotion.pay }}

E abaixo incorporar:

     {% elseif promotion.isCrossSelling %}
        {{ "Descuento" | translate }}

Ambas as mudanças deveriam aparecer duas vezes no mesmo arquivo.

CSS

Requisito:

Tenha adicionadas no seu design as classes helpers. Você pode seguir este pequeno tutorial para fazer isso (é apenas copiar e colar algumas classes, não leva mais de 1 minuto).

1. Para que os estilos sejam aplicados corretamente, é necessário colocá-los no lugar adequado. Todas as alterações serão feitas em static/css/style-async.scss.tpl.

Procuramos o seletor .modal que está fora das media queries. Em seguida, dentro deste mesmo, procuramos o seletor &-centered, e ali inserimos o seguinte:

&-md.modal-show {
   left: 50%;
   transform: translateX(-50%);
   &.modal-bottom-md,
   &.modal-bottom {
     top: 50%;
     bottom: auto;
     left: 50%;
     height: fit-content;
     transform: translate(-50%, -50%);
   }
 }

2. Adicionamos estes estilos, verifique que não estejam dentro de nenhuma media query.

.modal-full-width {
  width: 100%;
  max-width: 100%;
}
.modal-body-scrollable-auto .modal-body {
  max-height: calc(100vh - 100px);
  overflow-y: auto;
}

.label-top-left {
  top: 25px;
  left: 25px;
  z-index: 2;
}

.product-image-limited {
  max-height: 320px;
  max-width: 100%;
  object-fit: contain;
}

3. Na seção de media queries adicionamos o seguinte:

{# /* // Max width 767px */ #}
@media (max-width: 767px) {
  .product-image-limited {
    max-height: 210px;
  }
}

4. Dentro da mesma seção, devemos procurar a media query de min-width: 768px e, dentro dela, localizar a definição de .modal, como antes, mas desta vez para a versão desktop. Neste caso, o que devemos adicionar é o seguinte:

&-centered-md.modal-show {
      left: initial;
      transform: none;
      &.modal-bottom {
        top: 50%;
      }
    }

E mais abaixo:

&-md-width-400px {
      width: 400px;
      max-width: 90vw;
    }

JS

1. É necessário modificar o callback de adicionar um produto ao carrinho. Para isso, devemos ir para base/static/js/store.js.tpl e procurar a seguinte linha:

var callback_add_to_cart = function(html_notification_related_products){

Em seguida, substitua-a por:

var callback_add_to_cart = function(html_notification_related_products, html_notification_cross_selling) {

2. Dentro do mesmo callback precisamos adicionar este código ao final

{# Display cross-selling promotion modal #}

let shouldDisplayCrossSellingNotification = html_notification_cross_selling != null;

if (shouldDisplayCrossSellingNotification) {
    jQueryNuvem('.js-cross-selling-modal-body').html("");
    modalOpen('#js-cross-selling-modal');
    jQueryNuvem('.js-cross-selling-modal-body').html(html_notification_cross_selling).show();
}

{
    # Change prices on cross - selling promotion modal #
}

const crossSellingContainer = document.querySelector('.js-cross-selling-container');

if (crossSellingContainer) {
    const variants = JSON.parse(crossSellingContainer.dataset.variants || '[]');
    const addToCartText = crossSellingContainer.dataset.addToCartTranslation;
    const notAvailableText = crossSellingContainer.dataset.notAvailableTranslation;
    const pricesContainer = crossSellingContainer.querySelector('.js-cross-selling-prices-container');
    const originalPriceElem = crossSellingContainer.querySelector('.js-cross-selling-original-price');
    const promoPriceElem = crossSellingContainer.querySelector('.js-cross-selling-promo-price');
    const addToCartButton =   crossSellingContainer.querySelector('.js-cross-selling-add-to-cart');
    const variantOptionSelectors = [
        '#js-cross-selling-option-value-1',
        '#js-cross-selling-option-value-2',
        '#js-cross-selling-option-value-3'
    ];

    function formatPrice(cents) {
        return LS.currency.display_short +
            parseFloat(cents / 100).toLocaleString('de-DE', {
                minimumFractionDigits: 2
            });
    }

    function updatePrice() {
        const selectedValues = variantOptionSelectors.map(selector =>
            document.querySelector(selector)?.value || null
        );
        let currentVariant = null;
        if (variants.length === 1) {
            currentVariant = variants[0];
        } else {
            currentVariant = variants.find(variant =>
                (!variant.optionValue1 || variant.optionValue1 === selectedValues[0]) &&
                (!variant.optionValue2 || variant.optionValue2 === selectedValues[1]) &&
                (!variant.optionValue3 || variant.optionValue3 === selectedValues[2])
            );
        }

        if (currentVariant) {
            originalPriceElem.textContent = formatPrice(currentVariant.originalPriceInCents);
            promoPriceElem.textContent = formatPrice(currentVariant.promotionalPriceInCents);
            if (currentVariant.isAvailable) {
                pricesContainer.style.display = 'block';
                addToCartButton.disabled = false;
                addToCartButton.value = addToCartText;
            } else {
                pricesContainer.style.display = 'none';
                addToCartButton.disabled = true;
                addToCartButton.value = notAvailableText;
            }
        } else {
            originalPriceElem.textContent = '';
            promoPriceElem.textContent = '';
            pricesContainer.style.display = 'none';
            addToCartButton.disabled = true;
            addToCartButton.value = notAvailableText;
        }
    }

    variantOptionSelectors.forEach(selector => {
        const selectElem = document.querySelector(selector);
        if (selectElem) {
            selectElem.addEventListener('change', updatePrice);
        }
    });

    function selectFirstAvailableVariant() {
        const firstAvailable = variants.find(v => v.isAvailable);
        const select1 = crossSellingContainer.querySelector('#js-cross-selling-option-value-1');
        const select2 = crossSellingContainer.querySelector('#js-cross-selling-option-value-2');
        const select3 = crossSellingContainer.querySelector('#js-cross-selling-option-value-3');
        if (select1 && firstAvailable.optionValue1) {
            select1.value = firstAvailable.optionValue1;
        }
        if (select2 && firstAvailable.optionValue2) {
            select2.value = firstAvailable.optionValue2;
        }
        if (select3 && firstAvailable.optionValue3) {
            select3.value = firstAvailable.optionValue3;
        }
    }

    selectFirstAvailableVariant();

    updatePrice();
}

Traduções

Neste passo adicionamos os textos para as traduções no arquivo config/translations.txt

es "¡Descuento exclusivo!"
pt "Desconto exclusivo!"
en "Exclusive discount!"
es_mx "¡Descuento exclusivo!"

es "Descuento"
pt "Desconto"
en "Discount"
es_mx "Descuento"

Ativação

Pronto, para ativá-lo basta acessar a seção Descontos > Promoções no administrador da loja e criar uma nova promoção. Devemos obter uma referência semelhante à seguinte