Công thức — Giỏ hàng

Điều quan trọng nhất: giỏ hàng làm việc theo phiên bản

xStore.cart.addItem nhận variant_id, không phải product.id và cũng không phải product.slug. Một sản phẩm có nhiều phiên bản (màu, dung lượng…), và giỏ hàng phải biết chính xác khách mua phiên bản nào.

Trang chi tiết sản phẩm có sẵn biến variants để lấy giá trị này.

Thêm vào giỏ từ trang chi tiết sản phẩm

<select id="variant-select">
  {% for variant in variants %}
    <option value="{{ variant.id }}" {% unless variant.stock_quantity > 0 %}disabled{% endunless %}>
      {{ variant.title }} — {{ variant.price | money }}
    </option>
  {% endfor %}
</select>

<input type="number" id="quantity" value="1" min="1">
<button type="button" id="add-to-cart">Thêm vào giỏ</button>

<script>
  document.getElementById('add-to-cart').addEventListener('click', async (e) => {
    const btn       = e.currentTarget;
    const variantId = document.getElementById('variant-select').value;
    const quantity  = Number(document.getElementById('quantity').value) || 1;

    btn.disabled = true;
    try {
      await xStore.cart.addItem({ variant_id: variantId, quantity });
      alert('Đã thêm vào giỏ.');
    } catch (err) {
      if (err.status === 401) {
        window.location.href = '/dang-nhap?next=' + encodeURIComponent(location.pathname);
      } else {
        alert(err.message);
      }
    } finally {
      btn.disabled = false;
    }
  });
</script>

Khoá nút trong lúc chờ (btn.disabled) để khách bấm nhiều lần không tạo ra nhiều dòng hàng trùng.

Chọn phiên bản theo tuỳ chọn

Khi giao diện cho khách chọn từng tuỳ chọn riêng (màu, size) thay vì một danh sách phẳng, dùng xStore.product.findVariant để suy ra phiên bản:

<script>
  const variants = {{ variants | json }};

  function currentVariant() {
    const chosen = [...document.querySelectorAll('[data-option]')].map(el => el.value);
    return xStore.product.findVariant(variants, chosen);
  }

  document.getElementById('add-to-cart').addEventListener('click', async () => {
    const variant = currentVariant();
    if (!variant || !xStore.product.isAvailable(variant)) {
      alert('Phiên bản này tạm hết hàng.');
      return;
    }
    await xStore.cart.addItem({ variant_id: variant.id, quantity: 1 });
  });
</script>

Nút "thêm nhanh" trên thẻ sản phẩm

Danh sách sản phẩm không có sẵn variants, nên chỉ thêm nhanh được khi sản phẩm có đúng một phiên bản. Trường hợp còn lại, đưa khách sang trang chi tiết:

{% if product.url %}
  <a class="quick-add" href="{{ product.url }}">Xem & chọn</a>
{% endif %}

Cập nhật biểu tượng giỏ

Đừng tự cộng trừ số lượng — hãy nghe sự kiện. Giỏ còn thay đổi ở tab khác, hoặc sau khi gộp giỏ lúc đăng nhập:

<script>
  xStore.on('cart:updated', (cart) => {
    document.getElementById('cart-badge').textContent = cart.item_count;
  });
</script>

Sửa số lượng và xoá dòng

<script>
  // Đặt lại số lượng
  await xStore.cart.updateItem({ variant_id: variantId, quantity: 3 });

  // Xoá khỏi giỏ
  await xStore.cart.removeItem(variantId);
</script>

Trong cart.html, mỗi dòng hàng có sẵn item.variant_id:

{% for item in cart.items %}
  <button type="button" class="remove" data-variant="{{ item.variant_id }}">Xoá</button>
{% endfor %}

<script>
  document.querySelectorAll('.remove').forEach((btn) => {
    btn.addEventListener('click', async () => {
      await xStore.cart.removeItem(btn.dataset.variant);
      window.location.reload();
    });
  });
</script>

Áp mã giảm giá

<script>
  try {
    await xStore.cart.applyPromo('SALE10');
  } catch (e) {
    alert('Mã không hợp lệ hoặc đã hết hạn.');
  }

  // Gỡ mã đang áp
  await xStore.cart.applyPromo(null);
</script>

applyPromo trả về { promo_code } chứ không phải giỏ hàng. Muốn lấy tổng tiền mới, gọi lại xStore.cart.get() hoặc tải lại trang giỏ hàng.