1/* Alphadi â Cart sync helper. 2 * The cart lives in localStorage and can go stale: prices change, sales start, 3 * items get unpublished/sold out between the moment they were added and the 4 * moment the visitor opens the cart / checkout. This re-resolves every line 5 * against the server (authoritative price + availability) and patches the 6 * passed-in state object in place, so drawer, cart page and checkout all agree 7 * with what will actually be charged in SubmitOrder. It also reports what 8 * changed so the UI can show a notice ("X was removed", "price of Y changed"). 9 */ 10(function () { 11 'use strict'; 12 13 // "product-12__Hardcover" / "apt-5__p1699..." -> base id "product-12" / "apt-5" 14 function baseId(key) { 15 var raw = key || ''; 16 var us = raw.indexOf('__'); 17 return us >= 0 ? raw.slice(0, us) : raw; 18 } 19 20 // Keep requests on the current language route (matches the other /cart/* calls). 21 function langPrefix() { 22 var seg = (window.location.pathname.split('/').filter(Boolean)[0] || ''); 23 return /^[a-z]{2}$/i.test(seg) ? '/' + seg : ''; 24 } 25 26 function esc(s) { 27 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 28 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 29 }); 30 } 31 32 /** 33 * Mutates `state` in place: updates price/name/image, drops unavailable lines, 34 * and persists back to localStorage. Resolves to a result object: 35 * { changed: bool, removed: [name...], repriced: [{ name, price }...] } 36 */ 37 function applyTo(state) { 38 var result = { changed: false, removed: [], repriced: [] }; 39 if (!state || typeof state !== 'object') { return Promise.resolve(result); } 40 41 var keys = Object.keys(state).filter(function (k) { return state[k] && state[k].qty > 0; }); 42 if (!keys.length) { return Promise.resolve(result); } 43 44 var payload = keys.map(function (k) { 45 return { key: k, id: baseId(k), type: state[k].type || '' }; 46 }); 47 48 return fetch(langPrefix() + '/cart/refresh', { 49 method: 'POST', 50 headers: { 'Content-Type': 'application/json' }, 51 body: JSON.stringify(payload) 52 }) 53 .then(function (r) { return r.ok ? r.json() : null; }) 54 .then(function (data) { 55 if (!data || !data.items) { return result; } 56 57 var map = {}; 58 data.items.forEach(function (it) { map[it.key] = it; }); 59 60 keys.forEach(function (k) { 61 var fresh = map[k]; 62 var item = state[k]; 63 64 if (!fresh || !fresh.available) { 65 result.removed.push(item.name || k); 66 delete state[k]; 67 result.changed = true; 68 return; 69 } 70 71 var isBooking = item.type === 'training' || item.type === 'event'; 72 // Products with selectable options carry option surcharges baked into their 73 // stored price, so we can't safely overwrite it from the base price â the 74 // server flags those via `hasOptions`. Simple products and bookings are safe. 75 if (!fresh.hasOptions && typeof fresh.price === 'number' && fresh.price !== item.price) { 76 result.repriced.push({ name: fresh.name || item.name || k, price: fresh.price }); 77 item.price = fresh.price; 78 result.changed = true; 79 } 80 if (fresh.name && fresh.name !== item.name) { item.name = fresh.name; result.changed = true; } 81 if (!isBooking && typeof fresh.image === 'string' && fresh.image !== item.image) { 82 item.image = fresh.image; result.changed = true; 83 } 84 }); 85 86 if (result.changed) { 87 localStorage.setItem('alphadi-cart', JSON.stringify(state)); 88 } 89 return result; 90 }) 91 .catch(function () { return result; }); 92 } 93 94 /** 95 * Renders (or clears) a dismissible notice inside `container` describing what 96 * the last sync changed. Message templates are read from the container's 97 * data-msg-* attributes (translated server-side) with English fallbacks. 98 */ 99 function showNotice(result, container) { 100 if (!container) { return; } 101 var removed = (result && result.removed) || []; 102 var repriced = (result && result.repriced) || []; 103 104 if (!removed.length && !repriced.length) { 105 container.style.display = 'none'; 106 container.innerHTML = ''; 107 return; 108 } 109 110 var d = container.dataset || {}; 111 var tTitle = d.msgTitle || 'Your cart was updated'; 112 var tRemoved = d.msgRemoved || '{name} is no longer available and was removed.'; 113 var tRepriced = d.msgRepriced || 'Price of {name} changed to {price}.'; 114 115 var lines = []; 116 removed.forEach(function (name) { 117 lines.push('<li>' + tRemoved.replace('{name}', '<strong>' + esc(name) + '</strong>') + '</li>'); 118 });
119 repriced.forEach(function (it) { 120 lines.push('<li>' + tRepriced 121 .replace('{name}', '<strong>' + esc(it.name) + '</strong>') 122 .replace('{price}', (window.formatPrice ? window.formatPrice(it.price) : 'â¬' + (Number(it.price) || 0).toFixed(2))) + '</li>'); 123 }); 124 125 container.innerHTML = 126 '<div style="background:#fff8e6;border:1px solid #f0d38a;border-radius:8px;padding:.7rem .9rem;position:relative;font-family:var(--f-ui,sans-serif);">' + 127 '<div style="font-weight:700;font-size:.82rem;color:#8a6d1a;margin-bottom:.3rem;padding-right:1.2rem;">' + esc(tTitle) + '</div>' + 128 '<ul style="margin:0;padding-left:1.1rem;font-size:.78rem;color:#6b5514;line-height:1.5;">' + lines.join('') + '</ul>' + 129 '<button type="button" class="cart-notice__close" aria-label="close" style="position:absolute;top:.35rem;right:.5rem;background:none;border:none;font-size:1.15rem;line-height:1;color:#8a6d1a;cursor:pointer;">×</button>' + 130 '</div>'; 131 container.style.display = ''; 132 133 var closeBtn = container.querySelector('.cart-notice__close'); 134 if (closeBtn) { 135 closeBtn.addEventListener('click', function () { 136 container.style.display = 'none'; 137 container.innerHTML = ''; 138 }); 139 } 140 } 141 142 window.AlphadiCartSync = { applyTo: applyTo, showNotice: showNotice }; 143})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.