wineater-bot 3.8.2 → 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 +1549 -1452
- 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 -531
- /package/src/js/{widget-styles.js → widget/widget-styles.js} +0 -0
package/package.json
CHANGED
package/src/js/analytics.js
CHANGED
|
@@ -49,52 +49,67 @@ function setUniqueUserCookie() {
|
|
|
49
49
|
return userId;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
function trackWidgetEvent(event_type, store_id, data = {}, meta = {}) {
|
|
53
|
+
const endpoint = 'https://admin-api.wineater.com/api/widget-events';
|
|
54
|
+
|
|
55
|
+
// Ensure we have a unique user ID
|
|
56
|
+
let user_id = getCookie('uniqueUserID');
|
|
57
|
+
if (!user_id) {
|
|
58
|
+
user_id = setUniqueUserCookie();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get device info
|
|
62
|
+
const device_info = {
|
|
63
|
+
user_agent: navigator.userAgent,
|
|
64
|
+
screen_width: window.screen.width,
|
|
65
|
+
screen_height: window.screen.height,
|
|
66
|
+
language: navigator.language,
|
|
67
|
+
platform: navigator.platform
|
|
58
68
|
};
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
|
|
70
|
+
// Get current timestamp
|
|
71
|
+
const event_time = new Date().toISOString();
|
|
72
|
+
|
|
73
|
+
// Generate session ID if not exists
|
|
74
|
+
let session_id = getCookie('sessionID');
|
|
75
|
+
if (!session_id) {
|
|
76
|
+
session_id = generateUUID();
|
|
77
|
+
setCookie('sessionID', session_id, 1); // Session cookie expires in 1 day
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const payload = {
|
|
81
|
+
event_type,
|
|
82
|
+
store_id,
|
|
83
|
+
user_id,
|
|
84
|
+
session_id,
|
|
85
|
+
event_time,
|
|
86
|
+
data,
|
|
87
|
+
meta,
|
|
88
|
+
device_info,
|
|
89
|
+
region: navigator.language.split('-')[1] || null,
|
|
90
|
+
language: navigator.language.split('-')[0] || 'en'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return fetch(endpoint, {
|
|
61
94
|
method: 'POST',
|
|
62
95
|
headers: {
|
|
63
96
|
'Content-Type': 'application/json'
|
|
64
97
|
},
|
|
65
|
-
body: JSON.stringify(
|
|
66
|
-
})
|
|
98
|
+
body: JSON.stringify(payload)
|
|
99
|
+
})
|
|
100
|
+
.then(response => {
|
|
67
101
|
if (!response.ok) {
|
|
68
|
-
|
|
102
|
+
return response.json().then(err => {
|
|
103
|
+
throw new Error(err.message || 'Failed to track widget event');
|
|
104
|
+
});
|
|
69
105
|
}
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
return response.json();
|
|
107
|
+
})
|
|
108
|
+
.catch(error => {
|
|
109
|
+
console.error('Widget event tracking error:', error);
|
|
110
|
+
throw error;
|
|
72
111
|
});
|
|
73
112
|
}
|
|
74
113
|
|
|
75
|
-
|
|
76
|
-
trackEvent('Chatbot Opened', { distinct_id: getCookie('uniqueUserID'), client: getCookie('chatbotToken') });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function trackChatbotOpenAuto(url) {
|
|
80
|
-
trackEvent('Chatbot Opened Auto', { distinct_id: getCookie('uniqueUserID'), url, client: getCookie('chatbotToken') });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function trackChatbotClose() {
|
|
84
|
-
trackEvent('Chatbot Closed', { distinct_id: getCookie('uniqueUserID'), client: getCookie('chatbotToken') });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function trackMessageSent(message) {
|
|
88
|
-
trackEvent('Message Sent', { distinct_id: getCookie('uniqueUserID'), message_content: message, client: getCookie('chatbotToken') });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function trackRecommendationsGet(products) {
|
|
92
|
-
trackEvent('Products Got', { distinct_id: getCookie('uniqueUserID'), products: products, client: getCookie('chatbotToken') });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function trackProductClick(productId) {
|
|
96
|
-
trackEvent('Product Clicked', { distinct_id: getCookie('uniqueUserID'), product_id: productId, client: getCookie('chatbotToken') });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export { setCookie, getCookie, setUniqueUserCookie, trackChatbotOpen, trackMessageSent, trackProductClick, trackChatbotClose, trackRecommendationsGet, trackChatbotOpenAuto, deleteCookie };
|
|
114
|
+
export { setCookie, getCookie, setUniqueUserCookie, deleteCookie, trackWidgetEvent };
|
|
100
115
|
|
package/src/js/api.js
CHANGED
package/src/js/i18n.js
CHANGED
|
@@ -539,11 +539,14 @@ class Translator {
|
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
async fetchConfig(storeName) {
|
|
542
|
+
console.log('storeName', storeName);
|
|
542
543
|
try {
|
|
543
544
|
const config = await this.api.getStoreConfig(storeName);
|
|
544
545
|
this.storeLanguage = config.lang;
|
|
546
|
+
this.storeId = config.id;
|
|
545
547
|
this.supportedLanguages = config.available_languages;
|
|
546
548
|
this.OpenAIApiKey = config.open_ai_key;
|
|
549
|
+
console.log('this.storeId', this.storeId);
|
|
547
550
|
|
|
548
551
|
const urlParams = new URLSearchParams(window.location.search);
|
|
549
552
|
const urlLang = urlParams.get('lang') || urlParams.get('language');
|
|
@@ -557,15 +560,21 @@ class Translator {
|
|
|
557
560
|
}
|
|
558
561
|
|
|
559
562
|
this.currency = config.currency || 'USD';
|
|
563
|
+
return config;
|
|
560
564
|
} catch (e) {
|
|
561
565
|
const urlParams = new URLSearchParams(window.location.search);
|
|
562
566
|
const urlLang = urlParams.get('lang') || urlParams.get('language');
|
|
563
567
|
|
|
564
568
|
this.language = this.translations[urlLang] ? urlLang : 'en';
|
|
565
569
|
this.currency = 'USD';
|
|
570
|
+
return config;
|
|
566
571
|
}
|
|
567
572
|
}
|
|
568
573
|
|
|
574
|
+
getStoreId() {
|
|
575
|
+
return this.storeId;
|
|
576
|
+
}
|
|
577
|
+
|
|
569
578
|
getCurrency(key) {
|
|
570
579
|
const keys = key.split('.'); // Разбиваем ключ на части
|
|
571
580
|
let value = this.currencies[this.currency];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// widget-events.js
|
|
2
|
+
|
|
3
|
+
export const WIDGET_EVENTS = {
|
|
4
|
+
WIDGET_INITIALIZED: 'widget_initialized',
|
|
5
|
+
WIDGET_LOADED: 'widget_loaded',
|
|
6
|
+
SEARCH_PERFORMED: 'search_performed',
|
|
7
|
+
SEARCH_RESULTS_DISPLAYED: 'search_results_displayed',
|
|
8
|
+
NO_RESULTS_FOUND: 'no_results_found',
|
|
9
|
+
WINE_CARD_HOVER: 'wine_card_hover',
|
|
10
|
+
WINE_CARD_CLICK: 'wine_card_click',
|
|
11
|
+
WINE_DETAILS_VIEWED: 'wine_details_viewed',
|
|
12
|
+
WINE_DETAILS_CLOSED: 'wine_details_closed',
|
|
13
|
+
BUY_BUTTON_CLICKED: 'buy_button_clicked',
|
|
14
|
+
FILTER_APPLIED: 'filter_applied',
|
|
15
|
+
LANGUAGE_CHANGED: 'language_changed',
|
|
16
|
+
ERROR_OCCURRED: 'error_occurred',
|
|
17
|
+
MOBILE_VIEW_ACTIVATED: 'mobile_view_activated'
|
|
18
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// widget-language.js
|
|
2
|
+
import translator from '../i18n.js';
|
|
3
|
+
import { WIDGET_EVENTS } from './widget-events.js';
|
|
4
|
+
|
|
5
|
+
export class WidgetLanguage {
|
|
6
|
+
constructor(widget) {
|
|
7
|
+
this.widget = widget;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
createLanguageSwitcher() {
|
|
11
|
+
const langContainer = document.querySelector('#powered-by-widget .powered-by-lang');
|
|
12
|
+
const languages = translator.getSupportedLanguages();
|
|
13
|
+
if (!languages) return;
|
|
14
|
+
|
|
15
|
+
const currentLanguage = translator.language;
|
|
16
|
+
const dropdown = document.createElement('select');
|
|
17
|
+
dropdown.id = 'language-dropdown';
|
|
18
|
+
dropdown.className = 'language-dropdown';
|
|
19
|
+
|
|
20
|
+
languages.forEach(lang => {
|
|
21
|
+
const option = document.createElement('option');
|
|
22
|
+
option.value = lang;
|
|
23
|
+
option.textContent = lang.toUpperCase();
|
|
24
|
+
option.selected = lang === currentLanguage;
|
|
25
|
+
dropdown.appendChild(option);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
dropdown.onchange = (event) => {
|
|
29
|
+
const selectedLang = event.target.value;
|
|
30
|
+
|
|
31
|
+
this.widget.trackEvent(WIDGET_EVENTS.LANGUAGE_CHANGED, {
|
|
32
|
+
from_language: currentLanguage,
|
|
33
|
+
to_language: selectedLang
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const url = new URL(window.location.href);
|
|
37
|
+
url.searchParams.set('lang', selectedLang);
|
|
38
|
+
localStorage.setItem('selectedLanguage', selectedLang);
|
|
39
|
+
window.location.href = url.toString();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
langContainer.appendChild(dropdown);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async translatePopupContent(wines) {
|
|
46
|
+
if (translator.language === translator.storeLanguage || this.widget.widgetType !== 'bar') {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const possibleFields = ['Short Description', 'reason', 'pairing_recommendation', 'Tags', 'tags'];
|
|
51
|
+
const firstWine = wines[0];
|
|
52
|
+
const fieldsToTranslate = possibleFields.filter(field => field in firstWine);
|
|
53
|
+
const batchSize = 4;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < wines.length; i += batchSize) {
|
|
56
|
+
const batch = wines.slice(i, i + batchSize);
|
|
57
|
+
|
|
58
|
+
const translationPromises = batch.map(async (wine, index) => {
|
|
59
|
+
const globalIndex = i + index;
|
|
60
|
+
const entries = fieldsToTranslate
|
|
61
|
+
.map(field => {
|
|
62
|
+
const val = wine[field] || wine[field.toLowerCase()];
|
|
63
|
+
if (!val) return null;
|
|
64
|
+
return {
|
|
65
|
+
key: field,
|
|
66
|
+
text: Array.isArray(val) ? val.join(', ') : val
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
|
|
71
|
+
if (entries.length === 0) return;
|
|
72
|
+
|
|
73
|
+
const translatedArray = await translator.translateTexts(
|
|
74
|
+
Object.fromEntries(entries.map(entry => [entry.key, entry.text]))
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const popup = document.querySelector(`.card-popup[data-wine-index="${globalIndex}"]`);
|
|
78
|
+
if (!popup) return;
|
|
79
|
+
|
|
80
|
+
if (translatedArray['Short Description']) {
|
|
81
|
+
const el = popup.querySelector('.popup-short-desc');
|
|
82
|
+
if (el) el.textContent = translatedArray['Short Description'];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (translatedArray.reason) {
|
|
86
|
+
const el = popup.querySelector('.popup-reason');
|
|
87
|
+
if (el) el.textContent = translatedArray.reason;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (translatedArray.pairing_recommendation) {
|
|
91
|
+
const el = popup.querySelector('.popup-pairing');
|
|
92
|
+
if (el) el.textContent = translatedArray.pairing_recommendation;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const tags = translatedArray.tags || translatedArray.Tags;
|
|
96
|
+
if (tags) {
|
|
97
|
+
const tagArray = tags.split(',').map(tag => tag.trim());
|
|
98
|
+
const tagHTML = tagArray.map(tag => `<span class="popup-tag">${tag}</span>`).join('');
|
|
99
|
+
const tagsContainer = popup.querySelector('.popup-tags');
|
|
100
|
+
if (tagsContainer) tagsContainer.innerHTML = tagHTML;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await Promise.all(translationPromises);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// widget-search.js
|
|
2
|
+
import { WIDGET_EVENTS } from './widget-events.js';
|
|
3
|
+
|
|
4
|
+
export class WidgetSearch {
|
|
5
|
+
constructor(widget) {
|
|
6
|
+
this.widget = widget;
|
|
7
|
+
// this.addEventListeners(); // Removed to avoid attaching listeners before DOM is ready
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async handleSearch() {
|
|
11
|
+
const query = document.getElementById('wine-search-input').value.trim() + ';';
|
|
12
|
+
|
|
13
|
+
this.widget.trackEvent(WIDGET_EVENTS.SEARCH_PERFORMED, { query });
|
|
14
|
+
this.widget.ui.animateCardDisappearance();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const results = await this.widget.api.getWineRecommendationsSearch(query);
|
|
18
|
+
|
|
19
|
+
if (results && results.length > 0) {
|
|
20
|
+
console.log('Recommendations:', results);
|
|
21
|
+
this.widget.displayResults(results, true);
|
|
22
|
+
this.widget.trackEvent(WIDGET_EVENTS.SEARCH_RESULTS_DISPLAYED, {
|
|
23
|
+
results_count: results.length,
|
|
24
|
+
results: results.map(result => result.Title || result.title),
|
|
25
|
+
query
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
console.log('No recommendations found.');
|
|
29
|
+
this.widget.ui.displayNoResultsMessage();
|
|
30
|
+
this.widget.trackEvent(WIDGET_EVENTS.NO_RESULTS_FOUND, { query });
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error fetching wine recommendations:', error.message);
|
|
34
|
+
this.widget.ui.displayErrorMessage();
|
|
35
|
+
this.widget.trackEvent(WIDGET_EVENTS.ERROR_OCCURRED, {
|
|
36
|
+
error_message: error.message,
|
|
37
|
+
query
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async loadInstantRecommendations() {
|
|
43
|
+
this.widget.ui.showLoadingIndicator();
|
|
44
|
+
try {
|
|
45
|
+
const recommendations = await this.widget.api.getInstantRecommendations();
|
|
46
|
+
this.widget.displayResults(recommendations);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Error loading instant recommendations:', error.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addEventListeners() {
|
|
53
|
+
document
|
|
54
|
+
.getElementById('wine-search-button')
|
|
55
|
+
.addEventListener('click', () => {
|
|
56
|
+
this.handleSearch();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Add Enter key support
|
|
60
|
+
document
|
|
61
|
+
.getElementById('wine-search-input')
|
|
62
|
+
.addEventListener('keydown', (e) => {
|
|
63
|
+
if (e.key === 'Enter') {
|
|
64
|
+
this.handleSearch();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// widget-ui.js
|
|
2
|
+
import translator from '../i18n.js';
|
|
3
|
+
import { WIDGET_EVENTS } from './widget-events.js';
|
|
4
|
+
|
|
5
|
+
export class WidgetUI {
|
|
6
|
+
constructor(widget) {
|
|
7
|
+
this.widget = widget;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getTitle(title) {
|
|
11
|
+
return title ?
|
|
12
|
+
`<h1 class="wine-widget-big-title">${title}</h1>` :
|
|
13
|
+
`<h1 class="wine-widget-big-title">
|
|
14
|
+
${translator.translate('headerLine1')}
|
|
15
|
+
<span class="icon icon-meal"></span>
|
|
16
|
+
<span class="icon-label">${translator.translate('headerMeal')}</span>,
|
|
17
|
+
<span class="icon icon-mood"></span>
|
|
18
|
+
<span class="icon-label">${translator.translate('headerMood')}</span>
|
|
19
|
+
</br>
|
|
20
|
+
${translator.translate('headerOr')}
|
|
21
|
+
<span class="icon icon-occasion"></span>
|
|
22
|
+
<span class="icon-label">${translator.translate('headerOccasion')}</span>
|
|
23
|
+
${translator.translate('headerLine2')}
|
|
24
|
+
</h1>`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
initializeWidget() {
|
|
28
|
+
const widgetContainer = document.getElementById(this.widget.widgetWrapper);
|
|
29
|
+
const widgetHTML = `
|
|
30
|
+
<div id="wine-widget-container">
|
|
31
|
+
<div class="wine-widget-wrapper">
|
|
32
|
+
<div id="wine-widget-header">
|
|
33
|
+
${this.getTitle(this.widget.titleText)}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div id="wine-widget-search">
|
|
37
|
+
<span class="search-icon"></span>
|
|
38
|
+
<input
|
|
39
|
+
type="text"
|
|
40
|
+
id="wine-search-input"
|
|
41
|
+
placeholder="${translator.translate('searchPlaceholder')}"
|
|
42
|
+
>
|
|
43
|
+
<button id="wine-search-button">
|
|
44
|
+
${translator.translate('searchButton')}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div id="wine-widget-right">
|
|
49
|
+
<div id="loading-indicator" class="hidden">
|
|
50
|
+
<div id="typing-indicator" class="typing-indicator">
|
|
51
|
+
<span></span>
|
|
52
|
+
<span></span>
|
|
53
|
+
<span></span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div id="wine-placeholder"></div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div id="powered-by-widget">
|
|
60
|
+
<div class="powered-by-center">
|
|
61
|
+
<a href="https://wineater.com" target="_blank">
|
|
62
|
+
<span>${translator.translate('poweredBy')}</span>
|
|
63
|
+
<img
|
|
64
|
+
src="${this.widget.countryCode === 'ru'
|
|
65
|
+
? 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/divino_logo.svg?x=5'
|
|
66
|
+
: 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/logo_medium_gray.svg'}"
|
|
67
|
+
alt="Wineater Logo"
|
|
68
|
+
>
|
|
69
|
+
<span class="powered-by-sommelier">${translator.translate('AI sommelier')}</span>
|
|
70
|
+
</a>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="powered-by-lang"></div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
widgetContainer.insertAdjacentHTML('beforeend', widgetHTML);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
showLoadingIndicator() {
|
|
81
|
+
const loadingIndicator = document.getElementById('loading-indicator');
|
|
82
|
+
loadingIndicator.classList.remove('hidden');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
hideLoadingIndicator() {
|
|
86
|
+
const loadingIndicator = document.getElementById('loading-indicator');
|
|
87
|
+
loadingIndicator.classList.add('hidden');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
displayNoResultsMessage() {
|
|
91
|
+
const resultsContainer = document.getElementById('wine-placeholder');
|
|
92
|
+
resultsContainer.innerHTML = `
|
|
93
|
+
<div class="no-results">
|
|
94
|
+
<p>${translator.translate('noResultsMessage') || 'No recommendations found for your query.'}</p>
|
|
95
|
+
</div>
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
displayErrorMessage() {
|
|
100
|
+
const resultsContainer = document.getElementById('wine-placeholder');
|
|
101
|
+
resultsContainer.innerHTML = `
|
|
102
|
+
<div class="error-message">
|
|
103
|
+
<p>${translator.translate('errorMessage') || 'Something went wrong. Please try again later.'}</p>
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
animateCardDisappearance() {
|
|
109
|
+
const cards = document.querySelectorAll('.wine-recommendation-tile');
|
|
110
|
+
cards.forEach((card, index) => {
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
card.style.opacity = '0';
|
|
113
|
+
card.style.transform = 'translateY(-20px)';
|
|
114
|
+
}, index * 100);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
document.getElementById('wine-placeholder').innerHTML = '';
|
|
119
|
+
this.showLoadingIndicator();
|
|
120
|
+
}, cards.length * 100 + 300);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
initializeSearchAnimation() {
|
|
124
|
+
const searchInput = document.getElementById('wine-search-input');
|
|
125
|
+
const suggestions = translator.translate('placeholderSuggestions');
|
|
126
|
+
let index = 0;
|
|
127
|
+
let charIndex = 0;
|
|
128
|
+
let typingForward = true;
|
|
129
|
+
let isInputFocused = false;
|
|
130
|
+
|
|
131
|
+
const typeEffect = () => {
|
|
132
|
+
if (isInputFocused) return;
|
|
133
|
+
|
|
134
|
+
if (typingForward) {
|
|
135
|
+
searchInput.placeholder = suggestions[index].substring(0, charIndex++);
|
|
136
|
+
if (charIndex > suggestions[index].length) {
|
|
137
|
+
typingForward = false;
|
|
138
|
+
setTimeout(typeEffect, 2000);
|
|
139
|
+
} else {
|
|
140
|
+
setTimeout(typeEffect, 100);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
searchInput.placeholder = suggestions[index].substring(0, charIndex--);
|
|
144
|
+
if (charIndex < 0) {
|
|
145
|
+
typingForward = true;
|
|
146
|
+
index = (index + 1) % suggestions.length;
|
|
147
|
+
setTimeout(typeEffect, 500);
|
|
148
|
+
} else {
|
|
149
|
+
setTimeout(typeEffect, 50);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
typeEffect();
|
|
155
|
+
|
|
156
|
+
searchInput.addEventListener('focus', () => {
|
|
157
|
+
searchInput.placeholder = '';
|
|
158
|
+
isInputFocused = true;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
searchInput.addEventListener('blur', () => {
|
|
162
|
+
isInputFocused = false;
|
|
163
|
+
charIndex = 0;
|
|
164
|
+
typingForward = true;
|
|
165
|
+
typeEffect();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
enableHoverPopups() {
|
|
170
|
+
const tiles = document.querySelectorAll('.wine-recommendation-tile');
|
|
171
|
+
tiles.forEach(tile => {
|
|
172
|
+
const popup = tile.querySelector('.wine-hover-popup');
|
|
173
|
+
if (!popup) return;
|
|
174
|
+
|
|
175
|
+
tile.addEventListener('mouseenter', () => {
|
|
176
|
+
popup.style.display = 'block';
|
|
177
|
+
popup.classList.add('slide-in');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
tile.addEventListener('mouseleave', () => {
|
|
181
|
+
popup.classList.remove('slide-in');
|
|
182
|
+
popup.style.display = 'none';
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|