Icon rebuilt as a full-bleed 1200x1200 square (Shopify rounds corners itself). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
160 lines
6.9 KiB
JavaScript
160 lines
6.9 KiB
JavaScript
/* Offerlyst 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();
|
|
})();
|