1395 lines
52 KiB
JavaScript
1395 lines
52 KiB
JavaScript
///////////////////////////////////////////////////////////
|
||
// TEXT
|
||
// LITERAŁY
|
||
const idmHotspotTextObject = {
|
||
[<iai:variable vid="Kod rabatowy"/>]: <iai:variable vid="Kod rabatowy"/>,
|
||
[<iai:variable vid="Okazja"/>]: <iai:variable vid="Okazja"/>,
|
||
[<iai:variable vid="Promocja"/>]: <iai:variable vid="Promocja"/>,
|
||
[<iai:variable vid="Bestseller"/>]: <iai:variable vid="Bestseller"/>,
|
||
[<iai:variable vid="Nowość"/>]: <iai:variable vid="Nowość"/>,
|
||
[<iai:variable vid="Ilość"/>]: <iai:variable vid="Ilość"/>,
|
||
[<iai:variable vid="Porównaj"/>]: <iai:variable vid="Porównaj"/>,
|
||
[<iai:variable vid="Dodaj do ulubionych"/>]: <iai:variable vid="Dodaj do ulubionych"/>,
|
||
[<iai:variable vid="Najniższa cena"/>]: <iai:variable vid="Najniższa cena"/>,
|
||
[<iai:variable vid="Najniższa cena z 30 dni przed obniżką"/>]: <iai:variable vid="Najniższa cena z 30 dni przed obniżką"/>,
|
||
[<iai:variable vid="Zwiększ ilość"/>]: <iai:variable vid="Zwiększ ilość"/>,
|
||
[<iai:variable vid="Zmniejsz ilość"/>]: <iai:variable vid="Zmniejsz ilość"/>,
|
||
[<iai:variable vid="Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki"/>]: <iai:variable vid="Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki"/>,
|
||
[<iai:variable vid="Cena regularna"/>]: <iai:variable vid="Cena regularna"/>,
|
||
[<iai:variable vid="Cena bez kodu"/>]: <iai:variable vid="Cena bez kodu"/>,
|
||
[<iai:variable vid="Cena nadchodząca od"/>]: <iai:variable vid="Cena nadchodząca od"/>,
|
||
[<iai:variable vid="Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę"/>]: <iai:variable vid="Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę"/>,
|
||
[<iai:variable vid="Nie znaleziono produktów"/>]: <iai:variable vid="Nie znaleziono produktów"/>,
|
||
[<iai:variable vid="Błąd przy pobieraniu danych"/>]: <iai:variable vid="Błąd przy pobieraniu danych"/>,
|
||
[<iai:variable vid="Kliknij, by przejść do formularza kontaktu"/>]: <iai:variable vid="Kliknij, by przejść do formularza kontaktu"/>,
|
||
[<iai:variable vid="Cena na telefon"/>]: <iai:variable vid="Cena na telefon"/>,
|
||
[<iai:variable vid="Dodany"/>]: <iai:variable vid="Dodany"/>,
|
||
[<iai:variable vid="Wystąpił błąd"/>]: <iai:variable vid="Wystąpił błąd"/>,
|
||
[<iai:variable vid="Do koszyka"/>]: <iai:variable vid="Do koszyka"/>,
|
||
[<iai:variable vid="Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"/>]: <iai:variable vid="Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"/>,
|
||
[<iai:variable vid="Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"/>]: <iai:variable vid="Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"/>,
|
||
[<iai:variable vid="Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"/>]: <iai:variable vid="Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"/>,
|
||
[<iai:variable vid="Nie znaleziono kontenera"/>]: <iai:variable vid="Nie znaleziono kontenera"/>,
|
||
[<iai:variable vid="Nie znaleziono metody graphql"/>]: <iai:variable vid="Nie znaleziono metody graphql"/>,
|
||
};
|
||
// STRING
|
||
// 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",
|
||
// }
|
||
|
||
///////////////////////////////////////////////
|
||
// GraphQL
|
||
// ogolne
|
||
const IDM_PRICE_QUERY = `price {
|
||
rebateCodeActive
|
||
price {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
omnibusPrice {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
omnibusPriceDetails {
|
||
unit {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
youSavePercent
|
||
omnibusPriceIsHigherThanSellingPrice
|
||
newPriceEffectiveUntil {
|
||
formatted
|
||
}
|
||
}
|
||
max {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
unit {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
unitConvertedPrice {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
youSavePercent
|
||
beforeRebate {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
beforeRebateDetails {
|
||
youSavePercent
|
||
unit {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
}
|
||
advancePrice {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
suggested {
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
rebateNumber {
|
||
number
|
||
gross {
|
||
value
|
||
formatted
|
||
}
|
||
}
|
||
}`;
|
||
|
||
const IDM_PRODUCT_QUERY = `id
|
||
type
|
||
name
|
||
zones
|
||
icon
|
||
iconSecond
|
||
iconSmall
|
||
iconSmallSecond
|
||
link
|
||
zones
|
||
producer{
|
||
name
|
||
}
|
||
category{
|
||
name
|
||
}
|
||
sizes{
|
||
id
|
||
amount
|
||
name
|
||
${IDM_PRICE_QUERY}
|
||
}
|
||
group{
|
||
id
|
||
name
|
||
link
|
||
versions{
|
||
id
|
||
name
|
||
icon
|
||
iconSecond
|
||
iconSmall
|
||
iconSmallSecond
|
||
}
|
||
}
|
||
opinion{
|
||
rating,
|
||
count
|
||
}
|
||
awardedParameters {
|
||
name
|
||
id
|
||
description
|
||
values {
|
||
name
|
||
id
|
||
}
|
||
}
|
||
enclosuresImages {
|
||
position
|
||
url
|
||
}
|
||
points
|
||
unit{
|
||
id, name, singular, plural, fraction, sellBy, precision, unitConvertedFormat
|
||
}
|
||
${IDM_PRICE_QUERY}`;
|
||
// 1. products
|
||
const IDM_PRODUCTS_GQL = (args) => JSON.stringify({
|
||
query: `{
|
||
products(${args}){
|
||
took
|
||
products{
|
||
${IDM_PRODUCT_QUERY}
|
||
}
|
||
}
|
||
}`
|
||
});
|
||
|
||
// 2. hotspots
|
||
const IDM_HOTSPOTS_GQL = (args) => JSON.stringify({
|
||
query: `{
|
||
hotspots(${args}){
|
||
took
|
||
name
|
||
url
|
||
products{
|
||
${IDM_PRODUCT_QUERY}
|
||
}
|
||
}
|
||
}`
|
||
});
|
||
|
||
// 3. single product
|
||
const IDM_PRODUCT_GQL = (args) => JSON.stringify({
|
||
query: `{
|
||
product(${args}){
|
||
product{
|
||
${IDM_PRODUCT_QUERY}
|
||
}
|
||
}
|
||
}`
|
||
});
|
||
// ADD TO BASKET
|
||
const IDM_HOTSPOT_ADD_TO_BASKET = (t, e, a) => JSON.stringify({
|
||
query: `mutation {\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 }`
|
||
});
|
||
/////////////////////////////////////////
|
||
// JS
|
||
app_shop.fn.idmGetOmnibusDetails = (options) => {
|
||
const {
|
||
productData, sizeId, priceType = app_shop.vars.priceType,
|
||
} = options || {};
|
||
if (!productData) return false;
|
||
const sizeData = productData.sizes.find((size) => size.id === sizeId) || productData;
|
||
if (!sizeData?.price) return false;
|
||
const classes = {
|
||
add: [],
|
||
remove: ['--omnibus', '--omnibus-short', '--omnibus-code', '--omnibus-code-short', '--omnibus-new-price', '--omnibus-higher'],
|
||
};
|
||
const activeLabel = {};
|
||
|
||
const omnibusPrice = sizeData.price?.omnibusPriceDetails?.unit?.[priceType]?.formatted || sizeData.price.omnibusPrice[priceType]?.formatted;
|
||
if (!omnibusPrice) {
|
||
return {
|
||
classes,
|
||
};
|
||
}
|
||
// Omnibus
|
||
classes.add.push('--omnibus');
|
||
classes.remove = classes.remove.filter((item) => item !== '--omnibus');
|
||
|
||
const sellBy = productData?.unit?.sellBy;
|
||
|
||
const unitMaxPrice = sizeData?.price?.unit?.[priceType]?.formatted && sizeData.price.max?.[priceType]?.value ? format_price(parseFloat(sizeData.price.max?.[priceType]?.value) * parseFloat(sellBy), {
|
||
mask: app_shop.vars.currency_format,
|
||
currency: app_shop.vars?.currency?.symbol,
|
||
currency_space: app_shop.vars.currency_space,
|
||
currency_before_price: app_shop.vars.currency_before_value,
|
||
}) : false;
|
||
|
||
const maxPrice = unitMaxPrice || sizeData.price.max?.[priceType]?.formatted;
|
||
// Skrócona wersja omnibusa
|
||
if (!maxPrice || maxPrice === omnibusPrice) {
|
||
classes.add.push('--omnibus-short');
|
||
classes.remove = classes.remove.filter((item) => item !== '--omnibus-short');
|
||
}
|
||
// Aktywny kod rabatowy
|
||
if (app_shop.vars.omnibus?.rebateCodeActivate && sizeData.price?.rebateCodeActive) {
|
||
classes.add.push('--omnibus-code');
|
||
activeLabel.rebateCodeActive = `<span class="label --code --omnibus">${omnibusDetailsTxt?.['Kod rabatowy']}</span>`;
|
||
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 = `<span class="label --bargain --omnibus">${omnibusDetailsTxt?.['Okazja']}</span>`;
|
||
}
|
||
// label promocja
|
||
if (Object.keys(activeLabel)?.length === 0) {
|
||
activeLabel.bargain = `<span class="label --promo --omnibus">${omnibusDetailsTxt?.['Promocja']}</span>`;
|
||
}
|
||
|
||
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: `<span class="omnibus_price__text">${omnibusDetailsTxt['Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki']}: </span><del class="omnibus_price__value">${omnibusPrice}</del><span class="price_percent">${omnibusPercent}</span>`,
|
||
};
|
||
|
||
const max = (maxPrice) ? {
|
||
max: {
|
||
price: maxPrice,
|
||
visible: true,
|
||
percent: `-${sizeData.price.youSavePercent}%`,
|
||
html: `<span class="omnibus_label">${omnibusDetailsTxt['Cena regularna']}: </span>
|
||
<del>${maxPrice}</del><span class="price_percent">-${sizeData.price.youSavePercent}%</span>`,
|
||
},
|
||
} : {};
|
||
|
||
const beforeRebate = (beforeRebatePrice) ? {
|
||
beforeRebate: {
|
||
price: beforeRebatePrice,
|
||
visible: !!classes.add.includes('--omnibus-code'),
|
||
percent: `-${sizeData.price.beforeRebateDetails?.youSavePercent}%`,
|
||
html: `<span class="omnibus_label">${omnibusDetailsTxt['Cena bez kodu']}: </span>
|
||
<del>${beforeRebatePrice}</del>
|
||
<span class="price_percent">-${sizeData.price.beforeRebateDetails?.youSavePercent}%</span>`,
|
||
},
|
||
} : {};
|
||
|
||
const newPriceEffectiveUntil = (newDate) ? {
|
||
newPriceEffectiveUntil: {
|
||
date: newDate,
|
||
price: maxPrice,
|
||
visible: !!classes.add.includes('--omnibus-new-price'),
|
||
html: `<span class="omnibus_label">${omnibusDetailsTxt['Cena nadchodząca od']} </span>
|
||
<span class="new_price__date">${newDate}: </span>
|
||
<span class="new_price__value">${maxPrice}</span>`,
|
||
},
|
||
} : {};
|
||
const omnibusLabel = {
|
||
omnibusLabel: {
|
||
name: Object.keys(activeLabel)?.[0] || '',
|
||
html: activeLabel[Object.keys(activeLabel)?.[0]],
|
||
},
|
||
};
|
||
|
||
return {
|
||
classes,
|
||
omnibus,
|
||
...max,
|
||
...beforeRebate,
|
||
...newPriceEffectiveUntil,
|
||
...omnibusLabel,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Klasa IdmHotspot
|
||
* ============================
|
||
* Odpowiada za tworzenie, renderowanie i obsługę dynamicznych hotspotów produktowych.
|
||
* Pobiera dane przez GraphQL, renderuje produkty, obsługuje dodawanie do koszyka,
|
||
* inicjuje Swipera, ustawia wysokości, nasłuchuje zdarzeń i wykonuje lazy loading.
|
||
*/
|
||
class IdmHotspot{
|
||
// ============================
|
||
// DOMYŚLNE USTAWIENIA SWIPERA
|
||
// ============================
|
||
static idmDefaultSwiperConfig = { // true, false, obiekt z opcjami swipera
|
||
loop: false,
|
||
autoHeight: false,
|
||
spaceBetween: 16,
|
||
slidesPerView: 1.4,
|
||
centeredSlides: true,
|
||
centeredSlidesBounds: true,
|
||
breakpoints: {
|
||
550: {
|
||
slidesPerView: 3,
|
||
centeredSlides: true,
|
||
centeredSlidesBounds: true,
|
||
},
|
||
979: {
|
||
slidesPerView: 4,
|
||
centeredSlides: false,
|
||
},
|
||
}
|
||
}
|
||
// ============================
|
||
// DOMYŚLNE OPCJE HOTSPOTA
|
||
// ============================
|
||
static idmDefaultHotspotOptions = {
|
||
options: {
|
||
lazy: true,
|
||
devMode: false,
|
||
callbackFn: ()=>{},
|
||
omnibusTooltip: false,
|
||
// switchImage: false,
|
||
|
||
// POKAZANIE
|
||
showOpinions: false,
|
||
|
||
// DODAWANIE
|
||
addToBasket: true, // true, false, "range"
|
||
addToFavorites: false, // Wymaga zmian szablonowych
|
||
addToCompare: false,
|
||
|
||
// WYBÓR
|
||
// selectSize: false,
|
||
// selectVersion: false,
|
||
|
||
// SWIPER
|
||
swiper: true,
|
||
swiperScrollbar: false,
|
||
}
|
||
}
|
||
/**
|
||
* Konstruktor
|
||
* @param {object} object - Dane konfiguracyjne hotspotu
|
||
*/
|
||
constructor({id, title, classes, placement, source, query, options = {}, hotspotEl, products}){
|
||
this.id = id || "";
|
||
this.title = title || "";
|
||
this.classes = classes || "";
|
||
this.placement = placement || {};
|
||
this.source = source || {};
|
||
this.query = query || {};
|
||
// this.type = type;
|
||
this.products = products || null;
|
||
|
||
this.hotspotEl = hotspotEl || null;
|
||
|
||
|
||
// Merge defaults
|
||
this.options = {
|
||
...IdmHotspot.idmDefaultHotspotOptions.options,
|
||
...options,
|
||
};
|
||
|
||
//
|
||
// this.hotspots = {};
|
||
this.priceType = app_shop?.vars?.priceType || "gross";
|
||
|
||
|
||
// bind this do funkcji eventowych
|
||
this.handleAddToBasket = this.handleAddToBasket.bind(this);
|
||
this.handleQuantityButtonClick = this.handleQuantityButtonClick.bind(this);
|
||
this.handleQuantityInputChange = this.handleQuantityInputChange.bind(this);
|
||
this.handleAddToCompare = this.handleAddToCompare.bind(this);
|
||
this.handleAddToFav = this.handleAddToFav.bind(this);
|
||
|
||
this.init();
|
||
}
|
||
|
||
|
||
|
||
|
||
// ========================================================
|
||
// ASYNC – POBIERANIE DANYCH Z GRAPHQL
|
||
// ========================================================
|
||
/**
|
||
* Przygotowuje funkcję i zapytanie GraphQL w zależności od typu danych.
|
||
*/
|
||
getQueryData({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];
|
||
}
|
||
/**
|
||
* Ustawia dane zapytania GraphQL wewnątrz instancji.
|
||
*/
|
||
setQueryData(queryObj){
|
||
const [graphFn, queryString] = this.getQueryData(queryObj);
|
||
this.query.graphFn = graphFn;
|
||
this.query.string = queryString;
|
||
}
|
||
|
||
/**
|
||
* Pobiera dane hotspotu z API GraphQL.
|
||
*/
|
||
async getHotspotData(){
|
||
try{
|
||
const res = await fetch(`/graphql/v1/`, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: this.query.graphFn(this.query.string),
|
||
});
|
||
const data = await res.json();
|
||
const products = data?.data?.[this.query.graphFn === IDM_HOTSPOTS_GQL ? "hotspots" : "products"]?.products;
|
||
if(!products || !products.length) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]);
|
||
|
||
this.products = products;
|
||
this.title = this.title || data?.data?.hotspots?.name || "";
|
||
}catch(err){
|
||
console.error(idmHotspotTextObject["Błąd przy pobieraniu danych"], err);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ========================================================
|
||
// MARKUP – TWORZENIE HTML PRODUKTÓW
|
||
// ========================================================
|
||
|
||
/**
|
||
* Tworzy markup dla wszystkich produktów w hotspotcie.
|
||
*/
|
||
markup(){
|
||
let markup = "";
|
||
this.products.forEach((prod)=>{
|
||
markup += this.markupProduct(prod);
|
||
})
|
||
return markup;
|
||
}
|
||
|
||
/**
|
||
* Tworzy markup dla pojedynczego produktu.
|
||
*/
|
||
markupProduct(prod){
|
||
// IDM DO POPRAWKI
|
||
const prodExchangedData = app_shop?.fn?.getOmnibusDetails?.({productData: prod}) || app_shop.fn?.idmGetOmnibusDetails({productData: prod});
|
||
|
||
// markup pojedynczego produktu
|
||
let singleMarkup = "";
|
||
singleMarkup += `
|
||
<div class="product hotspot__product swiper-slide d-flex flex-column ${prod.price.price[this.priceType].value === 0 ? "--phone" : ""}" data-id="${prod.id}">
|
||
<div class="product__yousave --hidden">
|
||
<span class="product__yousave --label"></span>
|
||
<span class="product__yousave --value"></span>
|
||
</div>
|
||
<div class="product__additional">
|
||
${this.markupAdditional(prod)}
|
||
</div>
|
||
<a class="product__icon d-flex justify-content-center align-items-center" tabindex="-1" href="${prod.link}">
|
||
${this.markupImage(prod)}
|
||
<strong class="label_icons">
|
||
${this.markupLabel(prod)}
|
||
</strong>
|
||
</a>
|
||
<div class="product__content_wrapper">
|
||
${this.markupOpinions(prod)}
|
||
<a class="product__name" tabindex="0" href="${prod.link}" title="${prod.name}">${prod.name}</a>
|
||
<div class="product__prices mb-auto ${prodExchangedData?.classes?.add?.reduce((acc,val) => acc + " " + val,"")}">
|
||
${this.markupPrice(prod, prodExchangedData)}
|
||
</div>
|
||
${this.markupAddToBasket(prod)}
|
||
</div>
|
||
</div>`;
|
||
|
||
return singleMarkup;
|
||
}
|
||
|
||
markupAdditional(prod){
|
||
return `
|
||
${this.markupCompare(prod)}
|
||
${this.markupFavorite(prod)}
|
||
`;
|
||
}
|
||
|
||
markupCompare(prod){
|
||
if(!this.options?.addToCompare) return "";
|
||
|
||
return `<div class="idm-products-banner__compare-btn" data-compare-id="${prod.id}">
|
||
<button class="idm-products-banner__compare-icon" aria-label="${idmHotspotTextObject[<iai:variable vid="Porównaj"/>]}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 20 17" fill="none">
|
||
<path d="M19.3972 5.76602C19.3889 5.67435 19.3639 5.58268 19.3222 5.49102L16.5639 0.991016C16.5639 0.991016 16.5389 0.966016 16.5306 0.957682C16.4722 0.874349 16.3889 0.799349 16.2972 0.749349C16.1972 0.699349 16.0889 0.666016 16.0056 0.666016H15.9556C15.9556 0.666016 15.9139 0.666016 15.8889 0.666016C15.8472 0.666016 15.7972 0.666016 15.7472 0.682682L4.08056 4.10768C4.08056 4.10768 4.02223 4.13268 4.01389 4.14102C3.98056 4.15768 3.94723 4.17435 3.91389 4.19102C3.83889 4.24102 3.78056 4.29935 3.73889 4.35768L0.938894 8.87435C0.938894 8.87435 0.922228 8.91602 0.913894 8.93268C0.872228 9.01602 0.847228 9.10768 0.838894 9.19935V9.24935C0.788894 10.216 1.12223 11.1493 1.77223 11.866C2.42223 12.5827 3.31389 13.0077 4.29723 13.0577C5.26389 13.0077 6.15556 12.5827 6.81389 11.866C7.46389 11.1493 7.79723 10.216 7.75556 9.25768C7.75556 9.23268 7.75556 9.21602 7.74723 9.19935C7.73889 9.10768 7.71389 9.01602 7.67223 8.93268L5.40556 5.22435L9.39723 4.04935V15.5493H6.56389V16.991H13.6639V15.5493H10.8389V3.62435L14.3472 2.59102L12.5889 5.44102C12.5889 5.44102 12.5722 5.48268 12.5639 5.49935C12.5222 5.58268 12.4972 5.67435 12.4889 5.75768V5.81602C12.4306 6.78268 12.7722 7.71602 13.4222 8.43268C14.0722 9.14935 14.9639 9.57435 15.9472 9.62435C16.9139 9.57435 17.8056 9.14935 18.4639 8.43268C19.1139 7.71602 19.4472 6.78268 19.4056 5.82435V5.76602H19.3972ZM17.8806 6.54935C17.7889 6.96602 17.5639 7.35768 17.2389 7.64935C16.8806 7.97435 16.4222 8.16602 15.9472 8.19102C15.4722 8.15768 15.0139 7.96602 14.6556 7.64935C14.3389 7.35768 14.1139 6.97435 14.0139 6.54935H17.8806ZM14.4972 5.10768L15.9472 2.74935L17.3972 5.10768H14.4972ZM2.84723 8.53268L4.29723 6.17435L5.74723 8.53268H2.84723ZM6.23056 9.97435C6.13056 10.391 5.91389 10.7827 5.58889 11.0743C5.23056 11.3993 4.77223 11.591 4.29723 11.616C3.82223 11.5827 3.36389 11.391 3.00556 11.0743C2.68889 10.7827 2.46389 10.3993 2.36389 9.97435H6.23056Z" fill="#111111"/>
|
||
</svg>
|
||
</button>
|
||
</div>`
|
||
}
|
||
|
||
markupFavorite(prod){
|
||
if(!this.options?.addToFavorites || typeof this.addToFavFn !== "function") return "";
|
||
|
||
return `<span
|
||
data-href="#add_favorite"
|
||
class="product__favorite" role="button" tabindex="0"
|
||
data-product-id="${prod.id}"
|
||
data-product-size="${prod.sizes?.[0]?.id || 'uniw'}" aria-label="${idmHotspotTextObject[<iai:variable vid="Dodaj do ulubionych"/>]}">
|
||
</span>`;
|
||
}
|
||
|
||
markupImage(prod){
|
||
let markup = "";
|
||
if(prod.iconSmallSecond && prod.iconSecond) markup +=`<picture>
|
||
<source media="(min-width: 421px)" type="image/webp" srcset="${prod.icon}"/>
|
||
<source media="(min-width: 421px)" type="image/jpeg" srcset="${prod.iconSecond}"/>
|
||
<source media="(max-width: 420px)" type="image/webp" srcset="${prod.iconSmall}"/>
|
||
<source media="(max-width: 420px)" type="image/jpeg" srcset="${prod.iconSmallSecond}"/>
|
||
<img src="${prod.iconSecond}" loading="lazy" alt="${prod.name}">
|
||
</picture>`;
|
||
else if(prod?.iconSmall) markup += `<picture>
|
||
<source media="(min-width: 421px)" srcset="${prod.icon}"/>
|
||
<source media="(max-width: 420px)" srcset="${prod.iconSmall}"/>
|
||
<img src="${prod.iconSecond}" loading="lazy" alt="${prod.name}">
|
||
</picture>`;
|
||
else markup += `<img src="${prod.icon}" loading="lazy" alt="${prod.name}">`
|
||
return markup;
|
||
}
|
||
|
||
markupLabel(prod){
|
||
let labelMarkup = ""
|
||
// labele zones
|
||
if(prod.zones.find(zone => zone ==="bestseller")) labelMarkup += `<span class="label --bestseller --omnibus">${idmHotspotTextObject["Bestseller"]}</span>`;
|
||
if(prod.zones.find(zone => zone ==="news")) labelMarkup += `<span class="label --news --omnibus">${idmHotspotTextObject["Nowość"]}</span>`;
|
||
|
||
const omnibusPrice = prod.price?.omnibusPriceDetails?.unit?.[this.idmPriceType]?.formatted || prod.price.omnibusPrice[this.idmPriceType]?.formatted;
|
||
if(omnibusPrice){
|
||
const newDate = prod.price.omnibusPriceDetails?.newPriceEffectiveUntil?.formatted;
|
||
const higher = prod.price.omnibusPriceDetails?.omnibusPriceIsHigherThanSellingPrice;
|
||
if (app_shop.vars.omnibus?.rebateCodeActivate && prod.price?.rebateCodeActive) {
|
||
labelMarkup += `<span class="label --code --omnibus">${idmHotspotTextObject["Kod rabatowy"]}</span>`;
|
||
}else if ((!higher || newDate) && !activeLabel?.rebateCodeActive) {
|
||
labelMarkup += `<span class="label --bargain --omnibus">${idmHotspotTextObject["Okazja"]}</span>`;
|
||
}else {
|
||
labelMarkup += `<span class="label --promo --omnibus">${idmHotspotTextObject["Promocja"]}</span>`;
|
||
}
|
||
}
|
||
|
||
return labelMarkup;
|
||
}
|
||
|
||
markupOpinions(prod){
|
||
if(!this.options?.showOpinions) return "";
|
||
|
||
return `<div class="product__opinions">
|
||
<div class="product__opinions_stars">
|
||
<i class="icon-star ${prod.opinion?.rating > 0.5 ? "--active" : ""}"></i>
|
||
<i class="icon-star ${prod.opinion?.rating > 1.5 ? "--active" : ""}"></i>
|
||
<i class="icon-star ${prod.opinion?.rating > 2.5 ? "--active" : ""}"></i>
|
||
<i class="icon-star ${prod.opinion?.rating > 3.5 ? "--active" : ""}"></i>
|
||
<i class="icon-star ${prod.opinion?.rating > 4.5 ? "--active" : ""}"></i>
|
||
</div>
|
||
<span class="product__opinions_score">${prod.opinion.rating.toFixed(2)} / 5.00</span>
|
||
</div>`
|
||
}
|
||
|
||
markupPrice(prod, prodExchangedData){
|
||
const price = prod.price.price[this.priceType];
|
||
const unit = prod.unit;
|
||
const pointsPrice = prod?.points;
|
||
const convertedPrice = prod.price?.unitConvertedPrice?.[this.priceType]?.formatted;
|
||
|
||
return `
|
||
<strong class="price --normal --main ${price.value === 0 ? "--hidden" : ""}">
|
||
<span class="price__sub">${price.formatted}</span>
|
||
<span class="price_sellby">
|
||
<span class="price_sellby__sep">/</span>
|
||
<span class="price_sellby__sellby" data-sellby="${unit?.sellBy}">${unit?.sellBy}</span>
|
||
<span class="price_sellby__unit">${unit?.sellBy > 1 ? unit?.plural : unit?.singular}</span>
|
||
</span>
|
||
${convertedPrice ? `<span class="price --convert">${convertedPrice}</span>` : ""}
|
||
</strong>
|
||
${pointsPrice ? `<span class="price --points">${pointsPrice} pkt.</span>` : ""}
|
||
${price.value === 0 ? `<a class="price --phone" href="/contact.php" tabindex="-1" title="${idmHotspotTextObject["Kliknij, by przejść do formularza kontaktu"]}">${idmHotspotTextObject["Cena na telefon"]}</a>` : ""}
|
||
${prodExchangedData?.beforeRebate?.visible ? `<span class="price --before-rebate">${prodExchangedData?.beforeRebate?.html}</span>` : ""}
|
||
${prodExchangedData?.newPriceEffectiveUntil?.visible ? `<span class="price --new-price new_price">${prodExchangedData?.newPriceEffectiveUntil?.html}</span>` : ""}
|
||
${this.markupOmnibus(prodExchangedData?.omnibus)}
|
||
${prodExchangedData?.max?.visible ? `<span class="price --max">${prodExchangedData?.max?.html}</span>` : ""}
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Tworzy znacznik HTML dla omnibusa.
|
||
*
|
||
* @param {object} omnibus - dane omnibusa
|
||
* @param {string} omnibus.html - kod html zwykłego omnibusa
|
||
* @param {string} omnibus.percent - % przeceny omnibusa
|
||
* @param {string} omnibus.price - cena omnibusa
|
||
* @param {boolean} omnibus.visible - czy omnibus jest widoczny
|
||
* @returns {string} Zwraca HTML dla sekcji omnibusa lub pusty string, jeśli niewidoczny.
|
||
*/
|
||
|
||
markupOmnibus(omnibus){
|
||
if(!omnibus?.visible) return "";
|
||
|
||
if(!this.options.omnibusTooltip) return `<span class="price --omnibus omnibus_price">${omnibus?.html}</span>`;
|
||
else return `
|
||
<span class="price --omnibus omnibus_price">
|
||
<span class="omnibus_price__text">${idmHotspotTextObject[<iai:variable vid="Najniższa cena"/>]}:</span>
|
||
<del class="omnibus_price__value">${omnibus.price}</del>
|
||
<span class="price_percent">${omnibus.percent}</span>
|
||
<span class="idm_tooltip">
|
||
<span class="idm_tooltip__info_icon"></span>
|
||
<p class="idm_tooltip__content --one-line">${idmHotspotTextObject[<iai:variable vid="Najniższa cena z 30 dni przed obniżką"/>]}</p>
|
||
</span>
|
||
</span>
|
||
`;
|
||
}
|
||
|
||
markupAddToBasket(prod){
|
||
let markup = "";
|
||
if(!this.options.addToBasket) return markup;
|
||
|
||
// link do produktu jak nie jest to zwykły produkt
|
||
if(prod.type !== "product" || prod.sizes[0].amount === 0) markup = `<a class="btn --solid --medium add_to_basket__link" href="${prod.href}">Zobacz produkt</a>`;
|
||
else if(this.options.addToBasket === "range") // +-
|
||
markup = `<form class="add_to_basket --range" action="/basketchange.php" type="post">
|
||
<input name="mode" type="hidden" value="1">
|
||
<input name="product" type="hidden" value="${prod.id}">
|
||
<input name="size" type="hidden" value="${prod.sizes[0].id}">
|
||
<div class="idm-products-banner__qty"
|
||
data-sell-by="${prod.unit?.sellBy}"
|
||
data-precision="${prod.unit?.precision}"
|
||
data-max="${prod.sizes[0].amount === -1 ? 999999 : prod.sizes[0].amount}"
|
||
>
|
||
<button type="button" class="idm-products-banner__qty-decrease" aria-label="${idmHotspotTextObject["Zmniejsz ilość"]}">−</button>
|
||
|
||
<input type="number"
|
||
name="number"
|
||
class="idm-products-banner__qty-input"
|
||
value="${prod.unit?.sellBy}"
|
||
step="${prod.unit?.sellBy}"
|
||
min="${prod.unit?.sellBy}"
|
||
max="${prod.sizes[0].amount === -1 ? 999999 : prod.sizes[0].amount}"
|
||
aria-label="${idmHotspotTextObject["Ilość"]}"
|
||
>
|
||
|
||
<button type="button" class="idm-products-banner__qty-increase" aria-label="${idmHotspotTextObject["Zwiększ ilość"]}">+</button>
|
||
</div>
|
||
|
||
<button type="submit" class="btn --solid --medium add_to_basket__button" tabindex="0" data-success="${idmHotspotTextObject["Dodany"]}" data-error="${idmHotspotTextObject["Wystąpił błąd"]}" data-text="${idmHotspotTextObject["Do koszyka"]}">
|
||
<span>${idmHotspotTextObject["Do koszyka"]}</span>
|
||
</button>
|
||
</form>`;
|
||
else // Zwykłe dodanie do koszyka
|
||
markup = `
|
||
<form class="add_to_basket" action="/basketchange.php" type="post">
|
||
<input name="mode" type="hidden" value="1">
|
||
<input name="product" type="hidden" value="${prod.id}">
|
||
<input name="size" type="hidden" value="${prod.sizes[0].id}">
|
||
<input name="number" type="hidden" value="${prod.unit?.sellBy}">
|
||
<button type="submit" class="btn --solid --medium add_to_basket__button" tabindex="0" data-success="${idmHotspotTextObject["Dodany"]}" data-error="${idmHotspotTextObject["Wystąpił błąd"]}" data-text="${idmHotspotTextObject["Do koszyka"]}">
|
||
<span>${idmHotspotTextObject["Do koszyka"]}</span>
|
||
</button>
|
||
</form>`;
|
||
return markup;
|
||
}
|
||
|
||
markupHotspotContainer(){
|
||
return `<section id="${this.id}" class="hotspot__wrapper idm__hotspot --init idm-loading ${this?.classes}">
|
||
<div class="hotspot --initialized">
|
||
${this?.title ? `
|
||
<h3 class="hotspot__name headline__wrapper">
|
||
<span class="headline"><span class="headline__name" aria-label="${this.title}">${this.title}</span></span>
|
||
</h3>
|
||
` : ""}
|
||
<div class="products__wrapper swiper">
|
||
<div class="products hotspot__products swiper-wrapper">
|
||
|
||
</div>
|
||
</div>
|
||
<div class="swiper-button-prev --rounded --edge idm-button-prev"><i class="icon-angle-left"></i></div>
|
||
<div class="swiper-button-next --rounded --edge idm-button-next"><i class="icon-angle-right"></i></div>
|
||
<div class="swiper-pagination"></div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
// ========================================================
|
||
// HANDLERY ZDARZEŃ
|
||
// ========================================================
|
||
|
||
/**
|
||
* Obsługuje dodanie produktu do koszyka (GraphQL).
|
||
*/
|
||
async handleAddToBasket(e){
|
||
const formEl = e.target.closest("form.add_to_basket");
|
||
if(!formEl) return;
|
||
try{
|
||
// pobieranie danych i elementów
|
||
formEl.classList.add("--loading")
|
||
const buttonEl = formEl.querySelector(".add_to_basket__button");
|
||
e.preventDefault();
|
||
|
||
const id = formEl.querySelector("input[name='product']")?.value;
|
||
const size = formEl.querySelector("input[type='hidden'][name='size']")?.value;
|
||
const number = formEl.querySelector("input[name='number']")?.value;
|
||
|
||
// dodanie do koszyka
|
||
const res = await fetch(`/graphql/v1/`, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: IDM_HOTSPOT_ADD_TO_BASKET(id, size, number)
|
||
});
|
||
const data = await res.json();
|
||
|
||
// Błąd
|
||
if(data?.data?.addProductsToBasket?.status !== "success") throw new Error(data);
|
||
else{
|
||
localStorage.setItem('addedtoBasket', true);
|
||
// Obsługiwanie sukcesu
|
||
app_shop.graphql.trackingEvents(res);
|
||
buttonEl.classList.add("--success");
|
||
|
||
// Dodawanie do koszyka na stronie basketedit.php będzie wymagał innego indywidualnego kodu!!!!!
|
||
buttonEl.innerHTML = `<span>${buttonEl.dataset.success}</span>`;
|
||
setTimeout(()=>{
|
||
buttonEl.innerHTML = `<span>${buttonEl.dataset.text}</span>`;
|
||
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 = `<span>${buttonEl.dataset.error}</span>`;
|
||
buttonEl.classList.add("--error")
|
||
setTimeout(()=>{
|
||
buttonEl.classList.remove("--error")
|
||
buttonEl.innerHTML = `<span>${buttonEl.dataset.text}</span>`;
|
||
}, 3000);
|
||
}finally{
|
||
formEl.classList.remove("--loading")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Obsługuje dodanie produktu do Porównania
|
||
*/
|
||
async handleAddToCompare(e){
|
||
const compareBtnEl = e.target.closest(".idm-products-banner__compare-btn");
|
||
const compareId = compareBtnEl?.dataset?.compareId;
|
||
if (!compareBtnEl || !compareId) return;
|
||
e.preventDefault();
|
||
try {
|
||
compareBtnEl.classList.add("--loading");
|
||
const compareUrl = `/${app_shop?.vars?.language?.symbol || "pl"}/settings.html?comparers=add&product=${compareId}`;
|
||
|
||
const res = await fetch(compareUrl);
|
||
console.log(res);
|
||
if (!res.ok) throw new Error(`${<iai:variable vid="Wystąpił błąd"/>}`);
|
||
|
||
compareBtnEl.classList.add("--success");
|
||
|
||
const compareContainerQuery = "#menu_compare_product";
|
||
if (document.querySelector(compareContainerQuery)) {
|
||
app_shop.fn?.load(
|
||
window.location.pathname,
|
||
[[compareContainerQuery, compareContainerQuery]],
|
||
function () {},
|
||
"?set_render=content"
|
||
);
|
||
}
|
||
|
||
setTimeout(() => {
|
||
compareBtnEl.classList.remove("--success");
|
||
}, 2000);
|
||
} catch (err) {
|
||
console.error(err);
|
||
Alertek.Error(`${<iai:variable vid="Coś poszło nie tak. Spróbuj ponownie później"/>}`);
|
||
} finally {
|
||
compareBtnEl.classList.remove("--loading");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Obsługuje dodanie produktu do Listy zakupowej
|
||
*/
|
||
handleAddToFav(e){
|
||
const favEl = e.target.closest(".product__favorite");
|
||
|
||
if(!favEl) return;
|
||
|
||
this.addToFavFn([[favEl.dataset.productId, favEl.dataset.productSize]]);
|
||
}
|
||
|
||
/**
|
||
* Obsługuje kliknięcia w przyciski +/− przy wyborze ilości.
|
||
*/
|
||
handleQuantityButtonClick(e){
|
||
if(e.target.classList.contains("idm-products-banner__qty-input")) return e.target.select();
|
||
const wrapper = e.target.closest(".idm-products-banner__qty");
|
||
|
||
const input = wrapper.querySelector(".idm-products-banner__qty-input");
|
||
const step = parseFloat(wrapper.dataset.sellBy || "1");
|
||
const precision = parseInt(wrapper.dataset.precision || "0");
|
||
const max = parseFloat(wrapper.dataset.max || "999999");
|
||
let current = parseFloat(input.value) || 0;
|
||
|
||
if (e.target.classList.contains("idm-products-banner__qty-increase")) {
|
||
current += step;
|
||
if (current > max){
|
||
current = max;
|
||
this.rangeMaxAlert(max)
|
||
}
|
||
} else if (e.target.classList.contains("idm-products-banner__qty-decrease")) {
|
||
current -= step;
|
||
if (current < step){
|
||
current = step;
|
||
this.rangeMinAlert(step)
|
||
}
|
||
}
|
||
input.value = current.toFixed(precision);
|
||
}
|
||
|
||
/**
|
||
* Walidacja zmian ilości w polu input.
|
||
*/
|
||
|
||
handleQuantityInputChange(e){
|
||
if(e.target.value > +e.target.max){
|
||
this.rangeMaxAlert(e.target.max)
|
||
e.target.value = +e.target.max
|
||
}else if(e.target.value < +e.target.min){
|
||
this.rangeMinAlert(e.target.min)
|
||
e.target.value = +e.target.min;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Lazy-load hotspotu – wczytuje dane dopiero, gdy element pojawi się w viewportcie.
|
||
*/
|
||
handleObserveHotspotOnce() {
|
||
if (!this.hotspotEl) return;
|
||
|
||
const observer = new IntersectionObserver((entries, obs) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
this.fillHotspot(); // run your callback
|
||
obs.disconnect(); // stop observing after first trigger
|
||
}
|
||
});
|
||
}, { root: null, rootMargin: "0px", threshold: 0.1 });
|
||
|
||
observer.observe(this.hotspotEl);
|
||
}
|
||
|
||
// ========================================================
|
||
// FUNKCJE POMOCNICZE
|
||
// ========================================================
|
||
|
||
/**
|
||
* Wyświetla alert o maksymalnej ilości produktu.
|
||
*/
|
||
rangeMaxAlert(max){
|
||
Alertek.Error(`${idmHotspotTextObject["Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${max}`)
|
||
}
|
||
/**
|
||
* Wyświetla alert o minimalnej ilości produktu.
|
||
*/
|
||
rangeMinAlert(min){
|
||
Alertek.Error(`${idmHotspotTextObject["Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${min}`)
|
||
}
|
||
|
||
/**
|
||
* Ustawia jednakową wysokość elementów (np. nazw lub cen).
|
||
*/
|
||
setHeight(options){
|
||
const { selector, selectors, container } = options || {}
|
||
if ((!selector && !selectors) || !container) return
|
||
|
||
const containerElement = document.querySelector(container)
|
||
if (!containerElement) return
|
||
|
||
const adjustAllHeights = itemSelector => {
|
||
const targets = containerElement.querySelectorAll(itemSelector)
|
||
if (!targets.length) return
|
||
|
||
targets.forEach(el => (el.style.minHeight = ''))
|
||
|
||
const max = Math.max(...[...targets].map(el => el.offsetHeight || 0))
|
||
|
||
targets.forEach(el => (el.style.minHeight = `${max}px`))
|
||
}
|
||
|
||
if (selector) adjustAllHeights(selector)
|
||
if (selectors?.length) selectors.forEach(adjustAllHeights)
|
||
}
|
||
// ========================================================
|
||
// INICJALIZACJA
|
||
// ========================================================
|
||
|
||
/**
|
||
* Wykonywana po pełnej inicjalizacji hotspotu (Swiper, eventy, wysokości).
|
||
*/
|
||
async afterInit(){
|
||
try{
|
||
if(!this.hotspotEl) throw new Error("Nie znaleziono elementu");
|
||
|
||
if(this.title && !this.hotspotEl.querySelector(".hotspot__name.headline__wrapper")){
|
||
this.hotspotEl.querySelector(".hotspot.--initialized")?.insertAdjacentHTML("afterbegin", `<h3 class="hotspot__name headline__wrapper">
|
||
<span class="headline"><span class="headline__name" aria-label="${this.title}">${this.title}</span></span>
|
||
</h3>`);
|
||
}
|
||
|
||
await this.initSwiper();
|
||
// IDM setHeight
|
||
this.setHeight({
|
||
selectors: [
|
||
`#${this.id} .product__prices`,
|
||
`#${this.id} .product__name`,
|
||
],
|
||
container: `#${this.id} .products__wrapper`,
|
||
});
|
||
this.initEvents();
|
||
console.log(`Initialized hotspot #${this.id}`);
|
||
|
||
if(typeof this.options?.callbackFn === "function") this.options?.callbackFn();
|
||
}catch(err){
|
||
console.error(idmHotspotTextObject["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"], err);
|
||
}
|
||
}
|
||
|
||
initExternalFunctions(){
|
||
this.addToFavFn = app_shop.fn?.shoppingList?.addProductToList;
|
||
}
|
||
|
||
/**
|
||
* Pobiera dane, wypełnia markup i inicjuje Swipera.
|
||
*/
|
||
async fillHotspot(){
|
||
// Zdefiniowanie funkcji do dodawania do ulubionych
|
||
this.initExternalFunctions();
|
||
try{
|
||
if(!this.products){
|
||
if(!this?.query?.graphFn || !this?.query?.string) this.setQueryData({
|
||
productsID: this?.source?.productsId,
|
||
productsMenu: this?.source?.productsMenu,
|
||
hotspotsType: this?.source?.hotspotsType
|
||
});
|
||
|
||
// pobranie danych o produktach
|
||
await this.getHotspotData();
|
||
if(!this.products) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]);
|
||
}
|
||
|
||
|
||
// Wstawienie markupa na strone
|
||
this.hotspotEl.querySelector(".products.hotspot__products")?.insertAdjacentHTML("beforeend", this.markup());
|
||
this.hotspotEl.classList.remove("idm-loading");
|
||
|
||
// init swiper + add to basket
|
||
this.afterInit();
|
||
}catch(err){
|
||
console.error(idmHotspotTextObject["Wystąpił błąd"], err);
|
||
this.hotspotEl.remove();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Inicjuje instancję Swipera dla hotspotu.
|
||
*/
|
||
async initSwiper(){
|
||
try{
|
||
// swiper || slick
|
||
if(this.options?.swiper){
|
||
// Opcje swipera
|
||
if(typeof this?.options?.swiper === "boolean") this.options.swiper = IdmHotspot.idmDefaultSwiperConfig;
|
||
|
||
// Wywołanie swipera
|
||
const selectedSwiper = new HotspotSlider({
|
||
selector: `#${this.id} .swiper`,
|
||
hotspotName: `${this.id}`,
|
||
options: this.options.swiper,
|
||
});
|
||
await selectedSwiper.init();
|
||
|
||
if(this.options.swiperScrollbar) new IdmSwiperProgress(selectedSwiper, `#${this.id} .swiper`);
|
||
}
|
||
}catch(err){
|
||
console.error(idmHotspotTextObject["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"], err);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Inicjuje eventy dla produktów w hotspotcie.
|
||
*/
|
||
initEvents(){
|
||
this.hotspotEl.querySelectorAll(".product").forEach(prodEl=>{
|
||
this.initSingleEvent(prodEl);
|
||
})
|
||
}
|
||
initSingleEvent(prodEl){
|
||
// DODAWANIE DO KOSZYKA
|
||
if(this.options?.addToBasket){
|
||
const addToBasketEl = prodEl.querySelector("form.add_to_basket");
|
||
if(!addToBasketEl) return;
|
||
addToBasketEl.addEventListener("submit", this.handleAddToBasket);
|
||
|
||
// + -
|
||
if(this?.options?.addToBasket === "range"){
|
||
addToBasketEl.querySelector(".idm-products-banner__qty")?.addEventListener("click",this.handleQuantityButtonClick);
|
||
addToBasketEl.querySelector(".idm-products-banner__qty-input")?.addEventListener("input",this.handleQuantityInputChange);
|
||
}
|
||
}
|
||
// Tooltip
|
||
// if(this.options?.omnibusTooltip){
|
||
// const tooltipEl = prodEl.querySelector(".idm_tooltip");
|
||
|
||
// tooltipEl.addEventListener("click", ()=>{
|
||
// this.showTooltip(tooltipEl);
|
||
// })
|
||
// }
|
||
|
||
// Add to Favorites
|
||
if(this.options?.addToFavorites && typeof this.addToFavFn === "function") prodEl.querySelector(".product__favorite")?.addEventListener("click", this.handleAddToFav);
|
||
// Compare
|
||
if(this.options?.addToCompare) prodEl.querySelector(".idm-products-banner__compare-btn")?.addEventListener("click", this.handleAddToCompare);
|
||
}
|
||
|
||
/**
|
||
* Inicjuje kontener hotspotu w określonym miejscu DOM.
|
||
*/
|
||
initHotspotContainer(){
|
||
const selectedEl = document.querySelector(this?.placement.selector);
|
||
if(!selectedEl) throw new Error(idmHotspotTextObject["Nie znaleziono kontenera"]);
|
||
const markup = this.markupHotspotContainer();
|
||
selectedEl.insertAdjacentHTML(this.placement.insert || "afterend", markup);
|
||
|
||
this.hotspotEl = document.getElementById(this.id);
|
||
}
|
||
|
||
/**
|
||
* Główna metoda inicjalizująca hotspot (lazy lub natychmiast).
|
||
*/
|
||
async init(){
|
||
const queryString = window.location.search;
|
||
const urlParams = new URLSearchParams(queryString);
|
||
const dev = urlParams.get('dev')
|
||
|
||
if(this.options?.devMode && dev !== "true") return console.error(`Brak włączonego devMode. Ramka ${this.id} nie mogła zostać utworzona!`);
|
||
|
||
if(!this.hotspotEl || !document.contains(this.hotspotEl)) this.initHotspotContainer();
|
||
|
||
if(this.options?.lazy) this.handleObserveHotspotOnce();
|
||
else this.fillHotspot();
|
||
}
|
||
}
|
||
|
||
/*
|
||
==============================================================
|
||
SWIPER PASEK
|
||
==============================================================
|
||
*/
|
||
|
||
class IdmSwiperProgress {
|
||
constructor(swiper, selector) {
|
||
this.swiper = swiper?.slider?.slider ?? swiper?.slider ?? swiper;
|
||
this.selector = selector;
|
||
this.scrollbarEl = null;
|
||
this.progressEl = null;
|
||
this.isDragging = false;
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
const el = document.querySelector(this.selector);
|
||
if (!el || el.querySelector(".idm-scrollbar")) return;
|
||
|
||
el.insertAdjacentHTML(
|
||
"beforeend",
|
||
`<div class="idm-scrollbar swiper-scrollbar"><div class="idm-progress"></div></div>`
|
||
);
|
||
|
||
this.scrollbarEl = el.querySelector(".idm-scrollbar");
|
||
this.progressEl = this.scrollbarEl.querySelector(".idm-progress");
|
||
|
||
this.updateBarWidth();
|
||
this.addDragTracking();
|
||
|
||
this.swiper.on("progress", () => this.updateProgress());
|
||
this.swiper.on("breakpoint", () => {this.updateBarWidth()});
|
||
}
|
||
|
||
updateBarWidth() {
|
||
const { slidesPerGroup, slidesPerView } = this.swiper.params;
|
||
const totalSlides = this.swiper.slides.length;
|
||
|
||
const progressWidth = 100 / (((totalSlides - slidesPerView) / slidesPerGroup) + 1);
|
||
this.progressEl.style.width = `${progressWidth}%`;
|
||
|
||
if (progressWidth >= 100 || progressWidth <= 0) this.scrollbarEl.style.display = "none";
|
||
else this.scrollbarEl.style.display = "";
|
||
}
|
||
|
||
updateProgress() {
|
||
const progress = this.swiper.progress;
|
||
const { slidesPerGroup, slidesPerView } = this.swiper.params;
|
||
const totalSlides = this.swiper.slides.length;
|
||
|
||
const progressWidth = 100 / (((totalSlides - slidesPerView) / slidesPerGroup) + 1);
|
||
const newLeft = (100 - progressWidth) * progress;
|
||
this.progressEl.style.left = `${Math.min(
|
||
100 - progressWidth,
|
||
Math.max(0, newLeft)
|
||
)}%`;
|
||
}
|
||
|
||
addDragTracking() {
|
||
const handle = this.progressEl;
|
||
let grabOffset = 0;
|
||
let scrollbarWidth = 0;
|
||
let handleWidth = 0;
|
||
|
||
const startDrag = (e) => {
|
||
this.isDragging = true;
|
||
this.scrollbarEl.classList.add("--drag-start");
|
||
|
||
const rect = this.scrollbarEl.getBoundingClientRect();
|
||
const handleRect = handle.getBoundingClientRect();
|
||
scrollbarWidth = rect.width;
|
||
handleWidth = handleRect.width;
|
||
|
||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||
grabOffset = clientX - handleRect.left;
|
||
|
||
document.addEventListener("mousemove", handleDrag);
|
||
document.addEventListener("mouseup", stopDrag);
|
||
document.addEventListener("touchmove", handleDrag);
|
||
document.addEventListener("touchend", stopDrag);
|
||
};
|
||
|
||
const handleDrag = (e) => {
|
||
if (!this.isDragging) return;
|
||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||
const rect = this.scrollbarEl.getBoundingClientRect();
|
||
let newLeftPx = clientX - rect.left - grabOffset;
|
||
const maxLeft = scrollbarWidth - handleWidth;
|
||
newLeftPx = Math.max(0, Math.min(maxLeft, newLeftPx));
|
||
const progress = newLeftPx / maxLeft;
|
||
this.swiper.setProgress(progress, 0);
|
||
};
|
||
|
||
const stopDrag = () => {
|
||
if (!this.isDragging) return;
|
||
this.isDragging = false;
|
||
document.removeEventListener("mousemove", handleDrag);
|
||
document.removeEventListener("mouseup", stopDrag);
|
||
document.removeEventListener("touchmove", handleDrag);
|
||
document.removeEventListener("touchend", stopDrag);
|
||
this.scrollbarEl.classList.remove("--drag-start");
|
||
this.swiper.slideReset(400);
|
||
};
|
||
|
||
handle.addEventListener("mousedown", startDrag);
|
||
handle.addEventListener("touchstart", startDrag);
|
||
}
|
||
}
|
||
// ========================================================
|
||
// TOOLTIP
|
||
// ========================================================
|
||
function idmShowTooltip(tooltipEl){
|
||
const tooltipContentEl = tooltipEl.querySelector(".idm_tooltip__content");
|
||
if(!tooltipContentEl) return;
|
||
|
||
tooltipContentEl.classList.add("--visible");
|
||
|
||
// Logika pokazywania się i chowania tooltipa
|
||
let timeoutVar;
|
||
|
||
function onMouseLeave() {
|
||
timeoutVar = idmHideTooltipTimer(tooltipEl);
|
||
}
|
||
|
||
function onMouseEnter() {
|
||
clearTimeout(timeoutVar);
|
||
}
|
||
|
||
function onScroll() {
|
||
idmHideTooltip(tooltipEl);
|
||
}
|
||
|
||
// Store references for later removal
|
||
tooltipEl._onMouseLeave = onMouseLeave;
|
||
tooltipEl._onMouseEnter = onMouseEnter;
|
||
tooltipEl._onScroll = onScroll;
|
||
|
||
tooltipEl.addEventListener("mouseleave", onMouseLeave);
|
||
tooltipEl.addEventListener("mouseenter", onMouseEnter);
|
||
document.addEventListener("scroll", onScroll);
|
||
}
|
||
|
||
function idmHideTooltipTimer(tooltipEl){
|
||
return setTimeout(() => idmHideTooltip(tooltipEl), 1500);
|
||
}
|
||
function idmHideTooltip(tooltipEl){
|
||
const tooltipContentEl = tooltipEl.querySelector(".idm_tooltip__content");
|
||
if (!tooltipContentEl) return;
|
||
|
||
tooltipContentEl.classList.remove("--visible");
|
||
|
||
tooltipEl.removeEventListener("mouseleave", tooltipEl._onMouseLeave);
|
||
tooltipEl.removeEventListener("mouseenter", tooltipEl._onMouseEnter);
|
||
document.removeEventListener("scroll", tooltipEl._onScroll);
|
||
|
||
delete tooltipEl._onMouseLeave;
|
||
delete tooltipEl._onMouseEnter;
|
||
delete tooltipEl._onScroll;
|
||
}
|
||
|
||
|
||
// document.addEventListener("DOMContentLoaded", ()=>{
|
||
document.body.addEventListener("click", e=>{
|
||
const tooltipEl = e.target.closest(".idm_tooltip");
|
||
if(!e.target.closest(".idm_tooltip__info_icon") || !tooltipEl) return;
|
||
|
||
e.preventDefault();
|
||
idmShowTooltip(tooltipEl);
|
||
});
|
||
// });
|
||
// new IdmHotspot({
|
||
// id: "idmTestHotspot1",
|
||
// title: "tescik",
|
||
// placement: {
|
||
// selector: "#content",
|
||
// insert: "beforeend",
|
||
// },
|
||
// source: {
|
||
// productsMenu: 1649
|
||
// }
|
||
// });
|
||
// {
|
||
// id: "idmMainHotspot2",
|
||
// title: "Super ramka rekomendacji",
|
||
// placement: {
|
||
// selector: "#content",
|
||
// insert: "beforeend"
|
||
// },
|
||
// source: {
|
||
// productsMenu: 488,
|
||
// },
|
||
// options: {
|
||
// lazy: true,
|
||
// addToBasket: "range",
|
||
// swiper: true,
|
||
// }
|
||
// }
|
||
|
||
|
||
async function idmPrepareHotspotObject(selectedContainerEl){
|
||
selectedContainerEl.classList.add("--init");
|
||
const source = {};
|
||
|
||
if(selectedContainerEl?.dataset?.productsId) source.productsId = selectedContainerEl?.dataset?.productsId.split(",");
|
||
else if(selectedContainerEl?.dataset?.hotspotsType) source.hotspotsType = selectedContainerEl?.dataset?.hotspotsType;
|
||
else if(selectedContainerEl?.dataset?.productsMenu) source.productsMenu = selectedContainerEl?.dataset?.productsMenu;
|
||
else{
|
||
console.error();
|
||
selectedContainerEl?.remove();
|
||
return;
|
||
}
|
||
|
||
const idmHotspotObj = {
|
||
id: selectedContainerEl?.id,
|
||
source,
|
||
hotspotEl: selectedContainerEl
|
||
};
|
||
|
||
if(selectedContainerEl?.dataset?.lazy) idmHotspotObj.options = {lazy: selectedContainerEl?.dataset?.lazy === "true" ? true : false};
|
||
|
||
new IdmHotspot(idmHotspotObj)
|
||
}
|
||
|
||
|
||
document.querySelectorAll(".hotspot__wrapper.idm__hotspot").forEach(currentHotspot=>{
|
||
idmPrepareHotspotObject(currentHotspot)
|
||
})
|