From e7636c66ac46688c9466bdb98217caae1f94a07b Mon Sep 17 00:00:00 2001 From: "pawel.gaca" Date: Fri, 29 Aug 2025 14:09:11 +0200 Subject: [PATCH] Wrzucenie kodu do miejmy nadzieje dobrego folderu --- README.md | 80 +++ bundle.js | 1138 ++++++++++++++++++++++++++++++++ ramka.txt | 22 + sklad/1graphQL.js | 207 ++++++ sklad/2funkcje.js | 296 +++++++++ sklad/3markup.js | 147 +++++ sklad/4init.js | 157 +++++ sklad/5ainsertHotspotHTML.js | 109 +++ sklad/5binsertHotspotObject.js | 165 +++++ sklad/6style.js | 57 ++ 10 files changed, 2378 insertions(+) create mode 100644 README.md create mode 100644 bundle.js create mode 100644 ramka.txt create mode 100644 sklad/1graphQL.js create mode 100644 sklad/2funkcje.js create mode 100644 sklad/3markup.js create mode 100644 sklad/4init.js create mode 100644 sklad/5ainsertHotspotHTML.js create mode 100644 sklad/5binsertHotspotObject.js create mode 100644 sklad/6style.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..2561d7e --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Ramki rekomendacji # +Funkcje js składające się na customowe ramki rekomendacji + +### Pliki ### +- bundle.js - całość +- 1graphQL.js - graphQL + literały +- 2funkcje.js - ogólne funkcje jak dodawanie do koszyka, czy lazy loading +- 3markup.js - funkcje związane z markupem np zdjęć, cen +- 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) + +### Użycie ### +1. Wstawienie całego kodu do komponentu/dodatku +2. Ustawienie defaultowych ustawień w obiekcie idmGeneralHotspotObjData +3. Wstawienie HTML lub Obiektu js z odpowienimi danymi i wywołanie funkcji od tworzenia ramek + + +#### Przykład #### +##### Jedna ramka - obiekt ###### +``` +idmInsertHotspotObject({ + { + id: "idmMainHotspot1", + title: "Nowoczesna ramka rekomendacji", + classes: "abcdefg", + placement: { + selector: "#content", + insert: "afterbegin" + }, + query: { + string: `searchInput: {productsId : [589, 180, 181590,160740, 155978, 153632, 123350, 82542, 37321, 17040, 25065, 25114, 85452]}`, + graphFn: IDM_PRODUCTS_GQL + }, + options: { + lazy: false, + addToBasket: "range", + swiper: true, + } + }, +}) +``` +##### Wszystkie ramki - tablica obiektów ###### +``` +idmInsertAllObjectHotspots(hotspotArr); +``` + + + + +###### Jedna ramka - HTML ###### +``` +
+
+

+ + aaa + +

+
+
+ +``` + +###### Wszystkie ramki - HTML ###### +``` +idmInsertAllHTMLHotspots(); +``` + + + + +Created by • **[IdoMods](https://idomods.pl/)** • 2025 \ No newline at end of file diff --git a/bundle.js b/bundle.js new file mode 100644 index 0000000..29edfb8 --- /dev/null +++ b/bundle.js @@ -0,0 +1,1138 @@ +/////////////////////////////////////////////// +// 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); + +// style na razie tak doawane +document.querySelector("body").insertAdjacentHTML("beforeend", ``); diff --git a/ramka.txt b/ramka.txt new file mode 100644 index 0000000..f5a916b --- /dev/null +++ b/ramka.txt @@ -0,0 +1,22 @@ +1. Ramka + + +- ulubione? + porównywarka? (wymagają zmiany w komponencie idosella) +- zakres cen????????????? +- wybór rozmiaru/wersji?? +- Wybór kolorystyczny +- AAAAA - banner na hotspocie + +- własne klasy +- aplikacja do zarządania dodatkiem od obiektów hotspot + +Stara ramka +- getProductXML=t +- slick + + +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 diff --git a/sklad/1graphQL.js b/sklad/1graphQL.js new file mode 100644 index 0000000..60f803d --- /dev/null +++ b/sklad/1graphQL.js @@ -0,0 +1,207 @@ +/////////////////////////////////////////////// +// 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", +} \ No newline at end of file diff --git a/sklad/2funkcje.js b/sklad/2funkcje.js new file mode 100644 index 0000000..9d7d495 --- /dev/null +++ b/sklad/2funkcje.js @@ -0,0 +1,296 @@ +//////////////////////////////////////////////////////////////////////////\\\\\\\\\\\\ +// 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); +} diff --git a/sklad/3markup.js b/sklad/3markup.js new file mode 100644 index 0000000..98b6a42 --- /dev/null +++ b/sklad/3markup.js @@ -0,0 +1,147 @@ +////////////////////////////////////////////////////////////////////////// +// 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; +} diff --git a/sklad/4init.js b/sklad/4init.js new file mode 100644 index 0000000..f57f5ef --- /dev/null +++ b/sklad/4init.js @@ -0,0 +1,157 @@ +//////////////////////////////////////////////////// +// 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") \ No newline at end of file diff --git a/sklad/5ainsertHotspotHTML.js b/sklad/5ainsertHotspotHTML.js new file mode 100644 index 0000000..604d74d --- /dev/null +++ b/sklad/5ainsertHotspotHTML.js @@ -0,0 +1,109 @@ + +//////////////////////////////////////////////////// +// 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 + +

+
+
+ */ diff --git a/sklad/5binsertHotspotObject.js b/sklad/5binsertHotspotObject.js new file mode 100644 index 0000000..30e5643 --- /dev/null +++ b/sklad/5binsertHotspotObject.js @@ -0,0 +1,165 @@ +//////////////////////////////////////////////////// +// 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 diff --git a/sklad/6style.js b/sklad/6style.js new file mode 100644 index 0000000..4964127 --- /dev/null +++ b/sklad/6style.js @@ -0,0 +1,57 @@ + +// style na razie tak doawane +document.querySelector("body").insertAdjacentHTML("beforeend", ``); \ No newline at end of file