Hi Team,
I’m facing issue with cart value not updated at real time,
Please help to fix this
Hi Team,
I’m facing issue with cart value not updated at real time,
Please help to fix this
<section id="ProductShowcaseSplit-{{ section.id }}" class="pss" style="
--pss-container-pad: {{ section.settings.container_side_padding }}%;
--pss-row-gap: {{ section.settings.row_gap }}px;
--pss-left-bg: {{ section.settings.left_bg }};
--pss-text: {{ section.settings.text_color }};
--pss-line: {{ section.settings.line_color }};
--pss-btn-bg: {{ section.settings.button_bg }};
--pss-btn-text: {{ section.settings.button_text }};
--pss-sep-width: {{ section.settings.separator_width }}%;
--pss-desc-lines: {{ section.settings.desc_lines }};
--pss-title-size: {{ section.settings.title_size }}px;
--pss-subtitle-size: {{ section.settings.subtitle_size }}px;
--pss-desc-size: {{ section.settings.desc_size }}px;
">
<div class="pss__container">
{% if section.blocks.size == 0 %}
<div class="pss__empty">Add product blocks in this section.</div>
{% else %}
<product-showcase-split class="pss__list" data-section-id="{{ section.id }}">
{% for block in section.blocks %}
{% liquid
assign product = block.settings.product
if product != blank
assign variant = product.selected_or_first_available_variant
assign subtitle = block.settings.subtitle
assign subtitle_2 = block.settings.subtitle_2
assign img = block.settings.image
if img == blank
assign img = product.featured_image
endif
assign img_hover = null
if product.images.size > 1
assign img_hover = product.images[1]
endif
assign current_price = variant.price
assign compare_price = variant.compare_at_price
assign has_discount = false
if compare_price and compare_price > current_price
assign has_discount = true
assign discount = compare_price | minus: current_price | times: 100.0 | divided_by: compare_price | round
endif
assign override_desc = block.settings.override_description
endif
%}
{% if product != blank and variant != blank %}
<article class="pss__row" {{ block.shopify_attributes }} data-row data-variant="{{ variant.id }}">
<div class="pss__col pss__col--left">
<div class="pss__content">
<a href="{{ product.url }}" class="pss__title-link">
<h3 class="pss__title">{{ product.title }}</h3>
</a>
<hr class="pss__hr">
{% if subtitle != blank %}
<div class="pss__subtitle">{{ subtitle }}</div>
<hr class="pss__hr">
{% endif %}
{% if subtitle_2 != blank %}
<div class="pss__subtitle pss__subtitle--secondary">{{ subtitle_2 }}</div>
<hr class="pss__hr">
{% endif %}
<div class="pss__meta">
{% if block.settings.weight != blank %}
<span class="pss__weight">{{ block.settings.weight }}</span>
<span class="pss__divider">|</span>
{% endif %}
{% if has_discount %}
<span class="pss__price--compare">{{ compare_price | money }}</span>
<span class="pss__price--current">{{ current_price | money }}</span>
<span class="pss__discount">{{ discount }}% off</span>
{% else %}
<span class="pss__price--current">{{ current_price | money }}</span>
{% endif %}
</div>
<hr class="pss__hr">
{% assign final_desc = '' %}
{% if override_desc != blank %}
{% assign final_desc = override_desc %}
{% elsif product.description != blank %}
{% assign final_desc = product.description %}
{% endif %}
{% if final_desc != blank %}
<div class="pss__desc">{{ final_desc | strip_html }}</div>
{% endif %}
<div class="pss__actions">
<button
type="button"
class="pss__btn pss__btn--add"
data-add
data-variant-id="{{ variant.id }}"
{% unless variant.available %}disabled{% endunless %}
>
{% if variant.available %}Quick add{% else %}Out of stock{% endif %}
</button>
<div class="pss__qty" data-qty hidden>
<button type="button" class="pss__qty-btn" data-dec aria-label="Decrease">−</button>
<span class="pss__qty-val" data-val>1</span>
<button type="button" class="pss__qty-btn" data-inc aria-label="Increase">+</button>
</div>
</div>
<div class="pss__status" data-status aria-live="polite"></div>
</div>
</div>
<div class="pss__col pss__col--right">
<a href="{{ product.url }}" class="pss__image-link">
<div class="pss__image {% if img_hover %}pss__image--has-hover{% endif %}">
{% if img %}
{{ img | image_url: width: 1200 | image_tag:
class: 'pss__img pss__img--primary',
loading: 'lazy',
alt: img.alt | default: product.title
}}
{% if img_hover %}
{{ img_hover | image_url: width: 1200 | image_tag:
class: 'pss__img pss__img--hover',
loading: 'lazy',
alt: img_hover.alt | default: product.title
}}
{% endif %}
{% else %}
<div class="pss__img-placeholder">
{{ 'product-1' | placeholder_svg_tag }}
</div>
{% endif %}
</div>
</a>
</div>
</article>
{% endif %}
{% endfor %}
</product-showcase-split>
{% if section.settings.show_view_all and section.settings.view_all_url != blank %}
<div class="pss__view-all-wrapper">
<a href="{{ section.settings.view_all_url }}" class="pss__btn pss__btn--view-all">
{{ section.settings.view_all_text }}
</a>
</div>
{% endif %}
{% endif %}
</div>
</section>
<style>
.pss { width: 100%; }
.pss__container {
width: 100%;
padding-left: var(--pss-container-pad);
padding-right: var(--pss-container-pad);
box-sizing: border-box;
}
.pss__row {
display: flex;
align-items: stretch;
gap: 0;
margin-bottom: var(--pss-row-gap);
overflow: hidden;
}
.pss__row:last-child { margin-bottom: 0; }
.pss__col { flex: 1; min-width: 0; }
.pss [hidden] { display: none !important; }
.pss__col--left {
background: var(--pss-left-bg);
display: flex;
}
.pss__content {
width: 100%;
padding: 60px 75px;
box-sizing: border-box;
color: var(--pss-text);
display: flex;
flex-direction: column;
}
.pss__title-link {
text-decoration: none;
color: inherit;
display: inline-block;
transition: opacity 0.3s ease;
}
.pss__title-link:hover {
opacity: 0.7;
}
.pss__title {
margin: 0 0 10px 0;
font-size: var(--pss-title-size);
font-weight: 700;
line-height: 1;
color: var(--pss-text);
letter-spacing: 0;
}
.pss__subtitle {
margin: 0;
padding: 6px 0;
font-size: var(--pss-subtitle-size);
font-weight: 500;
letter-spacing: 0;
color: var(--pss-text);
line-height: 100%;
}
.pss__hr {
height: 1px;
background: var(--pss-line);
width: var(--pss-sep-width);
margin: 0;
border: none;
opacity: 1;
}
.pss__meta {
display: flex;
align-items: center;
gap: 3px;
flex-wrap: wrap;
font-size: 18px;
font-weight: 500;
color: var(--pss-text);
letter-spacing: 0;
}
.pss__price--compare {
text-decoration: line-through;
opacity: 0.6;
font-weight: 500;
}
.pss__price--current { font-weight: 700; }
.pss__discount {
padding-left: 5px;
background: #000;
color: #fff;
font-size: 14px;
font-style: italic;
padding: 2px;
}
.pss__desc {
margin: 6px 0;
font-size: var(--pss-desc-size);
line-height: 1;
color: var(--pss-text);
display: -webkit-box;
-webkit-line-clamp: var(--pss-desc-lines);
-webkit-box-orient: vertical;
overflow: hidden;
letter-spacing: 0;
font-weight: 300;
}
.pss__actions {
margin-top: 5px;
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.pss__btn {
background: var(--pss-btn-bg);
color: var(--pss-btn-text);
border: 0;
padding: 6px 10px;
font-size: 18px;
font-weight: 500;
cursor: pointer;
text-transform: capitalize;
letter-spacing: 0.05em;
transition: opacity 0.3s ease, background 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.pss__btn:hover {
opacity: 0.85;
background: #22BD77 !important;
}
.pss__btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.pss__qty {
display: flex;
align-items: center;
gap: 12px;
border: 1px solid var(--pss-line);
padding: 5px;
}
.pss__qty-btn {
width: 35px;
height: 35px;
background: transparent;
color: var(--pss-text);
border: 0;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
transition: opacity 0.3s ease;
}
.pss__qty-btn:hover { opacity: 0.7; }
.pss__qty-val {
min-width: 30px;
text-align: center;
font-weight: 700;
font-size: 16px;
color: var(--pss-text);
}
.pss__status {
margin-top: 10px;
min-height: 18px;
font-size: 13px;
opacity: 0.8;
}
.pss__col--right {
background: #fff;
display: flex;
}
.pss__image-link {
width: 100%;
height: 100%;
display: block;
text-decoration: none;
}
.pss__image {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.pss__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: opacity 0.4s ease;
}
.pss__img--primary {
position: relative;
z-index: 2;
}
.pss__img--hover {
position: absolute;
top: 0;
left: 0;
z-index: 1;
opacity: 0;
}
.pss__image--has-hover:hover .pss__img--primary {
opacity: 0;
}
.pss__image--has-hover:hover .pss__img--hover {
opacity: 1;
}
.pss__img-placeholder {
width: 100%;
min-height: 450px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
.pss__view-all-wrapper {
display: flex;
margin-top: 20px;
padding: 5px 0;
}
.pss__btn--view-all {
padding: 6px 10px;
font-size: 18px;
}
@media (max-width: 900px) {
.pss__container { padding: 0 10px; }
.pss__row { flex-direction: column-reverse; }
.pss__content { padding: 40px 20px; }
.pss__hr { width: 100%; }
.pss__title { font-size: {{ section.settings.title_size | times: 0.8 }}px; }
.pss__btn--view-all { width: 100%; max-width: 300px; }
}
@media (max-width: 600px) {
.pss__content { padding: 30px 15px; }
.pss__title { font-size: {{ section.settings.title_size | times: 0.7 }}px; }
.pss__subtitle { font-size: {{ section.settings.subtitle_size | times: 0.9 }}px; }
}
</style>
<script>
(function(){
class ProductShowcaseSplit extends HTMLElement {
constructor(){
super();
this.cart = new Map(); // variantId -> qty
this.updateQueue = new Map(); // variantId -> timeoutId
}
connectedCallback(){
this.init();
}
async init(){
await this.loadCart();
this.addEventListener('click', this.handleClick.bind(this));
this.updateAllRows();
// Ensure badge is correct on load too
this.updateCartCount();
}
handleClick(e){
const addBtn = e.target.closest('[data-add]');
const decBtn = e.target.closest('[data-dec]');
const incBtn = e.target.closest('[data-inc]');
if(addBtn) {
e.preventDefault();
this.quickAdd(addBtn);
} else if(decBtn) {
e.preventDefault();
this.decrease(decBtn);
} else if(incBtn) {
e.preventDefault();
this.increase(incBtn);
}
}
async quickAdd(btn){
const row = btn.closest('[data-row]');
const vid = btn.dataset.variantId;
if(!vid || btn.disabled) return;
const current = this.cart.get(String(vid)) || 0;
const next = current + 1;
// optimistic UI
this.cart.set(String(vid), next);
this.updateRow(row, next);
btn.disabled = true;
const originalText = btn.textContent;
btn.textContent = 'Adding…';
try {
await this.addToCart(String(vid), 1);
// hard sync after server confirms
await this.loadCart();
this.updateAllRows();
this.updateCartCount();
} catch(err) {
// rollback UI
this.cart.set(String(vid), current);
this.updateRow(row, current);
this.showStatus(row, 'Failed to add');
console.error(err);
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
}
decrease(btn){
const row = btn.closest('[data-row]');
const vid = String(row.dataset.variant);
const current = this.cart.get(vid) || 1;
const next = Math.max(0, current - 1);
this.cart.set(vid, next);
this.updateRow(row, next);
this.debouncedUpdate(vid, next);
}
increase(btn){
const row = btn.closest('[data-row]');
const vid = String(row.dataset.variant);
const current = this.cart.get(vid) || 1;
const next = current + 1;
this.cart.set(vid, next);
this.updateRow(row, next);
this.debouncedUpdate(vid, next);
}
debouncedUpdate(vid, qty){
if(this.updateQueue.has(vid)){
clearTimeout(this.updateQueue.get(vid));
}
const timeout = setTimeout(async () => {
await this.updateCartQty(vid, qty);
// hard sync after update (handles cart rules / limits)
await this.loadCart();
this.updateAllRows();
this.updateCartCount();
this.updateQueue.delete(vid);
}, 450);
this.updateQueue.set(vid, timeout);
}
updateRow(row, qty){
const addBtn = row.querySelector('[data-add]');
const qtyWrap = row.querySelector('[data-qty]');
const val = row.querySelector('[data-val]');
if(!addBtn || !qtyWrap || !val) return;
if(qty > 0){
addBtn.hidden = true;
qtyWrap.hidden = false;
val.textContent = qty;
} else {
addBtn.hidden = false;
qtyWrap.hidden = true;
val.textContent = 1;
}
}
updateAllRows(){
this.querySelectorAll('[data-row]').forEach(row => {
const vid = String(row.dataset.variant);
const qty = this.cart.get(vid) || 0;
this.updateRow(row, qty);
});
}
showStatus(row, msg){
const el = row.querySelector('[data-status]');
if(el){
el.textContent = msg;
setTimeout(() => el.textContent = '', 3000);
}
}
async loadCart(){
try {
const res = await fetch('/cart.js', { credentials: 'same-origin' });
const data = await res.json();
this.cart.clear();
(data.items || []).forEach(item => {
this.cart.set(String(item.variant_id), Number(item.quantity || 0));
});
return data;
} catch(err) {
console.error('Cart load failed:', err);
return null;
}
}
async addToCart(vid, qty){
const res = await fetch('/cart/add.js', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id: vid, quantity: qty })
});
if(!res.ok) throw new Error('Add failed');
const data = await res.json();
this.dispatchCartEvent(data);
return data;
}
async updateCartQty(vid, qty){
try {
const res = await fetch('/cart/update.js', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ updates: { [vid]: qty } })
});
if(!res.ok) throw new Error('Update failed');
const data = await res.json();
this.dispatchCartEvent(data);
return data;
} catch(err) {
console.error('Cart update failed:', err);
return null;
}
}
// ✅ FIXED: never overwrite .cart-count-bubble wrapper HTML
async updateCartCount(){
try {
const res = await fetch('/cart.js', { credentials: 'same-origin' });
const cart = await res.json();
const count = Number(cart.item_count || 0);
// Dawn/Sense: update bubble spans safely
document.querySelectorAll('.cart-count-bubble').forEach((bubble) => {
const visible = bubble.querySelector('span[aria-hidden="true"]');
const srOnly = bubble.querySelector('span.visually-hidden');
if (visible) visible.textContent = String(count);
if (srOnly) srOnly.textContent = `${count} item${count === 1 ? '' : 's'}`;
if (count > 0) {
bubble.classList.remove('hidden');
bubble.style.display = '';
} else {
bubble.classList.add('hidden');
bubble.style.display = 'none';
}
});
// Extra compatibility (if theme uses custom badge)
document.querySelectorAll('[data-cart-count]').forEach((el) => {
el.textContent = String(count);
});
// Notify listeners (cart drawer etc.)
document.dispatchEvent(new CustomEvent('cart:refresh', {
detail: { cart },
bubbles: true
}));
} catch(err) {
console.error('Cart count update failed:', err);
}
}
dispatchCartEvent(data){
document.dispatchEvent(new CustomEvent('cart:updated', {
detail: data,
bubbles: true
}));
// Some themes expose theme.cart
try {
if(typeof theme !== 'undefined' && theme.cart && typeof theme.cart.getCart === 'function'){
theme.cart.getCart();
}
} catch(e) {}
}
}
if(!customElements.get('product-showcase-split')){
customElements.define('product-showcase-split', ProductShowcaseSplit);
}
})();
</script>
{% schema %}
{
"name": "Split Product Feature",
"settings": [
{ "type": "header", "content": "Layout" },
{ "type": "range", "id": "container_side_padding", "min": 0, "max": 20, "step": 1, "unit": "%", "label": "Side padding", "default": 10 },
{ "type": "range", "id": "row_gap", "min": 0, "max": 100, "step": 5, "unit": "px", "label": "Gap between rows", "default": 40 },
{ "type": "range", "id": "separator_width", "min": 50, "max": 100, "step": 5, "unit": "%", "label": "Separator width", "default": 80 },
{ "type": "range", "id": "desc_lines", "min": 3, "max": 15, "step": 1, "label": "Description line clamp", "default": 6 },
{ "type": "header", "content": "Typography" },
{ "type": "range", "id": "title_size", "min": 20, "max": 80, "step": 2, "unit": "px", "label": "Title font size", "default": 34 },
{ "type": "range", "id": "subtitle_size", "min": 12, "max": 40, "step": 1, "unit": "px", "label": "Subtitle font size", "default": 18 },
{ "type": "range", "id": "desc_size", "min": 12, "max": 30, "step": 1, "unit": "px", "label": "Description font size", "default": 15 },
{ "type": "header", "content": "Colors" },
{ "type": "color", "id": "left_bg", "label": "Left column background", "default": "#F9F9F9" },
{ "type": "color", "id": "text_color", "label": "Text color", "default": "#121212" },
{ "type": "color", "id": "line_color", "label": "Separator line color", "default": "#DDDDDD" },
{ "type": "color", "id": "button_bg", "label": "Button background", "default": "#121212" },
{ "type": "color", "id": "button_text", "label": "Button text", "default": "#FFFFFF" },
{ "type": "header", "content": "View All Products" },
{ "type": "checkbox", "id": "show_view_all", "label": "Show 'View All' button", "default": true },
{ "type": "text", "id": "view_all_text", "label": "Button text", "default": "View All Products" },
{ "type": "url", "id": "view_all_url", "label": "Button link", "info": "Link to your products collection or shop page" }
],
"blocks": [
{
"type": "row",
"name": "Product row",
"settings": [
{ "type": "product", "id": "product", "label": "Product" },
{ "type": "text", "id": "subtitle", "label": "Subtitle 1", "default": "Add a subtitle here" },
{ "type": "text", "id": "subtitle_2", "label": "Subtitle 2", "default": "Soothing · Refreshing · Mild" },
{ "type": "text", "id": "weight", "label": "Weight/Volume", "default": "100 ml" },
{ "type": "image_picker", "id": "image", "label": "Override image (optional)" },
{
"type": "textarea",
"id": "override_description",
"label": "Override description (optional)",
"info": "If empty, it will use the product description."
}
]
}
],
"presets": [
{
"name": "Split Product Feature",
"blocks": [{ "type": "row" }]
}
]
}
{% endschema %}```
Full section code
Hi @Goodness-Michael ,
Using Sense theme