commit b9d6985ec727b4e5692b6cf708847da46954a4ea Author: atomkiv Date: Thu Oct 2 14:47:19 2025 +0200 Add app.js Quick View logic is here diff --git a/app.js b/app.js new file mode 100644 index 0000000..af4b44b --- /dev/null +++ b/app.js @@ -0,0 +1,622 @@ +class ProductQuickView { + constructor() { + this.product = null; + this.size = null; + this.idmProductId = null; + this.basketSubmitted = false; + this.htmlQuickView = ''; + this.htmlEl = document.querySelector('html'); + } + + // Escapes HTML to prevent XSS in outputs + escapeHtml(v = '') { + return String(v) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''); + } + + // Main listener to set up product quick view and handlers after DOM load + init = () => { + const self = this; + document.addEventListener("DOMContentLoaded", async () => { + const productImgLinks = document.querySelectorAll('#search .product'); + const prodInfoContainer = document.querySelector('.prod-info__container'); + const asideProdMenu = document.querySelector('.right-aside'); + const customAdded = document.getElementById('quick_view_basket'); + productImgLinks.forEach(prod => this.initProductCard(prod, prodInfoContainer, asideProdMenu, customAdded)); + this.renderProduct(asideProdMenu, prodInfoContainer, customAdded); + }); + } + + // Add quick view card buttons, setup close listeners + initProductCard(prod, prodInfoContainer, asideProdMenu, customAdded) { + const cardPictureContainer = document.createElement('div'); + if (!prod.querySelector('.hover-btn') && !prod.querySelector('.card__picture-container')) { + cardPictureContainer.className = 'card__picture-container'; + prod.insertAdjacentElement('afterbegin', cardPictureContainer); + + const productContentWrapper = prod.querySelector('.product__content_wrapper'); + const closeRightAsideBtn = document.querySelector('.right-aside__close'); + const closeRightAsideBlur = document.querySelector('.right-aside__bg-blur'); + const continueShopping = document.querySelector('.added__button.--close'); + + continueShopping.addEventListener('click', () => this.closeRightAside(prodInfoContainer, asideProdMenu)); + closeRightAsideBtn.addEventListener("click", () => this.closeRightAside(prodInfoContainer, asideProdMenu)); + closeRightAsideBlur.addEventListener("click", () => this.closeRightAside(prodInfoContainer, asideProdMenu)); + + const btn = document.createElement('button'); + btn.className = 'hover-btn'; + btn.textContent = 'CHOOSE OPTION'; + prod.insertAdjacentElement('afterbegin', btn); + + // Add button for mobile card + if (productContentWrapper && productContentWrapper.parentElement) { + const mobileBtn = btn.cloneNode(true); + productContentWrapper.insertAdjacentElement('afterend', mobileBtn); + } + } + // Attach anchors and buttons to quick view container + const link = prod.querySelector('a'); + const button = prod.querySelector('button'); + if (link) cardPictureContainer.appendChild(link); + if (button) cardPictureContainer.appendChild(button); + } + + // Animate/Close quick view sidebar, handle basket state + closeRightAside(prodInfoContainer, asideProdMenu) { + asideProdMenu.classList.remove('active'); + setTimeout(() => { + this.htmlEl.classList.remove('activeAside'); + }, 300); + if (this.basketSubmitted) { + const rightAsideProdInfo = document.querySelector('.right-aside__prod-info'); + const navRightAside = document.querySelector('.right-aside__nav'); + const customAdded = document.getElementById('quick_view_basket'); + rightAsideProdInfo.style.position = 'fixed'; + navRightAside.style.position = 'static'; + this.basketSubmitted = false; + prodInfoContainer.classList.remove("hidden"); + customAdded.classList.add("hidden"); + } + } + + // Set up main product quick view handler on search area + renderProduct(asideProdMenu, prodInfoContainer, customAdded) { + document.getElementById("search")?.addEventListener("click", async (e) => { + const hoverBtn = e.target.closest(".hover-btn"); + if (!hoverBtn) return; + e.stopPropagation(); + this.htmlEl.classList.add("activeAside"); + asideProdMenu.classList.add("active"); + if (prodInfoContainer.hasChildNodes()) { + prodInfoContainer.innerHTML = ""; + } + try { + this.idmProductId = Number(e.target.closest(".product").dataset.product_id); + await this.fetchQuickViewProduct(this.idmProductId, prodInfoContainer, customAdded); + } catch (err) { + console.error(err); + } + // Wait for dynamic HTML elements before binding option listeners + const interval2 = setInterval(() => { + const sizesContainer = document.querySelector('.idm-sizes'); + const versionsContainer = document.querySelector('.idm-versions'); + if (sizesContainer && versionsContainer) { + clearInterval(interval2); + this.bindOption(prodInfoContainer, sizesContainer, 'currentSize', this.product.sizes, 'currentSize', 'size-choose', 'size', true); + this.bindOption(prodInfoContainer, versionsContainer, 'currentVersion', this.product.group.versions, 'currentVersion', 'version-choose', 'product', false); + } + }, 100); + }); + } + + // Fetch product data and build modal quick view HTML + async fetchQuickViewProduct(idmProductId, prodInfoContainer, customAdded){ + const query = ` + query { + product(productId: ${idmProductId}) { + product{ + id + name + link + icon + enclosuresImages{ url } + producer { id name link } + sizes { + id name code amount + price { + omnibusPrice {gross {formatted}} + crossedPrice {gross {formatted}} + beforeRebate {gross {formatted}} + beforeRebateDetails {youSavePercent} + price {gross {formatted}} + } + availability{description} + } + group{ + id + versions{ + id name productIcon link iconSmall + } + } + price { + price { gross { formatted } } + } + } + } + } + `; + const response = await fetch("/graphql/v1/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query }), + }); + const data = await response.json(); + this.product = data.data.product.product; + this.size = this.product?.sizes?.[0]; + prodInfoContainer.innerHTML = ""; + this.htmlQuickView = ` +
+ +
+
+
+

${this.product.producer.name}

+
${this.product.name}
+
+
+
+
+ ${this.product?.sizes?.[0]?.price?.price?.gross?.formatted} + ${ + this.product?.sizes?.[0]?.price?.crossedPrice?.gross?.formatted + ? `${this.product?.sizes?.[0]?.price?.crossedPrice?.gross?.formatted}` + : "" + } + brutto / szt +
+ + ${this.product?.sizes?.[0]?.price?.omnibusPrice?.gross?.formatted + ? `Najniższa cena z 30 dni przed obniżką: ${this.product?.sizes?.[0]?.price?.omnibusPrice?.gross?.formatted}` + : ""} + +
+
+ ${this.size.amount > 3 ? + ` + + + + ` + : this.size.amount > 0 ? + ` + + + + ` + : ` + + + + ` + } +

+ ${this.product?.sizes?.[0]?.availability?.description} +

+
+
+
+
+ ${ + this.product.sizes && this.product.sizes.length > 1 + ? ` +

Size: ${this.product.sizes[0].name}

` + + '
' + + this.product.sizes + .filter((s) => s.id !== "uniw") + .map((s, i) => + `` + ) + .join("") + + '
' + : "" + } +
+
+ ${ + this.product.group.versions && this.product.group.versions.length > 1 + ? ` +

Type: ${this.product.group.versions.filter((s)=>s.id === this.product.id)?.[0]?.name}

` + + '
' + + this.product.group.versions + .filter((s) => s.id !== "uniw") + .sort((a, b) => a.name.localeCompare(b.name)) + .map((s, i) => + `` + ) + .join("") + + '
' + : "" + } +
+
+
+ ${this.idmAddToBasket(this.product, this.size)} +
+ +
+ `; + prodInfoContainer.insertAdjacentHTML("afterbegin", this.htmlQuickView); + // Button state may depend on async HTML render - poll for element + // const interval2 = setInterval(() => { + // const addToBasketButton = document.querySelector( + // ".btn.--solid.--medium.idm-products-banner__add-to-basket-button" + // ); + // if (addToBasketButton) { + // clearInterval(interval2); + // if (!this.size.amount) { + // addToBasketButton.classList.add("inactive"); + // } else { + // addToBasketButton.classList.remove("inactive"); + // } + // } + // }, 100); + if (!this.product) throw new Error("err"); + return this.product; + } + // Generic binding for size/version choices, handles UI and fetches updates as needed + bindOption(prodInfoContainer, container, inputName, collection, productProp, labelClass, basketHiddenInput, updatePrice = false) { + prodInfoContainer.addEventListener('click', async (e) => { + const itemEl = e.target.closest('button'); + if (!itemEl || !container.contains(itemEl)) return; + container.querySelectorAll('button.active').forEach(btn => btn.classList.remove('active')); + itemEl.classList.add('active'); + const itemId = itemEl.dataset.id; + let itemObj = collection.find(item => String(item.id) === String(itemId)); + this.product[productProp] = itemObj; + const labelEl = container.closest('.product-info__part').querySelector(`.${labelClass}`); + if (labelEl) labelEl.textContent = itemEl.textContent || itemObj.name; + const hiddenInput = container.closest('.product-info__part')?.querySelector(`input[name="${inputName}"]`); + if (hiddenInput) hiddenInput.value = this.escapeHtml(itemObj.id); + const hiddenBasketInputEl = document.querySelector('.idm-products-banner__add-to-basket-form') + ?.querySelector(`input[name="${basketHiddenInput}"]`); + if (hiddenBasketInputEl) hiddenBasketInputEl.value = this.escapeHtml(itemObj.id); + const quickViewPriceEl = container.closest('.product-info__part').querySelector('.idm__quick-view-price'); + const quickViewOmnibusEl = container.closest('.product-info__part').querySelector('.price-omnibus'); + if (!updatePrice) { + // When switching product version, fetch new product data and re-bind + const idmProductId = Number(itemId); + const newProduct = await this.fetchQuickViewProduct(idmProductId, prodInfoContainer); + this.bindOption( + prodInfoContainer, + prodInfoContainer.querySelector('.idm-versions'), + 'currentVersion', + newProduct.group.versions, + 'currentVersion', + 'version-choose', + 'product', + false + ); + this.bindOption( + prodInfoContainer, + prodInfoContainer.querySelector('.idm-sizes'), + 'currentSize', + newProduct.sizes, + 'currentSize', + 'size-choose', + 'size', + true + ); + return; + } + + // Fast price info update just for size pick + if (updatePrice && itemObj.price && quickViewPriceEl && quickViewOmnibusEl) { + const crossedPriceVal = itemObj.price.crossedPrice?.gross?.formatted; + const regularPriceVal = itemObj.price.price?.gross?.formatted; + const omnibusPriceVal = itemObj.price.omnibusPrice?.gross?.formatted; + const wrapper = document.querySelector('.idm-products-banner__qty'); + this.size = itemObj + this.idmAddToBasket(this.product, this.size) + this.amountToBasket(wrapper) + quickViewPriceEl.innerHTML = ` + ${regularPriceVal} + ${crossedPriceVal ? `${crossedPriceVal} ` : ""} + brutto / szt + `; + quickViewOmnibusEl.innerHTML = omnibusPriceVal + ? `Najniższa cena z 30 dni przed obniżką: ${omnibusPriceVal}` + : ""; + } + }); + } + + // Handles add-to-basket GraphQL mutation and in-app UI transitions + addToBasketQuery() { + const addToBasketButton = document.querySelector( + ".btn.--solid.--medium.idm-products-banner__add-to-basket-button" + ); + const customAdded = document.getElementById('quick_view_basket'); + const prodInfoContainer = document.querySelector('.prod-info__container'); + const addToBasketForm = document.querySelector(".idm-products-banner__add-to-basket-form"); + addToBasketForm.addEventListener("submit", async (el) => { + el.preventDefault(); + const qtyElement = document.querySelector('.idm-products-banner__qty-input'); + const sellBy = qtyElement.value; + try { + const query = ` + mutation { + addProductsToBasket(ProductInput: { + id: ${Number(this.product.id)}, + size: "${this.size.id}", + quantity: ${Number(sellBy)} + }) { + status + } + } + `; + const res = await fetch("/graphql/v1/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query }), + }); + if (!res.ok && res.status !== 301 && res.status !== 302) { + throw new Error(`Błąd HTTP: ${res.status}`); + } + const result = await res.json(); + const status = result?.data?.addProductsToBasket?.status; + if (status && (status.toLowerCase() === 'success' || status === 'SUCCESS')) { + this.updateBasketAfterAdd(); + } else { + throw new Error(`Basket update failed — status = ${status}`); + } + await this.renderCartItemsJS(); + } catch (error) { + if (typeof Alertek !== 'undefined') { + Alertek.show_alert("Coś poszło nie tak"); + } else { + alert("Coś poszło nie tak"); + } + } + + // Hide quickview and show confirmation after adding + const rightAsideProdInfo = document.querySelector('.right-aside__prod-info'); + const navRightAside = document.querySelector('.right-aside__nav'); + navRightAside.style.position = 'absolute'; + prodInfoContainer.classList.add("hidden"); + customAdded.classList.remove("hidden"); + this.basketSubmitted = true; + await this.getCartItems(); + return true; + }); + return false; + } + + updateBasketAfterAdd() { + if (typeof menu_basket_cache === 'function') { + menu_basket_cache(false); + } else { + console.warn("menu_basket_cache is not defined."); + } + } + + // Dynamically render add-to-basket UI form and connect handlers + idmAddToBasket(product, size) { + const getElAmmountInCart = app_shop.vars.basket; + + const getThisProduct = getElAmmountInCart.products.filter( + (s) => s.id === String(this.product.id) && s.size === String(this.size.id) + ); + + + const sellBy = size?.unitSellby || 1; + const precision = size?.unitPrecision || 0; + let max = (typeof size?.amount === 'number' && size.amount > 0) ? size.amount : ''; + const amountInCart = getThisProduct?.[0]?.count ? getThisProduct[0].count : 0; + + let inactive = "" + if (amountInCart && amountInCart >= max){ + inactive = "inactive"; + max = ''; + }else if(amountInCart){ + max -= amountInCart; + } + + console.log("getThisProduct", size.amount, "max", max) + + const buttonsToRender = ` +
+ + + +
+ + + +
+ +
`; + + // Wait for container before rendering/attaching qty logic + const interval = setInterval(() => { + const container = document.querySelector('.idm-products-banner__add-to-basket'); + if (container) { + clearInterval(interval); + container.innerHTML = buttonsToRender; + this.addToBasketQuery(); + } + }, 100); + const interval2 = setInterval(() => { + const wrapper = document.querySelector('.idm-products-banner__qty'); + if (wrapper) { + clearInterval(interval2); + this.amountToBasket(wrapper, max); + } + }, 100); + return ''; + } + + // Quantity +/- and validation for add to basket + amountToBasket(wrapper, max){ + wrapper.addEventListener('click', async (e) => { + if (!wrapper) return; + + const input = wrapper.querySelector('.idm-products-banner__qty-input'); + const step = parseFloat(wrapper.dataset.sellBy || '1'); + const precision = parseInt(wrapper.dataset.precision || '0'); + max = parseFloat(max || '999999'); + + let current = parseFloat(input.value) || 0; + if (e.target.classList.contains('idm-products-banner__qty-increase')) { + current += step; + if (current > max) current = max; + } else if (e.target.classList.contains('idm-products-banner__qty-decrease')) { + current -= step; + if (current < step) current = step; + } + input.value = current.toFixed(precision); + }); + document.addEventListener('blur', (e) => { + if (!e.target.classList.contains('idm-products-banner__qty-input')) return; + const input = e.target; + const wrapper = input.closest('.idm-products-banner__qty'); + const step = parseFloat(wrapper.dataset.sellBy || '1'); + const precision = parseInt(wrapper.dataset.precision || '0'); + const max = parseFloat(wrapper.dataset.max || '999999'); + let val = parseFloat(input.value); + if (isNaN(val) || val < step) { + val = step; + } else if (val > max) { + val = max; + } else { + val = Math.round(val / step) * step; + } + input.value = val.toFixed(precision); + }, true); + } + + // Returns after basket global variable is updated, for UI sync + getCartItems() { + const cartItems = app_shop.vars.basket; + return new Promise((resolve) => { + const interval = setInterval(() => { + const newCartItems = app_shop.vars.basket; + if (cartItems.productsNumber !== newCartItems.productsNumber) { + clearInterval(interval) + resolve(newCartItems); + } + }, 100); + }); + } + + // Rebuilds basket preview after add + async renderCartItemsJS() { + const container = document.getElementById('cartItemsContainer'); + container.innerHTML = ''; + const products = (await this.getCartItems()).products; + + products.forEach(product => { + const { + id, + link, + name, + icon, + size_name, + count, + price_unit, + prices + } = product; + const block = document.createElement('div'); + block.className = 'added__block'; + block.innerHTML = ` +
+ + ${name} + +
+

+ + ${name} + +

+
+
+ ${Number(prices.gros).toFixed(2).replace('.', ',')} zł + / ${price_unit} +
+ + + ${Number(prices.worth_gros).toFixed(2).replace('.', ',')} zł + + brutto + +
+
    +
  • Ilość: ${count}
  • + ${size_name && size_name !== 'UNIWERSALNY' ? ` +
  • Rozmiar: ${size_name}
  • + ` : ''} +
+
+
+ `; + this.updateShippingProgressBar() + container.appendChild(block); + }); + } + + // Updates shipping progress bar UI according to basket total + updateShippingProgressBar() { + const basket = app_shop.vars.basket; + if (!basket || !basket.shippingLimitFree) return; + const totalWorth = parseFloat(basket.worth || 0); + const shippingLimit = parseFloat(basket.shippingLimitFree || 0); + const toShippingFree = parseFloat(basket.toShippingFree || 0); + const toShippingFreeFormatted = basket.toShippingFree_formatted || "0,00 zł"; + const barEl = document.getElementById("shipping-bar"); + const barCompletion = document.getElementById("shipping-bar-completion"); + const amountEl = document.getElementById("shipping-left-amount"); + const freeEl = document.getElementById("shipping-bar-free"); + const notFreeText = document.getElementById("shipping-text-not-free"); + if (shippingLimit > 0 && toShippingFree > 0) { + const percentage = Math.min( + 100, + ((shippingLimit - toShippingFree) / shippingLimit) * 100 + ); + barCompletion.style.width = percentage + "%"; + amountEl.textContent = toShippingFreeFormatted; + barEl.style.display = "block"; + freeEl.style.display = "none"; + } else { + const shippingfreeBar = document.getElementById('shipping-bar-free').querySelector('.added__shippingfree_bar'); + shippingfreeBar.classList.add('enough'); + barEl.style.display = "none"; + freeEl.style.display = "block"; + } + } +} + +// Initialize quick view system +const quickView = new ProductQuickView(); +quickView.init();