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,13 @@
|
|
|
1
|
+
import { page } from '@vitest/browser/context';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { render } from 'vitest-browser-svelte';
|
|
4
|
+
import App from './App.svelte';
|
|
5
|
+
|
|
6
|
+
describe('App.svelte', () => {
|
|
7
|
+
it('should render h1', async () => {
|
|
8
|
+
render(App);
|
|
9
|
+
|
|
10
|
+
const heading = page.getByRole('heading', { level: 1 });
|
|
11
|
+
await expect.element(heading).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { trackWidgetEvent } from '../services/analytics/analytics';
|
|
3
|
+
import { WIDGET_EVENTS } from '../services/analytics/constants';
|
|
4
|
+
import { appStore } from '../services/store/app-store.svelte';
|
|
5
|
+
import { getTranslation } from '../services/i18n/i18n';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
countryCode?: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let { countryCode = null }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let appLanguage = $derived(appStore.getAppLanguage());
|
|
14
|
+
let supportedLanguages = $derived(appStore.getSupportedLanguages());
|
|
15
|
+
let storeId = $derived(appStore.getStoreId());
|
|
16
|
+
|
|
17
|
+
function handleLanguageChange(event: Event) {
|
|
18
|
+
const target = event.target as HTMLSelectElement;
|
|
19
|
+
const selectedLang = target.value;
|
|
20
|
+
|
|
21
|
+
trackWidgetEvent(WIDGET_EVENTS.LANGUAGE_CHANGED, storeId, {
|
|
22
|
+
from_language: appLanguage,
|
|
23
|
+
to_language: selectedLang
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
appStore.setStoreLanguage(selectedLang);
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div id="powered-by-widget">
|
|
31
|
+
<div class="powered-by-center">
|
|
32
|
+
<a href="https://wineater.com" target="_blank">
|
|
33
|
+
<span>{getTranslation({ key: 'poweredBy', lang: appLanguage })}</span>
|
|
34
|
+
<img
|
|
35
|
+
src={countryCode === 'ru'
|
|
36
|
+
? 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/divino_logo.svg?x=5'
|
|
37
|
+
: 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/logo_medium_gray.svg'}
|
|
38
|
+
alt="Wineater Logo"
|
|
39
|
+
/>
|
|
40
|
+
<span class="powered-by-sommelier"
|
|
41
|
+
>{getTranslation({ key: 'ai_sommelier', lang: appLanguage })}</span
|
|
42
|
+
>
|
|
43
|
+
</a>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="powered-by-lang">
|
|
46
|
+
{#if supportedLanguages.length > 1}
|
|
47
|
+
<select
|
|
48
|
+
id="language-dropdown"
|
|
49
|
+
class="language-dropdown"
|
|
50
|
+
value={appLanguage}
|
|
51
|
+
onchange={handleLanguageChange}
|
|
52
|
+
>
|
|
53
|
+
{#each supportedLanguages as lang}
|
|
54
|
+
<option value={lang} selected={lang === appLanguage}>
|
|
55
|
+
{lang.toUpperCase()}
|
|
56
|
+
</option>
|
|
57
|
+
{/each}
|
|
58
|
+
</select>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
#powered-by-widget {
|
|
65
|
+
position: relative;
|
|
66
|
+
border-top: 2px solid #f6f7fd;
|
|
67
|
+
font-family: 'Bricolage Grotesque', sans-serif;
|
|
68
|
+
font-weight: 200;
|
|
69
|
+
padding: 12px 24px;
|
|
70
|
+
min-height: 40px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#powered-by-widget .powered-by-center {
|
|
74
|
+
position: absolute;
|
|
75
|
+
left: 50%;
|
|
76
|
+
transform: translateX(-50%);
|
|
77
|
+
text-align: center;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#powered-by-widget .powered-by-lang {
|
|
81
|
+
position: absolute;
|
|
82
|
+
right: 0px;
|
|
83
|
+
top: 50%;
|
|
84
|
+
transform: translateY(-50%);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.language-dropdown {
|
|
88
|
+
padding: 4px;
|
|
89
|
+
border-radius: 6px;
|
|
90
|
+
border: 2px solid var(--wineater-color);
|
|
91
|
+
background-color: white;
|
|
92
|
+
color: var(--wineater-color);
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
font-family: 'Bricolage Grotesque', sans-serif;
|
|
95
|
+
font-size: 12px;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
transition: all 0.2s ease;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.language-dropdown:hover {
|
|
101
|
+
box-shadow: 0 4px 8px rgba(123, 43, 235, 0.2);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#powered-by-widget a {
|
|
105
|
+
text-decoration: none;
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
gap: 8px;
|
|
110
|
+
color: #a8a8a8;
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
line-height: 16px;
|
|
113
|
+
transition: opacity 0.2s ease;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#powered-by-widget a:hover {
|
|
117
|
+
opacity: 0.8;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#powered-by-widget img {
|
|
121
|
+
height: 16px;
|
|
122
|
+
width: auto;
|
|
123
|
+
color: #a8a8a8;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@media screen and (max-width: 768px) {
|
|
127
|
+
.powered-by-sommelier {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTranslation } from '../services/i18n/i18n';
|
|
3
|
+
import { appStore } from '../services/store/app-store.svelte';
|
|
4
|
+
|
|
5
|
+
let lang = $derived(appStore.getAppLanguage());
|
|
6
|
+
const titleText = window?.wineaterData?.TITLE_TEXT || null;
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div id="wine-widget-header">
|
|
10
|
+
{#if titleText}
|
|
11
|
+
<h1 class="wine-widget-big-title">{titleText}</h1>
|
|
12
|
+
{:else}
|
|
13
|
+
<!-- TODO: translation is not working -->
|
|
14
|
+
<h1 class="wine-widget-big-title">
|
|
15
|
+
{getTranslation({ key: 'headerLine1', lang })}
|
|
16
|
+
<span class="icon icon-meal"></span>
|
|
17
|
+
<span class="icon-label">{getTranslation({ key: 'headerMeal', lang })}</span>,
|
|
18
|
+
<span class="icon icon-mood"></span>
|
|
19
|
+
<span class="icon-label">{getTranslation({ key: 'headerMood', lang })}</span>
|
|
20
|
+
<br />
|
|
21
|
+
{getTranslation({ key: 'headerOr', lang })}
|
|
22
|
+
<span class="icon icon-occasion"></span>
|
|
23
|
+
<span class="icon-label">{getTranslation({ key: 'headerOccasion', lang })}</span>
|
|
24
|
+
{getTranslation({ key: 'headerLine2', lang })}
|
|
25
|
+
</h1>
|
|
26
|
+
{/if}
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
#wine-widget-header h1 {
|
|
31
|
+
font-family: 'Bricolage Grotesque', serif;
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
font-size: 24px;
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
line-height: 1.3;
|
|
36
|
+
color: #333;
|
|
37
|
+
text-align: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#wine-widget-header p {
|
|
41
|
+
font-size: 16px;
|
|
42
|
+
color: #666;
|
|
43
|
+
margin-top: 8px;
|
|
44
|
+
text-align: center;
|
|
45
|
+
padding: 0 15%;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.icon {
|
|
49
|
+
display: inline-block;
|
|
50
|
+
width: 1.2em;
|
|
51
|
+
height: 1.2em;
|
|
52
|
+
vertical-align: middle;
|
|
53
|
+
margin: 0 4px 0 2px;
|
|
54
|
+
background-repeat: no-repeat;
|
|
55
|
+
background-size: contain;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.icon-meal {
|
|
59
|
+
background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//meal1.svg');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.icon-meal:hover {
|
|
63
|
+
background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//meal3.svg');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.icon-mood {
|
|
67
|
+
background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//mood.svg');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.icon-budget {
|
|
71
|
+
background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//budget.svg');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.icon-occasion {
|
|
75
|
+
background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//ocasion.svg');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.icon-label {
|
|
79
|
+
color: var(--wineater-color);
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media screen and (max-width: 768px) {
|
|
84
|
+
#wine-widget-header h1 {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#wine-widget-header p {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<div id="loading-indicator">
|
|
2
|
+
<div id="typing-indicator" class="typing-indicator">
|
|
3
|
+
<span></span>
|
|
4
|
+
<span></span>
|
|
5
|
+
<span></span>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
#loading-indicator {
|
|
11
|
+
position: absolute;
|
|
12
|
+
top: 50%;
|
|
13
|
+
left: 50%;
|
|
14
|
+
transform: translate(-50%, -50%);
|
|
15
|
+
z-index: 10000;
|
|
16
|
+
text-align: center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.typing-indicator {
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
align-items: center;
|
|
23
|
+
height: 40px;
|
|
24
|
+
visibility: visible;
|
|
25
|
+
background-color: white;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.typing-indicator span {
|
|
29
|
+
display: block;
|
|
30
|
+
width: 8px;
|
|
31
|
+
height: 8px;
|
|
32
|
+
margin: 0 3px;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
background: linear-gradient(90deg, var(--wineater-color), var(--wineater-corp-color));
|
|
35
|
+
opacity: 0.7;
|
|
36
|
+
animation: typing 1s infinite;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes typing {
|
|
40
|
+
0%,
|
|
41
|
+
100% {
|
|
42
|
+
transform: translateY(0);
|
|
43
|
+
opacity: 0.7;
|
|
44
|
+
}
|
|
45
|
+
50% {
|
|
46
|
+
transform: translateY(-8px);
|
|
47
|
+
opacity: 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.typing-indicator span:nth-child(1) {
|
|
52
|
+
animation-delay: 0s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.typing-indicator span:nth-child(2) {
|
|
56
|
+
animation-delay: 0.15s;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.typing-indicator span:nth-child(3) {
|
|
60
|
+
animation-delay: 0.3s;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { appStore } from '../services/store/app-store.svelte';
|
|
4
|
+
import { getTranslation } from '../services/i18n/i18n';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
searchQuery: string;
|
|
8
|
+
onSubmit: (event: SubmitEvent) => void;
|
|
9
|
+
initialized: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { searchQuery = $bindable(), onSubmit, initialized }: Props = $props();
|
|
13
|
+
let searchEl: HTMLInputElement;
|
|
14
|
+
let isInputFocused = $state(false);
|
|
15
|
+
let animIndex = $state(0);
|
|
16
|
+
let animChar = $state(0);
|
|
17
|
+
let animForward = $state(true);
|
|
18
|
+
let animRunning = $state(false);
|
|
19
|
+
let lang = $derived(appStore.getAppLanguage());
|
|
20
|
+
let animationToken = 0;
|
|
21
|
+
|
|
22
|
+
export function getSearchElement() {
|
|
23
|
+
return searchEl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function initializeSearchAnimation() {
|
|
27
|
+
const localToken = ++animationToken;
|
|
28
|
+
const suggestions = getTranslation({ key: 'placeholderSuggestions', lang });
|
|
29
|
+
if (!searchEl || !suggestions.length) return;
|
|
30
|
+
animRunning = true;
|
|
31
|
+
const loop = () => {
|
|
32
|
+
// abort if a new animation has started or conditions changed
|
|
33
|
+
if (localToken !== animationToken) return;
|
|
34
|
+
if (!searchEl || !animRunning || isInputFocused || searchQuery.length) return;
|
|
35
|
+
const current = suggestions[animIndex];
|
|
36
|
+
if (animForward) {
|
|
37
|
+
searchEl.placeholder = current.substring(0, animChar++);
|
|
38
|
+
if (animChar > current.length) {
|
|
39
|
+
animForward = false;
|
|
40
|
+
setTimeout(loop, 1600);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setTimeout(loop, 90);
|
|
44
|
+
} else {
|
|
45
|
+
searchEl.placeholder = current.substring(0, animChar--);
|
|
46
|
+
if (animChar < 0) {
|
|
47
|
+
animForward = true;
|
|
48
|
+
animIndex = (animIndex + 1) % suggestions.length;
|
|
49
|
+
setTimeout(loop, 450);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setTimeout(loop, 45);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
loop();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stopSearchAnimation() {
|
|
59
|
+
animRunning = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onSearchFocus() {
|
|
63
|
+
isInputFocused = true;
|
|
64
|
+
stopSearchAnimation();
|
|
65
|
+
if (searchEl) searchEl.placeholder = '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function onSearchBlur() {
|
|
69
|
+
isInputFocused = false;
|
|
70
|
+
animChar = 0;
|
|
71
|
+
animForward = true;
|
|
72
|
+
animRunning = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
$effect(() => {
|
|
76
|
+
// update placeholder whenever lang or searchEl change
|
|
77
|
+
if (!searchEl) return;
|
|
78
|
+
if (searchQuery.length > 0) return;
|
|
79
|
+
// cancel any running animation on language change
|
|
80
|
+
animationToken++;
|
|
81
|
+
animRunning = false;
|
|
82
|
+
animIndex = 0;
|
|
83
|
+
animChar = 0;
|
|
84
|
+
animForward = true;
|
|
85
|
+
const suggestions = getTranslation({ key: 'placeholderSuggestions', lang });
|
|
86
|
+
if (suggestions && suggestions.length) {
|
|
87
|
+
searchEl.placeholder = suggestions[0];
|
|
88
|
+
} else {
|
|
89
|
+
searchEl.placeholder = getTranslation({ key: 'searchPlaceholder', lang });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
$effect(() => {
|
|
94
|
+
// control typing animation separately with latest lang
|
|
95
|
+
if (searchEl && initialized && !isInputFocused && !searchQuery.length && !animRunning) {
|
|
96
|
+
initializeSearchAnimation();
|
|
97
|
+
}
|
|
98
|
+
if (searchQuery.length > 0 && animRunning) {
|
|
99
|
+
animRunning = false;
|
|
100
|
+
animationToken++;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
onMount(() => {
|
|
105
|
+
// effects handle initial placeholder and animation
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<form id="wine-widget-search" onsubmit={onSubmit}>
|
|
110
|
+
<span class="search-icon"></span>
|
|
111
|
+
<input
|
|
112
|
+
id="wine-search-input"
|
|
113
|
+
bind:this={searchEl}
|
|
114
|
+
type="text"
|
|
115
|
+
bind:value={searchQuery}
|
|
116
|
+
onfocus={onSearchFocus}
|
|
117
|
+
onblur={onSearchBlur}
|
|
118
|
+
/>
|
|
119
|
+
<button id="wine-search-button" type="submit">
|
|
120
|
+
{getTranslation({ key: 'searchButton', lang })}
|
|
121
|
+
</button>
|
|
122
|
+
</form>
|
|
123
|
+
|
|
124
|
+
<style>
|
|
125
|
+
#wine-widget-search {
|
|
126
|
+
position: relative;
|
|
127
|
+
margin: 12px auto;
|
|
128
|
+
padding: 0 6px;
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
border: 2px solid #d5d9ee;
|
|
132
|
+
border-radius: 10px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.search-icon {
|
|
136
|
+
left: 12px;
|
|
137
|
+
width: 24px;
|
|
138
|
+
height: 24px;
|
|
139
|
+
flex: 0 0 auto;
|
|
140
|
+
background-image: url('https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media//search.svg?x=1');
|
|
141
|
+
background-repeat: no-repeat;
|
|
142
|
+
background-size: contain;
|
|
143
|
+
z-index: 2;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#wine-search-input {
|
|
147
|
+
flex: 1 1 0%;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
padding: 12px;
|
|
150
|
+
font-size: 16px;
|
|
151
|
+
border: none;
|
|
152
|
+
border-radius: 10px;
|
|
153
|
+
outline: none;
|
|
154
|
+
transition: border 0.2s ease;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#wine-search-input::placeholder {
|
|
158
|
+
color: var(--wineater-color);
|
|
159
|
+
opacity: 1;
|
|
160
|
+
transition:
|
|
161
|
+
color 0.3s ease,
|
|
162
|
+
opacity 0.3s ease-in-out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#wine-search-input:focus {
|
|
166
|
+
border-color: var(--wineater-color);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#wine-search-input:focus::placeholder {
|
|
170
|
+
opacity: 0.5;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#wine-search-button {
|
|
174
|
+
flex: 0 0 auto;
|
|
175
|
+
background-color: var(--wineater-color);
|
|
176
|
+
color: #fff;
|
|
177
|
+
border: none;
|
|
178
|
+
border-radius: 6px;
|
|
179
|
+
padding: 8px 16px;
|
|
180
|
+
font-size: 16px;
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
transition: opacity 0.2s ease;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#wine-search-button:hover {
|
|
186
|
+
opacity: 0.8;
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
title,
|
|
4
|
+
shortDescription,
|
|
5
|
+
reason,
|
|
6
|
+
pairing,
|
|
7
|
+
tags,
|
|
8
|
+
onClose,
|
|
9
|
+
isMobile
|
|
10
|
+
}: {
|
|
11
|
+
title: string;
|
|
12
|
+
shortDescription?: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
pairing?: string;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
isMobile?: boolean;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
const mobile = !!isMobile;
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#if mobile}
|
|
24
|
+
<div class="mobile-fullscreen-wrapper">
|
|
25
|
+
<div class="mobile-fullscreen-overlay" onclick={onClose}></div>
|
|
26
|
+
<div class="mobile-fullscreen-popup">
|
|
27
|
+
<div class="card-popup">
|
|
28
|
+
<div class="card-popup-header">
|
|
29
|
+
<h2>{title}</h2>
|
|
30
|
+
<button class="card-popup-close" onclick={onClose}>×</button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{#if shortDescription}
|
|
34
|
+
<p class="popup-short-desc">{shortDescription}</p>
|
|
35
|
+
{/if}
|
|
36
|
+
|
|
37
|
+
{#if reason}
|
|
38
|
+
<div class="popup-reason">{reason}</div>
|
|
39
|
+
{:else if pairing}
|
|
40
|
+
<p class="popup-pairing">{pairing}</p>
|
|
41
|
+
{/if}
|
|
42
|
+
|
|
43
|
+
{#if tags && tags.length}
|
|
44
|
+
<div class="popup-tags">
|
|
45
|
+
{#each tags as tag}
|
|
46
|
+
<span class="popup-tag">{tag}</span>
|
|
47
|
+
{/each}
|
|
48
|
+
</div>
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
{:else}
|
|
54
|
+
<div class="card-popup">
|
|
55
|
+
<div class="card-popup-header">
|
|
56
|
+
<h2>{title}</h2>
|
|
57
|
+
<button class="card-popup-close" onclick={onClose}>×</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{#if shortDescription}
|
|
61
|
+
<p class="popup-short-desc">{shortDescription}</p>
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
{#if reason}
|
|
65
|
+
<div class="popup-reason">{reason}</div>
|
|
66
|
+
{:else if pairing}
|
|
67
|
+
<p class="popup-pairing">{pairing}</p>
|
|
68
|
+
{/if}
|
|
69
|
+
|
|
70
|
+
{#if tags && tags.length}
|
|
71
|
+
<div class="popup-tags">
|
|
72
|
+
{#each tags as tag}
|
|
73
|
+
<span class="popup-tag">{tag}</span>
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
</div>
|
|
78
|
+
{/if}
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.card-popup {
|
|
82
|
+
position: absolute;
|
|
83
|
+
width: 100%;
|
|
84
|
+
background-color: white;
|
|
85
|
+
padding: 16px;
|
|
86
|
+
border-radius: 16px;
|
|
87
|
+
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.2);
|
|
88
|
+
text-align: left;
|
|
89
|
+
font-size: 14px;
|
|
90
|
+
box-sizing: border-box;
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.card-popup p {
|
|
96
|
+
margin: 5px 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.card-popup-header {
|
|
100
|
+
display: flex;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
align-items: flex-start;
|
|
103
|
+
gap: 12px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.card-popup-header h2 {
|
|
107
|
+
font-size: 16px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
margin: 0;
|
|
110
|
+
flex: 1;
|
|
111
|
+
line-height: 1.3;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.card-popup-close {
|
|
115
|
+
font-size: 20px;
|
|
116
|
+
font-weight: bold;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
color: #999;
|
|
119
|
+
border: none;
|
|
120
|
+
background: transparent;
|
|
121
|
+
line-height: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.card-popup-close:hover {
|
|
125
|
+
color: #000;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.popup-short-desc {
|
|
129
|
+
margin-top: 8px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.popup-reason {
|
|
133
|
+
font-size: 14px;
|
|
134
|
+
font-weight: 400;
|
|
135
|
+
color: #333;
|
|
136
|
+
margin-top: 8px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.popup-pairing {
|
|
140
|
+
margin-top: 8px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.popup-tags {
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-wrap: wrap;
|
|
146
|
+
gap: 8px;
|
|
147
|
+
margin-top: 8px;
|
|
148
|
+
justify-content: center;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.popup-tag {
|
|
152
|
+
display: inline-block;
|
|
153
|
+
padding: 4px 8px;
|
|
154
|
+
border-radius: 12px;
|
|
155
|
+
background-color: #f5f0ff;
|
|
156
|
+
border: 1px solid #ddd;
|
|
157
|
+
font-size: 13px;
|
|
158
|
+
color: #333;
|
|
159
|
+
text-transform: capitalize;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.mobile-fullscreen-wrapper {
|
|
163
|
+
position: fixed;
|
|
164
|
+
top: 0;
|
|
165
|
+
left: 0;
|
|
166
|
+
width: 100%;
|
|
167
|
+
height: 100%;
|
|
168
|
+
z-index: 10000;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.mobile-fullscreen-overlay {
|
|
172
|
+
position: absolute;
|
|
173
|
+
top: 0;
|
|
174
|
+
left: 0;
|
|
175
|
+
width: 100%;
|
|
176
|
+
height: 100%;
|
|
177
|
+
background: rgba(0, 0, 0, 0.5);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.mobile-fullscreen-popup {
|
|
181
|
+
position: absolute;
|
|
182
|
+
width: 100%;
|
|
183
|
+
height: 100%;
|
|
184
|
+
top: 50%;
|
|
185
|
+
left: 50%;
|
|
186
|
+
transform: translate(-50%, -50%);
|
|
187
|
+
max-width: 90%;
|
|
188
|
+
max-height: 80%;
|
|
189
|
+
overflow-y: auto;
|
|
190
|
+
background: transparent;
|
|
191
|
+
}
|
|
192
|
+
</style>
|