Produtos relacionados: Alternativos e complementares

Neste tutorial, adicionaremos 2 espaços para produtos relacionados (alternativos e complementares) exibidos na parte inferior da página de detalhes do produto.

Os tipos de produtos relacionados que serão exibidos são:

  • Produtos da mesma categoria do produto principal (este é o comportamento padrão)
  • Se os produtos forem vinculados manualmente a partir do formulário de produtos no administrador,  serão exibidos:
    • Produtos alternativos: devem ser produtos semelhantes ao produto principal
    • Produtos complementares: devem ser os produtos que complementam o produto principal


HTML

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

{# Default related products visibility conditions #}

{% set related_products = [] %}
{% set related_products_ids_from_app = product.metafields.related_products.related_products_ids %}
{% set has_related_products_from_app = related_products_ids_from_app | get_products | length > 0 %}
{% if has_related_products_from_app %}
    {% set related_products = related_products_ids_from_app | get_products %}
{% endif %}
{% if related_products is empty %}
    {% set max_related_products_length = 8 %}
    {% set max_related_products_achieved = false %}
    {% set related_products_without_stock = [] %}
    {% set max_related_products_without_achieved = false %}

    {% if related_tag %}
        {% set products_from_category = related_products_from_controller %}
    {% else %}
        {% set products_from_category = category.products | shuffle %}
    {% endif %}

    {% for product_from_category in products_from_category if not max_related_products_achieved and product_from_category.id != product.id %}
        {%  if product_from_category.stock is null or product_from_category.stock > 0 %}
            {% set related_products = related_products | merge([product_from_category]) %}
        {% elseif (related_products_without_stock | length < max_related_products_length) %}
            {% set related_products_without_stock = related_products_without_stock | merge([product_from_category]) %}
        {% endif %}
        {%  if (related_products | length == max_related_products_length) %}
            {% set max_related_products_achieved = true %}
        {% endif %}
    {% endfor %}
    {% if (related_products | length < max_related_products_length) %}
        {% set number_of_related_products_for_refill = max_related_products_length - (related_products | length) %}
        {% set related_products_for_refill = related_products_without_stock | take(number_of_related_products_for_refill) %}


        {% set related_products = related_products | merge(related_products_for_refill)  %}
    {% endif %}
{% endif %}

{% set complementary_products = complementary_product_list | length > 0 %}

{# Show alternative products when there are default category alternatives with no complementaries or manually selected alternatives #}
{% set alternative_products = related_products | length > 0 and not (complementary_products and source_alternative == 'default') %}

{# Set related products classes #}

{% set section_class = 'section-products-related my-3' %}
{% set container_class = 'container' %}
{% set title_class = 'h3 text-center' %}
{% set products_container_class = 'position-relative swiper-container-horizontal' %}
{% set slider_container_class = 'swiper-container' %}
{% set swiper_wrapper_class = 'swiper-wrapper' %}
{% set slider_control_pagination_class = 'swiper-pagination' %}
{% set slider_control_class = 'icon-inline icon-w-8 icon-2x svg-icon-text' %}
{% set slider_control_prev_class = 'swiper-button-prev' %}
{% set slider_control_next_class = 'swiper-button-next' %}
{% set control_prev = include ('snipplets/svg/chevron-left.tpl', {svg_custom_class: slider_control_class}) %}
{% set control_next = include ('snipplets/svg/chevron-right.tpl', {svg_custom_class: slider_control_class}) %}

{# Alternative products #}

{% set alternative_data_component = source_alternative == 'default' ? 'related-products' : 'alternative-products' %}

{% if alternative_products %}
    {{ component(
        'products-section',{
            title: settings.products_related_title,
            id: 'related-products',
            data_component: alternative_data_component,
            products_amount: related_products | length,
            products_array: related_products,
            product_template_path: 'snipplets/grid/item.tpl',
            product_template_params: {'slide_item': true},
            slider_controls_position: 'bottom',
            slider_pagination: true,
            svg_sprites: false,
            section_classes: {
                section: 'js-related-products ' ~ section_class,
                container: container_class,
                title: title_class,
                products_container: products_container_class,
                slider_container: 'js-swiper-related ' ~ slider_container_class,
                slider_wrapper: swiper_wrapper_class,
                slider_control_pagination: 'js-swiper-related-pagination ' ~ slider_control_pagination_class,
                slider_control_prev_container: 'js-swiper-related-prev ' ~ slider_control_prev_class,
                slider_control_prev: 'icon-flip-horizontal',
                slider_control_next_container: 'js-swiper-related-next ' ~ slider_control_next_class,
            },
            custom_control_prev: control_prev,
            custom_control_next: control_next,
        }) 
    }}
{% endif %}

{# Complementary products #}

{% set complementary_section_id = 'complementary-products' %}

{% if complementary_products %}
    {{ component(
        'products-section',{
            title: 'Para comprar con este producto' | translate,
            id: complementary_section_id,
            data_component: complementary_section_id,
            products_amount: complementary_product_list | length,
            products_array: complementary_product_list,
            product_template_path: 'snipplets/grid/item.tpl',
            product_template_params: {'slide_item': true},
            slider_controls_position: 'bottom',
            slider_pagination: true,
            svg_sprites: false,
            section_classes: {
                section: 'js-complementary-products ' ~ section_class,
                container: container_class,
                title: title_class,
                products_container: products_container_class,
                slider_container: 'js-swiper-complementary ' ~ slider_container_class,
                slider_wrapper: swiper_wrapper_class,
                slider_control_pagination: 'js-swiper-complementary-pagination ' ~ slider_control_pagination_class,
                slider_control_prev_container: 'js-swiper-complementary-prev ' ~ slider_control_prev_class,
                slider_control_prev: 'icon-flip-horizontal',
                slider_control_next_container: 'js-swiper-complementary-next ' ~ slider_control_next_class,
            },
            custom_control_prev: control_prev,
            custom_control_next: control_next,
        }) 
    }}
{% endif %}

Pode ver que estamos incluindo o snipplet item.tpl da pasta snipplets/grid (com base no layout Base), pode ser que você precise incluir o snipplet single_product.tpl. O importante é usar o mesmo snipplet que usamos para o item nas listagens de produtos, como aqueles nos templates category.tpl ou search.tpl.

Por outro lado, é importante mencionar que estamos utilizando o componente privado seção de produtos. Para mais informações sobre as opções deste componente, recomendamos este artigo.

2. Dentro de item.tpl ou single_product.tpl iremos adicionar na div onde temos as classes para as colunas do Bootstrap o condicional {% if slide_item %}js-item-slide swiper-slide{% endif %}.. Abaixo está um exemplo de como aplicá-lo:

<div class="{% if slide_item %}js-item-slide swiper-slide{% else %}col-12 col-sm-4{% endif %} item item-product">
... 
</div> 

3. Incluímos o snipplet de produtos relacionados no template product.tpl abaixo de tudo, como abaixo:

{% include 'snipplets/product/product-related.tpl' %}

4. Finalmente, 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.

chevron-left.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path d="M231.293 473.899l19.799-19.799c4.686-4.686 4.686-12.284 0-16.971L70.393 256 251.092 74.87c4.686-4.686 4.686-12.284 0-16.971L231.293 38.1c-4.686-4.686-12.284-4.686-16.971 0L4.908 247.515c-4.686 4.686-4.686 12.284 0 16.971L214.322 473.9c4.687 4.686 12.285 4.686 16.971-.001z"/></svg>

chevron-right.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path d="M24.707 38.101L4.908 57.899c-4.686 4.686-4.686 12.284 0 16.971L185.607 256 4.908 437.13c-4.686 4.686-4.686 12.284 0 16.971L24.707 473.9c4.686 4.686 12.284 4.686 16.971 0l209.414-209.414c4.686-4.686 4.686-12.284 0-16.971L41.678 38.101c-4.687-4.687-12.285-4.687-16.971 0z"/></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).

Como neste exemplo usamos um slider com o Swiper, precisamos adicionar o plugin. Para ver como fazer isso, você pode ler este pequeno artigo e continuar com este tutorial.

Se você preferir mostrar os produtos em um grid clássico sem slider ou se já tiver incluído o Swiper, pode pular esta etapa.

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.

Precisamos aplicar as funções no arquivo store.js.tpl (ou onde suas funções JS estiverem) com o seguinte código:

 
{% if template == 'product' %}

    {# /* // Product Related */ #}
        
        // Set loop for related products products sliders

        {% set columns = settings.grid_columns %}
        const desktopColumns = {% if columns == 1 %}3{% else %}4{% endif %};

        function calculateRelatedLoopVal(sectionSelector) {                
            let productsAmount = jQueryNuvem(sectionSelector).attr("data-related-amount");
            let loopVal = false;
            const applyLoop = (window.innerWidth < 768 && productsAmount > {{ columns }}) || (window.innerWidth > 768 && productsAmount > desktopColumns);
            
            if (applyLoop) {
                loopVal = true;
            }

            return loopVal;
        }

        let alternativeLoopVal = calculateRelatedLoopVal(".js-related-products");
        let complementaryLoopVal = calculateRelatedLoopVal(".js-complementary-products");

        {# Alternative products #}

        createSwiper('.js-swiper-related', {
            lazy: true,
            watchOverflow: true,
            loop: alternativeLoopVal,
            centerInsufficientSlides: true,
            spaceBetween: 30,
            slidesPerView: {{ columns }},
            pagination: {
                el: '.js-swiper-related-pagination',
                clickable: true,
            },
            navigation: {
                nextEl: '.js-swiper-related-next',
                prevEl: '.js-swiper-related-prev',
            },
            breakpoints: {
                767: {
                    slidesPerView: desktopColumns,
                }
            }
        });

        {# Complementary products #}

        createSwiper('.js-swiper-complementary', {
            lazy: true,
            watchOverflow: true,
            loop: complementaryLoopVal,
            centerInsufficientSlides: true,
            spaceBetween: 30,
            slidesPerView: {{ columns }},
            pagination: {
                el: '.js-swiper-complementary-pagination',
                clickable: true,
            },
            navigation: {
                nextEl: '.js-swiper-complementary-next',
                prevEl: '.js-swiper-complementary-prev',
            },
            breakpoints: {
                767: {
                    slidesPerView: desktopColumns,
                }
            }
        });

{% endif %}

Configurações

No arquivo config/settings.txt vamos adicionar a opção de alterar o título da seção dps produtos alternativos (você pode fazer isso também para as complementares se precisar) dentro da seção “Detalhes do produto”.

title
    title = Productos relacionados
i18n_input
    description = Título para los productos alternativos
    name = products_related_title

Traduções

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

es "Productos relacionados"
pt "Produtos relacionados"
en "Related products"
es_mx "Productos relacionados"

es "Título para los productos alternativos"
pt "Título para os produtos alternativos"
es_mx "Título para los productos alternativos"

Dentro de config/defaults.txt vamos adicionar os textos das mensagens padrão.

products_related_title_es = Productos similares
products_related_title_en = Similar products
products_related_title_pt = Produtos similares

Ativação

Depois que todas as alterações forem aplicadas ao seu código, precisamos que você entre em contato com a Nuvemshop para concluir a ativação em parceiros@nuvemshop.com.br

Pronto, uma vez relacionados os produtos a partir do formulário de produtos no administrador, serão visualizados os relacionados criados manualmente, caso contrário serão mostrados os padrão mencionados no início deste artigo. Excelente!