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/dist/wineater-chatbot.mjs +1710 -1609
- package/dist/wineater-chatbot.umd.js +105 -104
- package/package.json +1 -1
- package/src/js/analytics.js +53 -38
- package/src/js/api.js +1 -1
- package/src/js/i18n.js +9 -0
- package/src/js/widget/widget-events.js +18 -0
- package/src/js/widget/widget-language.js +107 -0
- package/src/js/widget/widget-search.js +68 -0
- package/src/js/widget/widget-ui.js +186 -0
- package/src/js/widget.js +191 -519
- /package/src/js/{widget-styles.js → widget/widget-styles.js} +0 -0
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
|
-
//
|
|
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.
|
|
15
|
-
console.log(this.titleText);
|
|
19
|
+
this.storeId = null;
|
|
16
20
|
|
|
17
|
-
//
|
|
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.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
async trackEvent(eventType, data = {}, meta = {}) {
|
|
41
|
+
await this.initialized; // <-- ждём, пока storeId будет получен
|
|
42
|
+
trackWidgetEvent(eventType, this.storeId, data, meta);
|
|
43
|
+
}
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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">×</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">×</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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
}, index *
|
|
216
|
+
currentCard.style.opacity = '1';
|
|
217
|
+
currentCard.style.transform = 'translateY(0)';
|
|
218
|
+
}, isAnimated ? index * 200 : 0);
|
|
361
219
|
});
|
|
362
220
|
|
|
363
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
}
|