/////////////////////////////////////////////////////////// // TEXT // LITERAŁY const idmHotspotTextObject = { ["Kod rabatowy"]: , ["Okazja"]: , ["Promocja"]: , ["Bestseller"]: , ["Nowość"]: , ["Ilość"]: , ["Porównaj"]: , ["Dodaj do ulubionych"]: , ["Najniższa cena"]: , ["Najniższa cena z 30 dni przed obniżką"]: , ["Zwiększ ilość"]: , ["Zmniejsz ilość"]: , ["Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki"]: , ["Cena regularna"]: , ["Cena bez kodu"]: , ["Cena nadchodząca od"]: , ["Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę"]: , ["Nie znaleziono produktów"]: , ["Błąd przy pobieraniu danych"]: , ["Kliknij, by przejść do formularza kontaktu"]: , ["Cena na telefon"]: , ["Dodany"]: , ["Wystąpił błąd"]: , ["Do koszyka"]: , ["Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]: , ["Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]: , ["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"]: , ["Nie znaleziono kontenera"]: , ["Nie znaleziono metody graphql"]: , ["Drugie Zdjęcie"]: , }; // STRING // const idmHotspotTextObject = { // "Kod rabatowy": "Kod rabatowy", // "Okazja": "Okazja", // "Promocja": "Promocja", // "Bestseller": "Bestseller", // "Nowość": "Nowość", // "Ilość": "Ilość", // "Porównaj": "Porównaj", // "Dodaj do ulubionych": "Dodaj do ulubionych", // "Najniższa cena": "Najniższa cena", // "Najniższa cena z 30 dni przed obniżką": "Najniższa cena z 30 dni przed obniżką", // "Zwiększ ilość": "Zwiększ ilość", // "Zmniejsz ilość": "Zmniejsz ilość", // "Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki": // "Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki", // "Cena regularna": "Cena regularna", // "Cena bez kodu": "Cena bez kodu", // "Cena nadchodząca od": "Cena nadchodząca od", // "Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę": // "Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę", // "Nie znaleziono produktów": "Nie znaleziono produktów", // "Błąd przy pobieraniu danych": "Błąd przy pobieraniu danych", // "Kliknij, by przejść do formularza kontaktu": // "Kliknij, by przejść do formularza kontaktu", // "Cena na telefon": "Cena na telefon", // "Dodany": "Dodany", // "Wystąpił błąd": "Wystąpił błąd", // "Do koszyka": "Do koszyka", // "Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:": // "Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:", // "Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:": // "Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:", // "Wystąpił błąd z inicjalizacją. Proszę odśwież stronę": // "Wystąpił błąd z inicjalizacją. Proszę odśwież stronę", // "Nie znaleziono kontenera": "Nie znaleziono kontenera", // "Nie znaleziono metody graphql": "Nie znaleziono metody graphql", // "Drugie Zdjęcie": "Drugie Zdjęcie", // }; /////////////////////////////////////////////// // GraphQL // ogolne const IDM_PRICE_QUERY = `price { rebateCodeActive price { gross { value formatted } } omnibusPrice { gross { value formatted } } omnibusPriceDetails { unit { gross { value formatted } } youSavePercent omnibusPriceIsHigherThanSellingPrice newPriceEffectiveUntil { formatted } } max { gross { value formatted } } unit { gross { value formatted } } unitConvertedPrice { gross { value formatted } } youSavePercent beforeRebate { gross { value formatted } } beforeRebateDetails { youSavePercent unit { gross { value formatted } } } advancePrice { gross { value formatted } } suggested { gross { value formatted } } rebateNumber { number gross { value formatted } } }`; const IDM_PRODUCT_QUERY = `id type name icon iconSecond iconSmall iconSmallSecond link zones producer{ name } category{ name } sizes{ id amount name ${IDM_PRICE_QUERY} } group{ id name link versions{ id name icon iconSecond iconSmall iconSmallSecond link } } opinion{ rating, count } awardedParameters { name id description values { name id } } enclosuresImages { position url } points unit{ id, name, singular, plural, fraction, sellBy, precision, unitConvertedFormat } ${IDM_PRICE_QUERY}`; // 1. products const IDM_PRODUCTS_GQL = (args) => JSON.stringify({ query: `{ products(${args}){ took products{ ${IDM_PRODUCT_QUERY} } } }` }); // 2. hotspots const IDM_HOTSPOTS_GQL = (args) => JSON.stringify({ query: `{ hotspots(${args}){ took name url products{ ${IDM_PRODUCT_QUERY} } } }` }); // 3. single product const IDM_PRODUCT_GQL = (args) => JSON.stringify({ query: `{ product(${args}){ product{ ${IDM_PRODUCT_QUERY} } } }` }); // ADD TO BASKET const IDM_HOTSPOT_ADD_TO_BASKET = (t, e, a) => JSON.stringify({ query: `mutation { addProductsToBasket(ProductInput: {id: ${t}, size: "${e}", quantity: ${a}}) { status results { status error { code message } } } }` }); ///////////////////////////////////////// // JS app_shop.fn.idmGetOmnibusDetails = (options) => { const { productData, sizeId, priceType = app_shop.vars.priceType, } = options || {}; if (!productData) return false; const sizeData = productData.sizes.find((size) => size.id === sizeId) || productData; if (!sizeData?.price) return false; const classes = { add: [], remove: ['--omnibus', '--omnibus-short', '--omnibus-code', '--omnibus-code-short', '--omnibus-new-price', '--omnibus-higher'], }; const activeLabel = {}; const omnibusPrice = sizeData.price?.omnibusPriceDetails?.unit?.[priceType]?.formatted || sizeData.price.omnibusPrice[priceType]?.formatted; if (!omnibusPrice) { return { classes, }; } // Omnibus classes.add.push('--omnibus'); classes.remove = classes.remove.filter((item) => item !== '--omnibus'); const sellBy = productData?.unit?.sellBy; const unitMaxPrice = sizeData?.price?.unit?.[priceType]?.formatted && sizeData.price.max?.[priceType]?.value ? format_price(parseFloat(sizeData.price.max?.[priceType]?.value) * parseFloat(sellBy), { mask: app_shop.vars.currency_format, currency: app_shop.vars?.currency?.symbol, currency_space: app_shop.vars.currency_space, currency_before_price: app_shop.vars.currency_before_value, }) : false; const maxPrice = unitMaxPrice || sizeData.price.max?.[priceType]?.formatted; // Skrócona wersja omnibusa if (!maxPrice || maxPrice === omnibusPrice) { classes.add.push('--omnibus-short'); classes.remove = classes.remove.filter((item) => item !== '--omnibus-short'); } // Aktywny kod rabatowy if (app_shop.vars.omnibus?.rebateCodeActivate && sizeData.price?.rebateCodeActive) { classes.add.push('--omnibus-code'); activeLabel.rebateCodeActive = `${omnibusDetailsTxt?.['Kod rabatowy']}`; classes.remove = classes.remove.filter((item) => item !== '--omnibus-code'); } // Skrócona wersja omnibusa, gdy aktywny kod rabatowy const beforeRebatePrice = sizeData.price.beforeRebateDetails?.unit?.[priceType]?.formatted || sizeData.price.beforeRebate[priceType]?.formatted; if (app_shop.vars.omnibus?.rebateCodeActivate && beforeRebatePrice === omnibusPrice && sizeData.price?.rebateCodeActive) { classes.add.push('--omnibus-code-short'); classes.remove = classes.remove.filter((item) => item !== '--omnibus-code-short'); } // Nadchodząca cena const newDate = sizeData.price.omnibusPriceDetails?.newPriceEffectiveUntil?.formatted; if (newDate && maxPrice) { classes.add.push('--omnibus-new-price'); classes.remove = classes.remove.filter((item) => item !== '--omnibus-new-price'); } // Cena omnibusa wyższa niż cena sprzedaży const higher = sizeData.price.omnibusPriceDetails?.omnibusPriceIsHigherThanSellingPrice; if (higher) { classes.add.push('--omnibus-higher'); classes.remove = classes.remove.filter((item) => item !== '--omnibus-higher'); } // label okazja if ((!higher || newDate) && !activeLabel?.rebateCodeActive) { activeLabel.bargain = `${omnibusDetailsTxt?.['Okazja']}`; } // label promocja if (Object.keys(activeLabel)?.length === 0) { activeLabel.bargain = `${omnibusDetailsTxt?.['Promocja']}`; } let omnibusPercentSign = ''; if (higher) { omnibusPercentSign = '-'; } else if (sizeData.price.omnibusPriceDetails?.youSavePercent !== 0) { omnibusPercentSign = '+'; } const omnibusPercent = `${omnibusPercentSign}${sizeData.price.omnibusPriceDetails?.youSavePercent}%`; const omnibus = { price: omnibusPrice, visible: true, percent: omnibusPercent, html: `${omnibusDetailsTxt['Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki']}: ${omnibusPrice}${omnibusPercent}`, }; const max = (maxPrice) ? { max: { price: maxPrice, visible: true, percent: `-${sizeData.price.youSavePercent}%`, html: `${omnibusDetailsTxt['Cena regularna']}: ${maxPrice}-${sizeData.price.youSavePercent}%`, }, } : {}; const beforeRebate = (beforeRebatePrice) ? { beforeRebate: { price: beforeRebatePrice, visible: !!classes.add.includes('--omnibus-code'), percent: `-${sizeData.price.beforeRebateDetails?.youSavePercent}%`, html: `${omnibusDetailsTxt['Cena bez kodu']}: ${beforeRebatePrice} -${sizeData.price.beforeRebateDetails?.youSavePercent}%`, }, } : {}; const newPriceEffectiveUntil = (newDate) ? { newPriceEffectiveUntil: { date: newDate, price: maxPrice, visible: !!classes.add.includes('--omnibus-new-price'), html: `${omnibusDetailsTxt['Cena nadchodząca od']} ${newDate}: ${maxPrice}`, }, } : {}; const omnibusLabel = { omnibusLabel: { name: Object.keys(activeLabel)?.[0] || '', html: activeLabel[Object.keys(activeLabel)?.[0]], }, }; return { classes, omnibus, ...max, ...beforeRebate, ...newPriceEffectiveUntil, ...omnibusLabel, }; }; /** * Klasa IdmHotspot * ============================ * Odpowiada za tworzenie, renderowanie i obsługę dynamicznych hotspotów produktowych. * Pobiera dane przez GraphQL, renderuje produkty, obsługuje dodawanie do koszyka, * inicjuje Swipera, ustawia wysokości, nasłuchuje zdarzeń i wykonuje lazy loading. */ class IdmHotspot{ // ============================ // DOMYŚLNE USTAWIENIA SWIPERA // ============================ static idmDefaultSwiperConfig = { // true, false, obiekt z opcjami swipera loop: false, autoHeight: false, spaceBetween: 16, slidesPerView: 1.4, centeredSlides: true, centeredSlidesBounds: true, breakpoints: { 550: { slidesPerView: 3, centeredSlides: true, centeredSlidesBounds: true, }, 979: { slidesPerView: 4, centeredSlides: false, }, } } // ============================ // DOMYŚLNE OPCJE HOTSPOTA // ============================ static idmDefaultHotspotOptions = { options: { lazy: true, devMode: false, callbackFn: ()=>{}, omnibusTooltip: false, // switchImage: false, // POKAZANIE showOpinions: false, showSecondImage: false, // DODAWANIE addToBasket: true, // true, false, "range" addToFavorites: false, // Wymaga zmian szablonowych addToCompare: false, // WYBÓR // selectSize: false, // selectVersion: false, // SWIPER swiper: true, swiperScrollbar: false, } } /** * Konstruktor * @param {object} object - Dane konfiguracyjne hotspotu */ constructor({id, title, classes, placement, source, query, options = {}, hotspotEl, products}){ this.id = id || ""; this.title = title || ""; this.classes = classes || ""; this.placement = placement || {}; this.source = source || {}; this.query = query || {}; // this.type = type; this.products = products || null; this.hotspotEl = hotspotEl || null; // Merge defaults this.options = { ...IdmHotspot.idmDefaultHotspotOptions.options, ...options, }; // // this.hotspots = {}; this.priceType = app_shop?.vars?.priceType || "gross"; // bind this do funkcji eventowych this.handleAddToBasket = this.handleAddToBasket.bind(this); this.handleQuantityButtonClick = this.handleQuantityButtonClick.bind(this); this.handleQuantityInputChange = this.handleQuantityInputChange.bind(this); this.handleAddToCompare = this.handleAddToCompare.bind(this); this.handleAddToFav = this.handleAddToFav.bind(this); this.handleShowSecondImage = this.handleShowSecondImage.bind(this); this.handleHideSecondImage = this.handleHideSecondImage.bind(this); this.handleSelectVersion = this.handleSelectVersion.bind(this); this.init(); } // ======================================================== // ASYNC – POBIERANIE DANYCH Z GRAPHQL // ======================================================== /** * Przygotowuje funkcję i zapytanie GraphQL w zależności od typu danych. */ getQueryData(){ let graphFn, query; let queryMarkup = ""; if(this.source?.hotspotsType){ graphFn = IDM_HOTSPOTS_GQL; queryMarkup += `hotspot: ${this.source.hotspotsType}, limit: 16`; }else{ graphFn = IDM_PRODUCTS_GQL; if(this.source?.productsId){ queryMarkup += `productsId: [${this.source.productsId}],`; } if(this.source?.productsMenu){ queryMarkup += `navigation: ${this.source.productsMenu},`; } if(this.source?.producersId){ queryMarkup += `producers: [${this.source.producersId}],`; } if(this.source?.seriesId){ queryMarkup += `series: [${this.source.seriesId}],`; } if(this.source?.parametersId){ queryMarkup += `parameters: [${this.source.parametersId.reduce((acc,val)=> acc + `{id: ${val}}`,"")}],`; } if(this.source?.priceRange){ queryMarkup += `priceRange: {from: ${+this.source.priceRange?.from || 0}, to: ${+this.source.priceRange?.to || 0}},`; } } query = `searchInput: { ${queryMarkup} }` return [graphFn, query]; } /** * Ustawia dane zapytania GraphQL wewnątrz instancji. */ setQueryData(){ const [graphFn, queryString] = this.getQueryData(); this.query.graphFn = graphFn; this.query.string = queryString; } /** * Pobiera dane hotspotu z API GraphQL. */ async getHotspotData(){ if(this.products) return; try{ const res = await fetch(`/graphql/v1/`, { method: "POST", headers: { "Content-Type": "application/json", }, body: this.query.graphFn(this.query.string), }); const data = await res.json(); const products = data?.data?.[this.query.graphFn === IDM_HOTSPOTS_GQL ? "hotspots" : "products"]?.products; if(!products || !products.length) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); this.products = products; this.title = this.title || data?.data?.hotspots?.name || ""; }catch(err){ console.error(idmHotspotTextObject["Błąd przy pobieraniu danych"], err); return null; } } // ======================================================== // MARKUP – TWORZENIE HTML PRODUKTÓW // ======================================================== /** * Tworzy markup dla wszystkich produktów w hotspotcie. */ markup(){ let markup = ""; this.products.forEach((prod)=>{ markup += this.markupProduct(prod); }) return markup; } /** * Tworzy markup dla pojedynczego produktu. */ markupProduct(prod){ // markup pojedynczego produktu let singleMarkup = ""; singleMarkup += `
${this.markupProductInnerHTML(prod)}
`; return singleMarkup; } markupProductInnerHTML(prod){ // IDM DO POPRAWKI const prodExchangedData = app_shop?.fn?.getOmnibusDetails?.({productData: prod}) || app_shop.fn?.idmGetOmnibusDetails({productData: prod}); return `
${this.markupAdditional(prod)}
${this.markupImage(prod)} ${this.markupLabel(prod)}
${this.markupVersions(prod)} ${this.markupOpinions(prod)} ${prod.name}
${this.markupPrice(prod, prodExchangedData)}
${this.markupAddToBasket(prod)}
`; } markupAdditional(prod){ return ` ${this.markupCompare(prod)} ${this.markupFavorite(prod)} `; } markupCompare(prod){ if(!this.options?.addToCompare) return ""; return `` } markupFavorite(prod){ if(!this.options?.addToFavorites || typeof this.addToFavFn !== "function") return ""; return ` `; } markupVersions(prod){ if(!this.options?.selectVersion || !prod.group?.versions || prod.group?.versions?.length === 1 ) return ""; return `
${prod.group.versions.reduce((acc, val)=>{ return acc + `${val.name}`; },"")}
` } markupImage(prod){ let markup = ""; if(prod.iconSmallSecond && prod.iconSecond) markup +=` ${prod.name} `; else if(prod?.iconSmall) markup += ` ${prod.name} `; else markup += `${prod.name}`; if(this.options?.showSecondImage && prod.enclosuresImages?.[1]?.url) markup += `${prod.name} - ${idmHotspotTextObject[`; return markup; } markupLabel(prod){ let labelMarkup = "" // labele zones if(prod.zones?.find(zone => zone ==="bestseller")) labelMarkup += `${idmHotspotTextObject["Bestseller"]}`; if(prod.zones?.find(zone => zone ==="news")) labelMarkup += `${idmHotspotTextObject["Nowość"]}`; const omnibusPrice = prod.price?.omnibusPriceDetails?.unit?.[this.idmPriceType]?.formatted || prod.price.omnibusPrice[this.idmPriceType]?.formatted; if(omnibusPrice){ const newDate = prod.price.omnibusPriceDetails?.newPriceEffectiveUntil?.formatted; const higher = prod.price.omnibusPriceDetails?.omnibusPriceIsHigherThanSellingPrice; if (app_shop.vars.omnibus?.rebateCodeActivate && prod.price?.rebateCodeActive) { labelMarkup += `${idmHotspotTextObject["Kod rabatowy"]}`; }else if ((!higher || newDate) && !activeLabel?.rebateCodeActive) { labelMarkup += `${idmHotspotTextObject["Okazja"]}`; }else { labelMarkup += `${idmHotspotTextObject["Promocja"]}`; } } return labelMarkup; } markupOpinions(prod){ if(!this.options?.showOpinions) return ""; return `
${prod.opinion.rating.toFixed(2)} / 5.00
` } markupPrice(prod, prodExchangedData){ const price = prod.price.price[this.priceType]; const unit = prod.unit; const pointsPrice = prod?.points; const convertedPrice = prod.price?.unitConvertedPrice?.[this.priceType]?.formatted; return ` ${price.formatted} / ${unit?.sellBy} ${unit?.sellBy > 1 ? unit?.plural : unit?.singular} ${convertedPrice && prod.unit?.unitConvertedFormat ? `(${convertedPrice} / ${prod.unit?.unitConvertedFormat})` : ""} ${pointsPrice ? `${pointsPrice} pkt.` : ""} ${price.value === 0 ? `${idmHotspotTextObject["Cena na telefon"]}` : ""} ${prodExchangedData?.beforeRebate?.visible ? `${prodExchangedData?.beforeRebate?.html}` : ""} ${prodExchangedData?.newPriceEffectiveUntil?.visible ? `${prodExchangedData?.newPriceEffectiveUntil?.html}` : ""} ${this.markupOmnibus(prodExchangedData?.omnibus)} ${prodExchangedData?.max?.visible ? `${prodExchangedData?.max?.html}` : ""} `; } /** * Tworzy znacznik HTML dla omnibusa. * * @param {object} omnibus - dane omnibusa * @param {string} omnibus.html - kod html zwykłego omnibusa * @param {string} omnibus.percent - % przeceny omnibusa * @param {string} omnibus.price - cena omnibusa * @param {boolean} omnibus.visible - czy omnibus jest widoczny * @returns {string} Zwraca HTML dla sekcji omnibusa lub pusty string, jeśli niewidoczny. */ markupOmnibus(omnibus){ if(!omnibus?.visible) return ""; if(!this.options.omnibusTooltip) return `${omnibus?.html}`; else return ` ${idmHotspotTextObject["Najniższa cena"]}: ${omnibus.price} ${omnibus.percent}

${idmHotspotTextObject["Najniższa cena z 30 dni przed obniżką"]}

`; } markupAddToBasket(prod){ let markup = ""; if(!this.options.addToBasket) return markup; // link do produktu jak nie jest to zwykły produkt if(prod.type !== "product" || prod.sizes[0].amount === 0) markup = `Zobacz produkt`; else if(this.options.addToBasket === "range") // +- markup = `
${this.markupSize(prod)}
`; else // Zwykłe dodanie do koszyka markup = `
${this.markupSize(prod)}
`; return markup; } markupSize(prod){ return ``; // if(!this.options?.selectSize || prod.sizes.length === 1) return ``; // const sizesName = `${this.id}-${prod.id}`; // return ` //
// ${prod.sizes.reduce((acc, val)=>{ // const inputId = `${sizesName}-${val.id}`; // return acc + ` // // ` // }, "")} //
// `; } markupHotspotContainer(){ return `
${this?.title ? `

${this.title}

` : ""} ${this.markupHotspotSwiperContainer()}
`; } markupHotspotSwiperContainer(productsHTML=""){ return `
${productsHTML}
`; } // ======================================================== // HANDLERY ZDARZEŃ // ======================================================== /** * Obsługuje dodanie produktu do koszyka (GraphQL). */ async handleAddToBasket(e){ const formEl = e.target.closest("form.add_to_basket"); if(!formEl) return; try{ // pobieranie danych i elementów formEl.classList.add("--loading") const buttonEl = formEl.querySelector(".add_to_basket__button"); e.preventDefault(); const id = formEl.querySelector("input[name='product']")?.value; const size = formEl.querySelector("input[type='hidden'][name='size']")?.value; const number = formEl.querySelector("input[name='number']")?.value; // dodanie do koszyka const res = await fetch(`/graphql/v1/`, { method: "POST", headers: { "Content-Type": "application/json", }, body: IDM_HOTSPOT_ADD_TO_BASKET(id, size, number) }); const data = await res.json(); // Błąd if(data?.data?.addProductsToBasket?.status !== "success") throw new Error(data); else{ localStorage.setItem('addedtoBasket', true); // Obsługiwanie sukcesu app_shop.graphql.trackingEvents(res); buttonEl.classList.add("--success"); // Dodawanie do koszyka na stronie basketedit.php będzie wymagał innego indywidualnego kodu!!!!! app_shop.fn?.menu_basket_cache?.(); // STRONA KOSZYKA if(typeof app_shop.fn?.basket?.reloadForm === "function"){ const existingBasketBlockQuantity = document.querySelector(`.basket__block[data-product-id="${id}"][data-product-size="${size}"] input.buy__more_input.quantity__input[type="number"]`); // Dodanie do ilości produktu jeśli już był dodany do koszyka if(existingBasketBlockQuantity) existingBasketBlockQuantity.value = +existingBasketBlockQuantity.value + 1; // Przeładowanie koszyka na stronie basketedit.html app_shop.fn?.basket?.reloadForm(); } buttonEl.innerHTML = `${buttonEl.dataset.success}`; setTimeout(()=>{ buttonEl.innerHTML = `${buttonEl.dataset.text}`; buttonEl.classList.remove("--success"); }, 3000); } }catch(err){ console.error(err); Alertek.Error(idmHotspotTextObject["Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę"]); buttonEl.innerHTML = `${buttonEl.dataset.error}`; buttonEl.classList.add("--error") setTimeout(()=>{ buttonEl.classList.remove("--error") buttonEl.innerHTML = `${buttonEl.dataset.text}`; }, 3000); }finally{ formEl.classList.remove("--loading") } } /** * Obsługuje dodanie produktu do Porównania */ async handleAddToCompare(e){ const compareBtnEl = e.target.closest(".idm-products-banner__compare-btn"); const compareId = compareBtnEl?.dataset?.compareId; if (!compareBtnEl || !compareId) return; e.preventDefault(); try { compareBtnEl.classList.add("--loading"); const compareUrl = `/${app_shop?.vars?.language?.symbol || "pl"}/settings.html?comparers=add&product=${compareId}`; const res = await fetch(compareUrl); console.log(res); if (!res.ok) throw new Error(`${idmHotspotTextObject["Wystąpił błąd"]}`); compareBtnEl.classList.add("--success"); const compareContainerQuery = "#menu_compare_product"; if (document.querySelector(compareContainerQuery)) { app_shop.fn?.load( window.location.pathname, [[compareContainerQuery, compareContainerQuery]], function () {}, "?set_render=content" ); } setTimeout(() => { compareBtnEl.classList.remove("--success"); }, 2000); } catch (err) { console.error(err); Alertek.Error(`${idmHotspotTextObject["Coś poszło nie tak. Spróbuj ponownie później"]}`); } finally { compareBtnEl.classList.remove("--loading"); } } /** * Obsługuje dodanie produktu do Listy zakupowej */ handleAddToFav(e){ const favEl = e.target.closest(".product__favorite"); if(!favEl) return; this.addToFavFn([[favEl.dataset.productId, favEl.dataset.productSize]]); } /** * Obsługuje kliknięcia w przyciski +/− przy wyborze ilości. */ handleQuantityButtonClick(e){ if(e.target.classList.contains("idm-products-banner__qty-input")) return e.target.select(); const wrapper = e.target.closest(".idm-products-banner__qty"); const input = wrapper.querySelector(".idm-products-banner__qty-input"); const step = parseFloat(wrapper.dataset.sellBy || "1"); const precision = parseInt(wrapper.dataset.precision || "0"); const max = parseFloat(wrapper.dataset.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; this.rangeMaxAlert(max) } } else if (e.target.classList.contains("idm-products-banner__qty-decrease")) { current -= step; if (current < step){ current = step; this.rangeMinAlert(step) } } input.value = current.toFixed(precision); } /** * Obsługuje Pokazywanie zdjęcia na hover */ handleShowSecondImage(e){ const prodIconEl = e.target.closest(".product__icon"); if(!prodIconEl) return; prodIconEl.classList.add("--toggle-icon"); } handleHideSecondImage(e){ const prodIconEl = e.target.closest(".product__icon"); if(!prodIconEl) return; prodIconEl.classList.remove("--toggle-icon"); } /** * Walidacja zmian ilości w polu input. */ handleQuantityInputChange(e){ if(e.target.value > +e.target.max){ this.rangeMaxAlert(e.target.max) e.target.value = +e.target.max }else if(e.target.value < +e.target.min){ this.rangeMinAlert(e.target.min) e.target.value = +e.target.min; } } handleSelectVersion(e){ e.preventDefault(); const closestVersion = e.target.closest(".product__version_single:not(.--active)"); const prodEl = e.target.closest(".product"); if(!closestVersion || !prodEl) return; this.reloadProduct(prodEl, closestVersion.dataset.productId); } /** * Lazy-load hotspotu – wczytuje dane dopiero, gdy element pojawi się w viewportcie. */ handleObserveHotspotOnce() { if (!this.hotspotEl) return; const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { this.fillHotspot(); // run your callback obs.disconnect(); // stop observing after first trigger } }); }, { root: null, rootMargin: "0px", threshold: 0.1 }); observer.observe(this.hotspotEl); } // ======================================================== // FUNKCJE POMOCNICZE // ======================================================== /** * Wyświetla alert o maksymalnej ilości produktu. */ rangeMaxAlert(max){ Alertek.Error(`${idmHotspotTextObject["Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${max}`) } /** * Wyświetla alert o minimalnej ilości produktu. */ rangeMinAlert(min){ Alertek.Error(`${idmHotspotTextObject["Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${min}`) } /** * Ustawia jednakową wysokość elementów (np. nazw lub cen). */ setHeight(options){ const { selector, selectors, container } = options || {} if ((!selector && !selectors) || !container) return const containerElement = document.querySelector(container) if (!containerElement) return const adjustAllHeights = itemSelector => { const targets = containerElement.querySelectorAll(itemSelector) if (!targets.length) return targets.forEach(el => (el.style.minHeight = '')) const max = Math.max(...[...targets].map(el => el.offsetHeight || 0)) targets.forEach(el => (el.style.minHeight = `${max}px`)) } if (selector) adjustAllHeights(selector) if (selectors?.length) selectors.forEach(adjustAllHeights) } /** * Przeładowanie pojedynczego produktu */ async reloadProduct(prodEl, newProdId){ try{ prodEl.classList.add("--loading"); const res = await fetch(`/graphql/v1/`, { method: "POST", headers: { "Content-Type": "application/json", }, body: IDM_PRODUCT_GQL(`productId: ${newProdId}`), }); const data = await res.json(); const productData = data?.data?.product?.product; if(!productData) throw new Error("Nie udało się pobrać danych o produkcie"); const prodHTML = this.markupProductInnerHTML(productData); prodEl.dataset.id = newProdId; prodEl.innerHTML = prodHTML; if(productData.price.price[this.priceType].value === 0) prodEl.classList.add("--phone"); else prodEl.classList.remove("--phone"); this.initSingleEvent(prodEl); this.setHeight({ selectors: [ `#${this.id} .product__prices`, `#${this.id} .product__name`, ], container: `#${this.id} .products__wrapper`, }); }catch(err){ Alertek?.Error(idmHotspotTextObject["Błąd przy pobieraniu danych"]); console.error(err); }finally{ prodEl.classList.remove("--loading"); } } // ======================================================== // INICJALIZACJA // ======================================================== /** * Wykonywana po pełnej inicjalizacji hotspotu (Swiper, eventy, wysokości). */ async afterInit(){ try{ if(!this.hotspotEl) throw new Error("Nie znaleziono elementu"); if(this.title && !this.hotspotEl.querySelector(".hotspot__name.headline__wrapper")){ this.hotspotEl.querySelector(".hotspot.--initialized")?.insertAdjacentHTML("afterbegin", `

${this.title}

`); } await this.initSwiper(); // IDM setHeight this.setHeight({ selectors: [ `#${this.id} .product__prices`, `#${this.id} .product__name`, ], container: `#${this.id} .products__wrapper`, }); this.initEvents(); console.log(`Initialized hotspot #${this.id}`); if(typeof this.options?.callbackFn === "function") this.options?.callbackFn(); // WCZYTANIE PONOWNIE DLA kOSZYKA if(typeof app_shop.fn?.basket?.reloadForm === "function" && this.hotspotEl.closest("#content")){ app_shop.run(()=>{ this.init(); }, "all", "#Basket", true) } }catch(err){ console.error(idmHotspotTextObject["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"], err); } } initExternalFunctions(){ this.addToFavFn = app_shop.fn?.shoppingList?.addProductToList; } /** * Pobiera dane, wypełnia markup i inicjuje Swipera. */ async fillHotspot(){ // Zdefiniowanie funkcji do dodawania do ulubionych this.initExternalFunctions(); try{ if(!this.products){ if(!this?.query?.graphFn || !this?.query?.string) this.setQueryData(); // pobranie danych o produktach await this.getHotspotData(); if(!this.products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); } // Wstawienie markupa na strone if(this.hotspotEl.querySelector(".products.hotspot__products")) this.hotspotEl.querySelector(".products.hotspot__products").insertAdjacentHTML("beforeend", this.markup()); else if(this.hotspotEl.querySelector(".hotspot")){ this.hotspotEl.querySelector(".hotspot")?.insertAdjacentHTML("beforeend", this.markupHotspotSwiperContainer(this.markup())); } else{ throw new Error("Nie udało się wstawić produktów! Zła struktura HTML") } this.hotspotEl.classList.remove("idm-loading"); // init swiper + add to basket this.afterInit(); }catch(err){ console.error(idmHotspotTextObject["Wystąpił błąd"], err); this.hotspotEl.remove(); } } /** * Inicjuje instancję Swipera dla hotspotu. */ async initSwiper(){ try{ // swiper || slick if(this.options?.swiper){ // Opcje swipera if(typeof this?.options?.swiper === "boolean") this.options.swiper = IdmHotspot.idmDefaultSwiperConfig; // Wywołanie swipera const selectedSwiper = new HotspotSlider({ selector: `#${this.id} .swiper`, hotspotName: `${this.id}`, options: this.options.swiper, }); await selectedSwiper.init(); if(this.options.swiperScrollbar) new IdmSwiperProgress(selectedSwiper, `#${this.id} .swiper`); } }catch(err){ console.error(idmHotspotTextObject["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"], err); } } /** * Inicjuje eventy dla produktów w hotspotcie. */ initEvents(){ this.hotspotEl.querySelectorAll(".product").forEach(prodEl=>{ this.initSingleEvent(prodEl); }) } initSingleEvent(prodEl){ // DODAWANIE DO KOSZYKA if(this.options?.addToBasket){ const addToBasketEl = prodEl.querySelector("form.add_to_basket"); addToBasketEl?.addEventListener("submit", this.handleAddToBasket); // + - if(this?.options?.addToBasket === "range"){ addToBasketEl?.querySelector(".idm-products-banner__qty")?.addEventListener("click",this.handleQuantityButtonClick); addToBasketEl?.querySelector(".idm-products-banner__qty-input")?.addEventListener("input",this.handleQuantityInputChange); } } // Dodaj do ulubionych if(this.options?.addToFavorites && typeof this.addToFavFn === "function") prodEl.querySelector(".product__favorite")?.addEventListener("click", this.handleAddToFav); // Porównanie if(this.options?.addToCompare) prodEl.querySelector(".idm-products-banner__compare-btn")?.addEventListener("click", this.handleAddToCompare); // Hover drugie zdjęcie if(this.options?.showSecondImage){ const prodIconEl = prodEl.querySelector(".product__icon"); if(prodIconEl.querySelector(".product__image.--second")){ prodIconEl?.addEventListener("mouseover", this.handleShowSecondImage); prodIconEl?.addEventListener("mouseleave", this.handleHideSecondImage); } } // Wybór wersji if(this.options?.selectVersion) prodEl.querySelector(".product__versions")?.addEventListener("click", this.handleSelectVersion); } /** * Inicjuje kontener hotspotu w określonym miejscu DOM. */ initHotspotContainer(){ const selectedEl = document.querySelector(this?.placement.selector); if(!selectedEl) throw new Error(idmHotspotTextObject["Nie znaleziono kontenera"]); const markup = this.markupHotspotContainer(); selectedEl.insertAdjacentHTML(this.placement.insert || "afterend", markup); this.hotspotEl = document.getElementById(this.id); } /** * Główna metoda inicjalizująca hotspot (lazy lub natychmiast). */ async init(){ const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const dev = urlParams.get('dev') if(this.options?.devMode && dev !== "true") return console.error(`Brak włączonego devMode. Ramka ${this.id} nie mogła zostać utworzona!`); if(!this.hotspotEl || !document.contains(this.hotspotEl)) this.initHotspotContainer(); if(this.options?.lazy) this.handleObserveHotspotOnce(); else this.fillHotspot(); } } /* ============================================================== SWIPER PASEK ============================================================== */ class IdmSwiperProgress { constructor(swiper, selector) { this.swiper = swiper?.slider?.slider ?? swiper?.slider ?? swiper; this.selector = selector; this.scrollbarEl = null; this.progressEl = null; this.isDragging = false; this.init(); } init() { const el = document.querySelector(this.selector); if (!el || el.querySelector(".idm-scrollbar")) return; el.insertAdjacentHTML( "beforeend", `
` ); this.scrollbarEl = el.querySelector(".idm-scrollbar"); this.progressEl = this.scrollbarEl.querySelector(".idm-progress"); this.updateBarWidth(); this.addDragTracking(); this.swiper.on("progress", () => this.updateProgress()); this.swiper.on("breakpoint", () => {this.updateBarWidth()}); } updateBarWidth() { const { slidesPerGroup, slidesPerView } = this.swiper.params; const totalSlides = this.swiper.slides.length; const progressWidth = 100 / (((totalSlides - slidesPerView) / slidesPerGroup) + 1); this.progressEl.style.width = `${progressWidth}%`; if (progressWidth >= 100 || progressWidth <= 0) this.scrollbarEl.style.display = "none"; else this.scrollbarEl.style.display = ""; } updateProgress() { const progress = this.swiper.progress; const { slidesPerGroup, slidesPerView } = this.swiper.params; const totalSlides = this.swiper.slides.length; const progressWidth = 100 / (((totalSlides - slidesPerView) / slidesPerGroup) + 1); const newLeft = (100 - progressWidth) * progress; this.progressEl.style.left = `${Math.min( 100 - progressWidth, Math.max(0, newLeft) )}%`; } addDragTracking() { const handle = this.progressEl; let grabOffset = 0; let scrollbarWidth = 0; let handleWidth = 0; const startDrag = (e) => { this.isDragging = true; this.scrollbarEl.classList.add("--drag-start"); const rect = this.scrollbarEl.getBoundingClientRect(); const handleRect = handle.getBoundingClientRect(); scrollbarWidth = rect.width; handleWidth = handleRect.width; const clientX = e.touches ? e.touches[0].clientX : e.clientX; grabOffset = clientX - handleRect.left; document.addEventListener("mousemove", handleDrag); document.addEventListener("mouseup", stopDrag); document.addEventListener("touchmove", handleDrag); document.addEventListener("touchend", stopDrag); }; const handleDrag = (e) => { if (!this.isDragging) return; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const rect = this.scrollbarEl.getBoundingClientRect(); let newLeftPx = clientX - rect.left - grabOffset; const maxLeft = scrollbarWidth - handleWidth; newLeftPx = Math.max(0, Math.min(maxLeft, newLeftPx)); const progress = newLeftPx / maxLeft; this.swiper.setProgress(progress, 0); }; const stopDrag = () => { if (!this.isDragging) return; this.isDragging = false; document.removeEventListener("mousemove", handleDrag); document.removeEventListener("mouseup", stopDrag); document.removeEventListener("touchmove", handleDrag); document.removeEventListener("touchend", stopDrag); this.scrollbarEl.classList.remove("--drag-start"); this.swiper.slideReset(400); }; handle.addEventListener("mousedown", startDrag); handle.addEventListener("touchstart", startDrag); } } // ======================================================== // TOOLTIP // ======================================================== function idmShowTooltip(tooltipEl){ const tooltipContentEl = tooltipEl.querySelector(".idm_tooltip__content"); if(!tooltipContentEl) return; tooltipContentEl.classList.add("--visible"); // Logika pokazywania się i chowania tooltipa let timeoutVar; function onMouseLeave() { timeoutVar = idmHideTooltipTimer(tooltipEl); } function onMouseEnter() { clearTimeout(timeoutVar); } function onScroll() { idmHideTooltip(tooltipEl); } // Store references for later removal tooltipEl._onMouseLeave = onMouseLeave; tooltipEl._onMouseEnter = onMouseEnter; tooltipEl._onScroll = onScroll; tooltipEl.addEventListener("mouseleave", onMouseLeave); tooltipEl.addEventListener("mouseenter", onMouseEnter); document.addEventListener("scroll", onScroll); } function idmHideTooltipTimer(tooltipEl){ return setTimeout(() => idmHideTooltip(tooltipEl), 1500); } function idmHideTooltip(tooltipEl){ const tooltipContentEl = tooltipEl.querySelector(".idm_tooltip__content"); if (!tooltipContentEl) return; tooltipContentEl.classList.remove("--visible"); tooltipEl.removeEventListener("mouseleave", tooltipEl._onMouseLeave); tooltipEl.removeEventListener("mouseenter", tooltipEl._onMouseEnter); document.removeEventListener("scroll", tooltipEl._onScroll); delete tooltipEl._onMouseLeave; delete tooltipEl._onMouseEnter; delete tooltipEl._onScroll; } document.addEventListener("DOMContentLoaded", ()=>{ document.body.addEventListener("click", e=>{ const tooltipEl = e.target.closest(".idm_tooltip"); if(!e.target.closest(".idm_tooltip__info_icon") || !tooltipEl) return; e.preventDefault(); idmShowTooltip(tooltipEl); }); }); // new IdmHotspot({ // id: "idmTestHotspot1", // title: "tescik", // products: [] // Tablica produktów // placement: { // selector: "#content", // insert: "beforeend", // }, // source: { // productsMenu: 1649, // producersId: [], // seriesId: [], // parametersId: [], // priceRange: { // from: 0, // to: 150, // } // } // options: { // lazy: true, // addToBasket: "range", // swiper: true, // } // }); // { // id: "idmMainHotspot2", // title: "Super ramka rekomendacji", // placement: { // selector: "#content", // insert: "beforeend" // }, // source: { // productsMenu: 488, // }, // options: { // lazy: true, // addToBasket: "range", // swiper: true, // } // } async function idmPrepareHotspotObject(selectedContainerEl){ selectedContainerEl.classList.add("--init"); const source = {}; if(selectedContainerEl.dataset?.hotspotsType) source.hotspotsType = selectedContainerEl.dataset.hotspotsType; else { if(selectedContainerEl.dataset?.productsId) source.productsId = selectedContainerEl.dataset.productsId.split(","); if(selectedContainerEl.dataset?.productsMenu) source.productsMenu = selectedContainerEl.dataset.productsMenu; if(selectedContainerEl.dataset?.producersId) source.producersId = selectedContainerEl.dataset.producersId; if(selectedContainerEl.dataset?.seriesId) source.seriesId = selectedContainerEl.dataset.seriesId; if(selectedContainerEl.dataset?.parametersId) source.seriesId = selectedContainerEl.dataset.parametersId; if(selectedContainerEl.dataset?.priceFrom && selectedContainerEl.dataset?.priceTo) source.priceRange = {from: +selectedContainerEl.dataset.priceFrom, to: +selectedContainerEl.dataset.priceTo}; } if(Object.keys(source).length === 0){ console.error(); selectedContainerEl?.remove(); return; } const idmHotspotObj = { id: selectedContainerEl?.id, source, hotspotEl: selectedContainerEl }; if(selectedContainerEl?.dataset?.lazy) idmHotspotObj.options = {lazy: selectedContainerEl?.dataset?.lazy === "true" ? true : false}; new IdmHotspot(idmHotspotObj) } document.querySelectorAll(".hotspot__wrapper.idm__hotspot").forEach(currentHotspot=>{ idmPrepareHotspotObject(currentHotspot) })