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. Abaixo temos um código simples de como é listado o conteúdo da página de detalhe, e como podemos ver, sempre utilizamos 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.
Breadcrumb
No elemento template/breadcrumb
listado acima, temos o seguinte código:
<div class="breadcrumb container">
<a {{ _href('/') }}>Home</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 as 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 }}"
>
</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.
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 product 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 atributodesconto
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)}}
. -
Note que estamos utilizando um método padrão chamado
_price
, ele nos auxilia a trazer corretamente o preço desejado, uma vez que existem algumas variações de preço. Todos os métodos padrão estarão destacados com um '_' antes do nome. -
Veja abaixo um exemplo do elemento
product page/price
:
{% 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 %}
Parcelas: Podemos criar um atributo para auxiliar na validação do parcelamento do produto, assim temos controle sobre exibir ou não o parcelamento caso ele 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 o objeto do qual 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 é listado através do campo
{{ _price('format', maiorParcela.price) }}
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 importado na página product.html. Talvez essa seja uma das partes mais árduas a se fazer quando se trata de código puro, mas disponibilizaremos 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>
A chamada do script que possui window._productsLinkeds
é usada para fazer o cálculo do estoque no momento da seleção de cor
, tamanho
ou 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 chamado updateVariationsStock()
, mas além dele, existem outros método auxiliares para o funcionamento correto das opções e variações de produto. 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
Semelhante às Variações temos as Opções do produto. Nesse caso, as opções do produto serão importadas diretamente do elemento product page/options form
no nosso código da página product.html. Para facilitar, vamos disponibilizar o código que traz as opções de um produto abaixo, permitindo que você apenas copie e cole no 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ções de um produto podem surgir alterações no preço. Por esse motivo é necessário que no código HTML de preço do produto, localizado em Elementos > product page > price.html, sejam aplicados alguns atributos na tag de preço que será identificada pelo script do arquivo Assets > global functions > product.js
. Esse script 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 exista um 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 aplicar todos os códigos, o resultado será algo semelhante a imagem abaixo:
Fique tranquilo, caso o seu produto possua múltiplas variações e opções, tudo virá de forma automática.
Kits e produtos agrupados
Kits
Vamos aos passos para podermos criar/aplicar kits de produtos 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 dentro de um único elemento do seu projeto e fazer a chamada do mesmo dentro da tag
<form>
na páginaproduct.html
.
É 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 }}"
/>
<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 tudo funcione perfeitamente, verifique se o script das regras da tela de detalhe de produto está atualizado. O script fica localizado em Assets > global functions > product.js
. Clique aqui para ver o script com mais detalhes.
Produtos agrupados
Semelhantemente aos kits, vamos seguir passos abaixo:
Primeiro crie um elemento para os produtos agrupados e cole nele 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 }}" />
<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>
localizada 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. O script fica localizado em Assets > global functions > product.js
. Clique aqui para ver o script com mais detalhes.
Adicionar produto ao carrinho
Na página 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 da 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.
Cálculo de frete
Abaixo visualizaremos o código utilizado para realizar todo o processo de calcular o frete do produto da página atual e exibí-lo para o cliente. O código demonstrado está sendo importado na página 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ão:
_ecommerce
e_refresh
. Disponibilizamos eles internamente para facilitar chamadas de APIs e buscas mais complexas. Temos também o métodoaddToCart
, citado anteriormente em adicionar produto ao carrinho. O métodoaddToCart
está recebendo um parâmetro chamadoevent
que recebe o seletor/tag do formulário principal que adiciona um item no carrinho, que seria odocument.getElementById('product-form')
. O IDproduct-form
se encontra em detalhe de produto.
Na imagem abaixo podemos visualizar como seria o resultado do cálculo de frete, lembrando que é necessário aplicar o css:
Produtos Relacionados
O código de produtos relacionados está sendo importado através do elemento product page/related products
, dentro da página product.html. No elemento temos:
{% 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.
Na imagem abaixo podemos ver como ficaria o resultado de uma vitrine de produtos relacionados, lembrando que é necessário aplicar a estilização css:
Compre junto
A seção compre junto pode ser adicionada ao seu projeto, apesar de não fazer parte do template base. Ela é 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 Elementos > 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(--primary)">*</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:
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 um modelo de layout, 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
}
}