3DRap Elastomers Configurator
/* Limit height:100% to actual icons, not the cart badge */ .header__icon .icon, .header__icon svg, .header__icon .icon > * { height: 100%; } .header__icon span:not(.cart-count-bub ble):not([data-cart-count]):not([data-cart-count-bubble]) { height: 100%; } /* Enforce a perfect circle and use the CSS variables for size/position */ .header__icon .cart-count-bubble, .header__icon [data-cart-count], .header__icon [data-cart-count-bubble]{ position: absolute; width: var(--cart-badge-size) !important; height: var(--cart-badge-size) !important; aspect-ratio: 1 / 1; border-radius: 50%; top: var(--cart-badge-top) !important; right: var(--cart-badge-right) !important; left: auto !important; bottom: auto !important; align-self: auto; display: flex !important; align-items: center !important; justify-content: center !important; } /* Inner number shouldn't stretch vertically */ .header__icon .cart-count-bubble > span[aria-hidden="true"]{ height: auto !important; line-height: 1; } Configuratore Elastomero Dinamico body { margin:0; font-family:sans-serif; background:#1d1d1b; color:#fff; } .container { position:relative; max-width:720px; margin:auto; aspect-ratio:9/16; } .background { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; pointer-events:none; z-index:0; } .overlay { position:absolute; inset:0; z-index:1; } .select-container { position:absolute; top:69%; left:19.7%; display:flex; gap:4px; transform:translateY(-50%); } .elastomer-slot { font-size:16px; font-weight:700; background:#fff; width:68px; text-align:center; padding:.3em; border-radius:5px; } .elastomer-control { font-size:18px; padding:10px 18px; border-radius:6px; font-weight:700; background:#fff; color:#000; border:none; cursor:pointer; } input[type="number"] { font-size:18px; padding:.3em; width:65px; background:#fff; color:#000; border:1px solid #ccc; border-radius:5px; } .input-group { position:absolute; display:flex; flex-direction:column; gap:5px; font-size:18px; align-items:center; text-align:center; } .input-group label { margin-bottom:2px; } #input-L1 { top:63%; left:83.5%; width:100px; } #input-L2 { top:70%; left:72.5%; width:100px; } .graph { position:absolute; left:7%; top:4%; width:82%; height:44%; } .graph canvas { width:100% !important; height:100% !important; } .button-group { position:absolute; bottom:11%; left:50%; transform:translateX(-50%); display:flex; gap:15px; } .btn-reset,.btn-add,.btn-cart,.btn-download,.btn-choose,.btn-delete { border:none; border-radius:8px; font-weight:700; cursor:pointer; } .btn-reset { background:#fff; color:#000; padding:12px; font-size:18px; width:210px; } .btn-add { background:#d9ff00; color:#000; padding:12px; font-size:18px; width:210px; } .btn-cart { background:#d9ff00; color:#000; padding:6px; font-size:18px; width:210px; } .btn-download { background:#d9ff00; color:#000; padding:12px; font-size:18px; width:180px; } .btn-choose { background:#d9ff00; color:#000; padding:6px; font-size:18px; width:100px; } .btn-delete { background:#fff; color:#000; padding:6px 12px; font-size:18px; } .saved-configs { position:absolute; top:89.5%; left:45%; transform:translateX(-65%); display:grid; grid-template-columns:1fr 1fr; gap:1px 34px; max-width:400px; z-index:10; } #elastomerModal button,#elastomerModal input,#elastomerModal select,#elastomerModal textarea { font-size:18px; border-radius:5px; padding:12px; } #elastomerModal { position:absolute; inset:0; background:rgba(0,0,0,.7); z-index:10; display:none; } #elastomerModal>div { position:absolute; top:75%; left:50%; transform:translate(-50%,-50%); width:300px; min-height:220px; background:#2a2a28; padding:1em; border-radius:10px; color:#fff; font-size:18px; display:flex; flex-direction:column; justify-content:center; align-items:center; gap:12px; } #holeFilter { font-size:18px; padding:.3em; background:#fff; color:#000; border:1px solid #ccc; border-radius:5px; margin-top:8px; } .tooltip { position:relative; display:inline-block; cursor:help; } .tooltip .tooltiptext { visibility:hidden; width:280px; background:#2e2e2c; color:#fff; text-align:justify; padding:8px; border-radius:6px; position:absolute; z-index:100; bottom:125%; left:50%; transform:translateX(-50%); opacity:0; transition:opacity .3s; font-size:18px; line-height:1.3; } .tooltip:hover .tooltiptext { visibility:visible; opacity:1; } .tooltip.vertical-icon { display:inline-flex; flex-direction:column; align-items:center; text-align:center; gap:2px; } .tooltip.vertical-icon .info-icon { font-size:20px; color:#d9ff00; font-weight:bold; line-height:1; } @media (max-width:1024px){ body { width:1024px; transform:scale(1.3); transform-origin:top left; overflow-x:auto; } html { overflow-x:auto; } } /* === Cart badge finale 3DRAP (centrato, senza 9+) === */ :root{ --cart-badge-bg:#d9ff00; --cart-badge-size:22px; --cart-badge-font:13px; --cart-badge-top:-10px; --cart-badge-right:-2px; } [data-cart-toggle],[data-cart-drawer-toggle],.header__icon--cart,.header__icon,.header__icon .icon,a[href*="/cart"]{ position:relative; } .cart-count-bubble,.cart-count,[data-cart-count],[data-cart-count-bubble]{ position:absolute; top:var(--cart-badge-top); right:var(--cart-badge-right); width:var(--cart-badge-size); height:var(--cart-badge-size); min-width:var(--cart-badge-size); min-height:var(--cart-badge-size); padding:0 !important; border:0; border-radius:50%; box-sizing:border-box; display:flex !important; align-items:center !important; justify-content:center !important; background:var(--cart-badge-bg); color:#000; font-weight:700; font-size:var(--cart-badge-font); line-height:1; text-align:center; z-index:25; overflow:hidden; } .cart-count-bubble > span[aria-hidden="true"], .cart-count > span[aria-hidden="true"], [data-cart-count] > span[aria-hidden="true"]{ display:flex; align-items:center; justify-content:center; height:100%; transform:translateY(0px); } .cart-count-bubble .visually-hidden,.cart-count .visually-hidden,[data-cart-count] .visually-hidden{ position:absolute !important; width:1px; height:1px; margin:-1px; padding:0; border:0; clip:rect(0 0 0 0); overflow:hidden; } @media (max-width:990px){ :root{ --cart-badge-size:22px; --cart-badge-font:13px; --cart-badge-top:-10px; --cart-badge-right:-2px; } } (function ensureCartRefresh(){ function getOrCreateBubbles(){ const bubbles = Array.from(document.querySelectorAll('.cart-count-bubble, .cart-count, [data-cart-count], [data-cart-count-bubble]')); if (bubbles.length) return bubbles; const icon = document.querySelector('[data-cart-toggle], [data-cart-drawer-toggle], a[href*="/cart"], .header__icon--cart') || document.querySelector('cart-drawer-toggler') || document.querySelector('cart-drawer button, cart-drawer-toggle'); if (!icon) return []; const bubble = document.createElement('span'); bubble.className = 'cart-count-bubble'; bubble.innerHTML = '00 items'; bubble.style.display = 'none'; icon.appendChild(bubble); return [bubble]; } function applyCountToBubble(bubble, count){ const hidden = bubble.querySelector('span[aria-hidden="true"]') || bubble.querySelector('[data-count]'); const vis = bubble.querySelector('.visually-hidden') || bubble.querySelector('[data-a11y]'); const display = String(count); if (hidden) hidden.textContent = display; if (vis) vis.textContent = count === 1 ? '1 item' : `${count} items`; bubble.style.display = count > 0 ? '' : 'none'; bubble.classList?.remove('hidden'); bubble.setAttribute?.('data-cart-count', String(count)); } window.updateThemeCartBubble = function(count){ const bubbles = getOrCreateBubbles(); bubbles.forEach(b => applyCountToBubble(b, count)); }; window.refreshCartUI = async function(openAfter = false){ try { const cart = await fetch('/cart.js', { credentials:'same-origin' }).then(r => r.json()); const count = cart?.item_count ?? 0; window.updateThemeCartBubble(count); document.body.dispatchEvent(new CustomEvent('cart:updated', { detail: cart })); if (openAfter) { const drawer = document.querySelector('cart-drawer'); if (drawer && typeof drawer.open === 'function') drawer.open(); else { const t = document.querySelector('[data-cart-drawer-toggle],[data-cart-toggle]'); if (t) t.click(); } } } catch(e){ console.warn('refreshCartUI failed', e); } }; if (!window.__fetchPatchedForCart__) { window.__fetchPatchedForCart__ = true; const _fetch = window.fetch; window.fetch = async function(...args){ const res = await _fetch.apply(this, args); try { const input = args[0]; const url = typeof input === 'string' ? input : (input && input.url) || ''; if (/\/cart\/(add|change|update|clear)\.js(\?|$)/.test(url)) { setTimeout(() => window.refreshCartUI(false), 0); } } catch {} return res; }; } if (!window.__xhrPatchedForCart__) { window.__xhrPatchedForCart__ = true; const _open = XMLHttpRequest.prototype.open; const _send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url){ this.__isCartReq = typeof url === 'string' && /\/cart\/(add|change|update|clear)\.js(\?|$)/.test(url); return _open.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(){ if (this.__isCartReq) this.addEventListener('load', () => setTimeout(() => window.refreshCartUI(false), 0)); return _send.apply(this, arguments); }; } (function observeCartNotification(){ const parseCount = (root) => { const btn = root?.querySelector?.('#cart-notification-button, .cart-notification__link, [data-cart-notification-button]'); if (!btn) return null; const m = (btn.textContent || '').match(/\((\d+)\)/); return m ? parseInt(m[1], 10) : null; }; const attach = (node) => { const mo = new MutationObserver(() => { const n = parseCount(node); if (Number.isFinite(n)) window.updateThemeCartBubble(n); }); mo.observe(node, { childList:true, subtree:true, characterData:true, attributes:true }); const first = parseCount(node); if (Number.isFinite(first)) window.updateThemeCartBubble(first); }; const tryAttach = () => { document.querySelectorAll('#cart-notification, .cart-notification, [data-cart-notification]').forEach(n => { if (!n.__cn_observed__) { n.__cn_observed__ = true; attach(n); } }); }; tryAttach(); const moDoc = new MutationObserver(tryAttach); moDoc.observe(document.documentElement, { childList:true, subtree:true }); })(); // click generici "Add to cart" -> sync bolla document.addEventListener('click', (ev) => { const btn = ev.target.closest('[name="add"], button[type="submit"], [data-add-to-cart], .product-form__submit'); if (btn) setTimeout(() => window.refreshCartUI(false), 700); }, true); document.addEventListener('DOMContentLoaded', () => window.refreshCartUI(false)); })(); ℹ️L1 [mm] Please, define the distance between the pedal pivot point and the center of the pad ℹ️L2 [mm] Please, define the distance from the pedal pivot to the elastomer axle pin ℹ️ Total length [mm]: 0 Pay attention to the available length between the elastomer plates of your setup ⚠️ ℹ️Hole Diameter Please, select the diameter of the elastomers internal hole according to the axis diameter where they will be placed 6 mm 8 mm 10 mm Reset Add Configuration Save the current configuration for later 🛒 Add Cart Elastomer Selection Category: Linear Progressive Spacer Stiffness: Soft Medium Hard Number of Spacers (max 4): Confirm Delete /* ===== Dati elastomeri ===== */ const elastomerData = { El1:{SKU:"ACC027_01AF",Length:24,Type:"Progressive",Stiffness:"Soft",Mask:"P-S",ImageURL:"https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/Configuratore_Elastomeri_ACC027.jpg?v=1750670653",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,1.24,1.7,2.28,3.02,3.98,4.99,5.82,6.45,6.95,7.36,7.72,8.03,8.32,8.57,8.8,9.02,9.22,9.41,9.59,9.76,9.92,10.08,10.22,10.37,10.5,10.63,10.76,10.88,11,11.12,11.23,11.34,11.44,11.55,11.65,11.75,11.84,11.94,12.03,12.12,12.21,12.29,12.38,12.46,12.54,12.62,12.7,12.78,12.85,12.93]}, El2:{SKU:"ACC027_02AF",Length:24,Type:"Progressive",Stiffness:"Medium",Mask:"P-M",ImageURL:"https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/Configuratore_Elastomeri_ACC027.jpg?v=1750670653",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,1.09,1.43,1.81,2.23,2.7,3.23,3.8,4.37,4.93,5.44,5.9,6.31,6.68,7.01,7.3,7.58,7.83,8.07,8.29,8.49,8.69,8.87,9.04,9.21,9.37,9.52,9.67,9.81,9.95,10.08,10.21,10.33,10.45,10.56,10.68,10.79,10.89,11,11.1,11.2,11.3,11.39,11.49,11.58,11.67,11.75,11.84,11.92,12.01,12.09]}, El3:{SKU:"ACC027_03AF",Length:24,Type:"Progressive",Stiffness:"Hard",Mask:"P-H",ImageURL:"https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/Configuratore_Elastomeri_ACC027.jpg?v=1750670653",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,1.28,1.6,1.94,2.31,2.71,3.13,3.58,4.03,4.47,4.9,5.3,5.68,6.02,6.34,6.64,6.91,7.17,7.41,7.64,7.85,8.05,8.24,8.42,8.59,8.76,8.92,9.07,9.22,9.36,9.5,9.63,9.76,9.88,10,10.12,10.23,10.34,10.45,10.56,10.66,10.76,10.86,10.95,11.05,11.14,11.23,11.32,11.41,11.49,11.58]}, El4:{SKU:"ACC028_01AF",Length:24,Type:"Linear",Stiffness:"Soft",Mask:"L-S",ImageURL:"https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/Configuratore_Elastomeri_ACC028.jpg?v=1750670667",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,0.72,1.1,1.54,2.06,2.68,3.43,4.25,5.04,5.7,6.25,6.72,7.11,7.46,7.77,8.05,8.31,8.55,8.77,8.97,9.17,9.35,9.52,9.69,9.85,10,10.14,10.28,10.42,10.55,10.68,10.8,10.92,11.03,11.15,11.25,11.36,11.47,11.57,11.67,11.76,11.86,11.95,12.04,12.13,12.22,12.3,12.39,12.47,12.55,12.63]}, El5:{SKU:"ACC028_02AF",Length:24,Type:"Linear",Stiffness:"Medium",Mask:"L-M",ImageURL:"https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/Configuratore_Elastomeri_ACC028.jpg?v=1750670667",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,0.83,1.12,1.43,1.77,2.13,2.54,2.97,3.43,3.92,4.41,4.89,5.34,5.76,6.15,6.51,6.83,7.13,7.41,7.67,7.91,8.13,8.35,8.55,8.74,8.92,9.09,9.26,9.42,9.57,9.72,9.86,10,10.13,10.26,10.39,10.51,10.63,10.74,10.85,10.96,11.07,11.17,11.27,11.37,11.47,11.56,11.66,11.75,11.84,11.93]}, El6:{SKU:"ACC028_03AF",Length:24,Type:"Linear",Stiffness:"Hard",Mask:"L-H",ImageURL:"https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/Configuratore_Elastomeri_ACC028.jpg?v=1750670667",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,0.8,1.02,1.25,1.5,1.75,2.02,2.31,2.61,2.92,3.24,3.57,3.91,4.25,4.59,4.93,5.26,5.57,5.88,6.17,6.44,6.7,6.95,7.19,7.42,7.63,7.84,8.03,8.22,8.4,8.57,8.74,8.9,9.05,9.2,9.35,9.49,9.62,9.75,9.88,10.01,10.13,10.25,10.36,10.47,10.58,10.69,10.79,10.9,11,11.1]}, }; const accessoryMap = { 6:"ACC029_01AP", 8:"ACC029_02AP", 10:"ACC029_03AP" }; const spacerMap = { 6:"ACC030_01SP", 8:"ACC030_02SP", 10:"ACC030_03SP" }; let _productData = null; const PRODUCT_HANDLE_FALLBACK = 'elastomer-configurator-sim'; async function loadProductData() { if (_productData) return _productData; const handleFromUrl = (() => { try { const m = location.pathname.match(/\/products\/([^\/?#]+)/); return m ? m[1] : null; } catch { return null; } })(); const handle = handleFromUrl || PRODUCT_HANDLE_FALLBACK; const res = await fetch(`/products/${handle}.js`, { credentials:'same-origin' }); if (!res.ok) throw new Error('Impossibile caricare il JSON del prodotto'); _productData = await res.json(); return _productData; } function matchHoleLabel(optionValue, holeMm) { if (!optionValue) return false; const norm = String(optionValue).replace(/\s+/g,'').toLowerCase(); const candidates = [String(holeMm), `${holeMm}mm`, `${holeMm} mm`].map(s => s.replace(/\s+/g,'').toLowerCase()); return candidates.includes(norm); } function findVariantIdByOptions(product, type, stiffness, holeMm) { const variants = product.variants || []; let v = variants.find(v => v.option1 === type && v.option2 === stiffness && matchHoleLabel(v.option3, holeMm)); if (v) return v.id; v = variants.find(v => Array.isArray(v.options) && v.options[0] === type && v.options[1] === stiffness && matchHoleLabel(v.options[2], holeMm)); return v ? v.id : null; } function findElKeyBySKU(sku) { return Object.keys(elastomerData).find(k => elastomerData[k].SKU === sku) || null; } async function resolveElastomerVariantIdBySKU(sku, holeMm) { const pd = await loadProductData(); const elKey = findElKeyBySKU(sku); if (!elKey) return null; const e = elastomerData[elKey]; return findVariantIdByOptions(pd, e.Type, e.Stiffness, holeMm); } async function resolveHoleAdapterVariantId(holeMm) { const pd = await loadProductData(); return findVariantIdByOptions(pd, 'HoleAdapter', 'Soft', holeMm); } async function resolveSpacerVariantId(holeMm) { const pd = await loadProductData(); return findVariantIdByOptions(pd, 'Spacer', 'Soft', holeMm); } let chart; let elastomerCount = 1; let savedConfigs = []; let elastomerSelections = Array(4).fill(null); let activeModalIndex = 0; let selectedConfigs = []; function updateSelectors() { const container = document.getElementById("selectContainer"); container.innerHTML = ""; for (let i = 0; i < elastomerCount; i++) { const btn = document.createElement("button"); btn.className = "elastomer-slot"; if (elastomerSelections[i]) { const selection = elastomerSelections[i]; if (typeof selection === "object" && selection.spacer) { btn.textContent = `Spacer x${selection.count}`; } else { const elData = elastomerData[selection]; btn.textContent = elData?.Mask || selection; } } else { btn.className = "elastomer-slot tooltip"; btn.innerHTML = `SelectPlease select the elastomer to compute the force`; } btn.onclick = () => openModal(i); container.appendChild(btn); } const subBtn = document.createElement("button"); subBtn.className = "tooltip elastomer-control"; subBtn.innerHTML = '−Remove one elastomer'; subBtn.onclick = () => { if (elastomerCount > 1) { elastomerCount--; elastomerSelections[elastomerCount] = null; updateSelectors(); updateCurrent(); } }; const addBtn = document.createElement("button"); addBtn.className = "tooltip elastomer-control"; addBtn.innerHTML = '+Add another elastomer'; addBtn.onclick = () => { if (elastomerCount < 4) { elastomerCount++; updateSelectors(); updateCurrent(); } }; container.appendChild(subBtn); container.appendChild(addBtn); const bgImg = document.querySelector(".background"); if (bgImg) bgImg.src = `https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/ConfiguratoreElastomeri_${elastomerCount}El.png`; updateLtot(); } function openModal(index){ activeModalIndex = index; document.getElementById("elastomerModal").style.display = "block"; refreshModalOptions(); } function closeModal(){ document.getElementById("elastomerModal").style.display = "none"; } function refreshModalOptions() { const type = document.getElementById("modalType").value; const stiffnessWrapper = document.getElementById("stiffnessWrapper"); const spacerWrapper = document.getElementById("spacerCountWrapper"); if (type === "Spacer") { stiffnessWrapper.style.display = "none"; spacerWrapper.style.display = "block"; const previewImg = document.getElementById("elastomerPreview"); previewImg.src = "https://cdn.shopify.com/s/files/1/0899/4816/0342/t/6/assets/Configuratore_Elastomeri_ACC030.jpg?v=1750672880"; previewImg.style.display = "block"; window._modalSelectedElastomer = "SPACER"; } else { stiffnessWrapper.style.display = "block"; spacerWrapper.style.display = "none"; const stiffness = document.getElementById("modalStiffness").value; const filtered = Object.entries(elastomerData).filter(([, e]) => e.Type === type && e.Stiffness === stiffness); const previewImg = document.getElementById("elastomerPreview"); if (filtered.length > 0) { const selectedKey = filtered[0][0]; const imgUrl = elastomerData[selectedKey]?.ImageURL; if (imgUrl) { previewImg.src = imgUrl; previewImg.style.display = "block"; } else previewImg.style.display = "none"; window._modalSelectedElastomer = selectedKey; } else { previewImg.style.display = "none"; window._modalSelectedElastomer = null; } } } document.getElementById("modalType").addEventListener("change", refreshModalOptions); document.getElementById("modalStiffness").addEventListener("change", refreshModalOptions); function confirmElastomerSelection() { const type = document.getElementById("modalType").value; if (type === "Spacer") { let count = parseInt(document.getElementById("spacerCountInput").value); if (isNaN(count) || count < 1) count = 1; if (count > 4) { alert("⚠️ You can add up to 4 spacers maximum."); count = 4; document.getElementById("spacerCountInput").value = 4; } elastomerSelections[activeModalIndex] = { spacer:true, count }; } else { const selected = window._modalSelectedElastomer; if (!selected) { alert("Elastomer not found in database."); return; } elastomerSelections[activeModalIndex] = selected; } closeModal(); updateSelectors(); updateCurrent(); updateLtot(); } function generateSeriesData(selected) { const L1 = parseFloat(document.getElementById("L1").value); const L2 = parseFloat(document.getElementById("L2").value); const RL = L1 / L2; const Fmax = 100 / RL; const data = []; for (let F = 0; F = 4) { alert("Warning! A maximum of 4 elastomers can be placed."); return; } const data = generateSeriesData(selected); const id = Date.now(); const label = `Config_${savedConfigs.length + 1}`; const color = `hsl(${(savedConfigs.length * 60) % 360}, 70%, 50%)`; const skus = selected.map((key) => (typeof key === "object" && key.spacer) ? { spacer:true, count:key.count } : elastomerData[key].SKU); savedConfigs.push({ label, data, borderColor: color, id, skus }); elastomerCount = 1; elastomerSelections = Array(4).fill(null); updateSelectors(); updateCurrent(); updateLtot(); renderSavedConfigs(); } function resetConfigurations() { savedConfigs = []; selectedConfigs = []; elastomerCount = 1; elastomerSelections = Array(4).fill(null); updateSelectors(); updateCurrent(); updateLtot(); renderSavedConfigs(); updateCartAndDownloadButtons(); } function renderSavedConfigs() { const container = document.getElementById("savedConfigsList"); container.innerHTML = ""; savedConfigs.forEach((cfg) => { const div = document.createElement("div"); div.style.marginBottom = "10px"; const row = document.createElement("div"); row.style.display = "flex"; row.style.alignItems = "center"; row.style.justifyContent = "center"; row.style.gap = "10px"; const label = document.createElement("span"); label.textContent = `${cfg.label}`; label.style.fontWeight = "bold"; label.style.color = cfg.borderColor; label.className = "config-label"; const chooseBtn = document.createElement("button"); chooseBtn.innerHTML = "✅Choose"; chooseBtn.className = "btn-choose"; chooseBtn.style.backgroundColor = "#d9ff00"; chooseBtn.style.color = "black"; const isAlreadySelected = selectedConfigs.some((sel) => sel.id === cfg.id); if (isAlreadySelected) { chooseBtn.disabled = true; chooseBtn.style.opacity = "0.5"; chooseBtn.style.pointerEvents = "none"; chooseBtn.title = "Already selected"; } chooseBtn.onclick = () => { if (!isAlreadySelected) { selectedConfigs.push(cfg); chooseBtn.disabled = true; chooseBtn.style.opacity = "0.5"; chooseBtn.style.pointerEvents = "none"; chooseBtn.title = "Selected"; updateCartAndDownloadButtons(); } }; const delBtn = document.createElement("button"); delBtn.className = "btn-delete"; delBtn.innerHTML = "🗑️Delete"; delBtn.onclick = () => { savedConfigs = savedConfigs.filter((c) => c.id !== cfg.id); selectedConfigs = selectedConfigs.filter((c) => c.id !== cfg.id); updateCurrent(); renderSavedConfigs(); updateCartAndDownloadButtons(); }; row.appendChild(label); row.appendChild(chooseBtn); row.appendChild(delBtn); div.appendChild(row); container.appendChild(div); }); } /* ====== Aggiunta al carrello: QUEUE + BATCH + RETRY ====== */ let __cartQueue = Promise.resolve(); function enqueueCartRequest(task){ __cartQueue = __cartQueue.then(() => task()).catch(() => task()); return __cartQueue; } function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); } function parseRetryAfter(res){ const h = res.headers?.get?.('Retry-After'); const n = h ? parseFloat(h) : NaN; return Number.isFinite(n) ? Math.max(0, n*1000) : null; } async function addItemsToCartBatch(items, { retries=6, baseDelay=1200 } = {}){ const byId = {}; for (const it of items){ const key = String(it.id); if (!byId[key]) byId[key] = { id: it.id, quantity: 0 }; byId[key].quantity += Number(it.quantity || 1); } const payload = Object.values(byId); return enqueueCartRequest(async () => { let lastTxt = ''; for (let attempt=0; attempt ''); const shouldRetry = res.status === 429 || /too[_ ]?many[_ ]?requests|throttled/i.test(lastTxt); if (shouldRetry && attempt < retries){ const retryAfter = parseRetryAfter(res); const jitter = Math.floor(Math.random()*250); const delay = (retryAfter ?? (baseDelay * Math.pow(2, attempt))) + jitter; await sleep(delay); continue; } throw new Error(`Cart add failed: ${lastTxt || `HTTP ${res.status}`}`); } throw new Error('Cart add failed: throttled (too many attempts)'); }); } async function addItemToCart(variantId, quantity, opts){ return addItemsToCartBatch([{ id: variantId, quantity }], opts); } function drawChart(datasets) { const ctx = document.getElementById("chart").getContext("2d"); if (chart) chart.destroy(); if (!datasets || datasets.length === 0) { datasets = [{ label:"Please, add at least one elastomer", data:[], borderColor:"gray", borderWidth:1, pointRadius:0, pointHoverRadius:0 }]; } const L1 = parseFloat(document.getElementById("L1").value); const L2 = parseFloat(document.getElementById("L2").value); const RL = L1 / L2; const Fmax = 100 / RL; chart = new Chart(ctx, { type:"line", data:{ datasets }, options:{ responsive:true, maintainAspectRatio:false, plugins:{ legend:{ display:true, labels:{ color:"#fff", font:{ size:18 }}} }, scales:{ x:{ type:"linear", min:0, title:{ display:true, text:"Displacement", color:"#fff", font:{ size:18 }}, grid:{ color:"#3A3A38" }, ticks:{ color:"#252525" } }, y:{ min:0, max:Fmax+5, title:{ display:true, text:"Force", color:"#fff", font:{ size:18 }}, grid:{ color:"#3A3A38" }, ticks:{ color:"#252525" } }, } } }); } function downloadSummaryPNG() { const scale = 2; const canvas = document.createElement("canvas"); canvas.width = 1000 * scale; canvas.height = 1800 * scale; const ctx = canvas.getContext("2d"); ctx.scale(scale, scale); ctx.fillStyle = "#000"; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle = "#fff"; ctx.font = "32px sans-serif"; const title = "Elastomer Configurations Report"; const textWidth = ctx.measureText(title).width; ctx.fillText(title, (canvas.width/scale - textWidth)/2, 40); const chartCanvas = document.getElementById("chart"); ctx.drawImage(chartCanvas, 50, 80, 900, 900); const tableX = 40, tableY = 1100, rowHeight = 42, colWidth = 180, maxCols = 5; ctx.font = "20px sans-serif"; ctx.fillStyle = "#fff"; ctx.fillText("Configurations Table:", tableX, tableY - 10); const headers = ["Config","Elastomer 1","Elastomer 2","Elastomer 3","Elastomer 4"]; headers.forEach((text,i)=>{ const x = tableX + i*colWidth; ctx.fillStyle="#222"; ctx.fillRect(x,tableY,colWidth,rowHeight); ctx.strokeStyle="#666"; ctx.strokeRect(x,tableY,colWidth,rowHeight); ctx.fillStyle="#fff"; ctx.fillText(text, x+10, tableY+26); }); const sortedConfigs = [...selectedConfigs].sort((a,b)=>parseInt(a.label.replace("Config_","")) - parseInt(b.label.replace("Config_",""))); sortedConfigs.forEach((cfg,rowIdx)=>{ const y = tableY + (rowIdx+1)*rowHeight; const types = cfg.skus.map((skuOrObj)=>{ if (typeof skuOrObj === "object" && skuOrObj.spacer) return `Spacer x${skuOrObj.count}`; const key = Object.keys(elastomerData).find(k => elastomerData[k].SKU === skuOrObj); const e = elastomerData[key]; return e ? `${e.Type} (${e.Stiffness})` : "-"; }); for (let col=0; col{ const logoWidth=360, logoHeight=120, x = canvas.width/scale - logoWidth - 30, y = canvas.height/scale - logoHeight - 30; ctx.drawImage(logo,x,y,logoWidth,logoHeight); const link = document.createElement("a"); link.download = "Elastomer_Report.png"; link.href = canvas.toDataURL("image/png"); link.click(); }; logo.onerror = ()=> alert("⚠️ Logo non caricato. Verifica il link."); } function updateLtot() { const selected = elastomerSelections.slice(0, elastomerCount); const allDefined = selected.every(Boolean); const ltotValue = document.getElementById("ltotValue"); const warningIcon = document.getElementById("ltotWarningIcon"); const ltotDisplay = document.getElementById("ltotDisplay"); if (!allDefined) { ltotValue.textContent = ""; warningIcon.style.display="none"; ltotDisplay.style.color="white"; ltotDisplay.classList.remove("warned"); return; } const ltot = selected.reduce((sum, item)=>{ if (typeof item === "object" && item.spacer) return sum + item.count * 5; const el = elastomerData[item]; return sum + (el?.Length || 0); }, 0); ltotValue.textContent = ltot; warningIcon.style.display="none"; ltotDisplay.style.color="white"; ltotDisplay.classList.remove("warned"); } function updateCartAndDownloadButtons() { const enabled = selectedConfigs.length > 0; [{ selector: ".btn-cart", tooltip: "Please choose at least one configuration to add to cart." }, { selector: ".btn-download", tooltip: "Please choose at least one configuration to download summary." }] .forEach(({ selector, tooltip }) => { const btn = document.querySelector(selector); if (btn) { btn.disabled = !enabled; btn.style.opacity = enabled ? "1" : "0.5"; btn.style.pointerEvents = enabled ? "auto" : "none"; btn.title = enabled ? "" : tooltip; } }); } /* ====== bulkAddToCart: una sola chiamata /cart/add.js ====== */ let __bulkRunning = false; async function bulkAddToCart() { if (__bulkRunning) return; __bulkRunning = true; if (selectedConfigs.length === 0) { alert("Please choose at least one configuration."); __bulkRunning = false; return; } const selectedIds = new Set(selectedConfigs.map((cfg) => cfg.id)); savedConfigs = savedConfigs.filter((cfg) => selectedIds.has(cfg.id)); updateCurrent(); renderSavedConfigs(); const matrix = selectedConfigs.map((cfg) => { const count = {}; for (const sku of cfg.skus) { if (typeof sku === "object" && sku.spacer) continue; count[sku] = (count[sku] || 0) + 1; } return count; }); const finalSkuCounts = {}; for (const sku of new Set(matrix.flatMap((cfg) => Object.keys(cfg)))) { finalSkuCounts[sku] = Math.max(...matrix.map((cfg) => cfg[sku] || 0)); } const hole = parseInt(document.getElementById("holeFilter").value, 10); const totalElastomers = Object.values(finalSkuCounts).reduce((acc, qty) => acc + qty, 0); const accessorySku = accessoryMap[hole]; const needsHoleAdapterQty = totalElastomers * 2; const spacerPerPosition = [0, 0, 0, 0]; for (const cfg of selectedConfigs) { cfg.skus.forEach((skuOrObj, index) => { if (typeof skuOrObj === "object" && skuOrObj.spacer) { spacerPerPosition[index] = Math.max(spacerPerPosition[index], skuOrObj.count); } }); } const totalSpacers = spacerPerPosition.reduce((a,b)=>a+b,0); const spacerSku = spacerMap[hole]; const items = []; for (const [sku, qty] of Object.entries(finalSkuCounts)) { const variantId = await resolveElastomerVariantIdBySKU(sku, hole); if (!variantId) { alert(`❌ Nessuna variante trovata per ${sku} (${hole} mm).`); __bulkRunning = false; return; } items.push({ id: variantId, quantity: qty }); } if (accessorySku && needsHoleAdapterQty > 0) { const holeAdapterId = await resolveHoleAdapterVariantId(hole); if (!holeAdapterId) { alert("❌ Nessuna variante trovata per HoleAdapter."); __bulkRunning = false; return; } items.push({ id: holeAdapterId, quantity: needsHoleAdapterQty }); } if (spacerSku && totalSpacers > 0) { const spacerId = await resolveSpacerVariantId(hole); if (!spacerId) { alert("❌ Nessuna variante trovata per Spacer."); __bulkRunning = false; return; } items.push({ id: spacerId, quantity: totalSpacers }); } const cartBtn = document.querySelector('.btn-cart'); if (cartBtn) { cartBtn.disabled = true; cartBtn.style.opacity = '0.5'; cartBtn.textContent = '🛒 Adding…'; } try { await addItemsToCartBatch(items, { retries: 6, baseDelay: 1200 }); await window.refreshCartUI(true); setTimeout(() => { const confirmDownload = confirm("Would you download the choosed configuration on your device?"); if (confirmDownload) downloadSummaryPNG(); selectedConfigs = []; }, 100); } catch (err) { console.error(err); alert("❌ Error adding items to cart.\n" + (err?.message || "")); } finally { if (cartBtn) { cartBtn.disabled = false; cartBtn.style.opacity = '1'; cartBtn.textContent = '🛒 Add Cart'; } __bulkRunning = false; } } // Intercetta QUALSIASI submit di form /cart/add e usa la nostra API batch function onCartAddSubmit(e) { const form = e.target.closest('form[action="/cart/add"]'); if (!form) return; e.preventDefault(); e.stopPropagation(); try { const fd = new FormData(form); const id = fd.get('id'); const qty = parseInt(fd.get('quantity') || '1', 10) || 1; if (!id) { alert('Variant ID mancante.'); return; } addItemToCart(id, qty).then(() => window.refreshCartUI(true)).catch(err => { console.error(err); alert('❌ Error adding item to cart.'); }); } catch (err) { console.error(err); alert('❌ Error adding item to cart.'); } } document.addEventListener('submit', onCartAddSubmit, true); // Init UI updateSelectors(); updateCurrent(); updateCartAndDownloadButtons(); document.getElementById("L1").addEventListener("input", () => { const input = document.getElementById("L1"); let value = parseFloat(input.value); if (isNaN(value)) value = 1; if (value < 1) { value = 1; alert("⚠️ Minimum L1 is 1 mm."); } else if (value > 500) { value = 500; alert("⚠️ Maximum L2 is 500 mm."); } input.value = value; updateCurrent(); }); document.getElementById("L2").addEventListener("input", () => { const input = document.getElementById("L2"); let value = parseFloat(input.value); if (isNaN(value)) value = 1; if (value < 1) { value = 1; alert("⚠️ Minimum L2 is 1 mm."); } else if (value > 500) { value = 500; alert("⚠️ Maximum L2 is 500 mm."); } input.value = value; updateCurrent(); }); document.getElementById("holeFilter").addEventListener("change", updateSelectors); function getSelectedValueByName(name) { const sel = document.querySelector(`input[name="options[${name}]"]:checked`); if (sel && sel.value) return sel.value; const btn = Array.from(document.querySelectorAll(`[data-options] button, .product-form__input button`)) .find(b => (b.getAttribute('name') === `options[${name}]` || b.closest(`[data-option-name="${name}"]`)) && b.getAttribute('aria-pressed') === 'true'); if (btn) return (btn.value || btn.textContent || '').trim(); const badge = document.querySelector(`.product-form__input [data-option-name="${name}"] .is-active, .product-form__input .is-selected[data-option-name="${name}"]`); if (badge) return (badge.getAttribute('data-value') || badge.textContent || '').trim(); return null; } function getCurrentPdpSelection() { const type = getSelectedValueByName('Type') || getSelectedValueByName('type') || 'Linear'; const stiffness = getSelectedValueByName('Stiffness') || getSelectedValueByName('stiffness') || 'Soft'; const hole = getSelectedValueByName('HoleDiameter') || getSelectedValueByName('holediameter') || '8mm'; const holeMm = parseInt(String(hole).replace(/\D+/g, ''), 10) || 8; return { type, stiffness, holeMm }; } function getCurrentQty() { const qtyInput = document.querySelector('input[name="quantity"]'); const v = parseInt(qtyInput && qtyInput.value, 10); return Number.isFinite(v) && v > 0 ? v : 1; } async function interceptPdpAddToCart(e) { const btn = e.target.closest('button[type="submit"][name="add"]'); const form = btn && btn.closest('form[action="/cart/add"]'); if (!form) return; e.preventDefault(); e.stopPropagation(); try { const { type, stiffness, holeMm } = getCurrentPdpSelection(); const pd = await loadProductData(); const variantId = findVariantIdByOptions(pd, type, stiffness, holeMm); if (!variantId) { alert(`❌ Variante non trovata per ${type} / ${stiffness} / ${holeMm}mm`); return; } const qty = getCurrentQty(); await addItemToCart(variantId, qty); await window.refreshCartUI(true); } catch (err) { console.error(err); alert('❌ Error adding item to cart.\n' + (err?.message || '')); } } function enablePdpIntercept() { document.addEventListener('click', interceptPdpAddToCart, true); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', enablePdpIntercept); } else { enablePdpIntercept(); }