wineater-bot 4.24.1 → 4.24.2
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/package.json +5 -1
- package/.claude/settings.local.json +0 -10
- package/.prettierignore +0 -9
- package/.prettierrc +0 -16
- package/CLAUDE.md +0 -115
- package/docs/CUSTOMIZATION.md +0 -133
- package/docs/SVELTE.md +0 -146
- package/e2e/demo.test.ts +0 -6
- package/eslint.config.js +0 -41
- package/index.html +0 -23
- package/playwright.config.ts +0 -9
- package/pnpm-workspace.yaml +0 -2
- package/src/App.svelte +0 -784
- package/src/App.svelte.test.ts +0 -13
- package/src/assets/biobio.png +0 -0
- package/src/components/CookieConsent.svelte +0 -73
- package/src/components/Footer.svelte +0 -122
- package/src/components/Header.svelte +0 -99
- package/src/components/Search.svelte +0 -177
- package/src/components/SkeletonTiles.svelte +0 -94
- package/src/components/WineBottle.svelte +0 -56
- package/src/components/WinePopup.svelte +0 -192
- package/src/components/WineTile.svelte +0 -458
- package/src/components/icons/Budget.svelte +0 -35
- package/src/components/icons/Diamond.svelte +0 -35
- package/src/components/icons/Fish.svelte +0 -56
- package/src/components/icons/Heart.svelte +0 -28
- package/src/components/icons/Meat.svelte +0 -32
- package/src/components/ui/Action.svelte +0 -83
- package/src/demo.spec.ts +0 -13
- package/src/index.js +0 -7
- package/src/services/analytics/analytics.ts +0 -142
- package/src/services/analytics/constants.ts +0 -18
- package/src/services/api/api.ts +0 -298
- package/src/services/api/deserialize.ts +0 -120
- package/src/services/bottle/bottle.spec.ts +0 -32
- package/src/services/bottle/bottle.ts +0 -78
- package/src/services/cache.ts +0 -28
- package/src/services/i18n/i18n.ts +0 -146
- package/src/services/i18n/translations.json +0 -658
- package/src/services/profile/consent.svelte.ts +0 -115
- package/src/services/profile/events.ts +0 -73
- package/src/services/profile/uid.ts +0 -87
- package/src/services/shopify/cart.ts +0 -105
- package/src/services/shopify/shopify.spec.ts +0 -66
- package/src/services/shopify/shopify.ts +0 -150
- package/src/services/store/app-store.svelte.ts +0 -172
- package/src/services/store/deserialize.ts +0 -25
- package/src/services/store/types.ts +0 -12
- package/src/services/ui-config/get-config.ts +0 -25
- package/src/services/ui-config/projects-config.ts +0 -156
- package/src/services/ui-config/types.ts +0 -15
- package/src/services/ui-config/utils.ts +0 -40
- package/tsconfig.json +0 -23
- package/vite.config.js +0 -84
- package/vite.config.js.timestamp-1772121492941-e00587396dc48.mjs +0 -71
- package/vitest-setup-client.ts +0 -2
package/src/App.svelte.test.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
});
|
package/src/assets/biobio.png
DELETED
|
Binary file
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
// Consent banner for shops with Stores.requires_cookie_consent (EU).
|
|
3
|
-
// Rendered inside the widget, not over the host page: the shop has its own
|
|
4
|
-
// site-wide banner, ours covers only what the widget itself stores/sends.
|
|
5
|
-
import Action from './ui/Action.svelte';
|
|
6
|
-
import { getTranslation } from '../services/i18n/i18n';
|
|
7
|
-
import { setConsent } from '../services/profile/consent.svelte';
|
|
8
|
-
|
|
9
|
-
let { lang }: { lang: string } = $props();
|
|
10
|
-
|
|
11
|
-
// optional link to the shop's privacy policy
|
|
12
|
-
const privacyUrl: string | undefined = window?.wineaterData?.PRIVACY_URL;
|
|
13
|
-
|
|
14
|
-
const t = (key: string) => getTranslation({ key, lang });
|
|
15
|
-
</script>
|
|
16
|
-
|
|
17
|
-
<div class="wr-Consent" role="region" aria-label="Cookies">
|
|
18
|
-
<p class="wr-Consent-text">
|
|
19
|
-
{t('consentText')}
|
|
20
|
-
{#if privacyUrl}
|
|
21
|
-
<a class="wr-Consent-link" href={privacyUrl} target="_blank" rel="noopener noreferrer">
|
|
22
|
-
{t('consentPrivacy')}
|
|
23
|
-
</a>
|
|
24
|
-
{/if}
|
|
25
|
-
</p>
|
|
26
|
-
<div class="wr-Consent-actions">
|
|
27
|
-
<Action variant="outlined" handleClick={() => setConsent('denied')}>
|
|
28
|
-
{t('consentDecline')}
|
|
29
|
-
</Action>
|
|
30
|
-
<Action variant="primary" handleClick={() => setConsent('granted')}>
|
|
31
|
-
{t('consentAccept')}
|
|
32
|
-
</Action>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<style>
|
|
37
|
-
.wr-Consent {
|
|
38
|
-
display: flex;
|
|
39
|
-
align-items: center;
|
|
40
|
-
justify-content: space-between;
|
|
41
|
-
gap: 16px;
|
|
42
|
-
margin: 12px 0 0;
|
|
43
|
-
padding: 12px 16px;
|
|
44
|
-
border-radius: 12px;
|
|
45
|
-
background: #f6f4f2;
|
|
46
|
-
font-size: 13px;
|
|
47
|
-
line-height: 1.4;
|
|
48
|
-
color: #333;
|
|
49
|
-
}
|
|
50
|
-
.wr-Consent-text {
|
|
51
|
-
margin: 0;
|
|
52
|
-
flex: 1;
|
|
53
|
-
}
|
|
54
|
-
.wr-Consent-link {
|
|
55
|
-
color: inherit;
|
|
56
|
-
text-decoration: underline;
|
|
57
|
-
margin-left: 4px;
|
|
58
|
-
}
|
|
59
|
-
.wr-Consent-actions {
|
|
60
|
-
display: flex;
|
|
61
|
-
gap: 8px;
|
|
62
|
-
flex-shrink: 0;
|
|
63
|
-
}
|
|
64
|
-
@media (max-width: 640px) {
|
|
65
|
-
.wr-Consent {
|
|
66
|
-
flex-direction: column;
|
|
67
|
-
align-items: stretch;
|
|
68
|
-
}
|
|
69
|
-
.wr-Consent-actions {
|
|
70
|
-
justify-content: flex-end;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
</style>
|
|
@@ -1,122 +0,0 @@
|
|
|
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
|
-
<footer class="wr-Footer">
|
|
31
|
-
<div class="wr-Footer-center">
|
|
32
|
-
<a href="https://wineater.com" class="wr-Footer-link" target="_blank">
|
|
33
|
-
<span class="wr-Footer-text">{getTranslation({ key: 'poweredBy', lang: appLanguage })}</span>
|
|
34
|
-
<img
|
|
35
|
-
class="wr-Footer-logo"
|
|
36
|
-
src={countryCode === 'ru'
|
|
37
|
-
? 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/divino_logo.svg?x=5'
|
|
38
|
-
: 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/logo_medium_gray.svg'}
|
|
39
|
-
alt="Wineater Logo"
|
|
40
|
-
loading="lazy"
|
|
41
|
-
decoding="async"
|
|
42
|
-
/>
|
|
43
|
-
<span class="wr-Footer-text wr-Footer-text--sommelier">
|
|
44
|
-
{getTranslation({ key: 'ai_sommelier', lang: appLanguage })}
|
|
45
|
-
</span>
|
|
46
|
-
</a>
|
|
47
|
-
</div>
|
|
48
|
-
{#if supportedLanguages.length > 1}
|
|
49
|
-
<select class="wr-Footer-dropdown" value={appLanguage} onchange={handleLanguageChange}>
|
|
50
|
-
{#each supportedLanguages as lang}
|
|
51
|
-
<option class="wr-Footer-lang" value={lang} selected={lang === appLanguage}>
|
|
52
|
-
{lang.toUpperCase()}
|
|
53
|
-
</option>
|
|
54
|
-
{/each}
|
|
55
|
-
</select>
|
|
56
|
-
{/if}
|
|
57
|
-
</footer>
|
|
58
|
-
|
|
59
|
-
<style>
|
|
60
|
-
.wr-Footer {
|
|
61
|
-
position: relative;
|
|
62
|
-
border-top: 2px solid #f6f7fd;
|
|
63
|
-
font-weight: 200;
|
|
64
|
-
padding: 12px 24px;
|
|
65
|
-
min-height: 40px;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.wr-Footer-center {
|
|
69
|
-
position: absolute;
|
|
70
|
-
left: 50%;
|
|
71
|
-
transform: translateX(-50%);
|
|
72
|
-
text-align: center;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.wr-Footer-dropdown {
|
|
76
|
-
position: absolute;
|
|
77
|
-
right: 0px;
|
|
78
|
-
top: 50%;
|
|
79
|
-
transform: translateY(-50%);
|
|
80
|
-
padding: 4px;
|
|
81
|
-
border-radius: 6px;
|
|
82
|
-
border: 2px solid var(--wr-brand-color);
|
|
83
|
-
background-color: white;
|
|
84
|
-
color: var(--wr-brand-color);
|
|
85
|
-
font-weight: 500;
|
|
86
|
-
font-size: 12px;
|
|
87
|
-
cursor: pointer;
|
|
88
|
-
transition: all 0.2s ease;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.wr-Footer-dropdown:hover {
|
|
92
|
-
box-shadow: 0 4px 8px rgba(123, 43, 235, 0.2);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.wr-Footer-link {
|
|
96
|
-
text-decoration: none;
|
|
97
|
-
display: flex;
|
|
98
|
-
align-items: center;
|
|
99
|
-
white-space: nowrap;
|
|
100
|
-
gap: 8px;
|
|
101
|
-
color: #a8a8a8;
|
|
102
|
-
font-size: 12px;
|
|
103
|
-
line-height: 16px;
|
|
104
|
-
transition: opacity 0.2s ease;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.wr-Footer-link:hover {
|
|
108
|
-
opacity: 0.8;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.wr-Footer-logo {
|
|
112
|
-
height: 16px;
|
|
113
|
-
width: auto;
|
|
114
|
-
color: #a8a8a8;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
@media screen and (max-width: 768px) {
|
|
118
|
-
.wr-Footer-text--sommelier {
|
|
119
|
-
display: none;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
</style>
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getTranslation } from '../services/i18n/i18n';
|
|
3
|
-
import { appStore } from '../services/store/app-store.svelte';
|
|
4
|
-
import Diamond from './icons/Diamond.svelte';
|
|
5
|
-
import Heart from './icons/Heart.svelte';
|
|
6
|
-
import Meat from './icons/Meat.svelte';
|
|
7
|
-
import Fish from './icons/Fish.svelte';
|
|
8
|
-
|
|
9
|
-
let lang = $derived(appStore.getAppLanguage());
|
|
10
|
-
let uiConfig = $derived(appStore.getUiConfig());
|
|
11
|
-
let titleText = $derived(window?.wineaterData?.TITLE_TEXT || uiConfig?.titleText || null);
|
|
12
|
-
let ShowedIcon = $state(Meat);
|
|
13
|
-
|
|
14
|
-
// splits a custom title into text/keyword segments so the meal/mood/occasion
|
|
15
|
-
// icons can be placed right next to their matching word
|
|
16
|
-
function splitTitleText(text: string) {
|
|
17
|
-
return text
|
|
18
|
-
.split(/(meal|mood|occasion)/gi)
|
|
19
|
-
.map((part) => ({ keyword: /^(meal|mood|occasion)$/i.test(part) ? part.toLowerCase() : null, text: part }));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let titleSegments = $derived(titleText ? splitTitleText(titleText) : null);
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
<div class="wr-Header">
|
|
26
|
-
{#if titleSegments}
|
|
27
|
-
<h1 class="wr-Header-title">
|
|
28
|
-
{#each titleSegments as segment}
|
|
29
|
-
{#if segment.keyword === 'meal'}
|
|
30
|
-
<span onmouseenter={() => (ShowedIcon = Fish)} onmouseleave={() => (ShowedIcon = Meat)}>
|
|
31
|
-
<ShowedIcon className="wr-Header-icon" size="28" />
|
|
32
|
-
</span><span class="wr-Header-label">{segment.text}</span>
|
|
33
|
-
{:else if segment.keyword === 'mood'}
|
|
34
|
-
<Heart className="wr-Header-icon" size="28" /><span class="wr-Header-label"
|
|
35
|
-
>{segment.text}</span
|
|
36
|
-
>
|
|
37
|
-
{:else if segment.keyword === 'occasion'}
|
|
38
|
-
<Diamond className="wr-Header-icon" size="28" /><span class="wr-Header-label"
|
|
39
|
-
>{segment.text}</span
|
|
40
|
-
>
|
|
41
|
-
{:else}
|
|
42
|
-
{segment.text}
|
|
43
|
-
{/if}
|
|
44
|
-
{/each}
|
|
45
|
-
</h1>
|
|
46
|
-
{:else}
|
|
47
|
-
<h1 class="wr-Header-title">
|
|
48
|
-
{getTranslation({ key: 'headerLine1', lang })}
|
|
49
|
-
<!-- <span class="wr-Header-icon wr-Header-icon--meal"></span> -->
|
|
50
|
-
<!-- TODO: show fish on hover -->
|
|
51
|
-
|
|
52
|
-
<span onmouseenter={() => (ShowedIcon = Fish)} onmouseleave={() => (ShowedIcon = Meat)}>
|
|
53
|
-
<ShowedIcon className="wr-Header-icon" size="28" />
|
|
54
|
-
</span>
|
|
55
|
-
|
|
56
|
-
<span class="wr-Header-label">{getTranslation({ key: 'headerMeal', lang })}</span>,
|
|
57
|
-
<!-- <span class="wr-Header-icon wr-Header-icon--mood"></span> -->
|
|
58
|
-
<Heart className="wr-Header-icon" size="28" />
|
|
59
|
-
|
|
60
|
-
<span class="wr-Header-label">{getTranslation({ key: 'headerMood', lang })}</span>
|
|
61
|
-
<br />
|
|
62
|
-
{getTranslation({ key: 'headerOr', lang })}
|
|
63
|
-
<!-- <span class="wr-Header-icon wr-Header-icon--occasion"></span> -->
|
|
64
|
-
<Diamond className="wr-Header-icon" size="28" />
|
|
65
|
-
<span class="wr-Header-label">{getTranslation({ key: 'headerOccasion', lang })}</span>
|
|
66
|
-
{getTranslation({ key: 'headerLine2', lang })}
|
|
67
|
-
</h1>
|
|
68
|
-
{/if}
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<style>
|
|
72
|
-
.wr-Header-title {
|
|
73
|
-
font-weight: 600;
|
|
74
|
-
font-size: 24px;
|
|
75
|
-
margin: 0 auto;
|
|
76
|
-
line-height: 1.3;
|
|
77
|
-
color: #333;
|
|
78
|
-
text-align: center;
|
|
79
|
-
|
|
80
|
-
/* TODO: add wineater-color to svg */
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
:global(.wr-Header-icon) {
|
|
84
|
-
display: inline-block;
|
|
85
|
-
vertical-align: middle;
|
|
86
|
-
color: var(--wr-wineater-color);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.wr-Header-label {
|
|
90
|
-
color: var(--wr-wineater-color);
|
|
91
|
-
font-weight: 500;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
@media screen and (max-width: 768px) {
|
|
95
|
-
.wr-Header-title {
|
|
96
|
-
display: none;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
</style>
|
|
@@ -1,177 +0,0 @@
|
|
|
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
|
-
import Action from './ui/Action.svelte';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
searchQuery: string;
|
|
9
|
-
onSubmit: (event: SubmitEvent) => void;
|
|
10
|
-
initialized: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let { searchQuery = $bindable(), onSubmit, initialized }: Props = $props();
|
|
14
|
-
let searchEl: HTMLInputElement;
|
|
15
|
-
let isInputFocused = $state(false);
|
|
16
|
-
let animIndex = $state(0);
|
|
17
|
-
let animChar = $state(0);
|
|
18
|
-
let animForward = $state(true);
|
|
19
|
-
let animRunning = $state(false);
|
|
20
|
-
let lang = $derived(appStore.getAppLanguage());
|
|
21
|
-
let animationToken = 0;
|
|
22
|
-
|
|
23
|
-
export function getSearchElement() {
|
|
24
|
-
return searchEl;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function initializeSearchAnimation() {
|
|
28
|
-
const localToken = ++animationToken;
|
|
29
|
-
const suggestions = getTranslation({ key: 'placeholderSuggestions', lang });
|
|
30
|
-
if (!searchEl || !suggestions.length) return;
|
|
31
|
-
animRunning = true;
|
|
32
|
-
const loop = () => {
|
|
33
|
-
// abort if a new animation has started or conditions changed
|
|
34
|
-
if (localToken !== animationToken) return;
|
|
35
|
-
if (!searchEl || !animRunning || isInputFocused || searchQuery.length) return;
|
|
36
|
-
const current = suggestions[animIndex];
|
|
37
|
-
if (animForward) {
|
|
38
|
-
searchEl.placeholder = current.substring(0, animChar++);
|
|
39
|
-
if (animChar > current.length) {
|
|
40
|
-
animForward = false;
|
|
41
|
-
setTimeout(loop, 1600);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
setTimeout(loop, 90);
|
|
45
|
-
} else {
|
|
46
|
-
searchEl.placeholder = current.substring(0, animChar--);
|
|
47
|
-
if (animChar < 0) {
|
|
48
|
-
animForward = true;
|
|
49
|
-
animIndex = (animIndex + 1) % suggestions.length;
|
|
50
|
-
setTimeout(loop, 450);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
setTimeout(loop, 45);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
loop();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function stopSearchAnimation() {
|
|
60
|
-
animRunning = false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function onSearchFocus() {
|
|
64
|
-
isInputFocused = true;
|
|
65
|
-
stopSearchAnimation();
|
|
66
|
-
if (searchEl) searchEl.placeholder = '';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function onSearchBlur() {
|
|
70
|
-
isInputFocused = false;
|
|
71
|
-
animChar = 0;
|
|
72
|
-
animForward = true;
|
|
73
|
-
animRunning = false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
$effect(() => {
|
|
77
|
-
// update placeholder whenever lang or searchEl change
|
|
78
|
-
if (!searchEl) return;
|
|
79
|
-
if (searchQuery.length > 0) return;
|
|
80
|
-
// cancel any running animation on language change
|
|
81
|
-
animationToken++;
|
|
82
|
-
animRunning = false;
|
|
83
|
-
animIndex = 0;
|
|
84
|
-
animChar = 0;
|
|
85
|
-
animForward = true;
|
|
86
|
-
const suggestions = getTranslation({ key: 'placeholderSuggestions', lang });
|
|
87
|
-
if (suggestions && suggestions.length) {
|
|
88
|
-
searchEl.placeholder = suggestions[0];
|
|
89
|
-
} else {
|
|
90
|
-
searchEl.placeholder = getTranslation({ key: 'searchPlaceholder', lang });
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
$effect(() => {
|
|
95
|
-
// control typing animation separately with latest lang
|
|
96
|
-
if (searchEl && initialized && !isInputFocused && !searchQuery.length && !animRunning) {
|
|
97
|
-
initializeSearchAnimation();
|
|
98
|
-
}
|
|
99
|
-
if (searchQuery.length > 0 && animRunning) {
|
|
100
|
-
animRunning = false;
|
|
101
|
-
animationToken++;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
onMount(() => {
|
|
106
|
-
// effects handle initial placeholder and animation
|
|
107
|
-
});
|
|
108
|
-
</script>
|
|
109
|
-
|
|
110
|
-
<form class="wr-Search" onsubmit={onSubmit}>
|
|
111
|
-
<span class="wr-Search-icon"></span>
|
|
112
|
-
<input
|
|
113
|
-
class="wr-Search-input"
|
|
114
|
-
bind:this={searchEl}
|
|
115
|
-
type="text"
|
|
116
|
-
bind:value={searchQuery}
|
|
117
|
-
onfocus={onSearchFocus}
|
|
118
|
-
onblur={onSearchBlur}
|
|
119
|
-
/>
|
|
120
|
-
<Action className="wr-Search-button" type="submit" variant="primary">
|
|
121
|
-
{getTranslation({ key: 'searchButton', lang })}
|
|
122
|
-
</Action>
|
|
123
|
-
</form>
|
|
124
|
-
|
|
125
|
-
<style>
|
|
126
|
-
.wr-Search {
|
|
127
|
-
position: relative;
|
|
128
|
-
margin: 12px auto;
|
|
129
|
-
padding: 0 6px;
|
|
130
|
-
display: flex;
|
|
131
|
-
align-items: center;
|
|
132
|
-
border: 2px solid #d5d9ee;
|
|
133
|
-
border-radius: 10px;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.wr-Search-icon {
|
|
137
|
-
left: 12px;
|
|
138
|
-
width: 24px;
|
|
139
|
-
height: 24px;
|
|
140
|
-
flex: 0 0 auto;
|
|
141
|
-
background-image: url("data:image/svg+xml,%3Csvg%20width=%2224%22%20height=%2225%22%20viewBox=%220%200%2024%2025%22%20fill=%22none%22%20xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cpath%20d=%22M11%2019.3884C15.4183%2019.3884%2019%2015.8067%2019%2011.3884C19%206.97015%2015.4183%203.38843%2011%203.38843C6.58172%203.38843%203%206.97015%203%2011.3884C3%2015.8067%206.58172%2019.3884%2011%2019.3884Z%22%20stroke=%22%238250D2%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22/%3E%3Cpath%20d=%22M21%2021.3884L16.7%2017.0884%22%20stroke=%22%238250D2%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22/%3E%3C/svg%3E");
|
|
142
|
-
background-repeat: no-repeat;
|
|
143
|
-
background-size: contain;
|
|
144
|
-
z-index: 2;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.wr-Search-input {
|
|
148
|
-
flex: 1 1 0%;
|
|
149
|
-
min-width: 0;
|
|
150
|
-
padding: 12px;
|
|
151
|
-
font-size: 16px;
|
|
152
|
-
border: none;
|
|
153
|
-
border-radius: 10px;
|
|
154
|
-
outline: none;
|
|
155
|
-
transition: border 0.2s ease;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.wr-Search-input::placeholder {
|
|
159
|
-
color: var(--wr-wineater-color);
|
|
160
|
-
opacity: 1;
|
|
161
|
-
transition:
|
|
162
|
-
color 0.3s ease,
|
|
163
|
-
opacity 0.3s ease-in-out;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.wr-Search-input:focus {
|
|
167
|
-
border-color: var(--wr-brand-color);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.wr-Search-input:focus::placeholder {
|
|
171
|
-
opacity: 0.5;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.wr-Search-button {
|
|
175
|
-
flex: 0 0 auto;
|
|
176
|
-
}
|
|
177
|
-
</style>
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
<!-- Placeholder cards that match the WineTile layout so the grid does not jump when real data arrives -->
|
|
2
|
-
<div class="wr-Skeleton" aria-hidden="true">
|
|
3
|
-
{#each [0, 1, 2, 3] as n (n)}
|
|
4
|
-
<div class="wr-Skeleton-tile">
|
|
5
|
-
<div class="wr-Skeleton-block wr-Skeleton-image"></div>
|
|
6
|
-
<div class="wr-Skeleton-block wr-Skeleton-price"></div>
|
|
7
|
-
<div class="wr-Skeleton-block wr-Skeleton-title"></div>
|
|
8
|
-
<div class="wr-Skeleton-block wr-Skeleton-title wr-Skeleton-title--short"></div>
|
|
9
|
-
<div class="wr-Skeleton-block wr-Skeleton-button"></div>
|
|
10
|
-
</div>
|
|
11
|
-
{/each}
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<style>
|
|
15
|
-
.wr-Skeleton {
|
|
16
|
-
display: grid;
|
|
17
|
-
grid-template-columns: repeat(4, 1fr);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.wr-Skeleton-tile {
|
|
21
|
-
display: flex;
|
|
22
|
-
flex-direction: column;
|
|
23
|
-
align-items: center;
|
|
24
|
-
gap: 10px;
|
|
25
|
-
padding: 16px 8px;
|
|
26
|
-
border-right: 2px solid #f6f7fd;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.wr-Skeleton-tile:last-child {
|
|
30
|
-
border-right: none;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.wr-Skeleton-block {
|
|
34
|
-
border-radius: 6px;
|
|
35
|
-
background: linear-gradient(90deg, #f1f2f8 25%, #f8f9fc 50%, #f1f2f8 75%);
|
|
36
|
-
background-size: 200% 100%;
|
|
37
|
-
animation: wr-shimmer 1.2s linear infinite;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.wr-Skeleton-image {
|
|
41
|
-
width: 60%;
|
|
42
|
-
height: 200px;
|
|
43
|
-
margin-bottom: 6px;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.wr-Skeleton-price {
|
|
47
|
-
width: 40px;
|
|
48
|
-
height: 16px;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.wr-Skeleton-title {
|
|
52
|
-
width: 80%;
|
|
53
|
-
height: 14px;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.wr-Skeleton-title--short {
|
|
57
|
-
width: 55%;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.wr-Skeleton-button {
|
|
61
|
-
width: 96px;
|
|
62
|
-
height: 36px;
|
|
63
|
-
margin-top: 14px;
|
|
64
|
-
border-radius: 10px;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
@keyframes wr-shimmer {
|
|
68
|
-
from {
|
|
69
|
-
background-position: 200% 0;
|
|
70
|
-
}
|
|
71
|
-
to {
|
|
72
|
-
background-position: -200% 0;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
@media (prefers-reduced-motion: reduce) {
|
|
77
|
-
.wr-Skeleton-block {
|
|
78
|
-
animation: none;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
@media screen and (max-width: 768px) {
|
|
83
|
-
.wr-Skeleton {
|
|
84
|
-
display: flex;
|
|
85
|
-
gap: 8px;
|
|
86
|
-
overflow: hidden;
|
|
87
|
-
padding: 8px 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.wr-Skeleton-tile {
|
|
91
|
-
flex: 0 0 240px;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
</style>
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { BOTTLES, BUBBLES, SHAPES, bottleKind } from '../services/bottle/bottle';
|
|
3
|
-
|
|
4
|
-
let { type }: { type?: string | null } = $props();
|
|
5
|
-
|
|
6
|
-
const kind = $derived(bottleKind(type));
|
|
7
|
-
const k = $derived(BOTTLES[kind]);
|
|
8
|
-
const sh = $derived(SHAPES[k.shoulder]);
|
|
9
|
-
const gid = `wr-bottle-${Math.random().toString(36).slice(2, 8)}`;
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<svg class="wr-WineBottle" viewBox="0 0 44 130" aria-hidden="true" focusable="false">
|
|
13
|
-
<defs>
|
|
14
|
-
<linearGradient id={gid} x1="0" x2="1" y1="0" y2="0">
|
|
15
|
-
<stop offset="0" stop-color={k.glass} stop-opacity="0.78" />
|
|
16
|
-
<stop offset="0.45" stop-color={k.glass} />
|
|
17
|
-
<stop offset="1" stop-color={k.glass} stop-opacity="0.88" />
|
|
18
|
-
</linearGradient>
|
|
19
|
-
</defs>
|
|
20
|
-
<path d={sh.body} fill="url(#{gid})" />
|
|
21
|
-
{#if kind === 'sparkling'}
|
|
22
|
-
<path d="M16.5 10h11l1.5 26h-14z" fill={k.foil} />
|
|
23
|
-
<rect x="14" y="2" width="16" height="9" rx="3.5" fill={k.foil} />
|
|
24
|
-
<rect x="14" y="8" width="16" height="2.4" fill="#000" opacity="0.12" />
|
|
25
|
-
<path
|
|
26
|
-
d="M16 15h12M15.6 20h12.8M15.2 25h13.6M14.8 30h14.4M22 11v21"
|
|
27
|
-
stroke="#8a6d12"
|
|
28
|
-
stroke-opacity="0.55"
|
|
29
|
-
stroke-width="0.9"
|
|
30
|
-
fill="none"
|
|
31
|
-
/>
|
|
32
|
-
<circle cx="22" cy="33" r="1.6" fill="none" stroke="#8a6d12" stroke-opacity="0.55" stroke-width="0.9" />
|
|
33
|
-
<g fill="#fff" opacity="0.4">
|
|
34
|
-
{#each BUBBLES as [cx, cy, r] (`${cx}-${cy}`)}
|
|
35
|
-
<circle {cx} {cy} {r} />
|
|
36
|
-
{/each}
|
|
37
|
-
</g>
|
|
38
|
-
{:else}
|
|
39
|
-
<rect x={sh.cap.x} y={sh.cap.y} width={sh.cap.w} height={sh.cap.h} rx="2.5" fill={k.foil} />
|
|
40
|
-
<rect x={sh.cap.x} y={sh.cap.y + sh.cap.h - 3} width={sh.cap.w} height="2.5" fill="#000" opacity="0.12" />
|
|
41
|
-
{/if}
|
|
42
|
-
<rect x={sh.label.x} y={sh.label.y} width={sh.label.w} height={sh.label.h} rx="2.5" fill="#fffdf8" opacity="0.94" />
|
|
43
|
-
<rect x={sh.label.x + sh.label.w * 0.2} y={sh.label.y + 8} width={sh.label.w * 0.6} height="2.4" rx="1.2" fill={k.accent} />
|
|
44
|
-
<rect x={sh.label.x + sh.label.w * 0.3} y={sh.label.y + 14} width={sh.label.w * 0.4} height="1.8" rx="0.9" fill={k.accent} opacity="0.5" />
|
|
45
|
-
<rect x={sh.label.x + sh.label.w * 0.3} y={sh.label.y + 19} width={sh.label.w * 0.4} height="1.8" rx="0.9" fill={k.accent} opacity="0.5" />
|
|
46
|
-
<path d={sh.shine} stroke="#fff" stroke-opacity="0.22" stroke-width="2.2" stroke-linecap="round" fill="none" />
|
|
47
|
-
</svg>
|
|
48
|
-
|
|
49
|
-
<style>
|
|
50
|
-
.wr-WineBottle {
|
|
51
|
-
display: block;
|
|
52
|
-
height: 100%;
|
|
53
|
-
width: auto;
|
|
54
|
-
margin: 0 auto;
|
|
55
|
-
}
|
|
56
|
-
</style>
|