(() => { "use strict"; if (!document.body.classList.contains("ex-u-3097861")) return; /* ----------------------------------------------------------------------- 1) Config ----------------------------------------------------------------------- */ const FREE_DELIVERY_THRESHOLD = 100; const PROGRESS_DURATION_MS = 5000; const CONGRATS_DURATION_MS = 10000; const STORAGE_KEY = "coFreeDeliveryReached"; const SVG_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"'; const ICONS = { progress: ``, congrats: ``, }; /* ----------------------------------------------------------------------- 2) Helpers ----------------------------------------------------------------------- */ const formatMoney = (amount) => { const isoCode = typeof window.$globalCurrencyISO !== "undefined" && window.$globalCurrencyISO ? window.$globalCurrencyISO : "GBP"; const symbol = typeof window.$globalCurrency !== "undefined" && window.$globalCurrency ? window.$globalCurrency : "£"; const value = Math.max(0, amount); try { return new Intl.NumberFormat(undefined, { style: "currency", currency: isoCode }).format(value); } catch (e) { return symbol + value.toFixed(2); } }; // Tries window.$mini_cart first, then falls back to common event.detail shapes. const getMinicartItems = (evt) => { if (Array.isArray(window.$mini_cart)) return window.$mini_cart; if (evt && evt.detail) { if (Array.isArray(evt.detail)) return evt.detail; if (Array.isArray(evt.detail.items)) return evt.detail.items; if (Array.isArray(evt.detail.minicart)) return evt.detail.minicart; if (Array.isArray(evt.detail.$mini_cart)) return evt.detail.$mini_cart; } return []; }; const getCartTotal = (items) => { const total = items.reduce((sum, item) => { if (!item) return sum; const lineTotal = typeof item.item_line_total === "number" ? item.item_line_total : parseFloat(item.item_line_total); return isNaN(lineTotal) ? sum : sum + lineTotal; }, 0); return Math.round(total * 100) / 100; }; const hasReachedThreshold = (total) => total >= FREE_DELIVERY_THRESHOLD; const safeSessionGet = (key) => { try { return sessionStorage.getItem(key); } catch (e) { return null; } }; const safeSessionSet = (key, value) => { try { sessionStorage.setItem(key, value); } catch (e) { /* private mode etc. */ } }; const wasReached = () => safeSessionGet(STORAGE_KEY) === "1"; const setReached = (reached) => safeSessionSet(STORAGE_KEY, reached ? "1" : "0"); // Headline is the primary (larger/bolder) line; sub is the optional secondary line. const buildMessage = (reached, total) => { if (reached) { return { type: "congrats", headline: "Congratulations! You've unlocked FREE delivery!", sub: "", }; } const remaining = FREE_DELIVERY_THRESHOLD - total; return { type: "progress", headline: `Spend another ${formatMoney(remaining)} to unlock FREE delivery`, sub: "", }; }; /* ----------------------------------------------------------------------- 3) Toast popup ----------------------------------------------------------------------- */ let popupEl = null; let popupTimer = null; const closePopup = () => { if (popupTimer) { clearTimeout(popupTimer); popupTimer = null; } if (!popupEl) return; const el = popupEl; popupEl = null; el.classList.remove("is-visible"); setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 300); }; const showPopup = (msg) => { closePopup(); const duration = msg.type === "congrats" ? CONGRATS_DURATION_MS : PROGRESS_DURATION_MS; const toast = document.createElement("div"); toast.className = `co-delivery-popup co-delivery-popup--${msg.type}`; toast.setAttribute("role", "status"); toast.setAttribute("aria-live", "polite"); toast.innerHTML = ` ${ICONS[msg.type]}

${msg.headline}

${msg.sub ? `

${msg.sub}

` : ""}
`; toast.querySelector(".co-delivery-popup__close").addEventListener("click", closePopup); document.body.appendChild(toast); popupEl = toast; // force reflow so the slide-in transition applies void toast.offsetWidth; toast.classList.add("is-visible"); popupTimer = setTimeout(closePopup, duration); }; /* ----------------------------------------------------------------------- 4) Cart update handler ----------------------------------------------------------------------- */ const handleCartUpdate = (evt) => { const total = getCartTotal(getMinicartItems(evt)); const reached = hasReachedThreshold(total); const previouslyReached = wasReached(); if (!reached) { // Empty cart -> nothing to nudge about if (total > 0) showPopup(buildMessage(false, total)); } else if (!previouslyReached) { // Just crossed the threshold -> congratulate once showPopup(buildMessage(true, total)); } // reached && previouslyReached -> stay silent setReached(reached); }; /* ----------------------------------------------------------------------- 5) Cart page info line (inside .cart-totals, right under the totals table) One element per .cart-totals — found and updated in place on every cart-total-update, so it can never be duplicated. ----------------------------------------------------------------------- */ const INFO_CLASS = "co-delivery-info"; const renderCartInfo = (evt) => { const total = getCartTotal(getMinicartItems(evt)); const reached = hasReachedThreshold(total); // Keep the popup state in sync so it doesn't re-congratulate on other pages setReached(reached); document.querySelectorAll(".cart-sidebar .cart-totals").forEach((totals) => { let info = totals.querySelector(`.${INFO_CLASS}`); if (total <= 0) { if (info) info.remove(); return; } const table = totals.querySelector("table"); if (!table) return; if (!info) { info = document.createElement("div"); info.setAttribute("role", "status"); table.insertAdjacentElement("afterend", info); } const msg = buildMessage(reached, total); const className = `${INFO_CLASS} ${INFO_CLASS}--${msg.type}`; const html = ` ${ICONS[msg.type]}

${msg.headline}

`; if (info.className !== className) info.className = className; if (info.innerHTML !== html) info.innerHTML = html; }); }; /* ----------------------------------------------------------------------- 6) Wire up ----------------------------------------------------------------------- */ if (document.body.classList.contains("ex-cart")) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => renderCartInfo()); } else { renderCartInfo(); } document.addEventListener("cart-total-update", renderCartInfo); } else { document.addEventListener("cart-total-update", handleCartUpdate); } document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePopup(); }); })();