From d50486c684d7f783f91f50e7cfe1beb4c338d70f Mon Sep 17 00:00:00 2001 From: pgaca Date: Wed, 1 Oct 2025 12:50:54 +0200 Subject: [PATCH] hotspot: setHeight + callbackFN --- README.md | 45 +- bundle.js | 2199 +++++++++++++++++++++++++++-------------------------- ramka.txt | 12 +- 3 files changed, 1173 insertions(+), 1083 deletions(-) diff --git a/README.md b/README.md index 2561d7e..0c02524 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Ramki rekomendacji # Funkcje js składające się na customowe ramki rekomendacji +## UWAGI PRZEDWDROŻENIOWE ## +- kod zawiera app_shop.fn.idmSetHeight używany do wyrównywania wysokości +- kod zawiera app_shop.fn.idmGetOmnibusDetails który jest przerobionym kodem idosella app_shop.fn.getOmnibusDetails używanym w zwykłych ramkach rekomendacji + ### Pliki ### - bundle.js - całość - 1graphQL.js - graphQL + literały @@ -9,7 +13,7 @@ Funkcje js składające się na customowe ramki rekomendacji - 4init.js - obiekt z ogólnymi ustawieniami Hotspota + init swipera - 5ainsertHotspotHTML.js - wstawienie ramki po kodzie html - 5binsertHotspotObject.js - wstawienie ramki po obiekcie js -- 6style.js - wstawienie styli na koniec body(do przeniesienia do css komponentu) +- 6style.css - ### Użycie ### 1. Wstawienie całego kodu do komponentu/dodatku @@ -75,6 +79,45 @@ idmInsertAllHTMLHotspots(); ``` +### LISTA GLOBALNYCH FUNKCJI, ZMIENNYCH ### +##### INIT ##### +- priceQuery +- productQuery +- IDM_PRODUCTS_GQL +- IDM_HOTSPOTS_GQL +- IDM_PRODUCT_GQL +- IDM_HOTSPOT_ADD_TO_BASKET +- idmHotspotTextObject +#### FUNKCJE #### +- app_shop.fn.idmGetOmnibusDetails +- idmHandleAddToBasket +- idmRangeMaxAlert +- idmRangeMinAlert +- idmQuantityButtonClick +- idmQuantityInputChange +- idmGetHotspotData +- idmGetQueryData +- idmObserveOnce +- app_shop.fn.idmSetHeight + + +#### MARKUP #### +- idmPrepareProductsMarkup +- idmPrepareSingleProductMarkup +- idmPrepareHotspotImgMarkup +- idmPrepareHotspotPriceMarkup +- idmPrepareHotspotAddToBasketMarkup + +#### INIT #### +- idmPriceType +- idmGeneralHotspotObjData +- idmHotspotInit + +#### INSERT #### +- idmInsertHotspotElement +- idmInsertAllHTMLHotspots +- idmInsertHotspotObject +- idmInsertAllObjectHotspots Created by • **[IdoMods](https://idomods.pl/)** • 2025 \ No newline at end of file diff --git a/bundle.js b/bundle.js index de84d36..46e75ac 100644 --- a/bundle.js +++ b/bundle.js @@ -1,1081 +1,1118 @@ -/////////////////////////////////////////////// -// GraphQL -// ogolne -const priceQuery = `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 productQuery = `id - type - name - zones - icon - iconSecond - iconSmall - iconSmallSecond - link - zones - producer{ - name - } - category{ - name - } - sizes{ - id - amount - name - ${priceQuery} - } - group{ - id - name - link - versions{ - id - name - icon - iconSecond - iconSmall - iconSmallSecond - } - } - awardedParameters { - name - id - description - values { - name - id - } - } - enclosuresImages { - position - url - } - points - unit{ - id, name, singular, plural, fraction, sellBy, precision, unitConvertedFormat - } - ${priceQuery}`; -// 1. products -const IDM_PRODUCTS_GQL = (args) => JSON.stringify({ - query: `{ - products(${args}){ - took - products{ - ${productQuery} - } - } -}` -}); - -// 2. hotspots -const IDM_HOTSPOTS_GQL = (args) => JSON.stringify({ - query: `{ - hotspots(${args}){ - took - products{ - ${productQuery} - } - } -}` -}); - -// 3. single product -const IDM_PRODUCT_GQL = (args) => JSON.stringify({ - query: `{ - product(${args}){ - product{ - ${productQuery} - } - } - }` -}); -// ADD TO BASKET -const IDM_HOTSPOT_ADD_TO_BASKET = (t, e, a) => JSON.stringify({ - query: `mutation {\n addProductsToBasket(ProductInput: {id: ${t}, size: "${e}", quantity: ${a}}) {\n status\n results {\n status\n error {\n code\n message\n }\n productCode\n productId\n sizeId\n quantity\n quantityAvailable\n }\n clientDetailsInBasket {\n id\n login\n firstname\n lastname\n participationPartnerProgram\n usesVat\n email\n isWholesaler\n isWholesalerOrder\n clientIdUpc\n }\n }\n }` -}); - - -/////////////////////////////////////////////////////////// -// TEXT -const idmHotspotTextObject = { - ["Kod rabatowy"]: "Kod rabatowy", - ["Okazja"]: "Okazja", - ["Promocja"]: "Promocja", - ["Bestseller"]: "Bestseller", - ["Nowość"]: "Nowość", - ["Ilość"]: "Ilość", - ["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", -} -//////////////////////////////////////////////////////////////////////////\\\\\\\\\\\\ -// IDOSELL omnibus details - -// omnibusDetailsTxt - nadpisać na własny obiekt - -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 = `${idmHotspotTextObject["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 = `${idmHotspotTextObject["Okazja"]}`; - } - // label promocja - if (Object.keys(activeLabel)?.length === 0) { - activeLabel.bargain = `${idmHotspotTextObject["Promocja"]}`; - } - - // labele zones - if(productData.zones.find(zone => zone ==="bestseller")) activeLabel.bestseller = `${idmHotspotTextObject["Bestseller"]}`; - if(productData.zones.find(zone => zone ==="news")) activeLabel.news = `${idmHotspotTextObject["Nowość"]}`; - - - - 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: `${idmHotspotTextObject['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: `${idmHotspotTextObject['Cena regularna']}: - ${maxPrice}-${sizeData.price.youSavePercent}%`, - }, - } : {}; - - const beforeRebate = (beforeRebatePrice) ? { - beforeRebate: { - price: beforeRebatePrice, - visible: !!classes.add.includes('--omnibus-code'), - percent: `-${sizeData.price.beforeRebateDetails?.youSavePercent}%`, - html: `${idmHotspotTextObject['Cena bez kodu']}: - ${beforeRebatePrice} - -${sizeData.price.beforeRebateDetails?.youSavePercent}%`, - }, - } : {}; - - const newPriceEffectiveUntil = (newDate) ? { - newPriceEffectiveUntil: { - date: newDate, - price: maxPrice, - visible: !!classes.add.includes('--omnibus-new-price'), - html: `${idmHotspotTextObject['Cena nadchodząca od']} - ${newDate}: - ${maxPrice}`, - }, - } : {}; - - - return { - classes, - omnibus, - ...max, - ...beforeRebate, - ...newPriceEffectiveUntil, - activeLabel, - }; -}; - -////////////////////////////////////////////////////////////////////////// -// EVENTY -// dodawanie do koszyka -async function idmHandleAddToBasket(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{ - // 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!!!!! - buttonEl.innerHTML = `${buttonEl.dataset.success}`; - setTimeout(()=>{ - buttonEl.innerHTML = `${buttonEl.dataset.text}`; - app_shop.fn?.menu_basket_cache?.(); - 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") - } -} - -///qty -const idmRangeMaxAlert = (max)=> Alertek.Error(`${idmHotspotTextObject["Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${max}`) -const idmRangeMinAlert = (min)=> Alertek.Error(`${idmHotspotTextObject["Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${min}`) - -function idmQuantityButtonClick(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; - idmRangeMaxAlert(max) - } - } else if (e.target.classList.contains("idm-products-banner__qty-decrease")) { - current -= step; - if (current < step){ - current = step; - idmRangeMinAlert(step) - } - } - input.value = current.toFixed(precision); -} -function idmQuantityInputChange(e){ - if(e.target.value > +e.target.max){ - idmRangeMaxAlert(e.target.max) - e.target.value = +e.target.max - } - if(e.target.value < +e.target.min){ - idmRangeMinAlert(e.target.min) - e.target.value = +e.target.min; - } -} - -////////////////////////////////////////////// -// DANE -// dwie funkcje zamiast jednej -async function idmGetHotspotData(query, graphFn){ - try{ - const res = await fetch(`/graphql/v1/`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: graphFn ? graphFn(query) : IDM_PRODUCTS_GQL(query) - }); - const data = await res.json(); - const products = data[graphFn === IDM_HOTSPOTS_GQL ? "hotspots" : "data"]?.products?.products; - if(!products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); - - console.log(data); - return products; - }catch(err){ - console.error(idmHotspotTextObject["Błąd przy pobieraniu danych"], err); - return null; - } -} - - -function idmGetQueryData({ - productsID, - productsMenu, - hotspotsType -}){ - let graphFn, query; - - if(productsID){ - graphFn = IDM_PRODUCTS_GQL; - query = `searchInput: {productsId: [${productsID}]}`; - }else if(productsMenu){ - graphFn = IDM_PRODUCTS_GQL; - query = `searchInput: {navigation: ${productsMenu}}`; - }else if(hotspotsType){ - graphFn = IDM_HOTSPOTS_GQL; - query = `searchInput: {hotspot: ${hotspotsType}, limit: 16}`; - } - - return {graphFn, query} -} - -////////////////////////////////////////////// -// LAZY LOADING -function idmObserveOnce(element, callback, options = { root: null, rootMargin: "0px", threshold: 0.1 }) { - if (!element) return; - - const observer = new IntersectionObserver((entries, obs) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - callback(entry); // run your callback - obs.disconnect(); // stop observing after first trigger - } - }); - }, options); - - observer.observe(element); -} -////////////////////////////////////////////////////////////////////////// -// Markup - -// Funkcja przygotująca markup dla wszystkich produktów -function idmPrepareProductsMarkup(products, addToBasket){ - let markup = ""; - products.forEach((prod)=>{ - markup += idmPrepareSingleProductMarkup(prod, addToBasket); - }) - return markup; -} - -// funkcja przygotowująca markup dla wybranego produktu -function idmPrepareSingleProductMarkup(prod, addToBasket){ - const prodExchangedData = app_shop.fn?.idmGetOmnibusDetails({productData: prod}); - - // pobranie labelek - let labelHTMLMarkup = ""; - if(typeof prodExchangedData.activeLabel === "object") Object.entries(prodExchangedData.activeLabel).forEach(([key,value])=>{ - labelHTMLMarkup += value; - }) - - // markup pojedynczego produktu - let singleMarkup = ""; - singleMarkup += ` -
-
- - -
- - ${idmPrepareHotspotImgMarkup(prod)} - - ${labelHTMLMarkup} - - -
- ${prod.name} -
- ${idmPrepareHotspotPriceMarkup(prod, prodExchangedData)} -
-
- ${idmPrepareHotspotAddToBasketMarkup(prod, addToBasket)} -
`; - - return singleMarkup; -} - -// markup zdjęcia -function idmPrepareHotspotImgMarkup(prod){ - let markup = ""; - if(prod.iconSmallSecond !== undefined && prod.iconSecond !== undefined) markup +=` - - - - - ${prod.name} - `; - else if(prod?.iconSmall !== undefined) markup += ` - - - ${prod.name} - `; - else markup += `${prod.name}` - return markup; -} - -// markup cen -function idmPrepareHotspotPriceMarkup(prod, prodExchangedData){ - const price = prod.price.price[idmPriceType]; - const unit = prod.unit; - const pointsPrice = prod?.points; - const convertedPrice = prod.price?.unitConvertedPrice?.[idmPriceType]?.formatted; - - return ` - - ${price.formatted} - - / - ${unit?.sellBy} - ${unit?.sellBy > 1 ? unit?.plural : unit?.singular} - - ${convertedPrice ? `${convertedPrice}` : ""} - - ${pointsPrice ? `${pointsPrice} pkt.` : ""} - ${price.value === 0 ? `${idmHotspotTextObject["Cena na telefon"]}` : ""} - ${prodExchangedData?.beforeRebate?.visible ? `${prodExchangedData?.beforeRebate?.html}` : ""} - ${prodExchangedData?.newPriceEffectiveUntil?.visible ? `${prodExchangedData?.newPriceEffectiveUntil?.html}` : ""} - ${prodExchangedData?.omnibus?.visible ? `${prodExchangedData?.omnibus?.html}` : ""} - ${prodExchangedData?.max?.visible ? `${prodExchangedData?.max?.html}` : ""} - `; -} - -// markup dodawania do koszyka -function idmPrepareHotspotAddToBasketMarkup(prod, addToBasket){ - let markup = ""; - if(!addToBasket && typeof addToBasket !== "undefined" || - (typeof addToBasket === "undefined" && typeof idmGeneralHotspotObjData === "object" && !idmGeneralHotspotObjData?.options?.addToBasket) || - !addToBasket && typeof idmGeneralHotspotObjData === "undefined") return markup; - - // link do produktu jak nie jest to zwykły produkt - if(prod.type !== "product") markup = `Zobacz produkt`; - else if(addToBasket === "range" - || typeof addToBasket === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.addToBasket === "range") // +- - markup = `
- - - -
- - - - - -
- - -
`; - else // Zwykłe dodanie do koszyka - markup = ` -
- - - - - -
`; - return markup; -} -//////////////////////////////////////////////////// -// INIT - -// brutto/netto -const idmPriceType = app_shop?.vars?.priceType || "gross"; - - -// Zmienna trzymająca dane o ustawieniach customowych ramek rekomendacji na całym sklepie -/** - * Obiekt konfiguracyjny ogólnych ustawień hotspotów rekomendacji. - * - * @typedef {object} idmGeneralHotspotObjData - * @property {object} options - Główne ustawienia hotspotów - * @property {boolean} options.lazy - Czy wczytywać zawartość w trybie lazy (required). - * @property {boolean|string} options.addToBasket - Zachowanie przy dodawaniu do koszyka: - * - true = przycisk dodaj do koszyka - * - false = brak przycisku - * - "range" = dodaj z wyborem zakresu (required). - * @property {boolean|object} options.swiper - Ustawienia slidera: - * - true/false = włącz/wyłącz - * - object = konfiguracja instancji Swiper (required jeśli obiekt). - */ -const idmGeneralHotspotObjData = { - options: { - lazy: true, - addToBasket: true, // true, false, "range" - swiper: { // 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, - }, - } - } - } -} - - -// Funkcja inicjalizująca wybranego hotspota(addtobasket range - swiper) -async function idmHotspotInit(id, options={}){ - try{ - const hotspotEl = document.getElementById(id); - if(!hotspotEl) throw new Error("Nie znaleziono elementu"); - - // add to basket + - - if(options?.addToBasket === "range" || - typeof options?.addToBasket === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.addToBasket === "range"){ - - // obsługa i sprawdzanie clicków - // hotspotEl.addEventListener("click", e=>{ - // const wrapper = e.target.closest(".idm-products-banner__qty"); - // if (!wrapper) return; - - // if(e.target.classList.contains("idm-products-banner__qty-input")) return e.target.select(); - - // 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; - // rangeMaxAlert(max) - // } - // } else if (e.target.classList.contains("idm-products-banner__qty-decrease")) { - // current -= step; - // if (current < step){ - // current = step; - // rangeMinAlert(step) - // } - // } - // input.value = current.toFixed(precision); - // }); - // sprawdzanie na input - // hotspotEl.querySelectorAll(".idm-products-banner__qty-input").forEach(inp=>{ - // inp.addEventListener("input", e=>{ - // if(e.target.value > +e.target.max){ - // rangeMaxAlert(e.target.max) - // e.target.value = +e.target.max - // } - // if(e.target.value < +e.target.min){ - // rangeMinAlert(e.target.min) - // e.target.value = +e.target.min; - // } - // }); - // }) - - } - - // swiper || slick - if(options?.swiper || - typeof options?.swiper === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.swiper){ - - - // Opcje swipera - let swiperOptions = typeof options.swiper === "object" ? options.swiper : ""; - if(typeof options.swiper === "object") swiperOptions = options.swiper; - else if(typeof idmGeneralHotspotObjData === "object" && typeof idmGeneralHotspotObjData?.options?.swiper === "object"){ - swiperOptions = idmGeneralHotspotObjData?.options?.swiper; - swiperOptions.navigation = { - nextEl: `#${id} .idm-button-next`, - prevEl: `#${id} .idm-button-prev`, - } - }else{ - swiperOptions = { - 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, - }, - }, - navigation: { - nextEl: `#${id} .idm-button-next`, - prevEl: `#${id} .idm-button-prev`, - }, - } - } - - // Wywołanie swipera - const selectedSwiper = new HotspotSlider({ - selector: `#${id} .swiper`, - hotspotName: `${id}`, - options: swiperOptions, - }); - await selectedSwiper.init(); - } - }catch(err){ - console.error(idmHotspotTextObject["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"], err); - } -} - -console.log("init") - -//////////////////////////////////////////////////// -// Funkcja init ELEMENT HTML -async function idmInsertHotspotElement(selectedContainerEl){ - - selectedContainerEl.classList.add("--init"); - const {graphFn, query} = idmGetQueryData({ - productsID: selectedContainerEl?.dataset?.productsId, - productsMenu: selectedContainerEl?.dataset?.productsMenu, - hotspotsType: selectedContainerEl.dataset.hotspotsType - }); - - if(!graphFn || !query){ - console.log(idmHotspotTextObject["Nie znaleziono metody graphql"], selectedContainerEl) - return selectedContainerEl.remove(); - } - - // Funkcja od uzupełniania danych - const idmFill = async ()=>{ - try{ - // pobranie danych o produktach - const products = await idmGetHotspotData(query, graphFn); - if(!products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); - - - // wstawienie produktów zależnie czy w section jest .hotspot czy nie - const hotspotInsideEl = selectedContainerEl.querySelector(".hotspot"); - if(hotspotInsideEl) hotspotInsideEl.innerHTML += `
-
- ${idmPrepareProductsMarkup(products, true)} -
-
-
-
-
`; - else selectedContainerEl.innerHTML = `
-
- ${idmPrepareProductsMarkup(products, true)} -
-
-
-
-
`; - - selectedContainerEl.classList.remove("idm-loading") - // init swipera - idmHotspotInit(selectedContainerEl.id) - }catch(err){ - console.error(idmHotspotTextObject["Wystąpił błąd"], err); - selectedContainerEl.remove(); - } - } - - - if(selectedContainerEl.dataset?.lazy || - !selectedContainerEl.dataset?.lazy && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.lazy) idmObserveOnce(selectedContainerEl, idmFill); - else idmFill(); -} - - - - - -// Zebranie wszystkich ramek HTML i wstawienie ich. -async function idmInsertAllHTMLHotspots(){ - try{ - const reqArr = [] - document.querySelectorAll(".idm__hotspot:not(.--init):not(.--lazy-hotspot)").forEach(hotspot=>{ - reqArr.push(idmInsertHotspotElement(hotspot)); - }); - await Promise.all(reqArr); - }catch(err){ - console.error(err) - } -} - -idmInsertAllHTMLHotspots(); - -///////////////////////////////////////////////////////////////////// -// wdrożenie dla elementu HTML -/** - * Struktura sekcji hotspotu w HTML. - * - * @typedef {HTMLElement} HotspotSection - * @property {string} id - Identyfikator elementu (np. "idmBlogHotspot1"). - * @property {string} class - Klasy CSS używane do stylowania. - * - * @attribute {string} data-products-id - Lista ID produktów (rozdzielona przecinkami). - * @attribute {number} data-products-menu - Identyfikator menu produktów. - * @attribute {string} data-hotspots-type - Typ hotspotu (np. "promotion"). - * @attribute {boolean} data-lazy - Czy sekcja ma być ładowana w trybie lazy. - * - * @example -
-
-

- - aaa - -

-
-
- */ -//////////////////////////////////////////////////// -// Funkcja init OBIEKT JS -async function idmInsertHotspotObject(idmHotspotObj){ - // Wstaw kontener - const selectedEl = document.querySelector(idmHotspotObj?.placement.selector); - if(!selectedEl) throw new Error(idmHotspotTextObject["Nie znaleziono kontenera"]); - - selectedEl.insertAdjacentHTML(idmHotspotObj.placement.insert || "afterend", `
-
- ${idmHotspotObj?.title ? ` -

- ${idmHotspotObj.title} -

- ` : ""} -
-
- -
-
-
-
-
-
-
`); - - - // Utworzenie markupa HTML - const selectedContainerEl = document.getElementById(idmHotspotObj.id); - if(!selectedContainerEl) throw new Error(idmHotspotTextObject["Nie znaleziono kontenera"]); - - const idmFill = async ()=>{ - try{ - let {graphFn, query} = idmGetQueryData({ - productsID: idmHotspotObj?.source?.productsId, - productsMenu: idmHotspotObj?.source?.productsMenu, - hotspotsType: idmHotspotObj.source.hotspotsType - }); - - if(idmHotspotObj?.query?.graphFn && idmHotspotObj?.query?.string){ - graphFn = idmHotspotObj.query.graphFn; - query = idmHotspotObj.query.string; - } - - // pobranie danych o produktach - const products = await idmGetHotspotData(query, graphFn); - if(!products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); - - - // Wstawienie markupa na strone - const hotspotMarkup = `${idmPrepareProductsMarkup(products, idmHotspotObj?.options?.addToBasket)}`; - selectedContainerEl.querySelector(".products.hotspot__products")?.insertAdjacentHTML("beforeend", hotspotMarkup); - selectedContainerEl.classList.remove("idm-loading"); - - // init swiper + add to basket - idmHotspotInit(idmHotspotObj.id, idmHotspotObj?.options) - }catch(err){ - console.error(idmHotspotTextObject["Wystąpił błąd"], err); - selectedContainerEl.remove(); - } - } - - - if(idmHotspotObj?.options?.lazy || - typeof idmHotspotObj?.options?.lazy === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.lazy) idmObserveOnce(selectedContainerEl, idmFill); - else idmFill(); -} - - -// obiekt js z przykładowymi danymi -/** - * Tablica konfiguracji hotspotów rekomendacji. - * - * @typedef {object} Hotspot - * @property {string} id - Identyfikator ramki (required). - * @property {string} title - Tytuł ramki. - * @property {string} classes - Dodatkowe klasy CSS. - * @property {object} placement - Określa, gdzie wstawić ramkę (required). - * @property {string} placement.selector - Selektor miejsca osadzenia. - * @property {string} placement.insert - Pozycja wstawienia względem selektora (np. "afterbegin", "beforeend"). - * @property {object} source - Dane źródłowe dla hotspotu (required). - * @property {string} [source.hotspotType] - Typ hotspotu (np. "promotion"). - * @property {number[]} [source.productsId] - Tablica ID produktów. - * @property {number} [source.productsMenu] - Identyfikator menu produktów. - * @property {object} query - Dane zapytania, nadpisują source (DEV). - * @property {string} query.string - Zapytanie w formacie GraphQL. - * @property {Function} query.graphFn - Funkcja do pobierania danych. - * @property {object} options - Ustawienia dla hotspotu (required). - * @property {boolean} options.lazy - Czy wczytywać w trybie lazy. - * @property {boolean|string} options.addToBasket - Obsługa koszyka: - * - true = włącz - * - false = wyłącz - * - "range" = dodaj z zakresem - * @property {boolean|object} options.swiper - Slider: - * - true = aktywny - * - false = nieaktywny - * - object = konfiguracja Swiper - * - * @type {Hotspot[]} - */ -// const idmHotspotArr = [ -// { -// id: "idmMainHotspot1",//!id ramki -// title: "Nowoczesna ramka rekomendacji",// -// classes: "abcdefg", -// placement: { -// selector: "#content", -// insert: "afterbegin" -// }, -// source: { -// hotspotType: "protomtion", -// productsId: [11,12], -// productsMenu: 122, -// }, -// query: { -// string: `searchInput: {productsId : [589, 180, 181590,160740, 155978, 153632, 123350, 82542, 37321, 17040, 25065, 25114, 85452]}`, -// graphFn: IDM_PRODUCTS_GQL -// }, -// // addToBasket: true, -// options: { -// lazy: false, -// addToBasket: "range", -// swiper: true, -// // swiper: albo true false - albo obiekt z opcjami swipera -// } -// }, -// { -// id: "idmMainHotspot2", -// title: "Super ramka rekomendacji", -// placement: { -// selector: "#content", -// insert: "beforeend" -// }, -// source: { -// productsMenu: 488, -// }, -// query: { -// string: `searchInput: {hotspot: promotion,limit: 16}`, -// graphFn: IDM_HOTSPOTS_GQL -// }, -// // addToBasket: true, -// options: { -// lazy: true, -// addToBasket: "range", -// swiper: true, -// // swiper: albo true false - albo obiekt z opcjami swipera -// } -// } -// ]; - - -// Wrzucenie na strone wszystkich ramek z obiektu js -async function idmInsertAllObjectHotspots(hotspotArr){ - try{ - const reqArr = [] - hotspotArr.forEach(hotspotObj=>{ - reqArr.push(idmInsertHotspotObject(hotspotObj)); - }); - await Promise.all(reqArr); - }catch(err){ - console.error(err) - } -} - - -// idmInsertAllObjectHotspots(idmHotspotArr); \ No newline at end of file +/////////////////////////////////////////////// +// GraphQL +// ogolne +const priceQuery = `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 productQuery = `id + type + name + zones + icon + iconSecond + iconSmall + iconSmallSecond + link + zones + producer{ + name + } + category{ + name + } + sizes{ + id + amount + name + ${priceQuery} + } + group{ + id + name + link + versions{ + id + name + icon + iconSecond + iconSmall + iconSmallSecond + } + } + awardedParameters { + name + id + description + values { + name + id + } + } + enclosuresImages { + position + url + } + points + unit{ + id, name, singular, plural, fraction, sellBy, precision, unitConvertedFormat + } + ${priceQuery}`; +// 1. products +const IDM_PRODUCTS_GQL = (args) => JSON.stringify({ + query: `{ + products(${args}){ + took + products{ + ${productQuery} + } + } +}` +}); + +// 2. hotspots +const IDM_HOTSPOTS_GQL = (args) => JSON.stringify({ + query: `{ + hotspots(${args}){ + took + products{ + ${productQuery} + } + } +}` +}); + +// 3. single product +const IDM_PRODUCT_GQL = (args) => JSON.stringify({ + query: `{ + product(${args}){ + product{ + ${productQuery} + } + } + }` +}); +// ADD TO BASKET +const IDM_HOTSPOT_ADD_TO_BASKET = (t, e, a) => JSON.stringify({ + query: `mutation {\n addProductsToBasket(ProductInput: {id: ${t}, size: "${e}", quantity: ${a}}) {\n status\n results {\n status\n error {\n code\n message\n }\n productCode\n productId\n sizeId\n quantity\n quantityAvailable\n }\n clientDetailsInBasket {\n id\n login\n firstname\n lastname\n participationPartnerProgram\n usesVat\n email\n isWholesaler\n isWholesalerOrder\n clientIdUpc\n }\n }\n }` +}); + + +/////////////////////////////////////////////////////////// +// TEXT +const idmHotspotTextObject = { + ["Kod rabatowy"]: "Kod rabatowy", + ["Okazja"]: "Okazja", + ["Promocja"]: "Promocja", + ["Bestseller"]: "Bestseller", + ["Nowość"]: "Nowość", + ["Ilość"]: "Ilość", + ["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", +} +//////////////////////////////////////////////////////////////////////////\\\\\\\\\\\\ +// IDOSELL omnibus details + +// omnibusDetailsTxt - nadpisać na własny obiekt + +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 = `${idmHotspotTextObject["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 = `${idmHotspotTextObject["Okazja"]}`; + } + // label promocja + if (Object.keys(activeLabel)?.length === 0) { + activeLabel.bargain = `${idmHotspotTextObject["Promocja"]}`; + } + + // labele zones + if(productData.zones.find(zone => zone ==="bestseller")) activeLabel.bestseller = `${idmHotspotTextObject["Bestseller"]}`; + if(productData.zones.find(zone => zone ==="news")) activeLabel.news = `${idmHotspotTextObject["Nowość"]}`; + + + + 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: `${idmHotspotTextObject['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: `${idmHotspotTextObject['Cena regularna']}: + ${maxPrice}-${sizeData.price.youSavePercent}%`, + }, + } : {}; + + const beforeRebate = (beforeRebatePrice) ? { + beforeRebate: { + price: beforeRebatePrice, + visible: !!classes.add.includes('--omnibus-code'), + percent: `-${sizeData.price.beforeRebateDetails?.youSavePercent}%`, + html: `${idmHotspotTextObject['Cena bez kodu']}: + ${beforeRebatePrice} + -${sizeData.price.beforeRebateDetails?.youSavePercent}%`, + }, + } : {}; + + const newPriceEffectiveUntil = (newDate) ? { + newPriceEffectiveUntil: { + date: newDate, + price: maxPrice, + visible: !!classes.add.includes('--omnibus-new-price'), + html: `${idmHotspotTextObject['Cena nadchodząca od']} + ${newDate}: + ${maxPrice}`, + }, + } : {}; + + + return { + classes, + omnibus, + ...max, + ...beforeRebate, + ...newPriceEffectiveUntil, + activeLabel, + }; +}; + +////////////////////////////////////////////////////////////////////////// +// EVENTY +// dodawanie do koszyka +async function idmHandleAddToBasket(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{ + // 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!!!!! + buttonEl.innerHTML = `${buttonEl.dataset.success}`; + setTimeout(()=>{ + buttonEl.innerHTML = `${buttonEl.dataset.text}`; + app_shop.fn?.menu_basket_cache?.(); + 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") + } +} + +///qty +const idmRangeMaxAlert = (max)=> Alertek.Error(`${idmHotspotTextObject["Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${max}`) +const idmRangeMinAlert = (min)=> Alertek.Error(`${idmHotspotTextObject["Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${min}`) + +function idmQuantityButtonClick(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; + idmRangeMaxAlert(max) + } + } else if (e.target.classList.contains("idm-products-banner__qty-decrease")) { + current -= step; + if (current < step){ + current = step; + idmRangeMinAlert(step) + } + } + input.value = current.toFixed(precision); +} +function idmQuantityInputChange(e){ + if(e.target.value > +e.target.max){ + idmRangeMaxAlert(e.target.max) + e.target.value = +e.target.max + } + if(e.target.value < +e.target.min){ + idmRangeMinAlert(e.target.min) + e.target.value = +e.target.min; + } +} + +////////////////////////////////////////////// +// DANE +// dwie funkcje zamiast jednej +async function idmGetHotspotData(query, graphFn){ + try{ + const res = await fetch(`/graphql/v1/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: graphFn ? graphFn(query) : IDM_PRODUCTS_GQL(query) + }); + const data = await res.json(); + const products = data[graphFn === IDM_HOTSPOTS_GQL ? "hotspots" : "data"]?.products?.products; + if(!products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); + + console.log(data); + return products; + }catch(err){ + console.error(idmHotspotTextObject["Błąd przy pobieraniu danych"], err); + return null; + } +} + + +function idmGetQueryData({ + productsID, + productsMenu, + hotspotsType +}){ + let graphFn, query; + + if(productsID){ + graphFn = IDM_PRODUCTS_GQL; + query = `searchInput: {productsId: [${productsID}]}`; + }else if(productsMenu){ + graphFn = IDM_PRODUCTS_GQL; + query = `searchInput: {navigation: ${productsMenu}}`; + }else if(hotspotsType){ + graphFn = IDM_HOTSPOTS_GQL; + query = `searchInput: {hotspot: ${hotspotsType}, limit: 16}`; + } + + return {graphFn, query} +} + +////////////////////////////////////////////// +// LAZY LOADING +function idmObserveOnce(element, callback, options = { root: null, rootMargin: "0px", threshold: 0.1 }) { + if (!element) return; + + const observer = new IntersectionObserver((entries, obs) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + callback(entry); // run your callback + obs.disconnect(); // stop observing after first trigger + } + }); + }, options); + + observer.observe(element); +} + + +//////////////////////////////////////////////// +// IDM SET HEIGHT +app_shop.fn.idmSetHeight = 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) +} +////////////////////////////////////////////////////////////////////////// +// Markup + +// Funkcja przygotująca markup dla wszystkich produktów +function idmPrepareProductsMarkup(products, addToBasket){ + let markup = ""; + products.forEach((prod)=>{ + markup += idmPrepareSingleProductMarkup(prod, addToBasket); + }) + return markup; +} + +// funkcja przygotowująca markup dla wybranego produktu +function idmPrepareSingleProductMarkup(prod, addToBasket){ + const prodExchangedData = app_shop.fn?.idmGetOmnibusDetails({productData: prod}); + + // pobranie labelek + let labelHTMLMarkup = ""; + if(typeof prodExchangedData.activeLabel === "object") Object.entries(prodExchangedData.activeLabel).forEach(([key,value])=>{ + labelHTMLMarkup += value; + }) + + // markup pojedynczego produktu + let singleMarkup = ""; + singleMarkup += ` +
+
+ + +
+ + ${idmPrepareHotspotImgMarkup(prod)} + + ${labelHTMLMarkup} + + +
+ ${prod.name} +
+ ${idmPrepareHotspotPriceMarkup(prod, prodExchangedData)} +
+
+ ${idmPrepareHotspotAddToBasketMarkup(prod, addToBasket)} +
`; + + return singleMarkup; +} + +// markup zdjęcia +function idmPrepareHotspotImgMarkup(prod){ + let markup = ""; + if(prod.iconSmallSecond !== undefined && prod.iconSecond !== undefined) markup +=` + + + + + ${prod.name} + `; + else if(prod?.iconSmall !== undefined) markup += ` + + + ${prod.name} + `; + else markup += `${prod.name}` + return markup; +} + +// markup cen +function idmPrepareHotspotPriceMarkup(prod, prodExchangedData){ + const price = prod.price.price[idmPriceType]; + const unit = prod.unit; + const pointsPrice = prod?.points; + const convertedPrice = prod.price?.unitConvertedPrice?.[idmPriceType]?.formatted; + + return ` + + ${price.formatted} + + / + ${unit?.sellBy} + ${unit?.sellBy > 1 ? unit?.plural : unit?.singular} + + ${convertedPrice ? `${convertedPrice}` : ""} + + ${pointsPrice ? `${pointsPrice} pkt.` : ""} + ${price.value === 0 ? `${idmHotspotTextObject["Cena na telefon"]}` : ""} + ${prodExchangedData?.beforeRebate?.visible ? `${prodExchangedData?.beforeRebate?.html}` : ""} + ${prodExchangedData?.newPriceEffectiveUntil?.visible ? `${prodExchangedData?.newPriceEffectiveUntil?.html}` : ""} + ${prodExchangedData?.omnibus?.visible ? `${prodExchangedData?.omnibus?.html}` : ""} + ${prodExchangedData?.max?.visible ? `${prodExchangedData?.max?.html}` : ""} + `; +} + +// markup dodawania do koszyka +function idmPrepareHotspotAddToBasketMarkup(prod, addToBasket){ + let markup = ""; + if(!addToBasket && typeof addToBasket !== "undefined" || + (typeof addToBasket === "undefined" && typeof idmGeneralHotspotObjData === "object" && !idmGeneralHotspotObjData?.options?.addToBasket) || + !addToBasket && typeof idmGeneralHotspotObjData === "undefined") return markup; + + // link do produktu jak nie jest to zwykły produkt + if(prod.type !== "product") markup = `Zobacz produkt`; + else if(addToBasket === "range" + || typeof addToBasket === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.addToBasket === "range") // +- + markup = `
+ + + +
+ + + + + +
+ + +
`; + else // Zwykłe dodanie do koszyka + markup = ` +
+ + + + + +
`; + return markup; +} +//////////////////////////////////////////////////// +// INIT + +// brutto/netto +const idmPriceType = app_shop?.vars?.priceType || "gross"; + + +// Zmienna trzymająca dane o ustawieniach customowych ramek rekomendacji na całym sklepie +/** + * Obiekt konfiguracyjny ogólnych ustawień hotspotów rekomendacji. + * + * @typedef {object} idmGeneralHotspotObjData + * @property {object} options - Główne ustawienia hotspotów + * @property {boolean} options.lazy - Czy wczytywać zawartość w trybie lazy (required). + * @property {boolean|string} options.addToBasket - Zachowanie przy dodawaniu do koszyka: + * - true = przycisk dodaj do koszyka + * - false = brak przycisku + * - "range" = dodaj z wyborem zakresu (required). + * @property {boolean|object} options.swiper - Ustawienia slidera: + * - true/false = włącz/wyłącz + * - object = konfiguracja instancji Swiper (required jeśli obiekt). + */ +const idmGeneralHotspotObjData = { + options: { + lazy: true, + addToBasket: true, // true, false, "range" + swiper: { // 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, + }, + } + } + } +} + +// Funkcja inicjalizująca wybranego hotspota(addtobasket range - swiper) +async function idmHotspotInit(id, options={}){ + try{ + const hotspotEl = document.getElementById(id); + if(!hotspotEl) throw new Error("Nie znaleziono elementu"); + + // add to basket + - + if(options?.addToBasket === "range" || + typeof options?.addToBasket === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.addToBasket === "range"){ + + // obsługa i sprawdzanie clicków + // hotspotEl.addEventListener("click", e=>{ + // const wrapper = e.target.closest(".idm-products-banner__qty"); + // if (!wrapper) return; + + // if(e.target.classList.contains("idm-products-banner__qty-input")) return e.target.select(); + + // 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; + // rangeMaxAlert(max) + // } + // } else if (e.target.classList.contains("idm-products-banner__qty-decrease")) { + // current -= step; + // if (current < step){ + // current = step; + // rangeMinAlert(step) + // } + // } + // input.value = current.toFixed(precision); + // }); + // sprawdzanie na input + // hotspotEl.querySelectorAll(".idm-products-banner__qty-input").forEach(inp=>{ + // inp.addEventListener("input", e=>{ + // if(e.target.value > +e.target.max){ + // rangeMaxAlert(e.target.max) + // e.target.value = +e.target.max + // } + // if(e.target.value < +e.target.min){ + // rangeMinAlert(e.target.min) + // e.target.value = +e.target.min; + // } + // }); + // }) + + } + + // swiper || slick + if(options?.swiper || + typeof options?.swiper === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.swiper){ + + + // Opcje swipera + let swiperOptions = typeof options.swiper === "object" ? options.swiper : ""; + if(typeof options.swiper === "object") swiperOptions = options.swiper; + else if(typeof idmGeneralHotspotObjData === "object" && typeof idmGeneralHotspotObjData?.options?.swiper === "object"){ + swiperOptions = idmGeneralHotspotObjData?.options?.swiper; + swiperOptions.navigation = { + nextEl: `#${id} .idm-button-next`, + prevEl: `#${id} .idm-button-prev`, + } + }else{ + swiperOptions = { + 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, + }, + }, + navigation: { + nextEl: `#${id} .idm-button-next`, + prevEl: `#${id} .idm-button-prev`, + }, + } + } + + // Wywołanie swipera + const selectedSwiper = new HotspotSlider({ + selector: `#${id} .swiper`, + hotspotName: `${id}`, + options: swiperOptions, + }); + await selectedSwiper.init(); + } + + + if(typeof options?.callbackFn === "function") options?.callbackFn(); + + // IDM setHeight + app_shop.fn.idmSetHeight({ + selectors: [ + `#${id} .product__prices`, + `#${id} .product__name`, + ], + container: `#${id} .products__wrapper`, + }); + console.log(`Initialized hotspot #${id}`); + }catch(err){ + console.error(idmHotspotTextObject["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"], err); + } +} + +//////////////////////////////////////////////////// +// Funkcja init ELEMENT HTML +async function idmInsertHotspotElement(selectedContainerEl){ + + selectedContainerEl.classList.add("--init"); + const {graphFn, query} = idmGetQueryData({ + productsID: selectedContainerEl?.dataset?.productsId, + productsMenu: selectedContainerEl?.dataset?.productsMenu, + hotspotsType: selectedContainerEl.dataset.hotspotsType + }); + + if(!graphFn || !query){ + console.log(idmHotspotTextObject["Nie znaleziono metody graphql"], selectedContainerEl) + return selectedContainerEl.remove(); + } + + // Funkcja od uzupełniania danych + const idmFill = async ()=>{ + try{ + // pobranie danych o produktach + const products = await idmGetHotspotData(query, graphFn); + if(!products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); + + + // wstawienie produktów zależnie czy w section jest .hotspot czy nie + const hotspotInsideEl = selectedContainerEl.querySelector(".hotspot"); + if(hotspotInsideEl) hotspotInsideEl.innerHTML += `
+
+ ${idmPrepareProductsMarkup(products, true)} +
+
+
+
+
`; + else selectedContainerEl.innerHTML = `
+
+ ${idmPrepareProductsMarkup(products, true)} +
+
+
+
+
`; + + selectedContainerEl.classList.remove("idm-loading") + // init swipera + idmHotspotInit(selectedContainerEl.id) + }catch(err){ + console.error(idmHotspotTextObject["Wystąpił błąd"], err); + selectedContainerEl.remove(); + } + } + + + if(selectedContainerEl.dataset?.lazy || + !selectedContainerEl.dataset?.lazy && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.lazy) idmObserveOnce(selectedContainerEl, idmFill); + else idmFill(); +} + + + + + +// Zebranie wszystkich ramek HTML i wstawienie ich. +async function idmInsertAllHTMLHotspots(){ + try{ + const reqArr = [] + document.querySelectorAll(".idm__hotspot:not(.--init):not(.--lazy-hotspot)").forEach(hotspot=>{ + reqArr.push(idmInsertHotspotElement(hotspot)); + }); + await Promise.all(reqArr); + }catch(err){ + console.error(err) + } +} + +idmInsertAllHTMLHotspots(); + +///////////////////////////////////////////////////////////////////// +// wdrożenie dla elementu HTML +/** + * Struktura sekcji hotspotu w HTML. + * + * @typedef {HTMLElement} HotspotSection + * @property {string} id - Identyfikator elementu (np. "idmBlogHotspot1"). + * @property {string} class - Klasy CSS używane do stylowania. + * + * @attribute {string} data-products-id - Lista ID produktów (rozdzielona przecinkami). + * @attribute {number} data-products-menu - Identyfikator menu produktów. + * @attribute {string} data-hotspots-type - Typ hotspotu (np. "promotion"). + * @attribute {boolean} data-lazy - Czy sekcja ma być ładowana w trybie lazy. + * + * @example +
+
+

+ + aaa + +

+
+
+ */ +//////////////////////////////////////////////////// +// Funkcja init OBIEKT JS +async function idmInsertHotspotObject(idmHotspotObj){ + // Wstaw kontener + const selectedEl = document.querySelector(idmHotspotObj?.placement.selector); + if(!selectedEl) throw new Error(idmHotspotTextObject["Nie znaleziono kontenera"]); + + selectedEl.insertAdjacentHTML(idmHotspotObj.placement.insert || "afterend", `
+
+ ${idmHotspotObj?.title ? ` +

+ ${idmHotspotObj.title} +

+ ` : ""} +
+
+ +
+
+
+
+
+
+
`); + + + // Utworzenie markupa HTML + const selectedContainerEl = document.getElementById(idmHotspotObj.id); + if(!selectedContainerEl) throw new Error(idmHotspotTextObject["Nie znaleziono kontenera"]); + + const idmFill = async ()=>{ + try{ + let {graphFn, query} = idmGetQueryData({ + productsID: idmHotspotObj?.source?.productsId, + productsMenu: idmHotspotObj?.source?.productsMenu, + hotspotsType: idmHotspotObj.source.hotspotsType + }); + + if(idmHotspotObj?.query?.graphFn && idmHotspotObj?.query?.string){ + graphFn = idmHotspotObj.query.graphFn; + query = idmHotspotObj.query.string; + } + + // pobranie danych o produktach + const products = await idmGetHotspotData(query, graphFn); + if(!products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]); + + + // Wstawienie markupa na strone + const hotspotMarkup = `${idmPrepareProductsMarkup(products, idmHotspotObj?.options?.addToBasket)}`; + selectedContainerEl.querySelector(".products.hotspot__products")?.insertAdjacentHTML("beforeend", hotspotMarkup); + selectedContainerEl.classList.remove("idm-loading"); + + // init swiper + add to basket + idmHotspotInit(idmHotspotObj.id, idmHotspotObj?.options) + }catch(err){ + console.error(idmHotspotTextObject["Wystąpił błąd"], err); + selectedContainerEl.remove(); + } + } + + + if(idmHotspotObj?.options?.lazy || + typeof idmHotspotObj?.options?.lazy === "undefined" && typeof idmGeneralHotspotObjData === "object" && idmGeneralHotspotObjData?.options?.lazy) idmObserveOnce(selectedContainerEl, idmFill); + else idmFill(); +} + + +// obiekt js z przykładowymi danymi +/** + * Tablica konfiguracji hotspotów rekomendacji. + * + * @typedef {object} Hotspot + * @property {string} id - Identyfikator ramki (required). + * @property {string} title - Tytuł ramki. + * @property {string} classes - Dodatkowe klasy CSS. + * @property {object} placement - Określa, gdzie wstawić ramkę (required). + * @property {string} placement.selector - Selektor miejsca osadzenia. + * @property {string} placement.insert - Pozycja wstawienia względem selektora (np. "afterbegin", "beforeend"). + * @property {object} source - Dane źródłowe dla hotspotu (required). + * @property {string} [source.hotspotType] - Typ hotspotu (np. "promotion"). + * @property {number[]} [source.productsId] - Tablica ID produktów. + * @property {number} [source.productsMenu] - Identyfikator menu produktów. + * @property {object} query - Dane zapytania, nadpisują source (DEV). + * @property {string} query.string - Zapytanie w formacie GraphQL. + * @property {Function} query.graphFn - Funkcja do pobierania danych. + * @property {object} options - Ustawienia dla hotspotu (required). + * @property {boolean} options.lazy - Czy wczytywać w trybie lazy. + * @property {boolean|string} options.addToBasket - Obsługa koszyka: + * - true = włącz + * - false = wyłącz + * - "range" = dodaj z zakresem + * @property {boolean|object} options.swiper - Slider: + * - true = aktywny + * - false = nieaktywny + * - object = konfiguracja Swiper + * @property {Function} options.callbackFn - Funkcja callback która dzieje się po wywołaniu wszystkiego włącznie ze swiperem + * + * @type {Hotspot[]} + */ +// const idmHotspotArr = [ +// { +// id: "idmMainHotspot1",//!id ramki +// title: "Nowoczesna ramka rekomendacji",// +// classes: "abcdefg", +// placement: { +// selector: "#content", +// insert: "afterbegin" +// }, +// source: { +// hotspotType: "protomtion", +// productsId: [11,12], +// productsMenu: 122, +// }, +// query: { +// string: `searchInput: {productsId : [589, 180, 181590,160740, 155978, 153632, 123350, 82542, 37321, 17040, 25065, 25114, 85452]}`, +// graphFn: IDM_PRODUCTS_GQL +// }, +// // addToBasket: true, +// options: { +// lazy: false, +// addToBasket: "range", +// swiper: true, +// callbackFn: ()=>{console.log("test")} +// // swiper: albo true false - albo obiekt z opcjami swipera +// } +// }, +// { +// id: "idmMainHotspot2", +// title: "Super ramka rekomendacji", +// placement: { +// selector: "#content", +// insert: "beforeend" +// }, +// source: { +// productsMenu: 488, +// }, +// query: { +// string: `searchInput: {hotspot: promotion,limit: 16}`, +// graphFn: IDM_HOTSPOTS_GQL +// }, +// // addToBasket: true, +// options: { +// lazy: true, +// addToBasket: "range", +// swiper: true, +// // swiper: albo true false - albo obiekt z opcjami swipera +// } +// } +// ]; + + +// Wrzucenie na strone wszystkich ramek z obiektu js +async function idmInsertAllObjectHotspots(hotspotArr){ + try{ + const reqArr = [] + hotspotArr.forEach(hotspotObj=>{ + reqArr.push(idmInsertHotspotObject(hotspotObj)); + }); + await Promise.all(reqArr); + }catch(err){ + console.error(err) + } +} + + +// idmInsertAllObjectHotspots(idmHotspotArr); diff --git a/ramka.txt b/ramka.txt index f5a916b..d4b2c48 100644 --- a/ramka.txt +++ b/ramka.txt @@ -6,6 +6,9 @@ - wybór rozmiaru/wersji?? - Wybór kolorystyczny - AAAAA - banner na hotspocie +- Callback FN +- set Height +- Pasek jak na Pasiastym Parzystnokopytnym Kosmetyku - własne klasy - aplikacja do zarządania dodatkiem od obiektów hotspot @@ -15,8 +18,15 @@ Stara ramka - slick +JAK ROBIĆ CALLBACKI??? +WIELE OPCJI W STYLU PRZED SWIPEREM, PO SWIPERZE + + bramka z hotspots jeszcze nie działa bad request -Get-Content 1graphQL.js,2funkcje.js,3markup.js,4init.js,5ainsertHotspotHTML.js,5binsertHotspotObject.js,6style.js | Set-Content bundle.js \ No newline at end of file +Get-Content 1graphQL.js,2funkcje.js,3markup.js,4init.js,5ainsertHotspotHTML.js,5binsertHotspotObject.js | Set-Content bundle.js + + +Get-Content sklad/1graphQL.js,sklad/2funkcje.js,sklad/3markup.js,sklad/4init.js,sklad/5ainsertHotspotHTML.js,sklad/5binsertHotspotObject.js | Set-Content bundle.js \ No newline at end of file