metatroncubeswdev ba7a7c89a2 Rename app to Offerlyst: Bundles & BOGO; add App Store icon and brand assets
Icon rebuilt as a full-bleed 1200x1200 square (Shopify rounds corners itself).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:03:32 -04:00

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();
})();