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.
Files changed (41) hide show
  1. package/.prettierignore +9 -0
  2. package/.prettierrc +16 -0
  3. package/README.md +86 -0
  4. package/dist/wineater-chatbot.css +1 -0
  5. package/dist/wineater-chatbot.mjs +6493 -5129
  6. package/dist/wineater-chatbot.umd.js +40 -985
  7. package/docs/SVELTE.md +146 -0
  8. package/e2e/demo.test.ts +6 -0
  9. package/eslint.config.js +41 -0
  10. package/index.html +11 -3
  11. package/package.json +30 -5
  12. package/playwright.config.ts +9 -0
  13. package/pnpm-workspace.yaml +2 -0
  14. package/src/App.svelte +394 -0
  15. package/src/App.svelte.test.ts +13 -0
  16. package/src/components/Footer.svelte +131 -0
  17. package/src/components/Header.svelte +92 -0
  18. package/src/components/Loader.svelte +62 -0
  19. package/src/components/Search.svelte +188 -0
  20. package/src/components/WinePopup.svelte +192 -0
  21. package/src/components/WineTile.svelte +331 -0
  22. package/src/demo.spec.ts +13 -0
  23. package/src/index.js +4 -36
  24. package/src/services/analytics/analytics.ts +103 -0
  25. package/src/{js/widget/widget-events.js → services/analytics/constants.ts} +0 -2
  26. package/src/services/api/api.ts +112 -0
  27. package/src/services/api/deserialize.ts +102 -0
  28. package/src/services/i18n/i18n.ts +134 -0
  29. package/src/services/i18n/translations.json +521 -0
  30. package/src/services/store/app-store.svelte.ts +84 -0
  31. package/tsconfig.json +23 -0
  32. package/vite.config.js +69 -19
  33. package/vitest-setup-client.ts +2 -0
  34. package/src/js/analytics.js +0 -117
  35. package/src/js/api.js +0 -138
  36. package/src/js/i18n.js +0 -703
  37. package/src/js/widget/widget-language.js +0 -107
  38. package/src/js/widget/widget-search.js +0 -68
  39. package/src/js/widget/widget-styles.js +0 -855
  40. package/src/js/widget/widget-ui.js +0 -186
  41. 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>
@@ -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 { setCookie, setUniqueUserCookie, getCookie } from './js/analytics.js';
2
- import flagsmith from 'flagsmith';
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 DEMO_TOKEN = 'HeUw6IXuhvz6tRuXdPEGEbj3SGQ3Q2YtEc1Z8jd41Kg9LtyPb4Epu0to31SYIadG';
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
+ }
@@ -1,5 +1,3 @@
1
- // widget-events.js
2
-
3
1
  export const WIDGET_EVENTS = {
4
2
  WIDGET_INITIALIZED: 'widget_initialized',
5
3
  WIDGET_LOADED: 'widget_loaded',
@@ -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
+ }