diff --git a/.changeset/solid-lands-thank.md b/.changeset/solid-lands-thank.md new file mode 100644 index 000000000..1cbe2bb5c --- /dev/null +++ b/.changeset/solid-lands-thank.md @@ -0,0 +1,5 @@ +--- +"meraki": minor +--- + +implement bundles diff --git a/themes/meraki/assets/add-to-cart.js b/themes/meraki/assets/add-to-cart.js index b9a02bb2c..d77bf4f9a 100644 --- a/themes/meraki/assets/add-to-cart.js +++ b/themes/meraki/assets/add-to-cart.js @@ -1,3 +1,5 @@ +let LATEST_CART = null; + async function addToCart(snippetId) { const parentSection = document.querySelector(`#s-${snippetId}`); const variantId = parentSection.querySelector(`#variantId`)?.value || undefined; @@ -30,7 +32,6 @@ async function addToCart(snippetId) { if (response.error) throw new Error(response.error); - updateCartCount(response.count); await updateCartDrawer(); stopLoad('#loading__cart'); @@ -48,7 +49,7 @@ async function addToCart(snippetId) { return; } - notify(ADD_TO_CART_EXPECTED_ERRORS.product_added, 'success'); + notify(CART_DRAWER_TRANSLATION.product_added, 'success'); toggleCartDrawer(); } catch (err) { stopLoad('#loading__cart'); @@ -56,6 +57,45 @@ async function addToCart(snippetId) { } } +async function addBundleToCart(snippetId) { + const parentSection = document.querySelector(`#s-${snippetId}`); + const bundleId = parentSection.querySelector('[data-bundle] input[type="checkbox"]:checked')?.value; + const quantity = 1; + + if (!bundleId) { + return notify(ADD_TO_CART_EXPECTED_ERRORS.select_bundle, 'warning'); + } + + const cartItems = Array.isArray(LATEST_CART?.items) ? LATEST_CART.items : []; + const isBundleAlreadyInCart = cartItems.some( + (item) => item.extra_fields?.is_bundle_item && item.extra_fields?.bundle_id === bundleId, + ); + + if (isBundleAlreadyInCart) { + return notify(ADD_TO_CART_EXPECTED_ERRORS.bundle_already_added, 'warning'); + } + + try { + requestAnimationFrame(() => { + load('#loading__bundle-cart'); + }); + + const response = await youcanjs.cart.addItem({ bundleId, isBundle: true, quantity }); + + if (response.error) throw new Error(response.error); + + await updateCartDrawer(); + + stopLoad('#loading__bundle-cart'); + + notify(CART_DRAWER_TRANSLATION.bundle_added, 'success'); + toggleCartDrawer(); + } catch (err) { + stopLoad('#loading__bundle-cart'); + notify(err.message, 'error'); + } +} + async function attachRemoveItemListeners() { document.querySelectorAll('.remove-item-btn').forEach((btn) => btn.addEventListener('click', async (event) => { @@ -64,8 +104,6 @@ async function attachRemoveItemListeners() { if (cartItemId && productVariantId) { await removeCartItem(cartItemId, productVariantId); - await updateCartDrawer(); - updateCartCount(-1, true); } }), ); @@ -132,6 +170,8 @@ async function updateCartDrawer() { try { const cartData = await youcanjs.cart.fetch(); + LATEST_CART = cartData; + updateCartCount(cartData.count); document.querySelector('.cart-drawer__close').addEventListener('click', toggleCartDrawer); @@ -151,11 +191,30 @@ async function updateCartDrawer() { cartDrawerContent.innerHTML += headerContainer; + // Partition items into regular and bundle groups via extra_fields + const bundleMap = new Map(); + const regularItems = []; + const items = Array.isArray(cartData.items) ? cartData.items : []; + + items.forEach((item) => { + const extra = item.extra_fields; + if (extra?.is_bundle_item) { + if (!bundleMap.has(extra.bundle_id)) { + bundleMap.set(extra.bundle_id, { id: extra.bundle_id, title: extra.bundle_title, items: [] }); + } + bundleMap.get(extra.bundle_id).items.push(item); + } else { + regularItems.push(item); + } + }); + + const cartBundles = [...bundleMap.values()]; + // Check if the cart has items - if (cartData.count > 0) { + if (regularItems.length > 0 || cartBundles.length > 0) { const products = document.createElement('ul'); - for (const item of cartData.items) { + for (const item of regularItems) { item.price = formatCurrency(item.price, CURRENCY_CODE, CUSTOMER_LOCALE); item.productVariant.price = formatCurrency(item.productVariant.price, CURRENCY_CODE, CUSTOMER_LOCALE); @@ -168,8 +227,13 @@ async function updateCartDrawer() { cartDrawerContent.appendChild(products); + cartBundles.forEach((bundle) => { + cartDrawerContent.appendChild(createBundleGroup(bundle)); + }); + // Attach event listeners to the newly added remove buttons attachRemoveItemListeners(); + attachBundleRemoveListeners(); } else { const p = document.createElement('p'); p.classList.add('empty-cart'); @@ -294,10 +358,9 @@ async function directAddToCart(event, productId) { if (response.error) throw new Error(response.error); - updateCartCount(response.count); await updateCartDrawer(); - notify(ADD_TO_CART_EXPECTED_ERRORS.product_added, 'success'); + notify(CART_DRAWER_TRANSLATION.product_added, 'success'); toggleCartDrawer(); } catch (err) { notify(err.message, 'error'); @@ -305,3 +368,101 @@ async function directAddToCart(event, productId) { stopLoad('#loading__cart'); } } + +function createBundleGroup(bundle) { + const groupTemplate = document.getElementById('cart-drawer-bundle-group'); + const itemTemplate = document.getElementById('cart-drawer-bundle-item'); + const group = groupTemplate.content.cloneNode(true); + + group.querySelector('[ui-slot="title"]').textContent = bundle.title; + + const total = bundle.items.reduce((sum, i) => sum + ((i.extra_fields?.bundle_product_price ?? i.price) * i.quantity), 0); + group.querySelector('[ui-slot="total"]').textContent = formatCurrency(total, CURRENCY_CODE, CUSTOMER_LOCALE); + + const removeBtn = group.querySelector('[ui-slot="remove"]'); + removeBtn.setAttribute('data-bundle-item-ids', bundle.items.map((i) => i.id).join(',')); + removeBtn.setAttribute('data-bundle-variant-ids', bundle.items.map((i) => i.productVariant?.id ?? '').join(',')); + + const list = group.querySelector('[ui-slot="items"]'); + bundle.items.forEach((item) => list.appendChild(createBundleItem(itemTemplate, item))); + + return group; +} + +function createBundleItem(itemTemplate, item) { + const el = itemTemplate.content.cloneNode(true); + + const image = el.querySelector('[ui-slot="image"]'); + image.src = item.productVariant?.image?.url ?? item.productVariant?.product?.thumbnail ?? defaultImage; + image.alt = item.productVariant?.product?.name ?? ''; + + const name = el.querySelector('[ui-slot="name"]'); + name.textContent = item.productVariant?.product?.name ?? ''; + if (item.productVariant?.product?.url) name.href = item.productVariant.product.url; + + const variantsEl = el.querySelector('[ui-slot="variants"]'); + const variations = item.productVariant?.variations || {}; + const entries = Object.entries(variations).filter(([key]) => key !== 'default'); + if (entries.length) { + variantsEl.innerHTML = entries.map(([key, value]) => `${key}: ${value}`).join(''); + variantsEl.removeAttribute('hidden'); + } else { + variantsEl.setAttribute('hidden', ''); + } + + const unitPrice = item.extra_fields?.bundle_product_price ?? item.price; + const subtotal = unitPrice * item.quantity; + const freeEl = el.querySelector('[ui-slot="free"]'); + const priceEl = el.querySelector('[ui-slot="price"]'); + + if (subtotal === 0) { + freeEl.removeAttribute('hidden'); + priceEl.setAttribute('hidden', ''); + } else { + priceEl.textContent = formatCurrency(subtotal, CURRENCY_CODE, CUSTOMER_LOCALE); + priceEl.removeAttribute('hidden'); + freeEl.setAttribute('hidden', ''); + } + + const compareEl = el.querySelector('[ui-slot="compare"]'); + const compareAtPrice = item.productVariant?.compare_at_price; + + if (compareAtPrice) { + compareEl.textContent = formatCurrency(compareAtPrice * item.quantity, CURRENCY_CODE, CUSTOMER_LOCALE); + compareEl.removeAttribute('hidden'); + } else { + compareEl.setAttribute('hidden', ''); + } + + return el; +} + +function attachBundleRemoveListeners() { + document.querySelectorAll('.remove-bundle-btn').forEach((btn) => { + btn.addEventListener('click', async () => { + const itemIds = (btn.getAttribute('data-bundle-item-ids') || '').split(',').map((s) => s.trim()).filter(Boolean); + const variantIds = (btn.getAttribute('data-bundle-variant-ids') || '').split(',').map((s) => s.trim()).filter(Boolean); + + if (!itemIds.length) return; + + const spinner = btn.querySelector('.spinner'); + const removeIcon = btn.querySelector('.remove-icon'); + btn.disabled = true; + spinner?.classList.remove('hidden'); + removeIcon?.classList.add('hidden'); + + try { + for (let i = 0; i < itemIds.length; i++) { + await youcanjs.cart.removeItem({ cartItemId: itemIds[i], productVariantId: variantIds[i] }); + } + + await updateCartDrawer(); + } catch (error) { + btn.disabled = false; + spinner?.classList.add('hidden'); + removeIcon?.classList.remove('hidden'); + notify(error.message, 'error'); + } + }); + }); +} diff --git a/themes/meraki/assets/bundles.css b/themes/meraki/assets/bundles.css new file mode 100644 index 000000000..c6d7c64c5 --- /dev/null +++ b/themes/meraki/assets/bundles.css @@ -0,0 +1,417 @@ +[data-bundle=single], +[data-bundle=multi], +[data-bundle=buyxgety]{ + display:flex; + flex-direction:column; + border:1px solid #e5e7eb; + cursor:pointer; + margin-bottom:12px; +} +[data-bundle=single] .head, +[data-bundle=multi] .head, +[data-bundle=buyxgety] .head{ + display:flex; + padding:12px; + gap:12px; + border-bottom:1px solid #e5e7eb; +} +[data-bundle=single] .head .detail, +[data-bundle=multi] .head .detail, +[data-bundle=buyxgety] .head .detail{ + display:grid; + flex:1; + grid-gap:8px; + gap:8px; +} +[data-bundle=single] .head .detail .box, +[data-bundle=multi] .head .detail .box, +[data-bundle=buyxgety] .head .detail .box{ + display:flex; + align-items:center; + justify-content:space-between; +} +[data-bundle=single] .head .detail .box .title, +[data-bundle=multi] .head .detail .box .title, +[data-bundle=buyxgety] .head .detail .box .title{ + font-weight:700; + font-size:18px; + line-height:120%; +} +[data-bundle=single] .head .detail .box .price, +[data-bundle=multi] .head .detail .box .price, +[data-bundle=buyxgety] .head .detail .box .price{ + display:flex; + align-items:center; + gap:4px; +} +[data-bundle=single] .head .detail .box .price .original, +[data-bundle=multi] .head .detail .box .price .original, +[data-bundle=buyxgety] .head .detail .box .price .original{ + font-weight:700; + color:var(--yc-primary-color); +} +[data-bundle=single] .head .detail .box .price .compare-at, +[data-bundle=multi] .head .detail .box .price .compare-at, +[data-bundle=buyxgety] .head .detail .box .price .compare-at{ + color:#8D8D8D; + font-size:13px; + font-weight:400; + line-height:120%; + text-decoration-line:line-through; +} +[data-bundle=single] .item, +[data-bundle=single] .bundle-card-product, +[data-bundle=multi] .item, +[data-bundle=multi] .bundle-card-product, +[data-bundle=buyxgety] .item, +[data-bundle=buyxgety] .bundle-card-product{ + display:flex; + justify-content:space-between; + gap:12px; +} +[data-bundle=single] .item .media, +[data-bundle=single] .bundle-card-product .media, +[data-bundle=multi] .item .media, +[data-bundle=multi] .bundle-card-product .media, +[data-bundle=buyxgety] .item .media, +[data-bundle=buyxgety] .bundle-card-product .media{ + width:65px; + height:65px; + flex-shrink:0; + overflow:hidden; + aspect-ratio:1/1; +} +[data-bundle=single] .item .media > img, +[data-bundle=single] .bundle-card-product .media > img, +[data-bundle=multi] .item .media > img, +[data-bundle=multi] .bundle-card-product .media > img, +[data-bundle=buyxgety] .item .media > img, +[data-bundle=buyxgety] .bundle-card-product .media > img{ + -o-object-fit:cover; + object-fit:cover; + width:100%; + height:100%; +} +[data-bundle=single] .item .info, +[data-bundle=single] .bundle-card-product .info, +[data-bundle=multi] .item .info, +[data-bundle=multi] .bundle-card-product .info, +[data-bundle=buyxgety] .item .info, +[data-bundle=buyxgety] .bundle-card-product .info{ + display:flex; + flex:1; + flex-direction:column; +} +[data-bundle=single] .item .info .name, +[data-bundle=single] .bundle-card-product .info .name, +[data-bundle=multi] .item .info .name, +[data-bundle=multi] .bundle-card-product .info .name, +[data-bundle=buyxgety] .item .info .name, +[data-bundle=buyxgety] .bundle-card-product .info .name{ + font-size:15px; + font-weight:700; + line-height:120%; +} +[data-bundle=single] .item .info .price, +[data-bundle=single] .bundle-card-product .info .price, +[data-bundle=multi] .item .info .price, +[data-bundle=multi] .bundle-card-product .info .price, +[data-bundle=buyxgety] .item .info .price, +[data-bundle=buyxgety] .bundle-card-product .info .price{ + display:flex; + gap:10px; + align-items:center; +} +[data-bundle=single] .item .info .price .original, +[data-bundle=single] .bundle-card-product .info .price .original, +[data-bundle=multi] .item .info .price .original, +[data-bundle=multi] .bundle-card-product .info .price .original, +[data-bundle=buyxgety] .item .info .price .original, +[data-bundle=buyxgety] .bundle-card-product .info .price .original{ + font-size:16px; + font-weight:700; + line-height:120%; + color:var(--yc-primary-color); +} +[data-bundle=single] .item .info .price .compare-at, +[data-bundle=single] .bundle-card-product .info .price .compare-at, +[data-bundle=multi] .item .info .price .compare-at, +[data-bundle=multi] .bundle-card-product .info .price .compare-at, +[data-bundle=buyxgety] .item .info .price .compare-at, +[data-bundle=buyxgety] .bundle-card-product .info .price .compare-at{ + color:#8D8D8D; + font-size:13px; + font-weight:400; + line-height:120%; + text-decoration-line:line-through; +} +[data-bundle=single] .item, +[data-bundle=multi] .item, +[data-bundle=buyxgety] .item{ + padding:6px 12px; +} +[data-bundle=single] .item:first-of-type, +[data-bundle=multi] .item:first-of-type, +[data-bundle=buyxgety] .item:first-of-type{ + padding-block-start:12px; +} +[data-bundle=single] .item:last-of-type, +[data-bundle=multi] .item:last-of-type, +[data-bundle=buyxgety] .item:last-of-type{ + padding-block-end:12px; +} +[data-bundle=single] .item:not(:has(.variants)), +[data-bundle=multi] .item:not(:has(.variants)), +[data-bundle=buyxgety] .item:not(:has(.variants)){ + align-items:center; +} +[data-bundle=single] .item .info, +[data-bundle=multi] .item .info, +[data-bundle=buyxgety] .item .info{ + gap:8px; +} +[data-bundle=single] .item .info .group, +[data-bundle=multi] .item .info .group, +[data-bundle=buyxgety] .item .info .group{ + display:flex; + gap:8px; +} +[data-bundle=single] .item .info .group .index, +[data-bundle=multi] .item .info .group .index, +[data-bundle=buyxgety] .item .info .group .index{ + width:22px; + opacity:0.8; +} +[data-bundle=single] .item .info .variants, +[data-bundle=multi] .item .info .variants, +[data-bundle=buyxgety] .item .info .variants{ + display:flex; + flex-wrap:wrap; + gap:8px; +} +[data-bundle=single] .item .info .variants span, +[data-bundle=multi] .item .info .variants span, +[data-bundle=buyxgety] .item .info .variants span{ + padding:2px 6px; + background-color:#f3f4f6; + font-size:0.875rem; +} +[data-bundle=single] .item.item-free, +[data-bundle=multi] .item.item-free, +[data-bundle=buyxgety] .item.item-free{ + border-top:1px solid #e5e7eb; +} +[data-bundle=single] .item.item-free .info .price, +[data-bundle=multi] .item.item-free .info .price, +[data-bundle=buyxgety] .item.item-free .info .price{ + display:grid; + grid-gap:4px; + gap:4px; +} +[data-bundle=single] .item.item-free .info .price.free-gift, +[data-bundle=multi] .item.item-free .info .price.free-gift, +[data-bundle=buyxgety] .item.item-free .info .price.free-gift{ + display:grid; + grid-gap:4px; + gap:4px; +} +[data-bundle=single] .item.item-free .info .price.free-gift .gift, +[data-bundle=multi] .item.item-free .info .price.free-gift .gift, +[data-bundle=buyxgety] .item.item-free .info .price.free-gift .gift{ + display:flex; + align-items:center; + gap:4px; +} +[data-bundle=single] .item.item-free .info .price.free-gift .gift svg, +[data-bundle=multi] .item.item-free .info .price.free-gift .gift svg, +[data-bundle=buyxgety] .item.item-free .info .price.free-gift .gift svg{ + fill:var(--yc-primary-color); +} +[data-bundle=single] .item.item-free .info .price.free-gift .gift .free, +[data-bundle=multi] .item.item-free .info .price.free-gift .gift .free, +[data-bundle=buyxgety] .item.item-free .info .price.free-gift .gift .free{ + font-size:16px; + font-weight:700; + line-height:120%; + color:var(--yc-primary-color); +} +[data-bundle=single] .list, +[data-bundle=multi] .list, +[data-bundle=buyxgety] .list{ + display:flex; + flex-direction:column; +} +[data-bundle=single] .bundle-checkbox, +[data-bundle=multi] .bundle-checkbox, +[data-bundle=buyxgety] .bundle-checkbox{ + -moz-appearance:none; + appearance:none; + -webkit-appearance:none; + box-sizing:border-box; + width:20px; + height:20px; + min-width:20px; + padding:0; + margin:0; + border:1px solid #B7B7B7; + border-radius:50%; + cursor:pointer; + flex-shrink:0; + position:relative; +} +[data-bundle=single] .bundle-checkbox:checked, +[data-bundle=multi] .bundle-checkbox:checked, +[data-bundle=buyxgety] .bundle-checkbox:checked{ + border-color:var(--yc-primary-color); +} +[data-bundle=single] .bundle-checkbox:checked::after, +[data-bundle=multi] .bundle-checkbox:checked::after, +[data-bundle=buyxgety] .bundle-checkbox:checked::after{ + content:""; + position:absolute; + inset:1.3px; + border-radius:50%; + background-color:var(--yc-primary-color); +} +[data-bundle=single] .bundle-save-badge, +[data-bundle=multi] .bundle-save-badge, +[data-bundle=buyxgety] .bundle-save-badge{ + display:inline-flex; + align-items:center; + padding:2px 8px; + font-size:0.75rem; + font-weight:600; +} +[data-bundle=single] .bundle-save-badge--stroke, +[data-bundle=multi] .bundle-save-badge--stroke, +[data-bundle=buyxgety] .bundle-save-badge--stroke{ + border:1px solid currentColor; +} +[data-bundle=single] .bundle-save-badge--success, +[data-bundle=multi] .bundle-save-badge--success, +[data-bundle=buyxgety] .bundle-save-badge--success{ + background-color:#dcfce7; + color:#15803d; +} +[data-bundle=single].bundle-card, +[data-bundle=multi].bundle-card, +[data-bundle=buyxgety].bundle-card{ + box-shadow:1px 2px 18px -2px #F2F1F1; +} +[data-bundle=single].bundle-card .bundle-card-head, +[data-bundle=multi].bundle-card .bundle-card-head, +[data-bundle=buyxgety].bundle-card .bundle-card-head{ + display:flex; + align-items:center; + gap:9px; + padding:16px; +} +[data-bundle=single].bundle-card .bundle-card-head .bundle-card-title, +[data-bundle=multi].bundle-card .bundle-card-head .bundle-card-title, +[data-bundle=buyxgety].bundle-card .bundle-card-head .bundle-card-title{ + margin:0; + font-weight:700; + font-size:18px; + line-height:normal; +} +[data-bundle=single].bundle-card .bundle-discount-tag, +[data-bundle=multi].bundle-card .bundle-discount-tag, +[data-bundle=buyxgety].bundle-card .bundle-discount-tag{ + display:inline-flex; + align-items:center; + padding:4px 16px; + font-size:13px; + font-weight:700; + white-space:nowrap; + background-color:var(--yc-primary-color); + color:#fff; +} +[data-bundle=single].bundle-card .bundle-card-product, +[data-bundle=multi].bundle-card .bundle-card-product, +[data-bundle=buyxgety].bundle-card .bundle-card-product{ + align-items:center; + padding:0 16px 16px; +} +[data-bundle=single].bundle-card .bundle-card-product .info, +[data-bundle=multi].bundle-card .bundle-card-product .info, +[data-bundle=buyxgety].bundle-card .bundle-card-product .info{ + gap:6px; +} +[data-bundle=single].bundle-card .bundle-card-variants, +[data-bundle=multi].bundle-card .bundle-card-variants, +[data-bundle=buyxgety].bundle-card .bundle-card-variants{ + display:flex; + flex-direction:column; + gap:12px; + margin:0 16px; + padding:12px 0; + border-top:1px solid #e5e7eb; +} +[data-bundle=single].bundle-card .bundle-card-variants .bundle-variant-row, +[data-bundle=multi].bundle-card .bundle-card-variants .bundle-variant-row, +[data-bundle=buyxgety].bundle-card .bundle-card-variants .bundle-variant-row{ + display:flex; + align-items:start; + gap:16px; + padding:10px 16px; + background-color:#f8f8f8; + line-height:normal; +} +[data-bundle=single].bundle-card .bundle-card-variants .bundle-variant-row .bundle-variant-info, +[data-bundle=multi].bundle-card .bundle-card-variants .bundle-variant-row .bundle-variant-info, +[data-bundle=buyxgety].bundle-card .bundle-card-variants .bundle-variant-row .bundle-variant-info{ + display:flex; + align-items:center; + justify-content:space-evenly; + flex-wrap:wrap; + gap:8px 16px; + width:100%; +} +[data-bundle=single].bundle-card .bundle-card-variants .bundle-variant-row .bundle-variant-index, +[data-bundle=multi].bundle-card .bundle-card-variants .bundle-variant-row .bundle-variant-index, +[data-bundle=buyxgety].bundle-card .bundle-card-variants .bundle-variant-row .bundle-variant-index{ + flex-shrink:0; + font-weight:700; +} +[data-bundle=single].bundle-card .bundle-card-variants .bundle-variant-group, +[data-bundle=multi].bundle-card .bundle-card-variants .bundle-variant-group, +[data-bundle=buyxgety].bundle-card .bundle-card-variants .bundle-variant-group{ + display:flex; + align-items:center; + gap:8px; +} +[data-bundle=single].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-label, +[data-bundle=multi].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-label, +[data-bundle=buyxgety].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-label{ + font-size:12px; + font-style:normal; + font-weight:400; + line-height:120%; +} +[data-bundle=single].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-qty, +[data-bundle=multi].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-qty, +[data-bundle=buyxgety].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-qty{ + font-size:15px; + font-style:normal; + font-weight:700; + line-height:120%; +} +[data-bundle=single].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-value, +[data-bundle=multi].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-value, +[data-bundle=buyxgety].bundle-card .bundle-card-variants .bundle-variant-group .bundle-variant-value{ + font-size:12px; + font-weight:400; + line-height:120%; +} + +.bundle-add-to-cart{ + margin-top:16px; +} +.bundle-add-to-cart span{ + font-size:14px; + font-weight:600; +} +.bundle-add-to-cart .add-to-cart-button:disabled{ + cursor:not-allowed; + opacity:0.5; +} diff --git a/themes/meraki/assets/cart-drawer.css b/themes/meraki/assets/cart-drawer.css index ff801d3d3..15bb16afe 100644 --- a/themes/meraki/assets/cart-drawer.css +++ b/themes/meraki/assets/cart-drawer.css @@ -40,6 +40,7 @@ top:0; background-color:white; z-index:100; + margin-bottom:20px; } .cart-drawer .header .cart{ font-weight:700; @@ -67,6 +68,7 @@ bottom:0; padding:26px 24px 32px; width:100%; + margin-top:20px; } .cart-drawer .footer .price-wrapper{ display:flex; @@ -114,6 +116,12 @@ gap:12px; } } +.cart-drawer ul .cart-item:first-child .item-body{ + padding-top:0; +} +.cart-drawer ul .cart-item:last-child .item-body{ + border-bottom:none; +} .cart-drawer ul .cart-item .remove-item-btn, .cart-drawer ul .cart-item .spinner{ position:absolute; left:0; @@ -204,3 +212,151 @@ visibility:visible; opacity:1; } + +.cart-bundle-group{ + overflow:hidden; + border:1px solid #ededed; +} +.cart-bundle-group .bundle-group-head{ + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + padding:12px 16px; + background-color:white; + border-bottom:1px solid #ededed; +} +.cart-bundle-group .bundle-group-head .bundle-heading{ + display:flex; + align-items:center; + gap:8px; +} +@media (max-width: 768px){ + .cart-bundle-group .bundle-group-head .bundle-heading{ + flex-direction:column; + align-items:flex-start; + } +} +.cart-bundle-group .bundle-group-head .bundle-title{ + font-weight:700; + font-size:16px; +} +.cart-bundle-group .bundle-group-head .bundle-total{ + font-weight:700; + font-size:16px; + color:var(--yc-primary-color); +} +.cart-bundle-group .bundle-group-head .remove-bundle-btn{ + background:none; + border:none; + cursor:pointer; + color:#cbcbcb; + padding:0; +} +.cart-bundle-group .bundle-group-head .remove-bundle-btn:disabled{ + cursor:not-allowed; + opacity:0.6; +} +.cart-bundle-group .bundle-item-row{ + display:grid; + grid-template-columns:80px 1fr auto; + grid-gap:12px; + gap:12px; + padding:12px 16px; + background:white; + border-bottom:1px solid #f3f4f6; + align-items:start; +} +.cart-bundle-group .bundle-item-row:last-child{ + border-bottom:none; +} +.cart-bundle-group .bundle-item-row .bundle-item-image{ + width:80px; + height:106px; + -o-object-fit:cover; + object-fit:cover; +} +.cart-bundle-group .bundle-item-row .bundle-item-info .bundle-item-name{ + font-weight:600; + font-size:14px; + display:block; + margin-bottom:4px; +} +.cart-bundle-group .bundle-item-row .bundle-item-info .bundle-item-variants span{ + font-size:13px; + font-weight:400; +} +.cart-bundle-group .bundle-item-row .bundle-item-info .bundle-item-variants span:not(:last-child)::after{ + content:", "; +} +.cart-bundle-group .bundle-item-row .bundle-item-price{ + display:flex; + flex-direction:column; + align-items:flex-end; + gap:2px; +} +.cart-bundle-group .bundle-item-row .bundle-item-price .price{ + font-weight:700; + font-size:15px; + color:var(--yc-primary-color); +} +.cart-bundle-group .bundle-item-row .bundle-item-price .compare-at{ + font-weight:400; + font-size:13px; + text-decoration-line:line-through; + color:#8d8d8d; +} +.cart-bundle-group .bundle-item-row .bundle-item-price .compare-at[hidden]{ + display:none; +} +.cart-bundle-group .bundle-item-row .bundle-item-price .gift{ + display:flex; + align-items:center; + gap:4px; +} +.cart-bundle-group .bundle-item-row .bundle-item-price .gift[hidden]{ + display:none; +} +.cart-bundle-group .bundle-item-row .bundle-item-price .gift svg{ + fill:var(--yc-primary-color); +} +.cart-bundle-group .bundle-item-row .bundle-item-price .gift .free{ + font-size:16px; + font-weight:700; + line-height:120%; + color:var(--yc-primary-color); +} +.cart-bundle-group[ui-context=drawer]{ + margin:20px; + margin:0 20px 20px; +} +@media (max-width: 768px){ + .cart-bundle-group[ui-context=drawer]{ + margin:0 16px 16px; + } +} +.cart-bundle-group[ui-context=drawer] .remove-bundle-btn ion-icon{ + width:16px; + height:16px; +} +.cart-bundle-group[ui-context=cart]{ + margin:0; +} +@media (min-width: 768px){ + .cart-bundle-group[ui-context=cart] .bundle-item-row{ + padding:16px 20px; + } +} +.cart-bundle-group[ui-context=cart] .remove-bundle-btn ion-icon{ + width:20px; + height:20px; +} +.cart-bundle-group[ui-context=cart] .bundle-item-price .price{ + font-size:18px; +} +.cart-bundle-group[ui-context=cart] .bundle-item-variants span{ + color:#000; + font-size:12px !important; + font-style:normal; + line-height:150%; +} diff --git a/themes/meraki/assets/cart.css b/themes/meraki/assets/cart.css index 3f7cdd8ec..7a2e0b27f 100644 --- a/themes/meraki/assets/cart.css +++ b/themes/meraki/assets/cart.css @@ -74,7 +74,8 @@ body{ .yc-cart .cart-table .table-container{ display:grid; margin:24px 0; - background-color:white; + grid-gap:16px; + gap:16px; } @media (min-width: 768px){ .yc-cart .cart-table .table-container{ @@ -82,14 +83,12 @@ body{ } } .yc-cart .cart-table .table-container .cart__item{ - padding:24px 0; - margin:0 20px; + padding:24px 16px; + background:white; } @media (min-width: 768px){ .yc-cart .cart-table .table-container .cart__item{ padding:24px 20px; - margin:0; - margin:initial; } } .yc-cart .cart-table .table-container .row{ @@ -156,13 +155,13 @@ body{ } .yc-cart .cart-table .table-container .row .cell .product-image{ width:80px; - height:auto; + height:106px; } .yc-cart .cart-table .table-container .row .cell .product-image img{ width:100%; height:100%; - -o-object-fit:contain; - object-fit:contain; + -o-object-fit:cover; + object-fit:cover; } .yc-cart .cart-table .table-container .row .cell .cart-item-loader-spinner .spinner{ border-right:2px solid #A4A4A4; diff --git a/themes/meraki/assets/cart.js b/themes/meraki/assets/cart.js index 29c191a40..63a5954c2 100644 --- a/themes/meraki/assets/cart.js +++ b/themes/meraki/assets/cart.js @@ -150,6 +150,35 @@ const CartUI = { }; // Events +async function removeBundleItems(button, itemIds, variantIds) { + const spinner = button?.querySelector('.spinner'); + const removeIcon = button?.querySelector('.remove-icon'); + + if (button) button.disabled = true; + spinner?.classList.remove('hidden'); + removeIcon?.classList.add('hidden'); + + try { + let updatedCart; + for (let i = 0; i < itemIds.length; i++) { + updatedCart = await CartService.removeItem(itemIds[i], variantIds[i]); + } + + button?.closest('.cart-bundle-group')?.remove(); + CartUI.updateCartBadge(updatedCart.count); + CartUI.updateTotalPrice(updatedCart.discounted_sub_total, updatedCart.items); + + if (updatedCart.count === 0) { + CartUI.handleEmptyCart(); + } + } catch (e) { + if (button) button.disabled = false; + spinner?.classList.add('hidden'); + removeIcon?.classList.remove('hidden'); + notify(e.message, 'error'); + } +} + async function updateQuantity(cartItemId, productVariantId, quantity) { let parsedQuantity = Number(quantity); @@ -162,6 +191,7 @@ async function updateQuantity(cartItemId, productVariantId, quantity) { CartUI.updateCartItem(cartItemId, productVariantId, parsedQuantity, itemSubtotal); CartUI.updateTotalPrice(updatedCart.discounted_sub_total, updatedCart.items); + CartUI.updateCartBadge(updatedCart.count); } } catch (e) { notify(e.message, 'error'); @@ -179,7 +209,7 @@ async function removeItem(cartItemId, productVariantId) { CartUI.updateCartBadge(updatedCart.count); CartUI.updateTotalPrice(updatedCart.discounted_sub_total, updatedCart.items); - if (updatedCart.items.length === 0) { + if (updatedCart.count === 0) { CartUI.handleEmptyCart(); } } catch (e) { @@ -224,8 +254,10 @@ document.addEventListener('DOMContentLoaded', async () => { try { const cart = await CartService.fetchCart(); - if (cart.items.length > 0) { - CartUI.updateTotalPrice(cart.discounted_sub_total, cart.items); + const items = Array.isArray(cart.items) ? cart.items : []; + + if (items.length > 0) { + CartUI.updateTotalPrice(cart.discounted_sub_total, items); } if (cart.coupon && cart.discountedPrice) { diff --git a/themes/meraki/assets/express-checkout.css b/themes/meraki/assets/express-checkout.css index 0ecfc926e..e33fb649b 100644 --- a/themes/meraki/assets/express-checkout.css +++ b/themes/meraki/assets/express-checkout.css @@ -1,5 +1,6 @@ @media (min-width: 768px){ - #express-checkout-form{ + #express-checkout-form, + #bundle-express-checkout-form{ border-width:1px; border-style:solid; background:#fff; @@ -7,7 +8,8 @@ grid-gap:18px; } } -#express-checkout-form *{ +#express-checkout-form *, +#bundle-express-checkout-form *{ font-family:var(--yc-font-family); } diff --git a/themes/meraki/assets/express-checkout.js b/themes/meraki/assets/express-checkout.js index 3a0f1e17a..c27c493af 100644 --- a/themes/meraki/assets/express-checkout.js +++ b/themes/meraki/assets/express-checkout.js @@ -1,11 +1,17 @@ -async function placeOrder() { - const expressCheckoutForm = document.querySelector('#express-checkout-form'); - +async function placeOrder(button) { + const expressCheckoutForm = (button && button.closest('form')) || document.querySelector('#express-checkout-form'); let fields = Object.fromEntries(new FormData(expressCheckoutForm)); + const isBundleForm = expressCheckoutForm?.id === 'bundle-express-checkout-form'; + const productVariantId = document.getElementById('variantId')?.value; + const bundleId = document.querySelector('[data-bundle] input[type="checkbox"]:checked')?.value; + + if (isBundleForm && !bundleId) { + return notify(ADD_TO_CART_EXPECTED_ERRORS.select_bundle, 'warning'); + } + load('#loading__checkout'); try { - const productVariantId = document.getElementById('variantId')?.value; const quantity = document.getElementById('quantity')?.value || 1; const attachedImage = document.querySelector('#yc-upload-link')?.value; @@ -13,14 +19,18 @@ async function placeOrder() { fields = { ...fields, attachedImage }; } - const response = await youcanjs.checkout.placeExpressCheckoutOrder({ productVariantId, quantity, fields }); + const response = await youcanjs.checkout.placeExpressCheckoutOrder({ + quantity, + fields, + ...(isBundleForm ? { bundleId, isBundle: true } : { productVariantId }), + }); response .onSuccess((data, redirectToThankyouPage) => { redirectToThankyouPage(); }) .onValidationErr((err) => { - const form = document.querySelector('#express-checkout-form'); + const form = expressCheckoutForm; const formFields = Object.keys(err.meta.fields); if (!form || !formFields) return; diff --git a/themes/meraki/assets/linked-fields.js b/themes/meraki/assets/linked-fields.js index 56dc3e177..df2e3e1f1 100644 --- a/themes/meraki/assets/linked-fields.js +++ b/themes/meraki/assets/linked-fields.js @@ -1,100 +1,113 @@ const TYPES = ['country', 'region', 'city']; - -let fields = {}; -let regionCode = null; -let countryCode = null; const locale = document.documentElement.lang || 'en'; -for (const type of TYPES) { - fields[type] = document.querySelector(`[data-linked-field='${type}']`); - fields[type]?.addEventListener('change', () => onChange(type)); -} +function initLinkedFields(container) { + if (container._linkedFieldsInit) return; + container._linkedFieldsInit = true; -fetchOptions(); + let fields = {}; + let regionCode = null; + let countryCode = null; -async function fetchOptions() { for (const type of TYPES) { - fields[type] && (await fetchLocationByType(type)); + fields[type] = container.querySelector(`[data-linked-field='${type}']`); + fields[type]?.addEventListener('change', () => onChange(type)); } -} - -function setUpOptions(type, options) { - fields[type].innerHTML = ''; - options.forEach((opt, index) => { - const label = typeof opt === 'string' ? opt : opt.name; - const value = typeof opt === 'string' ? opt : opt.code; - const isDefault = (type === 'country' && value === countryCode) || index === 0; + fetchOptions(); - const option = new Option(label, label); - option.dataset.value = value; - option.defaultSelected = isDefault; + async function fetchOptions() { + for (const type of TYPES) { + fields[type] && (await fetchLocationByType(type)); + } + } - fields[type].add(option); - }); -} + function setUpOptions(type, options) { + fields[type].innerHTML = ''; -async function onChange(type) { - const value = fields[type].selectedOptions[0]?.dataset.value; + options.forEach((opt, index) => { + const label = typeof opt === 'string' ? opt : opt.name; + const value = typeof opt === 'string' ? opt : opt.code; + const isDefault = (type === 'country' && value === countryCode) || index === 0; - if (type === 'country') countryCode = value; - if (type === 'region') regionCode = value; + const option = new Option(label, label); + option.dataset.value = value; + option.defaultSelected = isDefault; - const dependentFields = getDependentFields(type); - for (const next of dependentFields) { - fields[next] && (await fetchLocationByType(next)); + fields[type].add(option); + }); } -} -async function fetchLocationByType(type) { - const fetchMap = { - country: () => window.storeMarketCountries, - region: () => { - const key = `${countryCode}_${locale}`; + async function onChange(type) { + const value = fields[type].selectedOptions[0]?.dataset.value; - if (!window.storeRegions[key]) { - window.storeRegions[key] = youcanjs.misc.getCountryRegions(countryCode, locale); - } + if (type === 'country') countryCode = value; + if (type === 'region') regionCode = value; - return window.storeRegions[key]; - }, - city: () => { - const key = `${countryCode}_${regionCode}_${locale}`; + const dependentFields = getDependentFields(type); + for (const next of dependentFields) { + fields[next] && (await fetchLocationByType(next)); + } + } - if (!window.storeCities[key]) { - window.storeCities[key] = youcanjs.misc.getCountryCities(countryCode, regionCode, locale); - } + async function fetchLocationByType(type) { + const fetchMap = { + country: () => window.storeMarketCountries, + region: () => { + const key = `${countryCode}_${locale}`; - return window.storeCities[key]; - }, - }; + if (!window.storeRegions[key]) { + window.storeRegions[key] = youcanjs.misc.getCountryRegions(countryCode, locale); + } - try { - const response = await fetchMap[type]?.call(); - if (!response) throw new Error(`Unknown fetch type: ${type}`); + return window.storeRegions[key]; + }, + city: () => { + const key = `${countryCode}_${regionCode}_${locale}`; - const map = { - country: () => { - const customerCountryExists = response.countries.some(country => country.code === CUSTOMER_COUNTRY_CODE); - countryCode = customerCountryExists ? CUSTOMER_COUNTRY_CODE : response.countries[0].code; + if (!window.storeCities[key]) { + window.storeCities[key] = youcanjs.misc.getCountryCities(countryCode, regionCode, locale); + } - this.setUpOptions(type, response.countries); - }, - region: () => { - regionCode = response.states[0].code; - setUpOptions(type, response.states); + return window.storeCities[key]; }, - city: () => setUpOptions(type, response.cities), }; - map[type]?.call(); - } catch (error) { - console.error(error); + try { + const response = await fetchMap[type]?.call(); + if (!response) throw new Error(`Unknown fetch type: ${type}`); + + const map = { + country: () => { + const customerCountryExists = response.countries.some(country => country.code === CUSTOMER_COUNTRY_CODE); + countryCode = customerCountryExists ? CUSTOMER_COUNTRY_CODE : response.countries[0].code; + + setUpOptions(type, response.countries); + }, + region: () => { + regionCode = response.states[0].code; + setUpOptions(type, response.states); + }, + city: () => setUpOptions(type, response.cities), + }; + + map[type]?.call(); + } catch (error) { + console.error(error); + } } -} -function getDependentFields(type) { - if (type === 'country') return TYPES.slice(1); - if (type === 'region') return TYPES.slice(2); - return []; + function getDependentFields(type) { + if (type === 'country') return TYPES.slice(1); + if (type === 'region') return TYPES.slice(2); + return []; + } } + +const linkedFieldEls = document.querySelectorAll('[data-linked-field]'); +const linkedForms = new Set(); +linkedFieldEls.forEach(el => { + const form = el.closest('form'); + if (form) linkedForms.add(form); +}); +linkedForms.forEach(form => initLinkedFields(form)); diff --git a/themes/meraki/assets/main.css b/themes/meraki/assets/main.css index 3dca81cb7..1ffe0720b 100644 --- a/themes/meraki/assets/main.css +++ b/themes/meraki/assets/main.css @@ -198,7 +198,7 @@ span{ border:1px solid #f2f2f2 !important; } -input:not([type=radio]), select, +input:not([type=radio]):not(.bundle-checkbox), select, .nice-select, .selector-item_label{ transition:box-shadow 100ms ease; diff --git a/themes/meraki/assets/phone-validation.js b/themes/meraki/assets/phone-validation.js index 1e851450e..caa62fd28 100644 --- a/themes/meraki/assets/phone-validation.js +++ b/themes/meraki/assets/phone-validation.js @@ -1,145 +1,142 @@ const DEFAULT_COUNTRY_CODE = 'MA'; const customerCountryCode = CUSTOMER_COUNTRY_CODE || DEFAULT_COUNTRY_CODE; -const elements = { - phoneSelectCountryCode: null, - phoneDisplayedCountryCode: null, - phoneNumber: null, - phoneHiddenInput: null -}; - -function validatePhoneElements() { - elements.phoneSelectCountryCode = document.querySelector('[data-phone-select-country-code]'); - elements.phoneDisplayedCountryCode = document.querySelector('[data-phone-displayed-country-code]'); - elements.phoneNumber = document.querySelector('[data-phone-number]'); - elements.phoneHiddenInput = document.querySelector('[data-phone-hidden-input]'); - - return elements.phoneSelectCountryCode && elements.phoneDisplayedCountryCode && elements.phoneNumber && elements.phoneHiddenInput; -} -function displaySelectedCountryCode() { - const selectedOption = elements.phoneSelectCountryCode.options[elements.phoneSelectCountryCode.selectedIndex]; - if (selectedOption) { - elements.phoneDisplayedCountryCode.innerHTML = `+${selectedOption.value}`; - } -} +function initPhoneValidation(fieldset) { + if (fieldset._phoneInit) return; + fieldset._phoneInit = true; -async function buildCountryCodeOptions() { - try { - const { countries } = await window.storeMarketCountries; + const elements = { + phoneSelectCountryCode: fieldset.querySelector('[data-phone-select-country-code]'), + phoneDisplayedCountryCode: fieldset.querySelector('[data-phone-displayed-country-code]'), + phoneNumber: fieldset.querySelector('[data-phone-number]'), + phoneHiddenInput: fieldset.querySelector('[data-phone-hidden-input]'), + }; - if (!countries || !countries.length) return; + if (!elements.phoneSelectCountryCode || !elements.phoneDisplayedCountryCode || !elements.phoneNumber || !elements.phoneHiddenInput) return; - elements.phoneSelectCountryCode.innerHTML = ''; + function displaySelectedCountryCode() { + const selectedOption = elements.phoneSelectCountryCode.options[elements.phoneSelectCountryCode.selectedIndex]; + if (selectedOption) { + elements.phoneDisplayedCountryCode.innerHTML = `+${selectedOption.value}`; + } + } - const isRtl = document.documentElement.dir === 'rtl'; - const fragment = document.createDocumentFragment(); + async function buildCountryCodeOptions() { + try { + const { countries } = await window.storeMarketCountries; - countries.forEach(country => { - const option = document.createElement('option'); + if (!countries || !countries.length) return; - option.value = country.phone; - option.dataset.country = country.code; - option.textContent = isRtl - ? `${country.name} (${country.phone}+)` - : `${country.name} (+${country.phone})`; + elements.phoneSelectCountryCode.innerHTML = ''; - if (country.code === customerCountryCode) { - option.selected = true; - } + const isRtl = document.documentElement.dir === 'rtl'; + const fragment = document.createDocumentFragment(); - fragment.appendChild(option); - }); + countries.forEach(country => { + const option = document.createElement('option'); - elements.phoneSelectCountryCode.appendChild(fragment); - } catch (e) { - console.error('Failed to populate countries', e); - } -} + option.value = country.phone; + option.dataset.country = country.code; + option.textContent = isRtl + ? `${country.name} (${country.phone}+)` + : `${country.name} (+${country.phone})`; -function getFullPhoneNumber() { - const countryCode = elements.phoneSelectCountryCode.value; - const nationalNumber = elements.phoneNumber.value.trim(); + if (country.code === customerCountryCode) { + option.selected = true; + } - if (!nationalNumber || !countryCode) return ''; + fragment.appendChild(option); + }); - return `+${countryCode}${nationalNumber}`; -} + elements.phoneSelectCountryCode.appendChild(fragment); + } catch (e) { + console.error('Failed to populate countries', e); + } + } -function toggleError(show) { - const phoneErrorElement = document.querySelector('[data-phone-error]'); - const fieldsetElement = document.querySelector('[data-phone-fieldset]'); + function getFullPhoneNumber() { + const countryCode = elements.phoneSelectCountryCode.value; + const nationalNumber = elements.phoneNumber.value.trim(); - if (phoneErrorElement) phoneErrorElement.style.display = show ? 'block' : 'none'; - if (fieldsetElement) fieldsetElement.classList.toggle('error', show); -} + if (!nationalNumber || !countryCode) return ''; -function updatePhoneField() { - const fullNumber = getFullPhoneNumber(); + return `+${countryCode}${nationalNumber}`; + } - if (!fullNumber) { - elements.phoneHiddenInput.value = ''; - return; + function toggleError(show) { + const phoneErrorElement = fieldset.closest('form')?.querySelector('[data-phone-error]') || document.querySelector('[data-phone-error]'); + fieldset.classList.toggle('error', show); + if (phoneErrorElement) phoneErrorElement.style.display = show ? 'block' : 'none'; } - try { - const parsed = libphonenumber.parsePhoneNumber(fullNumber); - if (parsed && parsed.isValid()) { - elements.phoneHiddenInput.value = parsed.number; - toggleError(false); - } else { + function updatePhoneField() { + const fullNumber = getFullPhoneNumber(); + + if (!fullNumber) { + elements.phoneHiddenInput.value = ''; + return; + } + + try { + const parsed = libphonenumber.parsePhoneNumber(fullNumber); + if (parsed && parsed.isValid()) { + elements.phoneHiddenInput.value = parsed.number; + toggleError(false); + } else { + elements.phoneHiddenInput.value = ''; + toggleError(true); + } + } catch (e) { elements.phoneHiddenInput.value = ''; toggleError(true); } - } catch (e) { - elements.phoneHiddenInput.value = ''; - toggleError(true); } -} -function syncCountryCodeFromInput() { - const inputValue = elements.phoneNumber.value.trim(); - if (!inputValue.startsWith('+')) return; + function syncCountryCodeFromInput() { + const inputValue = elements.phoneNumber.value.trim(); + if (!inputValue.startsWith('+')) return; - try { - const parsed = libphonenumber.parsePhoneNumberFromString(inputValue); - if (parsed) { - elements.phoneNumber.value = parsed.nationalNumber; + try { + const parsed = libphonenumber.parsePhoneNumberFromString(inputValue); + if (parsed) { + elements.phoneNumber.value = parsed.nationalNumber; - const matchingOption = Array.from(elements.phoneSelectCountryCode.options).find( - option => option.value === parsed.countryCallingCode - ); + const matchingOption = Array.from(elements.phoneSelectCountryCode.options).find( + option => option.value === parsed.countryCallingCode + ); - if (matchingOption) { - elements.phoneSelectCountryCode.value = parsed.countryCallingCode; - displaySelectedCountryCode(); + if (matchingOption) { + elements.phoneSelectCountryCode.value = parsed.countryCallingCode; + displaySelectedCountryCode(); + } } + } catch (e) { + // Ignore parsing errors during typing } - } catch (e) { - // Ignore parsing errors during typing } -} -function attachListeners() { - elements.phoneSelectCountryCode.addEventListener('change', () => { - toggleError(false); - updatePhoneField(); - displaySelectedCountryCode(); - }); + function attachListeners() { + elements.phoneSelectCountryCode.addEventListener('change', () => { + toggleError(false); + updatePhoneField(); + displaySelectedCountryCode(); + }); - elements.phoneNumber.addEventListener('input', () => { - toggleError(false); - syncCountryCodeFromInput(); - }); + elements.phoneNumber.addEventListener('input', () => { + toggleError(false); + syncCountryCodeFromInput(); + }); - elements.phoneNumber.addEventListener('blur', updatePhoneField); -} + elements.phoneNumber.addEventListener('blur', updatePhoneField); + } -async function init() { - if (!validatePhoneElements()) return; + async function init() { + await buildCountryCodeOptions(); + attachListeners(); + displaySelectedCountryCode(); + } - await buildCountryCodeOptions(); - attachListeners(); - displaySelectedCountryCode(); + init(); } -init(); +document.querySelectorAll('[data-phone-fieldset]').forEach(fieldset => initPhoneValidation(fieldset)); diff --git a/themes/meraki/assets/product.js b/themes/meraki/assets/product.js index 9da9ac785..4683f51dd 100644 --- a/themes/meraki/assets/product.js +++ b/themes/meraki/assets/product.js @@ -599,12 +599,33 @@ function goToCheckoutStep(close = false) { showSelectedQuantity(); } +function setupBundles(parentSection) { + const bundleCheckboxes = parentSection.querySelectorAll('[data-bundle] input[type="checkbox"]'); + + if (!bundleCheckboxes.length) return; + + bundleCheckboxes.forEach((checkbox) => { + checkbox.addEventListener('change', () => { + bundleCheckboxes.forEach((cb) => { + if (cb !== checkbox) cb.checked = false; + }); + + const bundleIdInput = parentSection.querySelector('#bundleId'); + if (bundleIdInput) { + bundleIdInput.value = checkbox.checked ? checkbox.value : ''; + } + }); + }); +} + function setup() { const singleProductSections = document.querySelectorAll('.yc-single-product'); if (!singleProductSections || typeof defaultVariant === 'undefined') return; singleProductSections.forEach((section) => { + setupBundles(section); + const productDetails = section.querySelector('.product-options'); const variant = defaultVariant; diff --git a/themes/meraki/assets/thankyou.css b/themes/meraki/assets/thankyou.css index bc4570866..915c515f9 100644 --- a/themes/meraki/assets/thankyou.css +++ b/themes/meraki/assets/thankyou.css @@ -60,25 +60,26 @@ body{ .thankyou-page-container .cart-result-section .cart-items{ margin-bottom:6px; } -.thankyou-page-container .cart-result-section .cart-items .product-list{ +.thankyou-page-container .cart-result-section .product-list{ display:grid; - grid-gap:6px; - gap:6px; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item{ +.thankyou-page-container .cart-result-section .product-list .product-item:not(:last-child){ + border-bottom:1px solid #EBEBEB; +} +.thankyou-page-container .cart-result-section .product-list .product-item{ display:flex; gap:16px; background:white; padding:20px 24px; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item .product-image{ +.thankyou-page-container .cart-result-section .product-list .product-item .product-image{ width:62px; height:62px; -o-object-fit:contain; object-fit:contain; flex-shrink:0; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item .product-details .product-name{ +.thankyou-page-container .cart-result-section .product-list .product-item .product-details .product-name{ display:block; color:#000; font-size:14px; @@ -86,11 +87,11 @@ body{ line-height:120%; margin-bottom:10px; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item .product-details .product-name:hover{ +.thankyou-page-container .cart-result-section .product-list .product-item .product-details .product-name:hover{ -webkit-text-decoration:underline; text-decoration:underline; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item .product-details .product-variations{ +.thankyou-page-container .cart-result-section .product-list .product-item .product-details .product-variations{ display:flex; flex-wrap:wrap; gap:14px; @@ -99,19 +100,27 @@ body{ font-weight:400; line-height:150%; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item .product-details .price-wrapper{ +.thankyou-page-container .cart-result-section .product-list .product-item .product-details .price-wrapper{ display:flex; align-items:center; gap:6px; margin-top:10px; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item .product-details .price-wrapper .product-price{ +.thankyou-page-container .cart-result-section .product-list .product-item .product-details .price-wrapper .product-price{ color:var(--yc-primary-color); font-size:18px; font-weight:700; line-height:97%; } -.thankyou-page-container .cart-result-section .cart-items .product-list .product-item .product-details .price-wrapper .compare-price{ +.thankyou-page-container .cart-result-section .product-list .product-item .product-details .price-wrapper .product-price .free-badge{ + background-color:#dcfce7; + color:#15803d; + padding:2px 6px; + border-radius:4px; + font-size:12px; + font-weight:600; +} +.thankyou-page-container .cart-result-section .product-list .product-item .product-details .price-wrapper .compare-price{ color:#757575; font-size:14px; font-weight:400; @@ -119,6 +128,37 @@ body{ -webkit-text-decoration:line-through; text-decoration:line-through; } +.thankyou-page-container .cart-result-section .cart-bundles{ + display:grid; + grid-gap:6px; + gap:6px; + margin-bottom:6px; +} +.thankyou-page-container .cart-result-section .cart-bundles .bundle-group{ + display:grid; +} +.thankyou-page-container .cart-result-section .cart-bundles .bundle-top{ + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + background:white; + padding:16px 24px; + border-bottom:1px solid #EBEBEB; +} +.thankyou-page-container .cart-result-section .cart-bundles .bundle-top .bundle-title{ + color:#000; + font-size:14px; + font-weight:700; + line-height:120%; + text-transform:uppercase; +} +.thankyou-page-container .cart-result-section .cart-bundles .bundle-top .bundle-total{ + color:var(--yc-primary-color); + font-size:16px; + font-weight:700; + line-height:97%; +} .thankyou-page-container .cart-result-section .order-details{ padding:20px 24px; background-color:white; diff --git a/themes/meraki/locales/ar.default.json b/themes/meraki/locales/ar.default.json index 247709812..680e26fc6 100644 --- a/themes/meraki/locales/ar.default.json +++ b/themes/meraki/locales/ar.default.json @@ -3,7 +3,13 @@ "show_more_button": "عرض المزيد", "size_big_message": "المرجو تحميل صورة أقل من 2mb", "buy_now": "اشتري الآن", - "add_to_cart": "أضف إلى السلة" + "add_to_cart": "أضف إلى السلة", + "add_bundle_to_cart": "أضف الباقة إلى السلة", + "quantity": "الكمية", + "you_save": "توفير", + "free": "مجاني", + "product_added": "تمت إضافة المنتج بنجاح", + "bundle_added": "تمت إضافة الباقة بنجاح" }, "head_metadata": { "collections": "التصنيفات", @@ -153,9 +159,10 @@ }, "errors": { "select_variant": "الرجاء اختيار عرض", + "select_bundle": "الرجاء اختيار باقة", + "bundle_already_added": "هذه الباقة مضافة بالفعل إلى سلتك", "quantity_smaller_than_zero": "يجب أن تكون الكمية أكبر من 0", "upload_image": "الرجاء تحميل صورة", - "product_added": "تمت إضافة المنتج بنجاح", "empty_inventory": "المُنتج غير متوفر حاليًا في المخزون", "max_quantity": "الكمية المتاحة لهذا العرض هي: ", "invalid_phone_number": "يرجى إدخال رقم هاتف صحيح." diff --git a/themes/meraki/locales/en.json b/themes/meraki/locales/en.json index f4260afc0..fd815556d 100644 --- a/themes/meraki/locales/en.json +++ b/themes/meraki/locales/en.json @@ -3,7 +3,13 @@ "show_more_button": "Show more", "size_big_message": "Please upload an image less than 2mb", "buy_now": "Buy now", - "add_to_cart": "Add to cart" + "add_to_cart": "Add to cart", + "add_bundle_to_cart": "Add bundle to cart", + "quantity": "Quantity", + "you_save": "You save", + "free": "Free", + "product_added": "Product has been added successfully", + "bundle_added": "Bundle has been added successfully" }, "head_metadata": { "collections": "Collections", @@ -153,9 +159,10 @@ }, "errors": { "select_variant": "Please select a variant", + "select_bundle": "Please select a bundle", + "bundle_already_added": "This bundle is already added to your cart", "quantity_smaller_than_zero": "Quantity must be greater than 0", "upload_image": "Please upload an image", - "product_added": "Product has been added successfully", "empty_inventory": "Product out of stock", "max_quantity": "The available quantity for this variant is: ", "invalid_phone_number": "Please enter a valid phone number." diff --git a/themes/meraki/locales/fr.json b/themes/meraki/locales/fr.json index 6a90437b3..c6bdad6df 100644 --- a/themes/meraki/locales/fr.json +++ b/themes/meraki/locales/fr.json @@ -3,7 +3,13 @@ "show_more_button": "Voir plus", "size_big_message": "Veuillez télécharger une image de moins de 2 Mo", "buy_now": "Acheter", - "add_to_cart": "Ajouter au panier" + "add_to_cart": "Ajouter au panier", + "add_bundle_to_cart": "Ajouter le pack au panier", + "quantity": "Quantité", + "you_save": "Vous économisez", + "free": "Gratuit", + "product_added": "Produit ajouté avec succès", + "bundle_added": "Pack ajouté avec succès" }, "head_metadata": { "collections": "Collections", @@ -153,9 +159,10 @@ }, "errors": { "select_variant": "Veuillez sélectionner une variante", + "select_bundle": "Veuillez sélectionner un pack", + "bundle_already_added": "Ce pack est déjà ajouté à votre panier", "quantity_smaller_than_zero": "La quantité doit être supérieure à 0", "upload_image": "Veuillez télécharger une image", - "product_added": "Produit ajouté avec succès", "empty_inventory": "Produit en rupture de stock", "max_quantity": "La quantité disponible pour cette variante est : ", "invalid_phone_number": "Veuillez saisir un numéro de téléphone valide." diff --git a/themes/meraki/sections/main-cart.liquid b/themes/meraki/sections/main-cart.liquid index 9d30d5c73..9acab022e 100644 --- a/themes/meraki/sections/main-cart.liquid +++ b/themes/meraki/sections/main-cart.liquid @@ -1,35 +1,35 @@ {{ 'cart.css' | asset_url | stylesheet_tag }} -