1/** 2 * Entourance Common JavaScript Utilities 3 * 4 * Funzioni comuni utilizzate da product-list e product-slider 5 * 6 * @package EntourancePlugin 7 */ 8 9/** 10 * Invia un evento analytics (POST JSON) senza mai bloccare la pagina: 11 * ignora errori di rete, CORS, URL non validi e promise non gestite. 12 * 13 * @param {string} url Endpoint completo. 14 * @param {Object} body Oggetto serializzabile in JSON. 15 */ 16function entouranceSafeAnalyticsPost(url, body) { 17 if (typeof fetch !== 'function') { 18 return; 19 } 20 if (!url || typeof url !== 'string') { 21 return; 22 } 23 body = body && typeof body === 'object' ? body : {}; 24 if ( 25 body.realm_id == null && 26 window.entouranceAnalyticsConfig && 27 window.entouranceAnalyticsConfig.realmId != null && 28 window.entouranceAnalyticsConfig.realmId !== '' 29 ) { 30 body.realm_id = window.entouranceAnalyticsConfig.realmId; 31 } 32 var json; 33 try { 34 json = JSON.stringify(body); 35 } catch (e) { 36 return; 37 } 38 var request; 39 try { 40 request = fetch(url, { 41 method: 'POST', 42 headers: { 'Content-Type': 'application/json' }, 43 body: json, 44 credentials: 'omit', 45 cache: 'no-store', 46 keepalive: true, 47 }); 48 } catch (e) { 49 return; 50 } 51 if (request && typeof request.then === 'function') { 52 request 53 .then( 54 function () { 55 return undefined; 56 }, 57 function () { 58 return undefined; 59 } 60 ) 61 .catch(function () { 62 return undefined; 63 }); 64 } 65} 66window.entouranceSafeAnalyticsPost = entouranceSafeAnalyticsPost; 67 68// Namespace per evitare conflitti 69window.EntouranceCommon = window.EntouranceCommon || {}; 70 71/** 72 * Funzioni per la gestione dei filtri 73 */ 74EntouranceCommon.Filters = { 75 /** 76 * Estrae filtri unici da un array di dati 77 * @param {Array} data - Array di oggetti 78 * @param {string} field - Campo da estrarre 79 * @returns {Array} Array di valori unici 80 */ 81 getUniqueFilters: function(data, field) { 82 const uniqueFilter = new Set(); 83 84 data.forEach(item => { 85 if (item[field] !== undefined && item[field] !== null) { 86 uniqueFilter.add(item[field]); 87 } 88 }); 89 90 return Array.from(uniqueFilter); 91 }, 92 93 /** 94 * Crea un elemento di filtro per città 95 * @param {Object} item - Oggetto città 96 * @param {string} type - Tipo di prodotto 97 * @returns {HTMLElement} Elemento DOM 98 */ 99 createCityFilterItem: function(item, type) { 100 const option = document.createElement('option'); 101 option.value = item; 102 option.textContent = item; 103 return option; 104 }, 105 106 /** 107 * Crea un elemento di filtro per categoria 108 * @param {Object} item - Oggetto categoria 109 * @param {string} type - Tipo di prodotto 110 * @returns {HTMLElement} Elemento DOM 111 */ 112 createCategoryFilterItem: function(item, type) { 113 const option = document.createElement('option'); 114 option.value = item; 115 option.textContent = item; 116 return option; 117 }, 118 119 /** 120 * Crea un elemento di filtro per POI 121 * @param {Object} item - Oggetto POI 122 * @param {string} type - Tipo di prodotto 123 * @returns {HTMLElement} Elemento DOM 124 */ 125 createPoiFilterItem: function(item, type) { 126 const option = document.createElement('option'); 127 option.value = item; 128 option.textContent = item; 129 return option; 130 }, 131 132 /** 133 * Crea un elemento di filtro per sottocategoria 134 * @param {Object} item - Oggetto sottocategoria 135 * @param {string} type - Tipo di prodotto 136 * @param {Array} relatedSubcategories - Sottocategorie correlate 137 * @returns {HTMLElement} Elemento DOM 138 */ 139 createSubcategoryFilterItem: function(item, type, relatedSubcategories) { 140 const option = document.createElement('option'); 141 option.value = item; 142 option.textContent = item; 143 return option; 144 }, 145 146 /** 147 * Crea un elemento di filtro per gruppo 148 * @param {Object} item - Oggetto gruppo 149 * @param {string} type - Tipo di prodotto 150 * @returns {HTMLElement} Elemento DOM 151 */ 152 createGroupFilterItem: function(item, type) { 153 const option = document.createElement('option'); 154 option.value = item; 155 option.textContent = item; 156 return option; 157 } 158}; 159 160/**
161 * Funzioni per la gestione dei prodotti 162 */ 163EntouranceCommon.Products = { 164 /** 165 * Genera l'URL per un prodotto in base al tipo 166 * @param {Object} product - Oggetto prodotto 167 * @param {string} type - Tipo di prodotto 168 * @param {string} baseUrl - URL base del sito 169 * @returns {string} URL del prodotto 170 */ 171 generateUrl: function(product, type, baseUrl) { 172 const slug = product.slug || product.id; 173 174 switch(type) { 175 case 'experience': 176 return `${baseUrl}/experience/${slug}`; 177 case 'event': 178 return `${baseUrl}/event/${slug}`; 179 case 'eatery': 180 return `${baseUrl}/eatery/${slug}`; 181 case 'host': 182 return `${baseUrl}/host/${slug}`; 183 case 'article': 184 return `${baseUrl}/article/${slug}`; 185 case 'itinerary': 186 return `${baseUrl}/itinerary/${slug}`; 187 case 'poi': 188 return `${baseUrl}/poi/${slug}`; 189 case 'experienceSupplier': 190 case 'experience-supplier': 191 return `${baseUrl}/service/${slug}`; 192 default: 193 return '#'; 194 } 195 }, 196 197 /** 198 * Formatta la durata rimuovendo i secondi 199 * @param {string} duration - Durata in formato HH:MM:SS 200 * @returns {string} Durata formattata 201 */ 202 formatDuration: function(duration) { 203 if (!duration) return ''; 204 return duration.split(':').slice(0, 2).join(':'); 205 }, 206 207 /** 208 * Traduce il tipo di prodotto 209 * @param {string} type - Tipo di prodotto 210 * @param {string} language - Lingua corrente 211 * @returns {string} Tipo tradotto 212 */ 213 translateType: function(type, language) { 214 const translations = { 215 'experience': language === 'it_IT' ? 'Esperienza' : 'Experience', 216 'eatery': language === 'it_IT' ? 'Ristorante' : 'Eatery', 217 'host': language === 'it_IT' ? 'Struttura' : 'Host', 218 'article': language === 'it_IT' ? 'Articolo' : 'Article', 219 'itinerary': language === 'it_IT' ? 'Itinerario' : 'Itinerary', 220 'poi': language === 'it_IT' ? 'Punto di interesse' : 'Point of interest', 221 'experienceSupplier': language === 'it_IT' ? 'Servizio' : 'Service', 222 'experience-supplier': language === 'it_IT' ? 'Servizio' : 'Service' 223 }; 224 return translations[type] || type; 225 }, 226 227 /** 228 * Crea una card prodotto 229 * @param {Object} product - Oggetto prodotto 230 * @param {HTMLElement} template - Template della card 231 * @param {string} type - Tipo di prodotto 232 * @param {string} language - Lingua corrente 233 * @param {string} baseUrl - URL base del sito 234 * @returns {HTMLElement} Card DOM 235 */ 236 createCard: function(product, template, type, language, baseUrl) { 237 const card = template.content.cloneNode(true); 238 239 // Immagine 240 const img = card.querySelector('img'); 241 const imgWrapper = card.querySelector('.card-img-wrapper'); 242 const imgSrc = Array.isArray(product.images) && product.images[0] ? product.images[0] : ''; 243 244 if (imgSrc) { 245 img.src = imgSrc; 246 img.alt = product.title || product.name; 247 } else { 248 // Placeholder 249 img.remove(); 250 const placeholder = document.createElement('div'); 251 placeholder.className = 'card-img-placeholder d-flex align-items-center justify-content-center'; 252 placeholder.innerHTML = '<i class="fas fa-image fa-3x text-secondary"></i>'; 253 imgWrapper.appendChild(placeholder); 254 imgWrapper.style.background = '#f5f5f5'; 255 imgWrapper.style.minHeight = '200px'; 256 } 257 258 // Link 259 const link = card.querySelector('.card-link'); 260 link.href = this.generateUrl(product, type, baseUrl); 261 262 // Titolo 263 const title = card.querySelector('.card-title'); 264 title.textContent = product.title || product.name; 265 266 // Duration 267 const duration = card.querySelector('.duration'); 268 if (duration && product.duration) { 269 duration.textContent = this.formatDuration(product.duration); 270 } 271 272 // Prezzo 273 const price = card.querySelector('.price'); 274 if (price && product.price) { 275 price.textContent = product.price; 276 } 277 278 // Badge tipo 279 const typeBadge = card.querySelector('.type-badge'); 280 if (typeBadge) { 281 typeBadge.textContent = this.translateType(type, language); 282 } 283 284 // Badge categoria 285 const categoryBadge = card.querySelector('.category-badge'); 286 if (categoryBadge && product.category) { 287 categoryBadge.textContent = product.category; 288 } 289 290 return card; 291 } 292}; 293 294/**
295 * Funzioni per la gestione delle date 296 */ 297EntouranceCommon.Dates = { 298 /** 299 * Calcola l'offset tra due mesi 300 * @param {number} month1 - Primo mese 301 * @param {number} month2 - Secondo mese 302 * @returns {number} Offset 303 */ 304 calculateMonthOffset: function(month1, month2) { 305 return month2 - month1; 306 }, 307 308 /** 309 * Controlla se un array contiene una sottostringa 310 * @param {Array} array - Array da controllare 311 * @param {string} substring - Sottostringa da cercare 312 * @returns {boolean} True se trovata 313 */ 314 hasSubstring: function(array, substring) { 315 return array.some(item => item.includes(substring)); 316 }, 317 318 /** 319 * Gestisce la pressione dei tasti 320 * @param {Event} event - Evento keypress 321 * @param {string} type - Tipo di prodotto 322 */ 323 handleKeyPress: function(event, type) { 324 if (event.key === 'Enter') { 325 // Logica per gestire Enter 326 } 327 }, 328 329 /** 330 * Ottiene il testo di ricerca 331 * @param {string} type - Tipo di prodotto 332 * @returns {string} Testo di ricerca 333 */ 334 getSearchText: function(type) { 335 const searchInput = document.getElementById('searchInput'); 336 return searchInput ? searchInput.value : ''; 337 } 338}; 339 340/** 341 * Funzioni per la gestione della ricerca 342 */ 343EntouranceCommon.Search = { 344 /** 345 * Filtra i prodotti in base al testo di ricerca 346 * @param {Array} products - Array di prodotti 347 * @param {string} searchText - Testo di ricerca 348 * @returns {Array} Prodotti filtrati 349 */ 350 filterProducts: function(products, searchText) { 351 if (!searchText) return products; 352 353 const lowerSearchText = searchText.toLowerCase(); 354 return products.filter(product => { 355 const title = (product.title || product.name || '').toLowerCase(); 356 const description = (product.description || '').toLowerCase(); 357 const category = (product.category || '').toLowerCase(); 358 359 return title.includes(lowerSearchText) || 360 description.includes(lowerSearchText) || 361 category.includes(lowerSearchText); 362 }); 363 }, 364 365 /** 366 * Inizializza la ricerca 367 * @param {string} type - Tipo di prodotto 368 * @param {Function} onSearch - Callback per la ricerca 369 */ 370 initSearch: function(type, onSearch) { 371 const searchInput = document.getElementById('searchInput'); 372 const searchButton = document.getElementById('searchButton'); 373 374 if (searchInput) { 375 searchInput.addEventListener('input', function() { 376 const searchText = this.value; 377 onSearch(searchText); 378 }); 379 } 380 381 if (searchButton) { 382 searchButton.addEventListener('click', function() { 383 const searchText = searchInput ? searchInput.value : ''; 384 onSearch(searchText); 385 }); 386 } 387 } 388}; 389 390/** 391 * Funzioni per la gestione del responsive 392 */ 393EntouranceCommon.Responsive = { 394 /** 395 * Determina il numero di card per slide in base alla dimensione dello schermo 396 * @returns {number} Numero di card per slide 397 */ 398 getCardsPerSlide: function() { 399 if (window.innerWidth <= 575.98) { 400 return 1; 401 } else if (window.innerWidth <= 991.98) { 402 return 2; 403 } else { 404 return 3; 405 } 406 }, 407 408 /** 409 * Aggiunge listener per il resize della finestra 410 * @param {Function} callback - Callback da eseguire al resize 411 */ 412 addResizeListener: function(callback) { 413 window.addEventListener('resize', function() { 414 callback(); 415 }); 416 } 417}; 418 419/** 420 * Funzioni per la gestione degli errori 421 */ 422EntouranceCommon.Errors = { 423 /** 424 * Gestisce gli errori di caricamento 425 * @param {Error} error - Errore 426 * @param {string} context - Contesto dell'errore 427 */ 428 handleError: function(error, context) { 429 console.error(`Entourance ${context} error:`, error); 430 431 // Mostra messaggio di errore all'utente 432 const errorMessage = document.createElement('div'); 433 errorMessage.className = 'alert alert-danger'; 434 errorMessage.textContent = 'Si è verificato un errore durante il caricamento dei dati.'; 435 436 // Inserisci il messaggio di errore nel DOM appropriato 437 const container = document.querySelector('.entourance-plugin'); 438 if (container) { 439 container.insertBefore(errorMessage, container.firstChild); 440 } 441 }, 442 443 /** 444 * Gestisce gli errori di rete 445 * @param {Response} response - Risposta della rete 446 * @param {string} context - Contesto dell'errore 447 */ 448 handleNetworkError: function(response, context) { 449 if (!response.ok) { 450 throw new Error(`HTTP error! status: ${response.status}`); 451 } 452 } 453}; 454 455/**
456 * Funzioni per la gestione del caricamento 457 */ 458EntouranceCommon.Loading = { 459 /** 460 * Mostra l'indicatore di caricamento 461 * @param {string} containerId - ID del container 462 */ 463 showLoading: function(containerId) { 464 const container = document.getElementById(containerId); 465 if (container) { 466 const loading = document.createElement('div'); 467 loading.className = 'text-center py-4'; 468 loading.innerHTML = '<div class="spinner-border" role="status"><span class="entourance-sr-only">Caricamento...</span></div>'; 469 container.appendChild(loading); 470 } 471 }, 472 473 /** 474 * Nasconde l'indicatore di caricamento 475 * @param {string} containerId - ID del container 476 */ 477 hideLoading: function(containerId) { 478 const container = document.getElementById(containerId); 479 if (container) { 480 const loading = container.querySelector('.spinner-border'); 481 if (loading) { 482 loading.remove(); 483 } 484 } 485 } 486}; 487 488// Esporta per compatibilità con moduli 489if (typeof module !== 'undefined' && module.exports) { 490 module.exports = EntouranceCommon; 491}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.