wineater-bot 3.9.4 → 4.0.0-beta.3
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/.prettierignore +9 -0
- package/.prettierrc +16 -0
- package/README.md +86 -0
- package/dist/wineater-chatbot.css +1 -0
- package/dist/wineater-chatbot.mjs +6493 -5129
- package/dist/wineater-chatbot.umd.js +40 -985
- package/docs/SVELTE.md +146 -0
- package/e2e/demo.test.ts +6 -0
- package/eslint.config.js +41 -0
- package/index.html +11 -3
- package/package.json +30 -5
- package/playwright.config.ts +9 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/App.svelte +394 -0
- package/src/App.svelte.test.ts +13 -0
- package/src/components/Footer.svelte +131 -0
- package/src/components/Header.svelte +92 -0
- package/src/components/Loader.svelte +62 -0
- package/src/components/Search.svelte +188 -0
- package/src/components/WinePopup.svelte +192 -0
- package/src/components/WineTile.svelte +331 -0
- package/src/demo.spec.ts +13 -0
- package/src/index.js +4 -36
- package/src/services/analytics/analytics.ts +103 -0
- package/src/{js/widget/widget-events.js → services/analytics/constants.ts} +0 -2
- package/src/services/api/api.ts +112 -0
- package/src/services/api/deserialize.ts +102 -0
- package/src/services/i18n/i18n.ts +134 -0
- package/src/services/i18n/translations.json +521 -0
- package/src/services/store/app-store.svelte.ts +84 -0
- package/tsconfig.json +23 -0
- package/vite.config.js +69 -19
- package/vitest-setup-client.ts +2 -0
- package/src/js/analytics.js +0 -117
- package/src/js/api.js +0 -138
- package/src/js/i18n.js +0 -703
- package/src/js/widget/widget-language.js +0 -107
- package/src/js/widget/widget-search.js +0 -68
- package/src/js/widget/widget-styles.js +0 -855
- package/src/js/widget/widget-ui.js +0 -186
- package/src/js/widget.js +0 -241
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { RecommendedWine, Wine, WineId, WinePrice } from '../services/api/api';
|
|
3
|
+
import { formatCurrency, getTranslation } from '../services/i18n/i18n';
|
|
4
|
+
import WinePopup from './WinePopup.svelte';
|
|
5
|
+
import { onMount } from 'svelte';
|
|
6
|
+
import { appStore } from '../services/store/app-store.svelte';
|
|
7
|
+
let lang = $derived(appStore.getAppLanguage());
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
wine,
|
|
11
|
+
index,
|
|
12
|
+
selectedWineId,
|
|
13
|
+
hoveredWineId,
|
|
14
|
+
onTileEnter,
|
|
15
|
+
onTileLeave,
|
|
16
|
+
onTileClick,
|
|
17
|
+
onBuyClick,
|
|
18
|
+
onPopupClose,
|
|
19
|
+
isAnimating,
|
|
20
|
+
translatedPopup
|
|
21
|
+
}: {
|
|
22
|
+
wine: Wine | RecommendedWine;
|
|
23
|
+
index: number;
|
|
24
|
+
selectedWineId: WineId | null;
|
|
25
|
+
hoveredWineId: WineId | null;
|
|
26
|
+
onTileEnter: (wine: Wine | RecommendedWine) => void;
|
|
27
|
+
onTileLeave: (wine: Wine | RecommendedWine) => void;
|
|
28
|
+
onTileClick: (event: MouseEvent, wine: Wine | RecommendedWine) => void;
|
|
29
|
+
onBuyClick: (wine: Wine | RecommendedWine, index: number) => void;
|
|
30
|
+
onPopupClose: () => void;
|
|
31
|
+
isAnimating: boolean;
|
|
32
|
+
translatedPopup: Partial<RecommendedWine> | null;
|
|
33
|
+
} = $props();
|
|
34
|
+
|
|
35
|
+
const getValue = (wine: Wine | RecommendedWine, key: string) =>
|
|
36
|
+
wine[key] || wine[key.charAt(0).toUpperCase() + key.slice(1)];
|
|
37
|
+
|
|
38
|
+
const joinParams = (wine: Wine | RecommendedWine, keys: string[]) =>
|
|
39
|
+
keys
|
|
40
|
+
.map((key) => getValue(wine, key))
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join(' • ');
|
|
43
|
+
|
|
44
|
+
function formatPrice(price: WinePrice) {
|
|
45
|
+
return formatCurrency(price, appStore.getCurrency?.() || 'USD', lang);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isPopupOpen(id: WineId) {
|
|
49
|
+
return selectedWineId === id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasHoverPopupContent(w: Wine | RecommendedWine) {
|
|
53
|
+
return !!(w.reason || w.shortDescription || w.tags);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getTags(translated: Wine | RecommendedWine, w: Wine | RecommendedWine) {
|
|
57
|
+
let tags: any = translated?.tags || w.tags;
|
|
58
|
+
if (!tags) return [];
|
|
59
|
+
if (Array.isArray(tags)) tags = tags.join(',');
|
|
60
|
+
if (typeof tags !== 'string') return [];
|
|
61
|
+
return tags
|
|
62
|
+
.split(',')
|
|
63
|
+
.map((tag) => tag.trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
let isMobile = $state(false);
|
|
67
|
+
|
|
68
|
+
onMount(() => {
|
|
69
|
+
const checkIsMobile = () => {
|
|
70
|
+
isMobile = window.innerWidth <= 768;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
checkIsMobile();
|
|
74
|
+
window.addEventListener('resize', checkIsMobile);
|
|
75
|
+
return () => {
|
|
76
|
+
window.removeEventListener('resize', checkIsMobile);
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
82
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
83
|
+
<div
|
|
84
|
+
class="wine-recommendation-tile {isAnimating ? 'ww-slide-up' : ''}"
|
|
85
|
+
onmouseenter={() => onTileEnter(wine)}
|
|
86
|
+
onmouseleave={() => onTileLeave(wine)}
|
|
87
|
+
onclick={(event) => onTileClick(event, wine)}
|
|
88
|
+
>
|
|
89
|
+
<div class="tile-top">
|
|
90
|
+
{#if wine.image}
|
|
91
|
+
<img src={wine.image} alt={wine.title} class="wine-image" />
|
|
92
|
+
{/if}
|
|
93
|
+
{#if wine.total}
|
|
94
|
+
<div class="wine-price">
|
|
95
|
+
{formatPrice(wine.total)}
|
|
96
|
+
</div>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
<h3 class="wine-title">{wine.title}</h3>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{#if joinParams(wine, ['type', 'country', 'region', 'grape'])}
|
|
103
|
+
<div class="wine-params">
|
|
104
|
+
{joinParams(wine, ['type', 'country', 'region', 'grape'])}
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
|
|
108
|
+
<div class="bottom-row">
|
|
109
|
+
{#if wine.url}
|
|
110
|
+
<a class="buy-btn" href={wine.url} target="_blank">
|
|
111
|
+
{getTranslation({ key: 'wineDetails.buy', lang })}
|
|
112
|
+
</a>
|
|
113
|
+
{:else}
|
|
114
|
+
<button class="buy-btn" type="button" onclick={() => onBuyClick(wine, index)}>
|
|
115
|
+
{getTranslation({ key: 'moreInfo', lang })}
|
|
116
|
+
</button>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{#if isPopupOpen(wine.id) && !isMobile}
|
|
121
|
+
<WinePopup
|
|
122
|
+
title={wine.title}
|
|
123
|
+
shortDescription={translatedPopup?.shortDescription || wine.shortDescription}
|
|
124
|
+
reason={translatedPopup?.reason || wine.reason}
|
|
125
|
+
pairing={translatedPopup?.pairingRecommendation || wine.pairingRecommendation}
|
|
126
|
+
tags={getTags(translatedPopup, wine)}
|
|
127
|
+
onClose={onPopupClose}
|
|
128
|
+
isMobile={false}
|
|
129
|
+
/>
|
|
130
|
+
{/if}
|
|
131
|
+
|
|
132
|
+
{#if hasHoverPopupContent(wine)}
|
|
133
|
+
<div class="wine-hover-popup {hoveredWineId === wine.id ? 'slide-in visible' : ''}">
|
|
134
|
+
{#if translatedPopup?.reason || translatedPopup?.shortDescription || wine.reason || wine.shortDescription}
|
|
135
|
+
<div class="popup-reason">
|
|
136
|
+
{translatedPopup?.reason ||
|
|
137
|
+
translatedPopup?.shortDescription ||
|
|
138
|
+
wine.reason ||
|
|
139
|
+
wine.shortDescription}
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
{#if translatedPopup?.tags || translatedPopup?.Tags || wine.tags}
|
|
143
|
+
<div class="popup-tags">
|
|
144
|
+
{#each getTags(translatedPopup, wine) as tag}
|
|
145
|
+
<span class="popup-tag">{tag}</span>
|
|
146
|
+
{/each}
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
</div>
|
|
150
|
+
{/if}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{#if isPopupOpen(wine.id) && isMobile}
|
|
154
|
+
<WinePopup
|
|
155
|
+
title={wine.title}
|
|
156
|
+
shortDescription={translatedPopup?.shortDescription || wine.shortDescription}
|
|
157
|
+
reason={translatedPopup?.reason || wine.reason}
|
|
158
|
+
pairing={translatedPopup?.pairingRecommendation || wine.pairingRecommendation}
|
|
159
|
+
tags={getTags(translatedPopup, wine)}
|
|
160
|
+
onClose={onPopupClose}
|
|
161
|
+
isMobile={true}
|
|
162
|
+
/>
|
|
163
|
+
{/if}
|
|
164
|
+
|
|
165
|
+
<style>
|
|
166
|
+
.wine-recommendation-tile {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
align-items: center;
|
|
170
|
+
text-align: center;
|
|
171
|
+
box-sizing: border-box;
|
|
172
|
+
padding: 16px 8px;
|
|
173
|
+
border-right: 2px solid #f6f7fd;
|
|
174
|
+
position: relative;
|
|
175
|
+
transform: translateY(0);
|
|
176
|
+
opacity: 1;
|
|
177
|
+
transition:
|
|
178
|
+
transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
179
|
+
opacity 180ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
180
|
+
will-change: transform, opacity;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.wine-recommendation-tile:last-child {
|
|
184
|
+
border-right: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.wine-recommendation-tile.ww-slide-up {
|
|
188
|
+
transform: translateY(-28px);
|
|
189
|
+
opacity: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.tile-top {
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-direction: column;
|
|
195
|
+
align-items: center;
|
|
196
|
+
justify-content: flex-start;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.wine-image {
|
|
200
|
+
height: 200px;
|
|
201
|
+
object-fit: contain;
|
|
202
|
+
margin: 0 auto 16px;
|
|
203
|
+
display: block;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.wine-title {
|
|
207
|
+
font-size: 15px;
|
|
208
|
+
font-weight: 500;
|
|
209
|
+
margin: 0 0 8px;
|
|
210
|
+
min-height: 3rem;
|
|
211
|
+
line-height: 1.2;
|
|
212
|
+
text-align: center;
|
|
213
|
+
color: #000;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.wine-params {
|
|
217
|
+
font-size: 14px;
|
|
218
|
+
color: #666;
|
|
219
|
+
white-space: normal;
|
|
220
|
+
text-align: center;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.bottom-row {
|
|
224
|
+
margin-top: auto;
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 12px;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.wine-price {
|
|
232
|
+
font-size: 16px;
|
|
233
|
+
font-weight: 400;
|
|
234
|
+
margin-bottom: 8px;
|
|
235
|
+
color: #333;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.buy-btn {
|
|
239
|
+
margin: 16px auto 0;
|
|
240
|
+
background-color: transparent;
|
|
241
|
+
color: var(--wineater-color);
|
|
242
|
+
border: 2px solid var(--wineater-color);
|
|
243
|
+
border-radius: 6px;
|
|
244
|
+
padding: 8px 16px;
|
|
245
|
+
font-size: 14px;
|
|
246
|
+
font-weight: 500;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
display: block;
|
|
249
|
+
text-decoration: none;
|
|
250
|
+
transition:
|
|
251
|
+
background-color 0.2s,
|
|
252
|
+
color 0.2s,
|
|
253
|
+
box-shadow 0.2s;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.buy-btn:hover {
|
|
257
|
+
box-shadow: 0 4px 8px rgba(123, 43, 235, 0.25);
|
|
258
|
+
background-color: var(--wineater-color);
|
|
259
|
+
color: #fff;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Hover popup inside tile */
|
|
263
|
+
.wine-hover-popup {
|
|
264
|
+
display: none;
|
|
265
|
+
position: absolute;
|
|
266
|
+
top: 0;
|
|
267
|
+
left: 0;
|
|
268
|
+
width: 100%;
|
|
269
|
+
padding: 12px;
|
|
270
|
+
background-color: #fff;
|
|
271
|
+
border: 1px solid #ddd;
|
|
272
|
+
border-radius: 8px;
|
|
273
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
274
|
+
transition: transform 0.3s ease;
|
|
275
|
+
z-index: 999;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.wine-hover-popup.visible {
|
|
279
|
+
display: block;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.wine-hover-popup.slide-in {
|
|
283
|
+
animation: whPopup 0.25s ease;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.popup-reason {
|
|
287
|
+
font-size: 14px;
|
|
288
|
+
font-weight: 400;
|
|
289
|
+
color: #333;
|
|
290
|
+
margin-bottom: 8px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.popup-tags {
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-wrap: wrap;
|
|
296
|
+
gap: 8px;
|
|
297
|
+
margin-top: 8px;
|
|
298
|
+
justify-content: center;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.popup-tag {
|
|
302
|
+
display: inline-block;
|
|
303
|
+
padding: 4px 8px;
|
|
304
|
+
border-radius: 12px;
|
|
305
|
+
background-color: #f5f0ff;
|
|
306
|
+
border: 1px solid #ddd;
|
|
307
|
+
font-size: 13px;
|
|
308
|
+
color: #333;
|
|
309
|
+
text-transform: capitalize;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@keyframes whPopup {
|
|
313
|
+
from {
|
|
314
|
+
opacity: 0;
|
|
315
|
+
transform: translateY(6px);
|
|
316
|
+
}
|
|
317
|
+
to {
|
|
318
|
+
opacity: 1;
|
|
319
|
+
transform: translateY(0);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@media screen and (max-width: 768px) {
|
|
324
|
+
.wine-recommendation-tile {
|
|
325
|
+
flex: 0 0 auto;
|
|
326
|
+
scroll-snap-align: center;
|
|
327
|
+
width: 240px;
|
|
328
|
+
text-align: center;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
</style>
|
package/src/demo.spec.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('sum test', () => {
|
|
4
|
+
it('adds 1 + 2 to equal 3', () => {
|
|
5
|
+
expect(1 + 2).toBe(3);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('handles ESM imports correctly', () => {
|
|
9
|
+
// Test that verifies ESM module loading works
|
|
10
|
+
const result = typeof import.meta !== 'undefined';
|
|
11
|
+
expect(result).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
});
|
package/src/index.js
CHANGED
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import Tracker from '@openreplay/tracker';
|
|
4
|
-
import { WineWidget } from './js/widget.js';
|
|
5
|
-
import translator from './js/i18n.js';
|
|
1
|
+
import SvelteApp from './App.svelte'
|
|
2
|
+
import { mount } from 'svelte';
|
|
6
3
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
class App {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.tracker = new Tracker({ projectKey: "PA8j9fAi49i6g2QnvpNs", '__DISABLE_SECURE_MODE': true });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async initTranslator() {
|
|
15
|
-
const storeName = this.store ? this.store.charAt(0).toUpperCase() + this.store.slice(1) : 'Test';
|
|
16
|
-
await translator.fetchConfig(storeName || 'Test');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
initCookies() {
|
|
20
|
-
setCookie('chatbotToken', window.wineaterData?.CLIENT_TOKEN || DEMO_TOKEN, 365);
|
|
21
|
-
setUniqueUserCookie();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async initialize() {
|
|
26
|
-
this.tracker.start();
|
|
27
|
-
await this.initTranslator();
|
|
28
|
-
this.initCookies();
|
|
29
|
-
new WineWidget();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Initialize the application on DOM ready
|
|
34
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
35
|
-
const app = new App();
|
|
36
|
-
app.initialize();
|
|
37
|
-
});
|
|
4
|
+
const app = mount(SvelteApp, { target: document.getElementById("wineater-widget-conteiner") });
|
|
5
|
+
export default app
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function generateUUID(): string {
|
|
2
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
3
|
+
const r = Math.random() * 16 | 0,
|
|
4
|
+
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
5
|
+
return v.toString(16);
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setCookie(name: string, value: string, days: number) {
|
|
10
|
+
let expires = "";
|
|
11
|
+
if (days) {
|
|
12
|
+
const date = new Date();
|
|
13
|
+
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
14
|
+
expires = "; expires=" + date.toUTCString();
|
|
15
|
+
}
|
|
16
|
+
document.cookie = name + "=" + (value || "") + expires + "; path=/; secure; samesite=strict";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getCookie(name: string): string | null {
|
|
20
|
+
const nameEQ = name + "=";
|
|
21
|
+
const ca = document.cookie.split(';');
|
|
22
|
+
for (let i = 0; i < ca.length; i++) {
|
|
23
|
+
let c = ca[i];
|
|
24
|
+
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
|
25
|
+
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setUniqueUserCookie(): string {
|
|
31
|
+
const cookieName = 'uniqueUserID';
|
|
32
|
+
let userId = getCookie(cookieName);
|
|
33
|
+
|
|
34
|
+
if (!userId) {
|
|
35
|
+
userId = generateUUID();
|
|
36
|
+
setCookie(cookieName, userId, 365);
|
|
37
|
+
}
|
|
38
|
+
return userId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function trackWidgetEvent(event_type: string, store_id, data = {}, meta = {}) {
|
|
42
|
+
const endpoint = 'https://admin-api.wineater.com/api/widget-events';
|
|
43
|
+
|
|
44
|
+
// Ensure we have a unique user ID
|
|
45
|
+
let user_id = getCookie('uniqueUserID');
|
|
46
|
+
if (!user_id) {
|
|
47
|
+
user_id = setUniqueUserCookie();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get device info
|
|
51
|
+
const device_info = {
|
|
52
|
+
user_agent: navigator.userAgent,
|
|
53
|
+
screen_width: window.screen.width,
|
|
54
|
+
screen_height: window.screen.height,
|
|
55
|
+
language: navigator.language,
|
|
56
|
+
platform: navigator.platform
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Get current timestamp
|
|
60
|
+
const event_time = new Date().toISOString();
|
|
61
|
+
|
|
62
|
+
// Generate session ID if not exists
|
|
63
|
+
let session_id = getCookie('sessionID');
|
|
64
|
+
if (!session_id) {
|
|
65
|
+
session_id = generateUUID();
|
|
66
|
+
setCookie('sessionID', session_id, 1); // Session cookie expires in 1 day
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const payload = {
|
|
70
|
+
event_type,
|
|
71
|
+
store_id,
|
|
72
|
+
session_id,
|
|
73
|
+
event_time,
|
|
74
|
+
data,
|
|
75
|
+
meta: {
|
|
76
|
+
...meta,
|
|
77
|
+
user_id
|
|
78
|
+
},
|
|
79
|
+
device_info,
|
|
80
|
+
region: navigator.language.split('-')[1] || null,
|
|
81
|
+
language: navigator.language.split('-')[0] || 'en'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return fetch(endpoint, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json'
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(payload)
|
|
90
|
+
})
|
|
91
|
+
.then(response => {
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
return response.json().then(err => {
|
|
94
|
+
throw new Error(err.message || 'Failed to track widget event');
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return response.json();
|
|
98
|
+
})
|
|
99
|
+
.catch(error => {
|
|
100
|
+
console.error('Widget event tracking error:', error);
|
|
101
|
+
throw error;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { deserializeInstantRecommendation, deserializeRecommendedWine } from "./deserialize";
|
|
2
|
+
|
|
3
|
+
export type WineId = string;
|
|
4
|
+
export type WinePrice = number;
|
|
5
|
+
export type Wine = {
|
|
6
|
+
id: WineId;
|
|
7
|
+
title: string;
|
|
8
|
+
price: WinePrice;
|
|
9
|
+
type: string;
|
|
10
|
+
region: string;
|
|
11
|
+
grape: string;
|
|
12
|
+
appellation: string | null;
|
|
13
|
+
url: string | null;
|
|
14
|
+
store: string;
|
|
15
|
+
storeId: string | null;
|
|
16
|
+
image: string;
|
|
17
|
+
producer: string | null;
|
|
18
|
+
country: string;
|
|
19
|
+
vintage: number | null;
|
|
20
|
+
tags: string[];
|
|
21
|
+
pairingRecommendation: string | null;
|
|
22
|
+
shortDescription: string;
|
|
23
|
+
score: number | null;
|
|
24
|
+
total: number | null;
|
|
25
|
+
itemId: string | null;
|
|
26
|
+
isPromote: boolean | null;
|
|
27
|
+
stock: number | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RecommendedWine = Omit<Wine, "shortDescription" | "producer" | "pairingRecommendation" | "itemId"> & {
|
|
31
|
+
description: string;
|
|
32
|
+
distance: number;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
class WineaterApi {
|
|
37
|
+
#baseUrl = 'https://wineater-backend.vercel.app/';
|
|
38
|
+
#token: string;
|
|
39
|
+
|
|
40
|
+
constructor(token: string) {
|
|
41
|
+
this.#token = token;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getInstantRecommendations(): Promise<Wine[]> {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`${this.#baseUrl}dish/recommendations/instant`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'Authorization': `Bearer ${this.#token}`
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const deserializedData: Wine[] = data.map(deserializeInstantRecommendation);
|
|
61
|
+
return deserializedData;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Error in getInstantRecommendations: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getWineRecommendationsSearch(text): Promise<RecommendedWine[]> {
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(`${this.#baseUrl}dish/recommendations/search`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Authorization': `Bearer ${this.#token}`
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({ text: text })
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const deserializedData: RecommendedWine[] = data.map(deserializeRecommendedWine);
|
|
85
|
+
return deserializedData;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Error in getWineRecommendations: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// TODO: define store type
|
|
92
|
+
async getStoreConfig(): Promise<any> {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`${this.#baseUrl}stores/store`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
'Authorization': `Bearer ${this.#token}`
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
|
102
|
+
return await response.json();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error fetching store configuration:', error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const DEMO_TOKEN = 'HeUw6IXuhvz6tRuXdPEGEbj3SGQ3Q2YtEc1Z8jd41Kg9LtyPb4Epu0to31SYIadG';
|
|
111
|
+
const token = window?.wineaterData?.CLIENT_TOKEN || DEMO_TOKEN;
|
|
112
|
+
export const wineaterApi = new WineaterApi(token)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { RecommendedWine, Wine } from './api.ts';
|
|
2
|
+
|
|
3
|
+
type RawInstantRecommendation = {
|
|
4
|
+
ID: string;
|
|
5
|
+
Appellation?: string;
|
|
6
|
+
Country?: string;
|
|
7
|
+
Grape: string;
|
|
8
|
+
Image: string;
|
|
9
|
+
Price: string;
|
|
10
|
+
Producer?: string;
|
|
11
|
+
Region: string;
|
|
12
|
+
Score?: string;
|
|
13
|
+
"Short Description"?: string;
|
|
14
|
+
Store: string;
|
|
15
|
+
Store_id?: string;
|
|
16
|
+
Tags: any[];
|
|
17
|
+
Title: string;
|
|
18
|
+
Type: string;
|
|
19
|
+
Url?: string;
|
|
20
|
+
Vintage?: string;
|
|
21
|
+
pairing_recommendation?: string;
|
|
22
|
+
Total?: string;
|
|
23
|
+
item_id?: string;
|
|
24
|
+
is_promote?: boolean;
|
|
25
|
+
stock?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function deserializeInstantRecommendation(raw: RawInstantRecommendation): Wine {
|
|
29
|
+
return {
|
|
30
|
+
id: String(raw.ID),
|
|
31
|
+
appellation: raw.Appellation || null,
|
|
32
|
+
country: raw.Country || null,
|
|
33
|
+
grape: raw.Grape,
|
|
34
|
+
image: raw.Image,
|
|
35
|
+
price: Number(raw.Price),
|
|
36
|
+
producer: raw.Producer || null,
|
|
37
|
+
region: raw.Region,
|
|
38
|
+
score: raw.Score ? Number(raw.Score) : null,
|
|
39
|
+
shortDescription: raw["Short Description"] || null,
|
|
40
|
+
store: raw.Store,
|
|
41
|
+
storeId: raw.Store_id || null,
|
|
42
|
+
tags: Array.isArray(raw.Tags) ? raw.Tags.map(String) : [],
|
|
43
|
+
title: raw.Title,
|
|
44
|
+
type: raw.Type,
|
|
45
|
+
url: raw.Url || null,
|
|
46
|
+
vintage: raw.Vintage ? Number(raw.Vintage) : null,
|
|
47
|
+
pairingRecommendation: raw.pairing_recommendation || null,
|
|
48
|
+
total: raw.Total ? Number(raw.Total) : null,
|
|
49
|
+
itemId: raw.item_id || null,
|
|
50
|
+
isPromote: raw.is_promote ? Boolean(raw.is_promote) : null,
|
|
51
|
+
stock: raw.stock ? Number(raw.stock) : null,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type RawRecommendedWine = {
|
|
56
|
+
id: string;
|
|
57
|
+
title: string;
|
|
58
|
+
description: string;
|
|
59
|
+
region: string;
|
|
60
|
+
price: string;
|
|
61
|
+
grape: string;
|
|
62
|
+
appellation?: string;
|
|
63
|
+
url?: string;
|
|
64
|
+
store: string;
|
|
65
|
+
storeId?: string;
|
|
66
|
+
image: string;
|
|
67
|
+
country?: string;
|
|
68
|
+
vintage?: string;
|
|
69
|
+
tags: any[];
|
|
70
|
+
score?: string;
|
|
71
|
+
total?: string;
|
|
72
|
+
is_promote?: boolean;
|
|
73
|
+
stock?: string;
|
|
74
|
+
distance: string;
|
|
75
|
+
reason: string;
|
|
76
|
+
type: string;
|
|
77
|
+
}
|
|
78
|
+
export function deserializeRecommendedWine(raw: RawRecommendedWine): RecommendedWine {
|
|
79
|
+
return {
|
|
80
|
+
id: String(raw.id),
|
|
81
|
+
title: String(raw.title),
|
|
82
|
+
description: String(raw.description),
|
|
83
|
+
region: String(raw.region),
|
|
84
|
+
price: Number(raw.price),
|
|
85
|
+
grape: String(raw.grape),
|
|
86
|
+
appellation: raw.appellation ? String(raw.appellation) : null,
|
|
87
|
+
url: raw.url ? String(raw.url) : null,
|
|
88
|
+
store: String(raw.store),
|
|
89
|
+
storeId: raw.storeId ? String(raw.storeId) : null,
|
|
90
|
+
image: String(raw.image),
|
|
91
|
+
country: raw.country ? String(raw.country) : null,
|
|
92
|
+
vintage: raw.vintage ? Number(raw.vintage) : null,
|
|
93
|
+
tags: Array.isArray(raw.tags) ? raw.tags.map(String) : [],
|
|
94
|
+
score: raw.score ? Number(raw.score) : null,
|
|
95
|
+
total: raw.total ? Number(raw.total) : null,
|
|
96
|
+
isPromote: raw.is_promote ? Boolean(raw.is_promote) : null,
|
|
97
|
+
stock: raw.stock ? Number(raw.stock) : null,
|
|
98
|
+
distance: Number(raw.distance),
|
|
99
|
+
reason: String(raw.reason),
|
|
100
|
+
type: String(raw.type),
|
|
101
|
+
}
|
|
102
|
+
}
|