wineater-bot 3.8.1 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/js/widget.js CHANGED
@@ -1,555 +1,227 @@
1
1
  // widget.js
2
- import './widget-styles.js';
2
+ import './widget/widget-styles.js';
3
3
  import { Api } from './api.js';
4
4
  import translator from './i18n.js';
5
+ import { trackWidgetEvent } from './analytics.js';
6
+ import { WIDGET_EVENTS } from './widget/widget-events.js';
7
+ import { WidgetUI } from './widget/widget-ui.js';
8
+ import { WidgetSearch } from './widget/widget-search.js';
9
+ import { WidgetLanguage } from './widget/widget-language.js';
5
10
 
6
11
  export class WineWidget {
7
12
  constructor() {
8
- // Определяем тип виджета: 'store' (старый) или 'bar' (новый)
9
- this.widgetType = window?.wineaterData?.WIDGET_TYPE || 'store';
10
- this.countryCode = window?.wineaterData?.COUNTRY_CODE || null;
11
- this.widgetWrapper = window?.wineaterData?.WIDGET_WRAPPER || 'wineater-widget-conteiner';
13
+ // Widget configuration
14
+ this.widgetType = window?.wineaterData?.WIDGET_TYPE || 'store';
15
+ this.countryCode = window?.wineaterData?.COUNTRY_CODE || null;
16
+ this.widgetWrapper = window?.wineaterData?.WIDGET_WRAPPER || 'wineater-widget-conteiner';
12
17
  this.titleText = window?.wineaterData?.TITLE_TEXT;
13
18
  this.subtitleText = window?.wineaterData?.SUBTITLE_TEXT;
14
- this.api = new Api();
15
- console.log(this.titleText);
19
+ this.storeId = null;
16
20
 
17
- // Фильтры для store
21
+ // Initialize API
22
+ this.api = new Api();
23
+
24
+ // Initialize components
25
+ this.ui = new WidgetUI(this);
26
+ this.search = new WidgetSearch(this);
27
+ this.language = new WidgetLanguage(this);
28
+
29
+ // Filter configuration
18
30
  this.selectedFilters = {
19
31
  price: null,
20
32
  color: null,
21
- intensity: null,
33
+ intensity: null
22
34
  };
23
35
 
24
- // Инициализируем виджет
25
- this.initializeWidget();
26
- this.initializeSearchAnimation();
27
- this.sendEvent('widgetInitialized', { widgetType: this.widgetType });
28
- this.loadInstantRecommendations();
29
- this.createLanguageSwitcher();
36
+ // Initialize widget
37
+ this.initialized = this.initialize();
30
38
  }
31
39
 
32
- sendEvent(eventType, data) {
33
- // Моковая реализация отправки событий в API
34
- console.log('Event sent:', eventType, data);
35
- }
40
+ async trackEvent(eventType, data = {}, meta = {}) {
41
+ await this.initialized; // <-- ждём, пока storeId будет получен
42
+ trackWidgetEvent(eventType, this.storeId, data, meta);
43
+ }
36
44
 
37
- getTitle(title) {
38
- return !!title ? `<h1 class="wine-widget-big-title">${title}</h1>` :`<h1 class="wine-widget-big-title">
39
- ${translator.translate('headerLine1')}
40
- <span class="icon icon-meal"></span>
41
- <span class="icon-label">${translator.translate('headerMeal')}</span>,
42
- <span class="icon icon-mood"></span>
43
- <span class="icon-label">${translator.translate('headerMood')}</span>
44
- </br>
45
- <!-- <span class="icon icon-budget"></span> -->
46
- <!-- <span class="icon-label">${translator.translate('headerBudget')}</span>, -->
47
- ${translator.translate('headerOr')}
48
- <span class="icon icon-occasion"></span>
49
- <span class="icon-label">${translator.translate('headerOccasion')}</span>
50
- ${translator.translate('headerLine2')}
51
- </h1>`
45
+ async initialize() {
46
+ await translator.fetchConfig('storeName');
47
+ this.storeId = translator.storeId;
48
+ this.ui.initializeWidget();
49
+ this.search.addEventListeners();
50
+ this.ui.initializeSearchAnimation();
51
+ this.search.loadInstantRecommendations();
52
+ this.language.createLanguageSwitcher();
52
53
  }
53
54
 
54
- initializeWidget() {
55
- const widgetContainer = document.getElementById(this.widgetWrapper);
56
- const widgetHTML = `
57
- <div id="wine-widget-container">
58
- <div class="wine-widget-wrapper">
59
- <div id="wine-widget-header">
60
- ${this.getTitle(this.titleText)}
61
- </div>
62
-
63
- <div id="wine-widget-search">
64
- <!-- Иконка поиска слева -->
65
- <span class="search-icon"></span>
66
-
67
- <!-- Поле ввода -->
68
- <input
69
- type="text"
70
- id="wine-search-input"
71
- placeholder="${translator.translate('searchPlaceholder')}">
72
-
73
- <!-- Кнопка -->
74
- <button id="wine-search-button">
75
- ${translator.translate('searchButton')}
76
- </button>
77
- </div>
78
- <div id="wine-widget-right">
79
- <div id="loading-indicator" class="hidden">
80
- <div id="typing-indicator" class="typing-indicator">
81
- <span></span><span></span><span></span>
82
- </div>
83
- </div>
84
- <div id="wine-placeholder"></div>
85
- </div>
86
- <div id="powered-by-widget">
87
- <div class="powered-by-center">
88
- <a href="https://wineater.com" target="_blank">
89
- <span>${translator.translate('poweredBy')}</span>
90
- <img src="${this.countryCode === 'ru' ? 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/divino_logo.svg?x=5' : 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/logo_medium_gray.svg'}" alt="Wineater Logo">
91
- <span class="powered-by-sommelier">${translator.translate('AI sommelier')}</span>
92
- </a>
93
- </div>
94
- <div class="powered-by-lang"></div>
95
- </div>
96
- </div>
97
- </div>
98
- `;
99
- widgetContainer.insertAdjacentHTML('beforeend', widgetHTML);
100
- this.addEventListenersStoreVersion();
101
- }
55
+ displayResults(wines, isAnimated = false) {
56
+ const resultsContainer = document.getElementById('wine-placeholder');
57
+ this.ui.hideLoadingIndicator();
58
+ resultsContainer.innerHTML = '';
59
+
60
+ const getValue = (wine, key) => wine[key] || wine[key.charAt(0).toUpperCase() + key.slice(1)];
61
+ const joinParams = (wine, keys) => keys.map(key => getValue(wine, key)).filter(Boolean).join(' • ');
62
+ const createTagHTML = tags => Array.isArray(tags) ? tags.map(tag => `<span class="popup-tag">${tag}</span>`).join('') : '';
63
+
64
+ const createPopupHTML = (wine) => {
65
+ const tags = wine.tags || wine.Tags;
66
+ const text = wine.reason || wine['Short Description'];
67
+ if (!text && !tags) return '';
68
+
69
+ return `
70
+ <div class="wine-hover-popup">
71
+ ${text ? `<div class="popup-reason">${text}</div>` : ''}
72
+ ${tags ? `<div class="popup-tags">${createTagHTML(tags)}</div>` : ''}
73
+ </div>`;
74
+ };
102
75
 
103
- addEventListenersStoreVersion() {
104
- document
105
- .getElementById('wine-search-button')
106
- .addEventListener('click', () => {
107
- this.sendEvent('searchButtonClicked', { query: document.getElementById('wine-search-input').value });
108
- this.handleSearch();
109
- });
110
-
111
- // Привязка фильтров
112
- this.setupFilterButtons('price-buttons', 'price');
113
- this.setupFilterButtons('color-buttons', 'color');
114
- this.setupFilterButtons('intensity-buttons', 'intensity');
115
-
116
- // Обработчик Enter в поле ввода
117
- document
118
- .getElementById('wine-search-input')
119
- .addEventListener('keydown', (event) => {
120
- if (event.key === 'Enter') {
121
- this.handleSearch();
76
+ const formatPrice = (price) => {
77
+ if (typeof price !== 'number') {
78
+ price = Number(price);
122
79
  }
123
- });
124
- }
125
-
126
- setupFilterButtons(containerId, filterKey) {
127
- const buttons = document.querySelectorAll(`#${containerId} button`);
128
- buttons.forEach(button => {
129
- button.addEventListener('click', (e) => {
130
- buttons.forEach(btn => btn.classList.remove('selected'));
131
- e.target.classList.add('selected');
132
- this.selectedFilters[filterKey] = e.target.dataset.value;
133
- this.handleSearch();
134
- });
135
- });
136
- }
137
-
138
- async handleSearch() {
139
- const query = 'wine for dish:' + document.getElementById('wine-search-input').value.trim() + ';';
140
-
141
- // Учитываем текущие выбранные фильтры
142
- const filters = this.selectedFilters;
143
-
144
- // Формируем органичную фразу с фильтрами
145
- const filterPhrases = [];
146
- if (filters.price) {
147
- filterPhrases.push(`price: ${filters.price || 'any'};`);
148
- }
149
- if (filters.color) {
150
- filterPhrases.push(`color: ${filters.color || 'any'}; `);
151
- }
152
- if (filters.intensity) {
153
- filterPhrases.push(`intensity: ${filters.intensity || 'any'}; `);
154
- }
155
-
156
- // Объединяем фильтры с основным запросом
157
- const fullQuery = filterPhrases.length > 0
158
- ? `${query} ${filterPhrases.join(', ')}`
159
- : query;
160
-
161
- // Логирование запроса
162
- console.log('Searching for:', fullQuery);
163
- console.log('Applied filters:', filters);
80
+ return price.toLocaleString('en-US');
81
+ };
164
82
 
165
- this.sendEvent('searchPerformed', { query: fullQuery, filters });
166
-
167
- // Показать эффект исчезновения карточек и индикатор загрузки
168
- this.animateCardDisappearance();
169
-
170
- try {
171
- // Вызываем API метод для получения рекомендаций
172
- const results = await this.api.getWineRecommendationsSearch(fullQuery);
173
-
174
- // Проверяем, есть ли данные
175
- if (results && results.length > 0) {
176
- console.log('Recommendations:', results);
177
- // Отображаем результаты
178
- this.displayResults(results, true);
179
- } else {
180
- console.log('No recommendations found.');
181
- this.displayNoResultsMessage();
182
- }
183
- } catch (error) {
184
- console.error('Error fetching wine recommendations:', error.message);
185
- this.displayErrorMessage();
186
- }
187
- }
83
+ wines.forEach(async (wine, index) => {
84
+ const title = getValue(wine, 'title');
85
+ const image = getValue(wine, 'image');
86
+ const price = getValue(wine, 'total');
87
+ const url = getValue(wine, 'url');
88
+ const tags = wine.tags || wine.Tags;
89
+ const line1 = joinParams(wine, ['type', 'country', 'region', 'grape']);
90
+ const tagsHTML = tags ? createTagHTML(tags) : '';
91
+ const reasonPopupHTML = createPopupHTML(wine);
92
+ const shortDescription = wine['Short Description'];
93
+ const pairing = wine.pairing_recommendation;
94
+ const isSomm = wine.isSommSelection;
95
+
96
+ const wineHTML = `
97
+ <div class="wine-recommendation-tile ${isSomm ? 'somm-selection' : ''}">
98
+ ${isSomm ? `<div class="somm-selection-badge">${translator.translate('wineDetails.sommSelection')}</div>` : ''}
99
+
100
+ <div class="tile-top">
101
+ ${image ? `<img src="${image}" alt="${title}" class="wine-image" />` : ''}
102
+ <div class="wine-price">${formatPrice(price)} ${translator.getCurrency('label')}</div>
103
+ <h3 class="wine-title">${title}</h3>
104
+ </div>
188
105
 
189
- enableHoverPopups() {
190
- const tiles = document.querySelectorAll('.wine-recommendation-tile');
191
-
192
- tiles.forEach(tile => {
193
- const popup = tile.querySelector('.wine-hover-popup');
194
- if (!popup) return;
195
-
196
- tile.addEventListener('mouseenter', () => {
197
- popup.style.display = 'block';
198
- popup.classList.add('slide-in');
199
- });
200
-
201
- tile.addEventListener('mouseleave', () => {
202
- popup.classList.remove('slide-in');
203
- popup.style.display = 'none';
204
- });
205
- });
206
- }
106
+ ${line1 ? `<div class="wine-params">${line1}</div>` : ''}
207
107
 
208
- displayResults(wines, isAnimated = false) {
209
- const resultsContainer = document.getElementById('wine-placeholder');
210
- this.hideLoadingIndicator();
211
- resultsContainer.innerHTML = '';
212
-
213
- this.sendEvent('resultsDisplayed', { resultsCount: wines.length });
214
-
215
- const getValue = (wine, key) => wine[key] || wine[key.charAt(0).toUpperCase() + key.slice(1)];
216
- const joinParams = (wine, keys) =>
217
- keys.map(key => getValue(wine, key)).filter(Boolean).join(' • ');
218
-
219
- const createTagHTML = tags =>
220
- Array.isArray(tags)
221
- ? tags.map(tag => `<span class="popup-tag">${tag}</span>`).join('')
222
- : '';
223
-
224
- const createPopupHTML = (wine) => {
225
- const tags = wine.tags || wine.Tags;
226
- const text = wine.reason || wine['Short Description'];
227
- if (!text && !tags) return '';
228
- return `
229
- <div class="wine-hover-popup">
230
- ${text ? `<div class="popup-reason">${text}</div>` : ''}
231
- ${tags ? `<div class="popup-tags">${createTagHTML(tags)}</div>` : ''}
232
- </div>`;
233
- };
108
+ <div class="bottom-row">
109
+ ${url
110
+ ? `<a class="buy-btn" href="${url}" target="_blank">${translator.translate('wineDetails.buy')}</a>`
111
+ : `<button class="buy-btn" data-toggle-info>${translator.translate('moreInfo')}</button>`
112
+ }
113
+ </div>
234
114
 
235
- function formatPrice(price) {
236
- if (typeof price !== 'number') {
237
- price = Number(price);
238
- }
239
-
240
- return price.toLocaleString('en-US'); // e.g., 1000 -> "1,000"
241
- }
242
-
243
- wines.forEach(async (wine, index) => {
244
- const title = getValue(wine, 'title');
245
- const image = getValue(wine, 'image');
246
- const price = getValue(wine, 'total');
247
- const url = getValue(wine, 'url');
248
- const tags = wine.tags || wine.Tags;
249
- const line1 = joinParams(wine, ['type', 'country', 'region', 'grape']);
250
- const tagsHTML = tags ? createTagHTML(tags) : '';
251
- const reasonPopupHTML = createPopupHTML(wine);
252
- const shortDescription = wine['Short Description'];
253
- const pairing = wine.pairing_recommendation;
254
- const isSomm = wine.isSommSelection;
255
-
256
- const wineHTML = `
257
- <div class="wine-recommendation-tile ${isSomm ? 'somm-selection' : ''}">
258
- ${isSomm ? `<div class="somm-selection-badge">${translator.translate('wineDetails.sommSelection')}</div>` : ''}
259
-
260
- <div class="tile-top">
261
- ${image ? `<img src="${image}" alt="${title}" class="wine-image" />` : ''}
262
- <div class="wine-price">${formatPrice(price)} ${translator.getCurrency('label')}</div>
263
- <h3 class="wine-title">${title}</h3>
264
- </div>
265
-
266
- ${line1 ? `<div class="wine-params">${line1}</div>` : ''}
267
-
268
- <div class="bottom-row">
269
- ${url
270
- ? `<a class="buy-btn" href="${url}" target="_blank">${translator.translate('wineDetails.buy')}</a>`
271
- : `<button class="buy-btn" data-toggle-info>${translator.translate('moreInfo')}</button>`
272
- }
273
- </div>
274
-
275
- <div class="card-popup hidden" data-wine-index="${index}">
276
- <div class="card-popup-header">
277
- <h2>${title}</h2>
278
- <span class="card-popup-close">&times;</span>
279
- </div>
280
- ${shortDescription ? `<p class="popup-short-desc" data-orig="${shortDescription}">${shortDescription}</p>` : ''}
281
- ${wine.reason ? `<div class="popup-reason" data-orig="${wine.reason}">${wine.reason}</div>`
282
- : pairing ? `<p class="popup-pairing" data-orig="${pairing}">${pairing}</p>` : ''}
283
- ${tagsHTML ? `<div class="popup-tags">${tagsHTML}</div>` : ''}
284
- </div>
285
-
286
- ${reasonPopupHTML}
287
- </div>
288
- `;
289
-
290
- resultsContainer.insertAdjacentHTML('beforeend', wineHTML);
291
- const currentCard = resultsContainer.children[index];
292
-
293
- if (!url) {
294
- const buyBtn = currentCard.querySelector('[data-toggle-info]');
295
- const popup = currentCard.querySelector('.card-popup');
296
- const closeBtn = currentCard.querySelector('.card-popup-close');
297
-
298
- closeBtn?.addEventListener('click', (e) => {
299
- e.stopPropagation();
300
- popup.classList.add('hidden');
301
- });
115
+ <div class="card-popup hidden" data-wine-index="${index}">
116
+ <div class="card-popup-header">
117
+ <h2>${title}</h2>
118
+ <span class="card-popup-close">&times;</span>
119
+ </div>
120
+ ${shortDescription ? `<p class="popup-short-desc" data-orig="${shortDescription}">${shortDescription}</p>` : ''}
121
+ ${wine.reason
122
+ ? `<div class="popup-reason" data-orig="${wine.reason}">${wine.reason}</div>`
123
+ : pairing
124
+ ? `<p class="popup-pairing" data-orig="${pairing}">${pairing}</p>`
125
+ : ''
126
+ }
127
+ ${tagsHTML ? `<div class="popup-tags">${tagsHTML}</div>` : ''}
128
+ </div>
302
129
 
303
- document.addEventListener('click', (e) => {
304
- const isInsideAnyPopup =
305
- e.target.closest('.card-popup') ||
306
- e.target.closest('.mobile-fullscreen-popup') ||
307
- e.target.closest('[data-toggle-info]');
308
-
309
- if (!isInsideAnyPopup) {
310
- document.querySelectorAll('.card-popup').forEach(p => p.classList.add('hidden'));
311
- document.querySelectorAll('.mobile-fullscreen-wrapper').forEach(wrapper => wrapper.remove());
312
- }
313
- });
314
-
315
- buyBtn?.addEventListener('click', (e) => {
316
- e.stopPropagation();
317
-
318
- // Закрыть все открытые popup'ы
319
- document.querySelectorAll('.card-popup').forEach(p => p.classList.add('hidden'));
320
- document.querySelectorAll('.mobile-fullscreen-wrapper').forEach(p => p.remove());
321
-
322
- if (window.innerWidth <= 768) {
323
- const popupContent = popup.innerHTML;
324
-
325
- const overlay = document.createElement('div');
326
- overlay.className = 'mobile-fullscreen-wrapper';
327
- overlay.innerHTML = `
328
- <div class="mobile-fullscreen-overlay"></div>
329
- <div class="card-popup mobile-fullscreen-popup">${popupContent}</div>
330
- `;
331
-
332
- overlay.querySelector('.card-popup-close')?.addEventListener('click', () => overlay.remove());
333
-
334
- document.body.appendChild(overlay);
130
+ ${reasonPopupHTML}
131
+ </div>
132
+ `;
133
+
134
+ resultsContainer.insertAdjacentHTML('beforeend', wineHTML);
135
+ const currentCard = resultsContainer.children[index];
136
+
137
+ if (!url) {
138
+ const buyBtn = currentCard.querySelector('[data-toggle-info]');
139
+ const popup = currentCard.querySelector('.card-popup');
140
+ const closeBtn = currentCard.querySelector('.card-popup-close');
141
+
142
+ closeBtn?.addEventListener('click', (e) => {
143
+ e.stopPropagation();
144
+ popup.classList.add('hidden');
145
+ this.trackEvent(WIDGET_EVENTS.WINE_DETAILS_CLOSED, {
146
+ wine_id: wine.ID,
147
+ wine_title: wine.title || wine.Title
148
+ });
149
+ });
150
+
151
+ document.addEventListener('click', (e) => {
152
+ const isInsideAnyPopup =
153
+ e.target.closest('.card-popup') ||
154
+ e.target.closest('.mobile-fullscreen-popup') ||
155
+ e.target.closest('[data-toggle-info]');
156
+
157
+ if (!isInsideAnyPopup) {
158
+ document.querySelectorAll('.card-popup').forEach(p => p.classList.add('hidden'));
159
+ document.querySelectorAll('.mobile-fullscreen-wrapper').forEach(wrapper => wrapper.remove());
160
+ }
161
+ });
162
+
163
+ if (this.widgetType === 'store') {
164
+ currentCard.addEventListener('mouseenter', () => {
165
+ popup.classList.remove('hidden');
166
+ this.trackEvent(WIDGET_EVENTS.WINE_CARD_HOVER, {
167
+ wine_id: wine.ID,
168
+ wine_title: wine.title || wine.Title
169
+ });
170
+ });
171
+
172
+ currentCard.addEventListener('mouseleave', () => {
173
+ popup.classList.add('hidden');
174
+ });
175
+ } else {
176
+ buyBtn?.addEventListener('click', (e) => {
177
+ e.stopPropagation();
178
+
179
+ document.querySelectorAll('.card-popup').forEach(p => p.classList.add('hidden'));
180
+ document.querySelectorAll('.mobile-fullscreen-wrapper').forEach(p => p.remove());
181
+
182
+ if (window.innerWidth <= 768) {
183
+ const popupContent = popup.innerHTML;
184
+ const overlay = document.createElement('div');
185
+ overlay.className = 'mobile-fullscreen-wrapper';
186
+ overlay.innerHTML = `
187
+ <div class="mobile-fullscreen-overlay"></div>
188
+ <div class="card-popup mobile-fullscreen-popup">${popupContent}</div>
189
+ `;
190
+
191
+ overlay.querySelector('.card-popup-close')?.addEventListener('click', () => overlay.remove());
192
+ document.body.appendChild(overlay);
193
+ } else {
194
+ popup.classList.remove('hidden');
195
+ }
196
+
197
+ this.trackEvent(WIDGET_EVENTS.WINE_DETAILS_VIEWED, {
198
+ wine_id: wine.ID,
199
+ wine_title: wine.title || wine.Title,
200
+ is_mobile: window.innerWidth <= 768
201
+ });
202
+ });
203
+ }
335
204
  } else {
336
- popup.classList.remove('hidden');
205
+ const buyBtn = currentCard.querySelector('.buy-btn');
206
+ buyBtn?.addEventListener('click', () => {
207
+ this.trackEvent(WIDGET_EVENTS.BUY_BUTTON_CLICKED, {
208
+ wine_id: wine.ID,
209
+ wine_title: wine.title || wine.Title,
210
+ wine_url: wine.url || wine.URL
211
+ });
212
+ });
337
213
  }
338
- });
339
- }
340
-
341
- setTimeout(() => {
342
- currentCard.style.opacity = '1';
343
- currentCard.style.transform = 'translateY(0)';
344
- }, isAnimated ? index * 200 : 0);
345
- });
346
-
347
- this.translatePopupContent(wines); // переводы подгружаются после отрисовки
348
-
349
- if (this.widgetType === 'store') {
350
- this.enableHoverPopups();
351
- }
352
- }
353
214
 
354
- animateCardDisappearance() {
355
- const cards = document.querySelectorAll('.wine-recommendation-tile');
356
- cards.forEach((card, index) => {
357
215
  setTimeout(() => {
358
- card.style.opacity = '0';
359
- card.style.transform = 'translateY(-20px)';
360
- }, index * 100); // Задержка между карточками
216
+ currentCard.style.opacity = '1';
217
+ currentCard.style.transform = 'translateY(0)';
218
+ }, isAnimated ? index * 200 : 0);
361
219
  });
362
220
 
363
- setTimeout(() => {
364
- document.getElementById('wine-placeholder').innerHTML = ''; // Удалить все карточки после анимации
365
- this.showLoadingIndicator();
366
- }, cards.length * 100 + 300); // Дождаться завершения анимации
367
- }
368
-
369
- showLoadingIndicator() {
370
- const loadingIndicator = document.getElementById('loading-indicator');
371
- loadingIndicator.classList.remove('hidden');
372
- }
373
-
374
- hideLoadingIndicator() {
375
- const loadingIndicator = document.getElementById('loading-indicator');
376
- loadingIndicator.classList.add('hidden');
377
- }
378
-
379
- displayNoResultsMessage() {
380
- const resultsContainer = document.getElementById('wine-placeholder');
381
- resultsContainer.innerHTML = `
382
- <div class="no-results">
383
- <p>${translator.translate('noResultsMessage') || 'No recommendations found for your query.'}</p>
384
- </div>
385
- `;
386
- }
387
-
388
- displayErrorMessage() {
389
- const resultsContainer = document.getElementById('wine-placeholder');
390
- resultsContainer.innerHTML = `
391
- <div class="error-message">
392
- <p>${translator.translate('errorMessage') || 'Something went wrong. Please try again later.'}</p>
393
- </div>
394
- `;
395
- }
396
-
397
- async translatePopupContent(wines) {
398
- console.log(translator.language, translator.storeLanguage);
399
- if (translator.language === translator.storeLanguage || this.widgetType !== 'bar') {
400
- return;
401
- }
402
-
403
- const possibleFields = ['Short Description', 'reason', 'pairing_recommendation', 'Tags', 'tags'];
404
- const firstWine = wines[0];
405
-
406
- const fieldsToTranslate = possibleFields.filter(field => field in firstWine);
407
- const batchSize = 4;
408
-
409
- for (let i = 0; i < wines.length; i += batchSize) {
410
- const batch = wines.slice(i, i + batchSize);
411
-
412
- const translationPromises = batch.map(async (wine, index) => {
413
- const globalIndex = i + index;
414
-
415
- // Сбор оригинальных текстов в формате [{ key, text }]
416
- const entries = fieldsToTranslate
417
- .map(field => {
418
- const val = wine[field] || wine[field.toLowerCase()];
419
- if (!val) return null;
420
- return { key: field, text: Array.isArray(val) ? val.join(', ') : val };
421
- })
422
- .filter(Boolean);
423
-
424
- if (entries.length === 0) return;
425
-
426
- // Перевод
427
- const translatedArray = await translator.translateTexts(
428
- Object.fromEntries(entries.map(entry => [entry.key, entry.text]))
429
- );
430
-
431
- const popup = document.querySelector(`.card-popup[data-wine-index="${globalIndex}"]`);
432
- if (!popup) return;
433
-
434
- // Применение перевода
435
- if (translatedArray['Short Description']) {
436
- const el = popup.querySelector('.popup-short-desc');
437
- if (el) el.textContent = translatedArray['Short Description'];
438
- }
439
-
440
- if (translatedArray.reason) {
441
- const el = popup.querySelector('.popup-reason');
442
- if (el) el.textContent = translatedArray.reason;
443
- }
444
-
445
- if (translatedArray.pairing_recommendation) {
446
- const el = popup.querySelector('.popup-pairing');
447
- if (el) el.textContent = translatedArray.pairing_recommendation;
448
- }
449
-
450
- const tags = translatedArray.tags || translatedArray.Tags;
451
- if (tags) {
452
- const tagArray = tags.split(',').map(tag => tag.trim());
453
- const tagHTML = tagArray.map(tag => `<span class="popup-tag">${tag}</span>`).join('');
454
- const tagsContainer = popup.querySelector('.popup-tags');
455
- if (tagsContainer) tagsContainer.innerHTML = tagHTML;
456
- }
457
- });
458
-
459
- await Promise.all(translationPromises);
460
- }
461
- }
221
+ this.language.translatePopupContent(wines);
462
222
 
463
- async loadInstantRecommendations() {
464
- this.showLoadingIndicator();
465
- try {
466
- const recommendations = await this.api.getInstantRecommendations();
467
- this.displayResults(recommendations); // Обновляем отображение рекомендаций
468
- } catch (error) {
469
- console.error('Error loading instant recommendations:', error.message);
223
+ if (this.widgetType === 'store') {
224
+ this.ui.enableHoverPopups();
470
225
  }
471
226
  }
472
-
473
- initializeSearchAnimation() {
474
- const searchInput = document.getElementById('wine-search-input');
475
- const suggestions = translator.translate('placeholderSuggestions');
476
- let index = 0;
477
- let charIndex = 0;
478
- let typingForward = true;
479
- let isInputFocused = false;
480
-
481
- const typeEffect = () => {
482
- if (isInputFocused) return; // Если фокус в поле ввода, эффект останавливается
483
- if (typingForward) {
484
- searchInput.placeholder = suggestions[index].substring(0, charIndex++);
485
- if (charIndex > suggestions[index].length) {
486
- typingForward = false;
487
- setTimeout(typeEffect, 2000); // Пауза после завершения строки
488
- } else {
489
- setTimeout(typeEffect, 100); // Скорость ввода
490
- }
491
- } else {
492
- searchInput.placeholder = suggestions[index].substring(0, charIndex--);
493
- if (charIndex < 0) {
494
- typingForward = true;
495
- index = (index + 1) % suggestions.length; // Переход к следующей подсказке
496
- setTimeout(typeEffect, 500); // Пауза перед началом следующей строки
497
- } else {
498
- setTimeout(typeEffect, 50); // Скорость удаления
499
- }
500
- }
501
- };
502
-
503
- typeEffect();
504
-
505
- // Очистка строки и остановка эффекта при фокусе
506
- searchInput.addEventListener('focus', () => {
507
- searchInput.placeholder = '';
508
- isInputFocused = true; // Останавливаем эффект
509
- });
510
-
511
- // Возобновление эффекта при потере фокуса
512
- searchInput.addEventListener('blur', () => {
513
- isInputFocused = false; // Возобновляем эффект
514
- charIndex = 0;
515
- typingForward = true;
516
- typeEffect();
517
- });
518
- }
519
-
520
- createLanguageSwitcher() {
521
- const langContainer = document.querySelector('#powered-by-widget .powered-by-lang');
522
- const languages = translator.getSupportedLanguages();
523
- if (!languages) return;
524
-
525
- const currentLanguage = translator.language;
526
-
527
- const dropdown = document.createElement('select');
528
- dropdown.id = 'language-dropdown';
529
- dropdown.className = 'language-dropdown';
530
-
531
- languages.forEach(lang => {
532
- const option = document.createElement('option');
533
- option.value = lang;
534
- option.textContent = lang.toUpperCase();
535
- option.selected = lang === currentLanguage;
536
- dropdown.appendChild(option);
537
- });
538
-
539
- dropdown.onchange = (event) => {
540
- const selectedLang = event.target.value;
541
-
542
- // Обновляем URL без перезагрузки страницы
543
- const url = new URL(window.location.href);
544
- url.searchParams.set('lang', selectedLang);
545
-
546
- // Обновляем localStorage тоже, на всякий случай
547
- localStorage.setItem('selectedLanguage', selectedLang);
548
-
549
- // Перенаправляем пользователя с обновлённым URL
550
- window.location.href = url.toString();
551
- };
552
-
553
- langContainer.appendChild(dropdown);
554
- }
555
227
  }