Skip to main content

Detalhe de produto

A página de detalhe de produto ou product.html como é listado no menu, traz uma abordagem de código semelhante a página home. Sem mais delongas, vamos direto a prática. Abaixo temos um código simples de como é listado o conteúdo da página de detalhe, e como podemos ver, sempre através de elementos. O HTML abaixo é um modelo exemplo do nosso projeto base padrão.

<main class="details">
{% include 'template/breadcrumb' %}

<!-- PRODUCT PANEL -->
<div class="product-panel">
<div class="product-details__wrapper">
{% include 'product page/images' %}

<section class="product-details__info padding-mobile">
<div class="product-details__info--title">
<div class="title">
<h1>{{ product.name | raw }}</h1>
<small>Código: {{ product.sku }}</small>
</div>

{% include 'product page/favorite' %}
</div>

<form id="product-form" onsubmit="addToCart(event, {{ _object(_only(product, 'id', 'sku')) }})" class="product-details__info--buttons">
{% include 'product page/price' %}
{% include 'product page/variation selector' %}
{% include 'product page/options form' %}

<input type="hidden" name="sku" value="{{ product.sku }}"/>

<div class="info--buttons__actions">
<div class="info--buttons__actions--accumulator">
<span onclick="changeQuantity('qty_input', {{ product.stock.increment * -1 }})">-</span>
<input
min="1"
max="{{ product.stock.quantity > 0 ? product.stock.quantity : 99 }}"
step="{{ product.stock.increment | number_format }}"
value="{{ product.stock.increment | number_format }}"
type="number" name="quantity"
id="qty_input"
required
data-product-quantity="{{ product.sku }}"
/>
<span onclick="changeQuantity('qty_input', {{ product.stock.increment }})">+</span>
</div>

<button class="info--buttons__actions--submitter">
Comprar
{% include 'icons/cart' %}
</button>
</div>
</form>

{% include 'product page/deadline' %}
</section>
</div>
</div>

<!-- PRODUCT DETAILED ACCORDION INFO -->
{% if product.description %}
<span class="product-panel__border"></span>

<article class="accordion container">
<div class="accordion__description active-accordion" id="accordion-button" onclick="accordionDescription()">
<h2 class="title">Descrição do produto</h2>
{% include 'icons/arrow-left' %}
</div>

<div class="accordion__description--text">
{{ product.description | raw }}
</div>
</article>
{% endif %}

{% include 'product page/related products' %}

{% include 'home page/benefits bar' %}
</main>

{% include 'imports/scripts' %}

<script src="{{ _asset('js pages/product.js') }}"></script>

Por estarmos em uma página padrão da PWA, a página product.html tem o benefício de poder listar as informações de um produto através do objeto product e também de outros atributos relacionados ao produto da tela atual. Explicaremos com mais detalhes a seguir sobre as informações do produto.

No elemento template/breadcrumb listado acima, temos o seguinte código:

<div class="breadcrumb container">
<a {{ _href('/') }}>Increazy</a>

<span class="arrow-right" data-grid="center-center">
{% include 'icons/arrow-left' %}
</span>

{% for path in product.breadcrumb %}
<a {{ _href(path.path) }} class="{{ loop.last ? 'item-active' : '' }}">
{{ path.name }}
</a>

{% if loop.last == false %}
<span class="arrow-right" data-grid="center-center">
{% include 'icons/arrow-left' %}
</span>
{% endif %}
{% endfor %}
</div>

Explicando de forma rápida, o atributo product.breadcrumb é quem faz a maior parte do trabalho, onde a lista dos 'caminhos' do breadcrumb é percorrida pelo {% for path in product.breadcrumb %}. Temos também o auxílio de um atributo extra chamado loop, que é reconhecido por padrão pelo core da PWA para facilitar nas validações dos itens do breadcrumb.

Imagens do produto

No elemento product page/images listado acima, temos o seguinte código.

<div>
{% for media in activeVariation.media %}
<figure>
<img
loading="lazy"
src="{{ media.url }}"
alt="{{ media.label }}"
onerror="this.src = 'https://storage.test.increazy.com/base_pwa/images/no-image.webp'"
>
</figure>
{% endfor %}
</div>

As imagens do produto podem ser listadas através do objeto activeVariation.media dentro de uma estrutura for como mostra no código. Assim o campo media que é a representação de uma imagem buscada dentro do for traz os principais campos da imagem chamados: url e label. Dessa forma você já terá a listagem de todas as imagens do seu produto. Caso precise de um fallback na imagem, basta seguir o exemplo do onerror acima, e aplicar a imagem desejada.

Informações gerais do produto

  • Nome do produto: Pode ser acessado por {{ product.name }}

  • Descrição do produto: Pode ser acessado por {{ product.description | raw }}, o raw traz o conteúdo da descrição no formato HTML. Para representar melhor a chamada da descrição, temos o seguinte código:

<div class="description">
{{ product.description | raw }}
</div>
  • Preços: O elemento produto page/price traz as informações dos preços do produto listado. Para simplificar vamos listar diretamente os atributos.

    • Preço atual: O preço atual é o preço do produto independente se ele possui desconto ou não, que pode ser acessado por {{_price('min', product)}}

    • Preço sem desconto: É criado um atributo auxiliar para facilitar na validação do desconto, ficando então: {% set desconto = _price('discount', product) %}, através do atributo desconto criado, você pode fazer uma validação para detectar se existe algum desconto ou não naquele produto, caso exista, você pode exibir o preço ** sem desconto ** do produto através do campo {{_price('max', product)}}

    Obs.: Note que estamos utilizando um método padrão chamado _price, ele te auxilia a trazer corretamente o preço desejado, pois sabemos que existem algumas variações de preço. Todos os métodos padrões da increzy sempre estará destacado com '_' antes do nome.

  • Parcelas: Criaremos um atributo para auxiliar na validação do parcelamento do produto, assim temos o controle de exibir o parcelamento ou não caso não exista. O atributo é criado da seguinte forma {% set maiorParcela = _price('installments', activeVariation) | last %}, o método _price recebe o tipo do preço que seria installments e de qual objeto ele vai capturar esse preço, que no caso seria o activeVariation.

    • Quantidade de parcelas: Com a ajuda do nosso atributo auxiliar criado acima, a quantidade de parcelas é listada através do campo {{ maiorParcela.qty }}

    • Preço das parcelas: O preço das parcelas é listada através do campo {{ _price('format', maiorParcela.price) }}

Preços

Vamos ao exemplo de código do elemento product page/price importado na página de detalhe de produto.

{% set customerGroup = cookies.customer_group ? cookies.customer_group : null %}

<strong class="price">
<span id="product-price" class="product-price" virgin-price="{{ _price('min', activeVariation, false) }}"> {{ _price('min', activeVariation, true, customerGroup) }} </span>
<span class="in-cash">à vista</span>
</strong>

{% set maiorParcela = _price('installments', activeVariation) | last %}
{% if maiorParcela.qty > 1 %}
<p>
Ou {{ maiorParcela.qty }}x
de {{ _price('format', maiorParcela.price, true, customerGroup) }} sem juros
</p>
{% endif %}
  • Outros campos: Caso houver algum outro campo que você deseja listar na tela, basta ver nas configurações do seu produto e acessar o campo através do objeto {{ product.nome_do_campo }}.

Variações e opções do produto

Variações

Vamos falar sobre o código do elemento product page/variation selector que está sendo importando na página de product.html. Talvez essa seja uma das partes mais arduas de fazer quando se trata de código puro, mas vamos disponibilizar o código já pronto para que você apenas copie e cole em seu projeto.

<script>
window._productsLinkeds = window._productsLinkeds || {};
window._productsLinkeds['{{ product.sku }}'] = {{ product.linkeds | json_encode | raw }};
</script>

<div class="variations" data-sku="{{ product.sku }}" id="product-variations-list">
{% for key, variation in product.variations %}
<div class="variations__item">
<p class="variations__item-name">
{{ variation.label }}:
</p>
<div class="variations__item-list">
{% for value in variation.values %}
<label for="variation_{{ product.id }}_{{ variation.code }}_{{ value.value }}">
<input
name="variations.{{ variation.id }}"
data-attribute="{{ variation.code }}"
id="variation_{{ product.id }}_{{ variation.code }}_{{ value.value }}"
type="radio"
value="{{ value.value }}"
required
onclick="updateVariationsStock('{{ product.sku }}', '{{ variation.code }}', event.target)"
/>
{% if value.source != value.label %}
{% if value.source starts with 'http' %}
<img loading="lazy" src="{{ value.source }}" />
{% else %}
<span style="background-color: {{ value.source }};"></span>
{% endif %}
{% else %}
<span>{{ value.label }}</span>
{% endif %}
</label>
{% endfor %}
</div>
</div>
{% endfor %}
</div>

<script>
updateVariationsStock('{{ product.sku }}');
startVariations();
disableVariation();
</script>
info

A chamada do script onde temos o window._productsLinkeds é usada para calculo de estoque no momento da seleção de cor e tamanho e outras variações do produto. É essencial que esse script seja chamado junto ao código de variações.

Note que no código temos a chamada de um método updateVariationsStock(), mas além dele, existem outros método auxiliares para o funcionamento correto das opções e variações de produtos, o arquivo responsável por esses scripts está localizado em Assets > global functions > product.js, nele você encontrará várias funções responsáveis pela seleção e alteração das variações.


Opções

De semelhante modo de Variações temos as Opções do produto. Nesse caso, as opções do produto está sendo importado direto do elemento product page/options form do nosso código da página product.html. Para facilitar, vamos disponibilizar o código para trazer as opções de um produto abaixo, assim você pode colar em seu projeto:

<div class="options" data-sku="{{ product.sku }}">
<script>
window._productsLinkeds = window._productsLinkeds || {};
window._productsLinkeds['{{ product.sku }}'] = {{ product.linkeds | json_encode | raw }};
</script>

{% if product.options | length > 0 %}
{% for option in product.options %}
<div class="options__item">
<label for="option_{{ product.id }}_{{ option.code }}_{{ value.value }}" class="options__item-name">
{{ option.label }}

{% if option.required %}
<span style="color:red">*</span>
{% endif %}

{% if option.price > 0 %}
<small>
(+ {{ option.action == 'fixed' ? _price('format', option.price) : option.price ~ '%' }})
</small>
{% endif %}
</label>

{% if option.type not in ['textarea', 'select', 'multiple'] %}
{% if option.values | length > 0 %}
<div class="options__item-values-list" data-grid>
{% for value in option.values %}
<label class="options__item-values" for="option_{{ product.id }}_{{ value.id }}_{{ value.label }}" data-grid="nowrap">
<input
id="option_{{ product.id }}_{{ value.id }}_{{ value.label }}"
name="options.{{ option.id }}"
type="{{ option.type }}"
data-price="{{ value.price }}"
data-price-type="{{ value.action }}"
value="{{ value.id }}"
onchange="updatePriceByOption()"
{{ option.required ? 'required' : '' }}
/>
<span class="options__item-values-label">{{ value.label }} (+ {{ _price('format', value.price) }})</span>
</label>
{% endfor %}
</div>
{% else %}
<input
id="option_{{ product.id }}_{{ option.id }}_{{ option.label }}"
name="options.{{ option.id }}"
type="{{ option.type }}"
data-price="{{ option.price }}"
data-price-type="{{ option.action }}"
oninput="updatePriceByOption()"
{{ option.required ? 'required' : '' }}
/>
{% endif %}
{% elseif option.type == 'textarea' %}
<textarea
oninput="updatePriceByOption()"
id="option_{{ product.id }}_{{ option.code }}_{{ value.value }}"
name="options.{{ option.id }}"
data-price="{{ option.price }}"
data-price-type="{{ option.action }}"
{{ option.required ? 'required' : '' }}
></textarea>
{% else %}
<select
id="option_{{ product.id }}_{{ option.code }}_{{ value.value }}"
name="options.{{ option.id }}"
onchange="updatePriceByOption()"
{{ option.required ? 'required' : '' }}
{{ option.type == 'multiple' ? 'multiple' : '' }}
>
{% for value in option.values %}
<option
data-price="{{ value.action == 'percent' ? (value.price / 100) * _price('min', activeVariation, false) : value.price }}"
value="{{ value.id }}"
>
{{ value.label }}

{% if value.price > 0 %}
<small>
(+ {{ value.action == 'percent' ? _price('format', (value.price / 100) * _price('min', activeVariation, false)) : _price('format', value.price) }})
</small>
{% endif %}
</option>
{% endfor %}
</select>
{% endif %}
</div>
{% endfor %}

<script> updatePriceByOption() </script>
{% endif %}
</div>

Sabemos que dependendo das opções ou variação de um produto, pode ser que surja alguma alteração de preço por exemplo. Então para finalizar, no código HTML de preço do produto localizado em Elementos > product page > price.html, temos que aplicar alguns atributos na tag de preço que é identificada pelo script do arquivo Assets > global functions > product.js que irá realizar a alteração do preço na tela caso identifique um acréscimo baseado nas opções/variações do produto. Veja como ficará a tag HTML abaixo:

<span id="product-price" class="product-price" virgin-price="{{ _price('min', activeVariation, false) }}"> {{ _price('min', activeVariation, true, customerGroup) }} </span>

É importante que na tag acima tenha o id="product-price" e virgin-price="{{ _price('min', activeVariation, false) }}" para que o script faça o seu trabalho corretamente.

Resultado de variações e opções

Após todos os códigos aplicados, temos o seguinte resultado da imagem abaixo.

E fique tranquilo, caso o seu produto possua multiplas variações e opções de produto, tudo virá de forma automática.

An image

Kits e produtos agrupados

Kits

Vamos aos passos para podermos criar/aplicar os produtos kits na tela de detalhe de produto. Temos um design simples para poder auxiliar nesse processo, vamos começar pelo código HTML.

  • Você pode copiar todo o código HTML para dentro de um único elemento do seu projeto, e fazer a chamada do mesmo dentro da página de product.html dentro da tag <form>.

    É importante que o elemento esteja dentro da tag <form> para que ao adicionar o produto no carrinho o método addToCart() localizado aqui, possa reconhecer os kits selecionados.

{% set kits = product.bundle %}

{% if kits | length > 0 %}
{% for kit in kits %}
<div class="product-kits__container">
<input type="checkbox" class="input-kit" {{ kit.required == true ? 'required' : '' }} />

{% set products_kit = _search('products', kit.links) %}
<div class="product-kits" data-grid="column">
<h3 class="product-kits__title"> {{ kit.title }}
{% if kit.required == true %}
<span class="is-required">*</span>
{% endif %}
</h3>

{% for kit_input in kit.links %}
{% set product_kit = _search('products', kit_input) %}
{% set product_kit = product_kit.content[0] %}

{% set kit_type = kit.type == 'checkbox' or kit.type == 'multi' ? 'checkbox' : 'radio' %}

<article class="product-kits__item {{ product_kit.stock.quantity > 0 ? '' : 'no-stock-kit' }}" data-grid>
{% if kit_type == 'checkbox' %}
<input type="checkbox" style="display: none" class="input-kit-product" name="bundle_option[{{ kit.option_id }}][{{product_kit.id}}]" value="{{ kit_input.id }}" id="{{ kit.position }}{{ product_kit.id }}" onchange="kitValidate(this)" />
{% else %}
<input type="radio" style="display: none" class="input-kit-product" name="bundle_option[{{ kit.option_id }}]" value="{{ kit_input.id }}" id="{{ kit.position }}{{ product_kit.id }}" onchange="kitValidate(this)"/>
{% endif %}

<label class="product-kits__item-label" data-grid="nowrap" for="{{ kit.position }}{{ product_kit.id }}">
<img
class="product-kits__item-image"
src="{{ product_kit.media[0].cached }}"
alt="{{ product_kit.name }}"
onerror="this.src = 'https://storage.test.increazy.com/base_pwa/images/no-image.webp'"
/>

<div class="product-kits__item-info" data-grid="column">
<h3 class="product-kits__item-info-name">{{ product_kit.name }}</h3>
<span class="product-kits__item-info-price">{{ _price('min', product_kit) }}</span>
</div>
</label>

{% if kit_input.change_quantity == true %}
<div class="info--buttons__actions--accumulator">
<span onclick="changeQuantity('{{ product_kit.sku }}', {{ product_kit.stock.increment * -1 }})">-</span>
<input
min="{{ kit_input.quantity }}"
max="{{ product_kit.stock.quantity > 0 ? product_kit.stock.quantity : 99 }}"
step="{{ product_kit.stock.increment | number_format }}"
value="{{ kit_input.quantity | number_format }}"
type="number"
name="bundle_option_qty[{{ kit.option_id }}]"
id="{{ product_kit.sku }}"
required
data-item-quantity="{{ product_kit.sku }}"
/>
<span onclick="changeQuantity('{{ product_kit.sku }}', {{ product_kit.stock.increment }})">+</span>
</div>
{% endif %}
</article>
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}

<script>
function initKit () {
const checkInput = document.querySelectorAll('.product-kits__container .input-kit');

checkInput.forEach(inputElement => {
inputElement.addEventListener('invalid', function () {
inputElement.nextElementSibling.classList.add('invalid');
inputElement.nextElementSibling.scrollIntoView();
}, false);
});
}
initKit();

function kitValidate(el) {
const inputs = el.closest('.product-kits').querySelectorAll('.input-kit-product');
const kit_input = el.closest('.product-kits__container').querySelector('.input-kit');

let isChecked = false;

if (inputs.length > 0) {
inputs.forEach((item) => {
if (item.checked) {
isChecked = true;
}
});

if (isChecked) {
kit_input.checked = true;
} else {
kit_input.checked = false;
}
}

if (el.closest('.product-kits')) {
el.closest('.product-kits').classList.remove('invalid');
}
}
</script>
  • Após copiar o elemento, abaixo temos o código do css minificado caso queira reaproveitar:
.product-kits__container .input-kit{display:none}.product-kits{margin-top:30px;background:#fff;padding:20px;border-radius:10px;position:relative;box-shadow:5px 5px 15px -9px rgb(0 0 0 / 15%)}.product-kits__item{flex-flow:nowrap;align-items:center}.product-kits__item.no-stock-kit{opacity:.5;pointer-events:none}.product-kits .product-kits__title{font-size:15px;margin-bottom:10px;color:var(--title-text)}.product-kits .product-kits__title .is-required{color:red}.product-kits .product-kits__item:not(:last-child){margin-bottom:15px}.product-kits__item .product-kits__item-label{justify-content:space-between;cursor:pointer;transition:.2s;border:0 solid var(--dark-primary);border-radius:5px;align-items:center;gap:15px;position:relative;width:calc(100% - 98px)}.product-kits__item .product-kits__item-label>*{pointer-events:none}.product-kits__item .product-kits__item-label .product-kits__item-image{width:80px;height:auto}.product-kits__item .product-kits__item-label .product-kits__item-info{width:calc(100% - 127px)}.product-kits__item .product-kits__item-label .product-kits__item-info .product-kits__item-info-name{width:100%;font-size:16px;color:var(--title-text)}.product-kits__item .product-kits__item-label .product-kits__item-info .product-kits__item-info-price{margin-top:3px;color:var(--text)}.product-kits__item .info--buttons__actions--accumulator{width:98px!important;pointer-events:none;opacity:.5}.product-kits .product-kits__item input:checked~.info--buttons__actions--accumulator{pointer-events:unset;opacity:1}.product-kits.invalid{border:1px solid var(--toast-red)}.product-kits.invalid:before{content:'Kit obrigatório';color:var(--toast-red);font-size:13px;margin-bottom:10px}.product-kits .product-kits__item input[type=checkbox]+.product-kits__item-label:before{content:'';width:16px;height:16px;border:2px solid var(--primary);border-radius:3px}.product-kits .product-kits__item input[type=checkbox]:checked+.product-kits__item-label:after{content:'';width:9px;height:6px;background:0 0;border-left:2px solid #fff;border-bottom:2px solid #fff;transform:rotate(-45deg) translate(1px,-1px);position:absolute;left:4px}.product-kits .product-kits__item input[type=checkbox]:checked+.product-kits__item-label:before{background:var(--primary)}.product-kits .product-kits__item input[type=radio]+.product-kits__item-label:before{content:'';width:16px;height:16px;border:2px solid var(--primary);border-radius:50%}.product-kits .product-kits__item input[type=radio]:checked+.product-kits__item-label:after{content:'';width:8px;height:8px;background:var(--primary);position:absolute;left:4px;border-radius:50%}
  • Para que fique tudo funcionando perfeitamente, por fim, verifique se o script das regras da tela de detalhe de produto está atualizado. Geralmente o script fica localizado em Assets > global functions > product.js, clique aqui para ver o script com mais detalhes.

Produtos agrupados

Da mesma forma do Kit, vamos seguir passoa abaixo:

  • Primeiro crie o elemento para os produtos agrupados e cole o HTML abaixo. No projeto ele será encontrado em Elementos > product page > associateds
{% if product.associated | length > 0 %}
{% set grouped = _search('products', product.associated) %}
<input type="hidden" name="quantity" value="0"/>
<ul class="associateds">
{% for item in grouped.content %}
<li class="associateds-items">
<img src="{{ _media(item.media).cached }}" onerror="this.src = 'https://storage.test.increazy.com/base_pwa/images/no-image.webp'"/>

<div class="associeted-description">
<p>{{ item.name }}</p>

{% if _price('discount', item) > 0 %}
<strong class="associateds-price__old">
{{ _price('max', item) }}
</strong>
{% endif %}
<strong class="associateds-price__sale">
{{ _price('min', item) }}
</strong>
</div>

{% if item.stock.quantity > 0 %}
<div class="info--buttons__actions--accumulator">
<span onclick="changeQuantity('qty_assoc_{{ loop.index }}', {{ item.stock.increment * -1 }})">-</span>
<input
min="{{ item.stock.min_qty }}"
max="{{ item.stock.quantity > 0 ? item.stock.quantity : 99 }}"
step="{{ item.stock.increment | number_format }}"
value="{{ product.associated[loop.index - 1].qty | number_format }}"
type="number"
name="super_group[{{ item.id }}]"
id="qty_assoc_{{ loop.index }}"
required
data-item-quantity="{{ item.sku }}"
/>
<span onclick="changeQuantity('qty_assoc_{{ loop.index }}', {{ item.stock.increment }})">+</span>
</div>
{% else %}
<span class="no-stock-associateds">Esgotado</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
  • Em seguida temos o CSS dos produtos agrupados minificado, caso queira reaproveitar o layout padrão do elemento:
.associateds{width:100%;display:flex;flex-flow:column;list-style:none}.associateds-items{display:flex;margin:6px 0;border-radius:8px;align-items:center;justify-content:space-around;padding:8px;background:var(--background)}.associateds-items img{width:calc(20% - 10px);margin-right:10px}.associeted-description{width:60%}.associeted-description p{padding-bottom:2px}.associeted-description .associateds-price__old{text-decoration:line-through;color:var(--clean-text);margin-right:10px;font-size:14px}.associeted-description .associateds-price__sale{font-family:var(--title-font);font-weight:700;line-height:46px;color:var(--title-text);font-size:20px}.associeted-description .info--buttons__actions--accumulator{width:20%}.product-details__info--buttons .info--buttons__actions--submitter{max-width:none}.no-stock-associateds{font-size:13px;color:var(--toast-red);pointer-events:none}
  • Faça a chamada do elemento criado dentro da tag <form> localizado em páginas > product.html, como mostra esse exemplo onde temos a chamada do elemento {% include 'product page/associateds' %}.

  • Lembre-se de verificar se o método de addToCart() está atualizado, juntamente com seus métodos auxiliares.

  • Para finalizar, verifique também se o script das regras da tela de detalhe de produto está atualizado. Geralmente o script fica localizado em Assets > global functions > product.js, clique aqui para ver o script com mais detalhes.

Adicionar produto ao carrinho

Na tela de product.html temos o código que traz todos os recursos necessários para incluir um produto ao carrinho, veja o exemplo abaixo:

<form id="product-form" onsubmit="addToCart(event, {{ _object(_only(product, 'id', 'sku')) }})" class="product-details__info--buttons">
{% include 'product page/price' %}
{% include 'product page/variation selector' %}
{% include 'product page/options form' %}
{% include 'product page/kits' %}
{% include 'product page/associateds' %}

<input type="hidden" name="sku" value="{{ product.sku }}"/>

<div class="info--buttons__actions">
<div class="info--buttons__actions--accumulator">
<span onclick="changeQuantity('qty_input', {{ product.stock.increment * -1 }})">-</span>
<input
min="1"
max="{{ product.stock.quantity > 0 ? product.stock.quantity : 99 }}"
step="{{ product.stock.increment | number_format }}"
value="{{ product.stock.increment | number_format }}"
type="number" name="quantity"
id="qty_input"
required
data-product-quantity="{{ product.sku }}"
/>
<span onclick="changeQuantity('qty_input', {{ product.stock.increment }})">+</span>
</div>

<button class="info--buttons__actions--submitter">
Comprar
{% include 'icons/cart' %}
</button>
</div>
</form>

Temos também os métodos que auxiliam no funcionamento de inserção de produto ao carrinho, são eles:

  // Localizado no arquivo: "Assets > global functions > product.js"
function changeQuantity(inputId, increment) {
const input = document.getElementById(inputId)

const newValue = (+input.value) + (+increment)
if (newValue >= +input.getAttribute('min') && newValue <= +input.getAttribute('max')) {
input.value = newValue
}
}

/**
* Auxilia na formatação dos campos detectados na tela de detalhe de produto,
* enviando da forma correta para adicionar o produto no carrinho. Esse método é
* chamado no médoto addToCart()
*/
function formatCartObject(obj) {
const formattedObj = {};

for (const key in obj) {
if (key.includes("[")) {
const keys = key.split(/[\[\]]/).filter(Boolean);
let currentObj = formattedObj;

for (let i = 0; i < keys.length - 1; i++) {
const currentKey = keys[i];
if (!currentObj[currentKey]) {
currentObj[currentKey] = {};
}
currentObj = currentObj[currentKey];
}

const lastKey = keys[keys.length - 1];
currentObj[lastKey] = obj[key];
} else {
formattedObj[key] = obj[key];
}
}

return formattedObj;
}

// Localizado no arquivo: "Assets > global functions > cart.js"
async function addToCart(event, product, id = null) {
if (event.preventDefault) {
event.preventDefault();
event.stopPropagation();
}

const byShipping = id != null;
if (id == null) {
id = _cookie('cart_id').read();
}

const formData = new FormData(event.target);
let object = formatCartObject(Object.fromEntries(formData.entries()));

if (!byShipping) {
loading('cart', 'on');
toggleCart();
}

const quantity = object.quantity;
delete object.quantity

const super_attribute = getProductVariations(object);
const options = getProductOptions(object);

const cart = await _ecommerce('cart/addItem', {
body: {
id,
product: {
...product,
sku: object.sku,
super_attribute,
options,
request_info: object
},
quantity,
},
async success(cart) {
if (!byShipping) {
await refreshCart({ cart });
toast('Produto adicionado', 'green');
}
}
})


loading('cart', 'off');

return cart;
}

// Localizado no arquivo: "Assets > global functions > cart.js"
function getProductVariations(object) {
return Object.keys(object).reduce((obj, name) => {
if (name.startsWith('variations.')) {
obj[name.replace('variations.', '')] = object[name];
}

return obj;
}, {})
}

// Localizado no arquivo: "Assets > global functions > cart.js"
function getProductOptions(object) {
return Object.keys(object).reduce((obj, name) => {
if (name.startsWith('options.') && object[name] != '') {
obj[name.replace('options.', '')] = object[name];
}

return obj;
}, {})
}

Temos então o resultado de um HTML semelhante à imagem abaixo.

An image


Calculo de frete

Temos abaixo o código para realizar todo o processo de exibir e calcular o frete para o cliente do produto atual da página. O código demonstrado abaixo, está sendo importado na página de product.html pelo elemento product page/deadline.

<form onsubmit="searchCep(event)" class="product-details__info--cep">
<label>
{% include 'icons/truck' %}
Calcule o frete e prazo
</label>

<div class="info--cep__submitter">
<input onkeyup="cepMask(event)" value="{{ postal }}" name="postal" required id="product-shipping" placeholder="Digite o seu CEP">
<button type="submit" id="submit-shipping">
<span class="text">Ok</span>
{% include 'loaders/element' with { id: 'freight'} %}
</button>
</div>
</form>

{% if fretesProduto %}
<ul class="product-details__info--shipping show-shipping" id="shipping-options">
{% if fretesProduto | length > 0 %}
{% for frete in fretesProduto %}
<li>
<span>{{ frete.price > 0 ? _price('format', frete.price) : 'Grátis' }}</span>
<span class="info--shipping__highlighted">{{ frete.title }}</span>
</li>
{% endfor %}
{% elseif fretesProduto %}
<li>
<span>Ops</span>
<span class="info--shipping__highlighted">Sem resultado encontrado</span>
</li>
<span class="freight__result">Sem resultado encontrado.</span>
{% endif %}
</ul>
{% endif %}

<script>
async function searchCep (event) {
event.stopPropagation();
event.preventDefault();

const textButton = document.querySelector('#submit-shipping .text');

loading('freight', 'on');
textButton.style.display = 'none';

const inputShipping = document.getElementById('product-shipping');
inputShipping.setAttribute('required', true);

const cart = await _ecommerce('cart/getOrCreate', {
body: {
id: ""
}
})

event = { target: document.getElementById('product-form') }

await addToCart(event, {{ _object(_only(product, 'id', 'sku')) }}, cart.id)

const fretesProduto = await _ecommerce('freight/byCart', {
body: {
id: cart.id,
postal: document.getElementById('product-shipping').value.replace('.', '').replace('-', '')
}
});

await _refresh('product page/deadline', {
context: {
fretesProduto,
postal: '_dom:#product-shipping$value'
}
})

loading('freight', 'off');
textButton.style.display = 'flex';
}
</script>

No script acima, temos alguns métodos padrões: _ecommerce e _refresh, que nós disponibilizamos internamente para facilitar chamadas de APIs e buscas mais complexas. Temos também o método addToCart que falamos sobre ele em adicionar produto ao carrinho, é o mesmo método. O método addToCart está recebendo um parâmetro chamado event que recebe o seletor/tag do formulário principal que adiciona um item no carrinho que seria o document.getElementById('product-form') o ID product-form se encontra em detalhe de produto.

Resultado do código de calculo do frete. Lembrando que é necessário aplicar o css.

An image


Produtos Relacionados

O código de produtos relacionados está sendo buscado da importação do elemento product page/related products que está dentro da página de product.html. Temos o seguinte código abaixo:

{% set produtosRelacionados = _search('products', product.cross_sell) %}
{% if (produtosRelacionados | length) > 0 %}
<section class="products__slider-suggest section__margin">
<div class="section-title title-mobile" data-grid="column">
<h2>Produtos Relacionados</h2>
<p>você também vai gostar</p>
</div>

<div class="showcase__slider">
<div class="swiper">
<div class="swiper-wrapper">
{% for product in produtosRelacionados.content %}
<div class="swiper-slide">
{% include 'template/product card' %}
</div>
{% endfor %}
</div>

<div class="swiper-button-next">
{% include 'icons/arrow-left' %}
</div>

<div class="swiper-button-prev">
{% include 'icons/arrow-left' %}
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</section>
{% endif %}

O funcionamento se dá pela busca dos produtos relacionados através do código {% set produtosRelacionados = _search('products', product.cross_sell) %}, que utiliza o método _search. O campo produtosRelacionados é validado por {% if ... %} e listado pelo {% for .. %}, importando o elemento product card que está localizado em Elementos > template > product card.

Resultado de como ficaria a vitrine de produtos relacionados. Lembrando que é necessário aplicar a estilização css.

An image

Assim finalizamos o essencial de uma tela de detalhe de produto.

Compre junto

A seção compre junto é utilizada para buscar dois produtos relacionados ao produto principal da página e adicioná-los em conjunto ao carrinho. Os elementos com seus respectivos códigos poderão ser implementados na pasta Elementos > product page.

O código abaixo traz o index da seção, onde estamos fazendo o set do CMS para o título da seção e a busca dos produtos com o cross_sell. Neste trecho também estamos definindo como productOne e produtTwo os dois primeiros produtos encontrados na busca, além dos scripts que atualizam o valor individual dos produtos de acordo com variações de opção e o valor total da soma dos dois produtos.

{% set groupBuyTitle = _cms('text', 'Titulo da sessão compre junto') %}
{% set crossSell = _search('products', product.cross_sell) %}

{% set productOne = {} %}
{% set productTwo = {} %}

{% if crossSell.content[0] %}
{% set productOne = crossSell.content[0] %}
{% endif %}

{% if crossSell.content[1] %}
{% set productTwo = crossSell.content[1] %}
{% endif %}

{% if crossSell.content|length > 1 %}
<section class="groupbuy" data-grid="column">
<h2 class="groupbuy__title">{{ groupBuyTitle.content.title }}</h2>

<div class="groupbuy__content" data-grid="nowrap">
<!-- PRODUCT ONE -->
<form id="form-product-{{ productOne.id }}" name="form-product-one" class="groupbuy__form">
<article class="item {{ productOne.stock.has ? '' : 'nostock'}}" data-grid="nowrap">
<figure class="item-image" {{ _href(productOne) }}>
<img src="{{ _image(productOne.media[0].cached, 'w=250') }}" alt="Imagem do produto {{ productOne.name }}"/>
</figure>

<div class="item-info" data-grid="column">
<h3 class="item-name">{{ productOne.name }}</h3>

{% set priceProductOne = _price('min', productOne, activeVariation, true, customerGroup) %}
<span class="item-price"
virgin-price="{{ _price('min', productOne, activeVariation, false) }}"
id="price-product-one"
>
{{ priceProductOne }}
</span>

{% include 'product page/group buy installments one' %}

<div class="item-variations" data-grid="column">
{% include 'product page/group buy variation selector' with {'variationSelectorProduct': [productOne]} %}
{% include 'product page/group buy options form' with {'optionsFormProduct': [productOne], 'installmentsId': 'one'} %}
</div>
</div>

<input type="hidden" name="sku" value="{{ productOne.sku }}"/>
<input type="hidden" name="product_id" value="{{ productOne.id }}"/>
</article>
<span class="outofstock">Sem estoque</span>
</form>

<span class="sum-icon" data-grid="center-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>
</span>

<!-- PRODUCT TWO -->
<form id="form-product-{{ productTwo.id }}" name="form-product-two" class="groupbuy__form">
<article class="item {{ productTwo.stock.has ? '' : 'nostock'}}" data-grid="nowrap">
<figure class="item-image" {{ _href(productTwo) }}>
<img src="{{ _image(productTwo.media[0].cached, 'w=250') }}" alt="Imagem do produto {{ productTwo.name }}"/>
</figure>

<div class="item-info" data-grid="column">

<h3 class="item-name">{{ productTwo.name }}</h3>

{% set priceProductTwo = _price('min', productTwo, activeVariation, true, customerGroup) %}
<span class="item-price"
virgin-price="{{ _price('min',productTwo, activeVariation, false) }}"
id="price-product-two"
>
{{ priceProductTwo }}
</span>

{% include 'product page/group buy installments two' %}

<div class="item-variations" data-grid="column">
{% include 'product page/group buy variation selector' with {'variationSelectorProduct': [productTwo]} %}
{% include 'product page/group buy options form' with {'optionsFormProduct': [productTwo], 'installmentsId': 'two'} %}
</div>
</div>

<input type="hidden" name="sku" value="{{ productTwo.sku }}"/>
<input type="hidden" name="product_id" value="{{ productTwo.id }}"/>
</article>
<span class="outofstock">Sem estoque</span>
</form>
</div>

<div class="groupbuy__summary" data-grid="column">
<div class="price" data-grid="nowrap">
<b>Total:</b>

<span id="groupbuy-total" class="value"></span>
</div>

<div class="actions" data-grid="nowrap">
<div class="quantity" data-grid="nowrap">
<span onclick="changeQuantity('groupbuy-input', -1)">-</span>
<input
min="1"
max="99"
value="1"
type="number"
name="quantity"
id="groupbuy-input"
required
aria-label="accumulator"
readonly
/>
<span onclick="changeQuantity('groupbuy-input', 1)">+</span>
</div>

<button id="send-groupbuy" class="buy-btn" onclick="submitGroupBuy(event)">Adicionar produtos ao carrinho</button>
</div>
</div>
</section>
{% endif %}

<script>
function updateGroupBuyPrice(product) {
const productId = product.getAttribute('data-product-id');
const form = document.querySelector(`#form-product-${productId}`);
const priceElement = form.querySelector('.item-price');
let currentPriceString = priceElement.getAttribute('virgin-price');
const currentPrice = parseFloat(currentPriceString.replace(/[^\d,]/g, '').replace(',', '.'));

if (currentPrice) {
let sumOptions = 0

form.querySelectorAll('.options [data-groupbuy-price]').forEach((s) => {
const groupBuyPrice = s.getAttribute('data-groupbuy-price');

if (groupBuyPrice && !isNaN(parseFloat(groupBuyPrice))) {
if (s.tagName == 'OPTION') {
if (s.selected) {
sumOptions += parseFloat(groupBuyPrice);
}
} else if (['radio', 'checkbox'].includes(s.getAttribute('type'))) {
validateRequiredOptions(s.closest('.options__item-values-list'))

if (s.checked) {
sumOptions += parseFloat(groupBuyPrice);
}
} else {
sumOptions += s.value.length > 0 ? parseFloat(groupBuyPrice) : 0
}
}
});
const newPrice = currentPrice + sumOptions;
priceElement.innerHTML = _price('format', newPrice);

const installmentsId = product.getAttribute('data-installments');

_refresh([`product page/group buy installments ${installmentsId}`], {
context: { changedPriceGroupBuyOptions: newPrice }
});
}

updateTotalPrice();
}

if (typeof updateGroupBuyPrice == 'function') {
const products = document.querySelectorAll('.options[data-sku]');

products.forEach((product, index) => {
if (index > 0) {
const firstInput = product.querySelector('input[data-product-id]');
const firstSelect = product.querySelector('select[data-product-id]');

if (firstInput) {
updateGroupBuyPrice(firstInput);
}

if (firstSelect) {
updateGroupBuyPrice(firstSelect);
}
}
});
}

function updateTotalPrice(){
let priceProductOne = document.getElementById('price-product-one').textContent.trim();
let priceProductTwo = document.getElementById('price-product-two').textContent.trim();

priceProductOne = priceProductOne.replace(/[^\d,]/g, '').replace(',', '.');
priceProductTwo = priceProductTwo.replace(/[^\d,]/g, '').replace(',', '.');

let valueOne = parseFloat(priceProductOne);
let valueTwo = parseFloat(priceProductTwo);
let quantityInput = parseInt(document.getElementById('groupbuy-input').value);

let productOneHasStock = {{ productOne.stock.has ? 'true' : 'false' }};
let productTwoHasStock = {{ productTwo.stock.has ? 'true' : 'false' }};

const totalValue = document.getElementById('groupbuy-total');

if (productOneHasStock && productTwoHasStock) {
const total = (valueOne + valueTwo) * quantityInput;
totalValue.innerHTML = _price('format', total);
} else if (productOneHasStock) {
const total = valueOne * quantityInput;
totalValue.innerHTML = _price('format', total);
} else if (productTwoHasStock) {
const total = valueTwo * quantityInput;
totalValue.innerHTML = _price('format', total);
} else {
totalValue.innerHTML = 'Produtos sem estoque';
}
}

let quantityDiv = document.querySelector('.quantity');
quantityDiv.addEventListener('click', (event) => {
updateTotalPrice();
});

updateTotalPrice();
</script>

Dentro do nosso index podemos encontrar dois elementos que também devem ser inseridos na pasta product page: product page/group buy installments one e product page/group buy installments two. Esses elementos contém o código responsável pela exibição do parcelamento de cada produto e devem ser inseridos de forma separada para que sejam atualizados dentro da função updateGroupBuyPrice();:

{% if changedPriceGroupBuyOptions > 0 %}
{% set productInstallment = _price('installments', changedPriceGroupBuyOptions) | last %}
{% else %}
{% set productInstallment = _price('installments', productOne) | last %}
{% endif %}

{% if productInstallment.qty > 1 %}
<span class="item-installments">
{{ productInstallment.qty }}x
de {{ _price('format', productInstallment.price, true, customerGroup) }}
</span>
{% endif %}
{% if changedPriceGroupBuyOptions > 0 %}
{% set productInstallment = _price('installments', changedPriceGroupBuyOptions) | last %}
{% else %}
{% set productInstallment = _price('installments', productTwo) | last %}
{% endif %}

{% if productInstallment.qty > 1 %}
<span class="item-installments">
{{ productInstallment.qty }}x
de {{ _price('format', productInstallment.price, true, customerGroup) }}
</span>
{% endif %}

Além desses elementos também devemos inserir outros dois, responsáveis pela seleção das variações e opções de cada produto, são eles product page/group buy variation selector e product page/group buy options form:

{% for product in variationSelectorProduct %}
<script>
window._productsLinkeds = window._productsLinkeds || {};
window._productsLinkeds['{{ product.sku }}'] = {{ product.linkeds | json_encode | raw }};
</script>

<div class="variations" data-sku="{{ product.sku }}" id="product-variations-list">
{% for variation in product.variations %}
<div class="variations__item">
<p class="variations__item-name">
{{ variation.label }}:
</p>

<div data-grid class="variations__item-list">
{% for value in variation.values %}
<input
required
type="radio"
name="variations.{{ variation.id }}"
id="variation_{{ product.id }}_{{ variation.code }}_{{ value.value }}"
value="{{ value.value }}"
data-attribute="{{ variation.code }}"
class="variations__radio"
onclick="updateVariationsStock('{{ product.sku }}', '{{ variation.code }}', event.target)"
/>

<label
data-grid
for="variation_{{ product.id }}_{{ variation.code }}_{{ value.value }}"
class="variations__label grid--center grid--va-center {{ value.source starts with 'http' ? 'variation-img' : '' }}"
title="{{ value.label }}"
>
<span>{{ value.label }}</span>
</label>
{% endfor %}
</div>
</div>
{% endfor %}
</div>

<script type="text/javascript">
(function () {
_dom('updateVariationsStock').waitVariable(function () {
updateVariationsStock('{{ product.sku }}');
startVariations();
disableVariation();
});
})();
</script>
{% endfor %}
{% for product in optionsFormProduct %}
<div class="options" data-sku="{{ product.sku }}">
<script>
window._productsLinkeds = window._productsLinkeds || {};
window._productsLinkeds['{{ product.sku }}'] = {{ product.linkeds | json_encode | raw }};
</script>

{% if product.options | length > 0 %}
{% for option in product.options %}
<div class="options__item">
<label for="option_{{ product.id }}_{{ option.code }}_{{ value.value }}" class="options__item-name">
{{ option.label }}

{% if option.required %}
<span style="color:var(--increazy)">*</span>
{% endif %}

{% if option.price > 0 %}
<small>
(+ {{ option.action == 'fixed' ? _price('format', option.price) : option.price ~ '%' }})
</small>
{% endif %}
</label>

{% if option.type not in ['textarea', 'select', 'multiple'] %}
{% if option.values | length > 0 %}
<div class="options__item-values-list" data-grid>
{% for value in option.values %}
<label class="options__item-values" for="option_{{ product.id }}_{{ value.id }}_{{ value.label }}" data-grid="nowrap">
<input
id="option_{{ product.id }}_{{ value.id }}_{{ value.label }}"
name="options.{{ option.id }}"
type="{{ option.type }}"
data-installments={{ installmentsId }}
data-product-id="{{ product.id }}"
data-groupbuy-price="{{ value.price }}"
data-groupbuy-price-type="{{ value.action }}"
value="{{ value.id }}"
onchange="updateGroupBuyPrice(this)"
{{ option.required ? 'required' : '' }}
/>
<span class="options__item-values-label">{{ value.label }} (+ {{ _price('format', value.price) }})</span>
</label>
{% endfor %}
</div>
{% else %}
<input
id="option_{{ product.id }}_{{ option.id }}_{{ option.label }}"
name="options.{{ option.id }}"
type="{{ option.type }}"
data-installments={{ installmentsId }}
data-product-id="{{ product.id }}"
data-groupbuy-price="{{ option.price }}"
data-groupbuy-price-type="{{ option.action }}"
oninput="updateGroupBuyPrice(this)"
{{ option.required ? 'required' : '' }}
/>
{% endif %}
{% elseif option.type == 'textarea' %}
<textarea
oninput="updateGroupBuyPrice(this)"
id="option_{{ product.id }}_{{ option.code }}_{{ value.value }}"
name="options.{{ option.id }}"
data-installments={{ installmentsId }}
data-product-id="{{ product.id }}"
data-groupbuy-price="{{ option.price }}"
data-groupbuy-price-type="{{ option.action }}"
{{ option.required ? 'required' : '' }}
></textarea>
{% else %}
<select
id="option_{{ product.id }}_{{ option.code }}_{{ value.value }}"
name="options.{{ option.id }}"
data-installments={{ installmentsId }}
data-product-id="{{ product.id }}"
onchange="updateGroupBuyPrice(this)"
{{ option.required ? 'required' : '' }}
{{ option.type == 'multiple' ? 'multiple' : '' }}
>
{% for value in option.values %}
<option
data-groupbuy-price="{{ value.action == 'percent' ? (value.price / 100) * _price('min', activeVariation, false) : value.price }}"
value="{{ value.id }}"
>
{{ value.label }}

{% if value.price > 0 %}
<small>
(+ {{ value.action == 'percent' ? _price('format', (value.price / 100) * _price('min', activeVariation, false)) : _price('format', value.price) }})
</small>
{% endif %}
</option>
{% endfor %}
</select>
{% endif %}
</div>
{% endfor %}
{% endif %}
</div>
{% endfor %}

Também deve ser inserido em Assets > global functions > cart.js a função responsável pela adição dos produtos da seção no carrinho:

async function submitGroupBuy(event) {
event.preventDefault();
event.stopPropagation();

const radios = document.querySelectorAll('.variations [type="radio"]');
let valid = false;

if (radios.length > 0) {
radios.forEach(radio => {
if (radio.checked) {
valid = true;
}
});

if (!valid) {
toast('Selecione a variação do(s) produto(s) desejado(s)', 'red');
return;
}
}

const id = _cookie('cart_id').read();

const formProductOne = document.getElementsByName('form-product-one')[0];
const formDataOne = new FormData(formProductOne);
let objectProductOne = formatCartObject(Object.fromEntries(formDataOne.entries()));

const formProductTwo = document.getElementsByName('form-product-two')[0];
const formDataTwo = new FormData(formProductTwo);
let objectProductTwo = formatCartObject(Object.fromEntries(formDataTwo.entries()));

const super_attributeOne = getProductVariations(objectProductOne);
const optionsOne = getProductOptions(objectProductOne);

const super_attributeTwo = getProductVariations(objectProductTwo);
const optionsTwo = getProductOptions(objectProductTwo);

console.log(formDataOne);
console.log(formDataTwo);

loading('cart', 'on');
toggleCart();

const buildProductObject = (formData) => ({
sku: formData.get('sku'),
id: formData.get('product_id'),
request_info: formData
});

const products = [
{
...buildProductObject(formDataOne),
quantity: document.getElementById('groupbuy-input').value,
super_attribute: super_attributeOne,
options: optionsOne
},
{
...buildProductObject(formDataTwo),
quantity: document.getElementById('groupbuy-input').value,
super_attribute: super_attributeTwo,
options: optionsTwo
}
];

console.log(products);

try {
const cart = await _ecommerce('cart/bundle', {
body: {
id: +id,
products
}
});

await refreshCart({ cart });
toast('Produtos adicionados', 'green');
} catch (error) {
toast('Erro ao adicionar os produtos', 'red');
}

loading('cart', 'off');
}

Com a aplicação dos elementos citados e uma estilização css o resultado final seria algo semelhante a:

An image

Caso queira utilizar a mesma estilização do exemplo a cima, disponibilizamos abaixo o código css:

.groupbuy {
max-width: 1334px;
padding-inline: 24px;
gap: 20px;
margin: 30px auto 0px auto;
}

.groupbuy * {
font-family: 'Roboto', sans-serif;
}

.groupbuy__title {
color: #000;
font: italic 700 2rem / 1.5 'Ubuntu', sans-serif !important;
font-size: 2.8rem;
text-align: center;
}

.groupbuy__content {
gap: 60px;
position: relative;
align-items: center;
justify-content: center;
}

.groupbuy__form {
width: 50%;
position: relative;
}

.groupbuy__content .item {
gap: 15px;
align-items: flex-start;
}

.outofstock {
position: absolute;
left: 5px;
top: 5px;
z-index: 1;
display: none;
align-items: center;
gap: 8px;
background-color: #FFBBBB;
color: #D60000;
font-family: 'Roboto', sans-serif;
font-weight: 500;
font-size: 14px;
line-height: 19.6px;
padding: 6px 8px;
border-radius: 4px;
cursor: default;
pointer-events: all;
}

.groupbuy__content .item.nostock {
opacity: 0.5;
}

.groupbuy__content .item.nostock+.outofstock {
display: flex;
}

.groupbuy__content .item-image {
min-width: 60%;
min-height: 350px;
cursor: pointer;
}

.groupbuy__content .item-image img {
width: 100%;
height: 100%;
object-fit: cover;
}

.groupbuy__content .item-info {
max-width: 40%;
}

.groupbuy__content .item-info .item-name {
color: #000;
font-size: 16px;
min-width: unset;
}

.groupbuy__content .item-info .item-price {
font-weight: bold;
font-size: 13px;
color: #000;
margin-top: 5px;
}

.groupbuy__content .item-info .item-installments {
font-size: 13px;
color: #000;
margin-top: 5px;
}

.groupbuy__content .item-info .item-variations {
margin-top: 15px;
gap: 15px;
}

.groupbuy__content .item-info .item-variations .variations__item-list {
flex-wrap: wrap;
width: 100%;
}

.groupbuy__content .variations__item-name {
color: #000!important;
}

.groupbuy__content .variations__label>span {
height: 32px;
width: 32px;
padding: 0 10px;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
font-weight: 600;
cursor: pointer;
background: #F7F7F7;
border: none;
}

.groupbuy__content .variations__radio:checked+.variations__label>span {
border: 1px solid #01141E;
color: #01141E;
}

.groupbuy__content .options .options__item .options__item-name {
color: #000;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}

.groupbuy__content .item-info .item-variations .options__item select,
.groupbuy__content .item-info .item-variations .options__item textarea,
.groupbuy__content .item-info .item-variations .options__item input {
width: 100%;
padding: 5px 8px;
border: 1px solid #E6E6E6;
border-radius: 4px;
background: #fff0;
color: #000;
font-size: 13px;
min-height: 40px;
background: #fff;
}

.groupbuy__content .sum-icon {
width: 32px;
min-width: 32px;
height: 32px;
border: 1px solid #E6E6E6;
min-height: auto;
border-radius: 50%;
box-shadow: 0 3px 13px -5px rgba(0, 0, 0, 0.2);
font-weight: bold;
font-size: 15px;
}

.groupbuy__content .sum-icon svg {
width: 16px;
}

.groupbuy__summary {
gap: 20px;
}

.groupbuy__summary .price {
font-size: 16px;
color: #000;
gap: 10px;
justify-content: center;
}

.groupbuy__summary .actions {
justify-content: center;
align-items: center;
gap: 20px;
}

.groupbuy__summary .actions .quantity {
width: 118px;
height: 44px;
border: 1px solid #E6E6E6;
align-items: center;
justify-content: space-between;
border-radius: 50px;
padding: 0 20px;
font-family: 'Roboto', sans-serif;
font-size: 14px;
line-height: 21px;
font-weight: 600;
color: #000;
}

.groupbuy__summary .actions .quantity span {
cursor: pointer;
user-select: none;
}

.groupbuy__summary .actions .quantity input {
width: 40px;
text-align: center;
border: 0;
cursor: default;
}

.groupbuy__summary .actions .buy-btn {
width: 716px;
height: 44px;
border-radius: 50px;
border: 0;
background: #01141E;
color: #fff;
}

@media (max-width: 1024px) {
.groupbuy__content {
flex-flow: column;
gap: 30px
}

.groupbuy__form {
width: 100%;
}

.groupbuy__content .item {
flex-flow: column;
}

.groupbuy__content .item-info {
max-width: 100%;
}

.groupbuy {
padding: 0 24px;
}

.groupbuy__content .item-image {
width: 100%;
}
}

Script padrão para o funcionamento tela de detalhe de produto

O script abaixo traz todas as principais funcionalidades da tela de detalhe de produto, como a seleção de variações, opções, quantidade de itens e etc.

Vale lembrar que o script segue o padrão do layout da increazy, verifique se os IDs e classes chamados dentro dele estão sendo também chamados no HTML do seu projeto.

Esse script pode ser aplicado todo dentro de um único arquivo js e chamado antes do final do <body> no arquivo Layout > layout.html.

function disableVariation(disabled = true) {
const variationsList = document.querySelector('#product-variations-list')
if ((variationsList?.children || []).length <= 1) return;

const lastVariation = document.querySelector('#product-variations-list .variations__item:last-child');
if (lastVariation) {
if (disabled) {
lastVariation.classList.add('disabled');
} else {
lastVariation.classList.remove('disabled');
}
}
}

function startVariations() {
const variationsList = document.querySelector('#product-variations-list')
if ((variationsList?.children || []).length > 1) {
for (var i = 1; i <= variationsList.children.length; i++) {
variationsList.children[i - 1].setAttribute('order-variation', i);
}
};
}

window.onUpdateBySKU = (sku, activeVariation) => {
const skuInput = document.querySelector('input[name="sku"]');

if (skuInput) {
skuInput.value = activeVariation.id;
}

_refresh(['product page/price', 'product page/images'], {
context: { activeVariation },
event: {
before: ['loader', 'variations', 'on'],
after: ['loader', 'variations', 'off']
}
})
}

function updatePriceByOption() {
let sumOptions = 0
const priceElement = document.querySelector('#product-price')
let currentPrice = priceElement.getAttribute('virgin-price');

if (currentPrice) {
document.querySelectorAll('.options [data-price]').forEach((s) => {
if (typeof +s.getAttribute('data-price') == 'number') {
if (s.tagName == 'OPTION') {
if (s.selected) {
sumOptions += (+s.getAttribute('data-price'))
}
} else if (['radio', 'checkbox'].includes(s.getAttribute('type'))) {
validateRequiredOptions(s.closest('.options__item-values-list'))

if (s.checked) {
sumOptions += (+s.getAttribute('data-price'))
}
} else {
sumOptions += s.value.length > 0 ? +s.getAttribute('data-price') : 0
}
}
})

priceElement.innerHTML = _price('format', (+currentPrice) + (+sumOptions))
}
}

function validateRequiredOptions(el) {
const inputs = el.querySelectorAll('input');

for (var i = 0; i < inputs.length; i++) {
inputs[i].required = !!el.querySelector('input:checked');
}
}

// RESETA AS VARIACOES DO SEGUNDO EM DIANTE
function resetVariations(element) {
if (!element?.closest('[order-variation="1"]')) return;

const variationsList = document.querySelector('#product-variations-list');

if ((variationsList?.children || []).length < 2) return;

for (var i = 1, lenI = variationsList.children.length; i < lenI; i++) {
const inputs = variationsList.children[i].querySelectorAll('input');

for (var j = 0, lenJ = inputs.length; j < lenJ; j++) {
inputs[j].checked = false;
}
}
}

function updateVariationsStock(sku, code = null, element = null) {
if (element) {
resetVariations(element);
disableVariation(false);


if (element.nextElementSibling?.getAttribute('data-stock-out')) {
element.checked = false;
return
}
}

const selector = '.variations[data-sku="' + sku + '"] .variations__item-list input';

const inputs = [...document.querySelectorAll(selector)].reduce((o, e) => {
const name = e.getAttribute('data-attribute');

if (o.all[name] == undefined) {
o.all[name] = [];
o.selecteds[name] = [];
}

o.all[name].push({
element: e,
value: e.value,
checked: e.checked,
});

if (e.checked) {
o.selecteds[name].push(e.value);
}

return o;
}, { all: {}, selecteds: {} })

const matches = window._productsLinkeds[sku].filter(p => {
include = false;

Object.keys(inputs.selecteds).forEach(attr => {
const inputToCheck = inputs.selecteds[attr].length > 0;

value = p[attr] || null;
value = value !== null ? (value.value || value) : null;

if (inputs.selecteds[attr].includes(`${value}`) && inputToCheck) {
include = true
}
})

return include
});

const exactMatches = window._productsLinkeds[sku].filter(p => {
include = true;

Object.keys(inputs.selecteds).forEach(attr => {
const inputToCheck = inputs.selecteds[attr].length > 0;

value = p[attr] || null;
value = value !== null ? (value.value || value) : null;

if (!inputs.selecteds[attr].includes(`${value}`) && inputToCheck) {
include = false
}
})

return include
});

if (code == null) return;

lastVariation = null;

Object.keys(inputs.all).forEach(attr => {
if (attr === code) return;

inputs.all[attr].forEach(input => {
const attachedLabel = input?.element.nextElementSibling;

if (!attachedLabel?.closest('[order-variation="1"]')) {
attachedLabel?.setAttribute('data-stock-out', 'true')
}

if (matches.length <= 0) {
attachedLabel?.removeAttribute('data-stock-out')
}

matches.forEach(p => {
value = p[attr] || null;
value = value !== null ? (value.value || value) : null;

if (input.value == value) {
if (p.stock.has) {
attachedLabel?.removeAttribute('data-stock-out');

const qtyInput = document.querySelector('input[data-product-quantity="' + sku + '"]');

if (qtyInput) {
qtyInput.setAttribute('max', p.stock.quantity);
}

lastVariation = p
}
}

if (!element?.checked) {
attachedLabel?.removeAttribute('data-stock-out');
}
})
});
});

window.onUpdateBySKU(sku, exactMatches[0])
}

function changeQuantity(inputId, increment) {
const input = document.getElementById(inputId)

const newValue = (+input.value) + (+increment)
if (newValue >= +input.getAttribute('min') && newValue <= +input.getAttribute('max')) {
input.value = newValue
}
}