/* Bundle Offers storefront script (app embed). Vanilla JS, no dependencies. * - converts shop-currency deal amounts when the customer shops in another currency * - bundle pages (/apps/bundles/...): quantity steppers, "N of M chosen", add all items in one request * - refreshes cart hints after AJAX cart changes */ (function () { 'use strict'; if (window.BundleOffers) return; var cfgEl = document.getElementById('bundle-offers-data'); var cfg = {}; try { cfg = cfgEl ? JSON.parse(cfgEl.textContent) : {}; } catch (e) { cfg = {}; } var strings = cfg.strings || {}; var cur = (window.Shopify && Shopify.currency) || {}; var rate = Number(cur.rate) || 1; var active = cur.active || cfg.currency; function fmt(amount) { try { return new Intl.NumberFormat(document.documentElement.lang || undefined, { style: 'currency', currency: active }).format(amount); } catch (e) { return amount.toFixed(2) + ' ' + active; } } /* ---- currency: $-values are stored in shop currency ---- */ function convertMoney(root) { if (rate === 1 && active === cfg.currency) return; root.querySelectorAll('[data-bo-money]').forEach(function (el) { el.textContent = fmt(Number(el.getAttribute('data-bo-money')) * rate); }); root.querySelectorAll('[data-bo-fixed]').forEach(function (el) { var regular = Number(el.getAttribute('data-regular')) / 100; // presentment already var dt = el.getAttribute('data-dt'); var dv = Number(el.getAttribute('data-dv')); var saving = dt === 'percent' ? regular * Math.min(dv, 100) / 100 : dt === 'amount' ? Math.min(dv * rate, regular) : Math.max(regular - dv * rate, 0); var b = el.querySelector('[data-bo-bundle]'); if (b) b.textContent = fmt(regular - saving); el.querySelectorAll('[data-bo-save]').forEach(function (s) { s.textContent = fmt(saving); }); }); } /* ---- cart helpers ---- */ var root = (window.Shopify && Shopify.routes && Shopify.routes.root) || '/'; function refreshHints() { var blocks = document.querySelectorAll('[data-bo-hints]'); if (!blocks.length) return; fetch(window.location.pathname + window.location.search, { credentials: 'same-origin', headers: { 'X-Requested-With': 'bundle-offers' } }) .then(function (r) { return r.text(); }) .then(function (html) { var doc = new DOMParser().parseFromString(html, 'text/html'); blocks.forEach(function (el) { var fresh = doc.querySelector('[data-bo-hints="' + el.getAttribute('data-bo-hints') + '"]'); if (fresh) { el.innerHTML = fresh.innerHTML; convertMoney(el); } }); }) .catch(function () {}); } // Themes update the cart with fetch; refresh hints after any cart mutation. var origFetch = window.fetch; if (origFetch) { window.fetch = function (input) { var url = typeof input === 'string' ? input : (input && input.url) || ''; var p = origFetch.apply(this, arguments); if (/\/cart\/(add|change|update|clear)/.test(url)) { p.then(function () { setTimeout(refreshHints, 150); }).catch(function () {}); } return p; }; } document.addEventListener('cart:updated', refreshHints); document.addEventListener('cart:refresh', refreshHints); function openDrawerOrCart() { var drawer = document.querySelector('cart-drawer'); if (cfg.after === 'drawer' && drawer && typeof drawer.renderContents === 'function') { // Dawn-family themes: re-render the drawer sections, then open it var ids = typeof drawer.getSectionsToRender === 'function' ? drawer.getSectionsToRender().map(function (s) { return s.id; }) : ['cart-drawer', 'cart-icon-bubble']; fetch(root + '?sections=' + ids.join(',')) .then(function (r) { return r.json(); }) .then(function (sections) { drawer.renderContents({ sections: sections }); }) .catch(function () { window.location.href = root + 'cart'; }); return; } document.dispatchEvent(new CustomEvent('cart:refresh', { bubbles: true })); window.location.href = root + 'cart'; } /* ---- bundle page form (rendered by the app proxy) ---- */ function setupBundleForm(form) { var need = Number(form.getAttribute('data-need')) || 0; var type = form.getAttribute('data-type'); var counter = form.querySelector('[data-bo-counter]'); var submit = form.querySelector('[type="submit"]'); var error = form.querySelector('[data-bo-error]'); var inputs = form.querySelectorAll('input[data-variant]'); function total() { var t = 0; inputs.forEach(function (i) { t += Math.max(0, Math.min(99, parseInt(i.value, 10) || 0)); }); return t; } function update() { var t = total(); if (counter) counter.textContent = counter.getAttribute('data-template').replace('{k}', t).replace('{n}', need); if (submit && type !== 'fixed') submit.disabled = type === 'mix' ? t < need : t < 1; } form.addEventListener('click', function (e) { var btn = e.target.closest('[data-step]'); if (!btn) return; var input = btn.parentNode.querySelector('input'); var v = (parseInt(input.value, 10) || 0) + Number(btn.getAttribute('data-step')); input.value = Math.max(0, Math.min(99, v)); update(); }); form.addEventListener('input', update); form.addEventListener('submit', function (e) { e.preventDefault(); var items = []; inputs.forEach(function (i) { var q = Math.max(0, Math.min(99, parseInt(i.value, 10) || 0)); if (q > 0) items.push({ id: Number(i.getAttribute('data-variant')), quantity: q }); }); if (type === 'mix' && total() < need) { showError(form.getAttribute('data-need-more')); return; } if (!items.length) { showError(form.getAttribute('data-nothing')); return; } var label = submit.textContent; submit.disabled = true; submit.textContent = strings.adding || '…'; origFetch(root + 'cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ items: items }), }) .then(function (r) { if (!r.ok) throw new Error(String(r.status)); return r.json(); }) .then(function () { submit.textContent = strings.added || label; openDrawerOrCart(); }) .catch(function () { submit.disabled = false; submit.textContent = label; showError(strings.error); }); }); function showError(msg) { if (!error) return; error.textContent = msg || ''; error.hidden = !msg; } update(); } function init() { convertMoney(document); document.querySelectorAll('form[data-bo-bundle-form]').forEach(setupBundleForm); } window.BundleOffers = { refreshHints: refreshHints, convertMoney: convertMoney }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();