@crm-market/template-shared 1.0.7 → 1.0.8
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/assets/scss/_variables.scss +3 -3
- package/assets/scss/components/_header.scss +1 -1
- package/assets/scss/style.css +4 -4
- package/components/AboutUs/StaticContent.vue +32 -0
- package/components/Cart/CartEmpty.vue +42 -0
- package/components/Cart/CartItem.vue +145 -0
- package/components/Cart/CartSummary.vue +57 -0
- package/components/Cart/ProductQuantity.vue +23 -13
- package/components/Cart/index.vue +68 -150
- package/components/Categories/index.vue +88 -291
- package/components/Checkout/AddressFields.vue +74 -0
- package/components/Checkout/CheckoutForm.vue +75 -0
- package/components/Checkout/CityAutocomplete.vue +68 -0
- package/components/Checkout/DeliveryMethodCard.vue +66 -0
- package/components/Checkout/DeliveryOptions.vue +148 -0
- package/components/Checkout/OrderSummary.vue +116 -0
- package/components/Checkout/PaymentOptions.vue +76 -0
- package/components/Checkout/PickupPoints.vue +46 -0
- package/components/Checkout/PromoCode.vue +166 -0
- package/components/Checkout/index.vue +235 -173
- package/components/Checkout/types.ts +45 -0
- package/components/Common/PageBanner.vue +22 -20
- package/components/Common/ProductCard.vue +139 -115
- package/components/Common/Services.vue +4 -4
- package/components/Contact/ContactForm.vue +44 -74
- package/components/Contact/ContactInfo.vue +35 -61
- package/components/FAQ/index.vue +6 -119
- package/components/Layout/Copyright.vue +11 -9
- package/components/Layout/Footer.vue +49 -136
- package/components/Layout/HeaderBar.vue +174 -0
- package/components/Layout/HeaderLogo.vue +27 -0
- package/components/Layout/MiddleHeader.vue +1 -228
- package/components/Layout/MiddleHeaderThree.vue +1 -203
- package/components/Layout/MiddleHeaderTwo.vue +1 -239
- package/components/Layout/NavLinks.vue +23 -0
- package/components/Layout/Navbar.vue +38 -151
- package/components/Layout/NavbarStyleFour.vue +1 -332
- package/components/Layout/NavbarStyleThree.vue +1 -188
- package/components/Layout/NavbarStyleTwo.vue +1 -108
- package/components/Layout/Preloader.vue +5 -12
- package/components/Layout/ResponsiveNavbar.vue +32 -418
- package/components/Layout/SitewideBanner.vue +285 -0
- package/components/Layout/TopHeader.vue +17 -83
- package/components/Layout/layoutData.ts +83 -0
- package/components/PrivacyPolicy/index.vue +6 -110
- package/components/ProductDetails/ProductDetailsTab.vue +31 -321
- package/components/ProductDetails/ProductGallery.vue +45 -0
- package/components/ProductDetails/ProductQuantity.vue +8 -14
- package/components/ProductDetails/RecentlyViewed.vue +53 -289
- package/components/ProductDetails/RelatedProducts.vue +64 -0
- package/components/ProductDetails/index.vue +90 -230
- package/components/Products/index.vue +78 -16
- package/components/TermsConditions/index.vue +6 -110
- package/components/Wishlist/index.vue +62 -88
- package/composables/useCart.ts +133 -81
- package/composables/useCategories.ts +66 -29
- package/composables/useCategoryImages.ts +45 -0
- package/composables/useCheckout.ts +302 -95
- package/composables/useContactForm.ts +57 -0
- package/composables/useHomeContent.ts +220 -0
- package/composables/useI18n.ts +12 -24
- package/composables/useJsonLd.ts +162 -0
- package/composables/useProducts.ts +244 -81
- package/composables/useRecent.ts +30 -0
- package/composables/useSanitize.ts +76 -0
- package/composables/useSiteConfig.ts +51 -45
- package/composables/useStaticPages.ts +48 -0
- package/composables/useStoreOrigin.ts +32 -0
- package/composables/useTemplateSections.ts +51 -3
- package/composables/useThemeVars.ts +113 -0
- package/composables/useWishlist.ts +72 -0
- package/layouts/default.vue +80 -19
- package/locales/en.json +255 -86
- package/locales/uk.json +251 -82
- package/modules/template-schema.ts +35 -0
- package/nuxt.config.ts +8 -11
- package/package.json +1 -1
- package/pages/about-us.vue +9 -9
- package/pages/cart.vue +0 -1
- package/pages/categories.vue +0 -1
- package/pages/category/[slug].vue +84 -0
- package/pages/checkout.vue +0 -1
- package/pages/contact.vue +6 -6
- package/pages/delivery.vue +12 -0
- package/pages/faq.vue +6 -6
- package/pages/order-success.vue +113 -0
- package/pages/privacy-policy.vue +6 -6
- package/pages/product/[slug].vue +82 -0
- package/pages/product-details.vue +14 -6
- package/pages/products.vue +12 -1
- package/pages/returns.vue +12 -0
- package/pages/terms-conditions.vue +6 -6
- package/pages/wishlist.vue +2 -2
- package/utils/seo-urls.ts +70 -0
- package/utils/store-origin.ts +36 -0
- package/components/AboutUs/AboutUsTuan.vue +0 -25
- package/components/AboutUs/Statistics.vue +0 -42
- package/components/AboutUs/SubscribeToTheNewsletter.vue +0 -57
- package/components/AddAddress/index.vue +0 -68
- package/components/BestSellers/Products.vue +0 -1562
- package/components/BestSellers/RecentlyViewed.vue +0 -304
- package/components/ChangePassword/index.vue +0 -69
- package/components/Common/DashboardNavigation.vue +0 -35
- package/components/Dashboard/RecentOrder.vue +0 -173
- package/components/Dashboard/index.vue +0 -77
- package/components/EditAddress/index.vue +0 -117
- package/components/EditProfile/index.vue +0 -95
- package/components/FeaturedProduct/FeaturedProducts.vue +0 -304
- package/components/FeaturedProduct/Products.vue +0 -1562
- package/components/ForgotPassword/index.vue +0 -49
- package/components/Layout/FooterTwo.vue +0 -159
- package/components/Layout/LocationOption.vue +0 -178
- package/components/Layout/RTLSwitchBtn.vue +0 -40
- package/components/Login/index.vue +0 -89
- package/components/MyAccount/index.vue +0 -154
- package/components/NewArrivals/Products.vue +0 -1969
- package/components/NewArrivals/RecentlyViewed.vue +0 -304
- package/components/OrderDetails/index.vue +0 -75
- package/components/OrderHistory/index.vue +0 -195
- package/components/Products/RecentlyViewed.vue +0 -302
- package/components/Register/index.vue +0 -83
- package/components/TrendingProducts/FeaturedProducts.vue +0 -304
- package/components/TrendingProducts/Products.vue +0 -1564
- package/components/Wishlist/ProductQuantity.vue +0 -29
- package/layouts/inner.vue +0 -70
- package/locales/ar.json +0 -234
- package/locales/ru.json +0 -234
- package/pages/address.vue +0 -11
- package/pages/change-password.vue +0 -11
- package/pages/dashboard.vue +0 -11
- package/pages/edit-address.vue +0 -11
- package/pages/edit-profile.vue +0 -11
- package/pages/forgot-password.vue +0 -11
- package/pages/login.vue +0 -11
- package/pages/my-account.vue +0 -11
- package/pages/order-details.vue +0 -11
- package/pages/order-history.vue +0 -11
- package/pages/register.vue +0 -11
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Наскрізний банер магазину (siteConfig.sitewideBanner, редактор «Банери» → «Наскрізний банер» в адмінці).
|
|
6
|
+
* Варіанти: full (фото-фон + текст), split-left / split-right (фото збоку), text (колір + текст), marquee (біжуча стрічка).
|
|
7
|
+
* Кольори без значень — з теми (--primary-color / --accent-color), тож банер підхоплює «Налаштування шаблону».
|
|
8
|
+
*/
|
|
9
|
+
const { siteConfig } = useSiteConfig();
|
|
10
|
+
|
|
11
|
+
const HEX = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
12
|
+
const str = (v: any) => (typeof v === 'string' ? v.trim() : '');
|
|
13
|
+
const hex = (v: any) => (HEX.test(str(v)) ? str(v) : '');
|
|
14
|
+
// Посилання лише внутрішні або http(s)
|
|
15
|
+
const link = (v: any) => {
|
|
16
|
+
const s = str(v);
|
|
17
|
+
return s && (/^\/(?!\/)/.test(s) || /^https?:\/\//i.test(s)) ? s : '/products';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const VARIANTS = ['full', 'split-left', 'split-right', 'text', 'marquee'];
|
|
21
|
+
|
|
22
|
+
const b = computed(() => {
|
|
23
|
+
const s: any = siteConfig.value?.sitewideBanner;
|
|
24
|
+
if (!s || s.enabled !== true) return null;
|
|
25
|
+
const variant = VARIANTS.includes(s.variant) ? s.variant : 'full';
|
|
26
|
+
const overlay = Number(s.overlay);
|
|
27
|
+
const speed = Number(s.marqueeSpeed);
|
|
28
|
+
return {
|
|
29
|
+
variant,
|
|
30
|
+
image: resolveMediaUrl(str(s.imageUrl)),
|
|
31
|
+
title: str(s.title),
|
|
32
|
+
subtitle: str(s.subtitle),
|
|
33
|
+
buttonLabel: str(s.buttonLabel),
|
|
34
|
+
buttonLink: link(s.buttonLink),
|
|
35
|
+
buttonStyle: s.buttonStyle === 'outline' ? 'outline' : 'solid',
|
|
36
|
+
buttonColor: hex(s.buttonColor),
|
|
37
|
+
textAlign: ['left', 'center', 'right'].includes(s.textAlign) ? s.textAlign : 'center',
|
|
38
|
+
height: ['compact', 'standard', 'tall'].includes(s.height) ? s.height : 'standard',
|
|
39
|
+
overlay: Number.isFinite(overlay) ? Math.min(80, Math.max(0, overlay)) / 100 : 0.35,
|
|
40
|
+
bgColor: hex(s.bgColor),
|
|
41
|
+
marqueeItems: (Array.isArray(s.marqueeItems) ? s.marqueeItems : []).map(str).filter(Boolean) as string[],
|
|
42
|
+
marqueeSpeed: Number.isFinite(speed) ? Math.min(60, Math.max(5, speed)) : 20,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Що показувати: стрічка — є пункти; text — є текст; full/split — є фото (без фото падаємо в text, якщо є текст)
|
|
47
|
+
const view = computed(() => {
|
|
48
|
+
const x = b.value;
|
|
49
|
+
if (!x) return '';
|
|
50
|
+
if (x.variant === 'marquee') return x.marqueeItems.length ? 'marquee' : x.title ? 'text' : '';
|
|
51
|
+
if (x.variant === 'text') return x.title || x.subtitle ? 'text' : '';
|
|
52
|
+
if (x.image) return x.variant;
|
|
53
|
+
return x.title || x.subtitle ? 'text' : '';
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const isExternal = computed(() => /^https?:\/\//i.test(b.value?.buttonLink || ''));
|
|
57
|
+
|
|
58
|
+
const rootStyle = computed(() => {
|
|
59
|
+
const x = b.value;
|
|
60
|
+
if (!x) return {};
|
|
61
|
+
return {
|
|
62
|
+
'--sw-bg': x.bgColor || 'var(--primary-color, #4a8d16)',
|
|
63
|
+
'--sw-btn': x.buttonColor || 'var(--accent-color, #f7931e)',
|
|
64
|
+
'--sw-overlay': String(x.overlay),
|
|
65
|
+
'--sw-speed': `${x.marqueeSpeed}s`,
|
|
66
|
+
'--sw-align': x.textAlign,
|
|
67
|
+
'--sw-justify': x.textAlign === 'left' ? 'flex-start' : x.textAlign === 'right' ? 'flex-end' : 'center',
|
|
68
|
+
} as Record<string, string>;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Стрічка: короткий список повторюємо (щоб заповнити широкий екран), потім подвоюємо для безшовного циклу
|
|
72
|
+
const marqueeBase = computed(() => {
|
|
73
|
+
const items = b.value?.marqueeItems || [];
|
|
74
|
+
if (!items.length) return [];
|
|
75
|
+
const times = Math.max(1, Math.ceil(8 / items.length));
|
|
76
|
+
return Array.from({ length: times }, () => items).flat();
|
|
77
|
+
});
|
|
78
|
+
const marqueeLoop = computed(() => [...marqueeBase.value, ...marqueeBase.value]);
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<section
|
|
83
|
+
v-if="view && b"
|
|
84
|
+
class="sitewide-banner"
|
|
85
|
+
:class="[`sw-${view}`, `sw-h-${b.height}`]"
|
|
86
|
+
:style="rootStyle"
|
|
87
|
+
aria-label="Оголошення магазину"
|
|
88
|
+
>
|
|
89
|
+
<!-- Біжуча стрічка -->
|
|
90
|
+
<div v-if="view === 'marquee'" class="sw-marquee">
|
|
91
|
+
<div class="sw-marquee-track">
|
|
92
|
+
<span v-for="(item, i) in marqueeLoop" :key="i" class="sw-marquee-item" :aria-hidden="i >= b.marqueeItems.length ? 'true' : undefined">{{ item }}</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Фото збоку + текст -->
|
|
97
|
+
<div v-else-if="view === 'split-left' || view === 'split-right'" class="sw-split" :class="{ 'sw-rev': view === 'split-right' }">
|
|
98
|
+
<div class="sw-split-img"><img :src="b.image" :alt="b.title || 'Банер'" loading="lazy" /></div>
|
|
99
|
+
<div class="sw-body container">
|
|
100
|
+
<div class="sw-text">
|
|
101
|
+
<div v-if="b.title" class="sw-title">{{ b.title }}</div>
|
|
102
|
+
<div v-if="b.subtitle" class="sw-subtitle">{{ b.subtitle }}</div>
|
|
103
|
+
<NuxtLink v-if="b.buttonLabel" :to="b.buttonLink" :target="isExternal ? '_blank' : undefined" :rel="isExternal ? 'noopener' : undefined" class="sw-btn" :class="`sw-btn-${b.buttonStyle}`">{{ b.buttonLabel }}</NuxtLink>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- Колір + текст / фото-фон + текст -->
|
|
109
|
+
<div v-else class="sw-box">
|
|
110
|
+
<template v-if="view === 'full'">
|
|
111
|
+
<img :src="b.image" :alt="b.title || 'Банер'" class="sw-bg-img" />
|
|
112
|
+
<div class="sw-overlay"></div>
|
|
113
|
+
</template>
|
|
114
|
+
<div class="sw-body container">
|
|
115
|
+
<div class="sw-text">
|
|
116
|
+
<div v-if="b.title" class="sw-title">{{ b.title }}</div>
|
|
117
|
+
<div v-if="b.subtitle" class="sw-subtitle">{{ b.subtitle }}</div>
|
|
118
|
+
<NuxtLink v-if="b.buttonLabel" :to="b.buttonLink" :target="isExternal ? '_blank' : undefined" :rel="isExternal ? 'noopener' : undefined" class="sw-btn" :class="`sw-btn-${b.buttonStyle}`">{{ b.buttonLabel }}</NuxtLink>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
</template>
|
|
124
|
+
|
|
125
|
+
<style scoped>
|
|
126
|
+
.sitewide-banner {
|
|
127
|
+
--sw-h: 160px;
|
|
128
|
+
position: relative;
|
|
129
|
+
width: 100%;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
background: var(--sw-bg);
|
|
132
|
+
color: #fff;
|
|
133
|
+
margin-bottom: 20px;
|
|
134
|
+
}
|
|
135
|
+
.sw-h-compact { --sw-h: 110px; }
|
|
136
|
+
.sw-h-standard { --sw-h: 160px; }
|
|
137
|
+
.sw-h-tall { --sw-h: 220px; }
|
|
138
|
+
|
|
139
|
+
.sw-box,
|
|
140
|
+
.sw-split {
|
|
141
|
+
position: relative;
|
|
142
|
+
min-height: var(--sw-h);
|
|
143
|
+
display: flex;
|
|
144
|
+
}
|
|
145
|
+
.sw-bg-img {
|
|
146
|
+
position: absolute;
|
|
147
|
+
inset: 0;
|
|
148
|
+
width: 100%;
|
|
149
|
+
height: 100%;
|
|
150
|
+
object-fit: cover;
|
|
151
|
+
}
|
|
152
|
+
.sw-overlay {
|
|
153
|
+
position: absolute;
|
|
154
|
+
inset: 0;
|
|
155
|
+
background: rgba(0, 0, 0, var(--sw-overlay));
|
|
156
|
+
}
|
|
157
|
+
.sw-body {
|
|
158
|
+
position: relative;
|
|
159
|
+
z-index: 1;
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: var(--sw-justify);
|
|
163
|
+
padding-top: 16px;
|
|
164
|
+
padding-bottom: 16px;
|
|
165
|
+
}
|
|
166
|
+
.sw-text {
|
|
167
|
+
max-width: 720px;
|
|
168
|
+
text-align: var(--sw-align);
|
|
169
|
+
}
|
|
170
|
+
.sw-title {
|
|
171
|
+
font-family: var(--fontFamilyBold);
|
|
172
|
+
font-size: 26px;
|
|
173
|
+
line-height: 1.2;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
color: #fff;
|
|
176
|
+
}
|
|
177
|
+
.sw-subtitle {
|
|
178
|
+
margin-top: 4px;
|
|
179
|
+
font-size: 16px;
|
|
180
|
+
opacity: 0.92;
|
|
181
|
+
}
|
|
182
|
+
.sw-btn {
|
|
183
|
+
display: inline-block;
|
|
184
|
+
margin-top: 12px;
|
|
185
|
+
padding: 8px 20px;
|
|
186
|
+
border-radius: 999px;
|
|
187
|
+
font-size: 14px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
text-decoration: none;
|
|
190
|
+
transition: var(--transition, all ease 0.3s);
|
|
191
|
+
}
|
|
192
|
+
.sw-btn-solid {
|
|
193
|
+
background: var(--sw-btn);
|
|
194
|
+
border: 2px solid var(--sw-btn);
|
|
195
|
+
color: #fff;
|
|
196
|
+
}
|
|
197
|
+
.sw-btn-outline {
|
|
198
|
+
background: transparent;
|
|
199
|
+
border: 2px solid var(--sw-btn);
|
|
200
|
+
color: var(--sw-btn);
|
|
201
|
+
}
|
|
202
|
+
.sw-btn:hover {
|
|
203
|
+
filter: brightness(0.92);
|
|
204
|
+
color: #fff;
|
|
205
|
+
}
|
|
206
|
+
.sw-btn-outline:hover {
|
|
207
|
+
background: var(--sw-btn);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Фото збоку */
|
|
211
|
+
.sw-split-img {
|
|
212
|
+
position: relative;
|
|
213
|
+
flex: 0 0 38%;
|
|
214
|
+
max-width: 38%;
|
|
215
|
+
}
|
|
216
|
+
/* Фото не розтягує банер: займає висоту блока (обрізається cover) */
|
|
217
|
+
.sw-split-img img {
|
|
218
|
+
position: absolute;
|
|
219
|
+
inset: 0;
|
|
220
|
+
width: 100%;
|
|
221
|
+
height: 100%;
|
|
222
|
+
object-fit: cover;
|
|
223
|
+
display: block;
|
|
224
|
+
}
|
|
225
|
+
.sw-split .sw-body {
|
|
226
|
+
flex: 1 1 auto;
|
|
227
|
+
max-width: none;
|
|
228
|
+
}
|
|
229
|
+
.sw-rev {
|
|
230
|
+
flex-direction: row-reverse;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Біжуча стрічка */
|
|
234
|
+
.sw-marquee {
|
|
235
|
+
overflow: hidden;
|
|
236
|
+
padding: 10px 0;
|
|
237
|
+
}
|
|
238
|
+
.sw-marquee-track {
|
|
239
|
+
display: inline-flex;
|
|
240
|
+
white-space: nowrap;
|
|
241
|
+
will-change: transform;
|
|
242
|
+
animation: sw-marquee var(--sw-speed) linear infinite;
|
|
243
|
+
}
|
|
244
|
+
.sw-marquee:hover .sw-marquee-track {
|
|
245
|
+
animation-play-state: paused;
|
|
246
|
+
}
|
|
247
|
+
.sw-marquee-item {
|
|
248
|
+
position: relative;
|
|
249
|
+
padding: 0 2.5rem;
|
|
250
|
+
font-weight: 600;
|
|
251
|
+
font-size: 15px;
|
|
252
|
+
}
|
|
253
|
+
.sw-marquee-item::after {
|
|
254
|
+
content: '•';
|
|
255
|
+
position: absolute;
|
|
256
|
+
right: 0;
|
|
257
|
+
opacity: 0.5;
|
|
258
|
+
}
|
|
259
|
+
@keyframes sw-marquee {
|
|
260
|
+
from { transform: translateX(0); }
|
|
261
|
+
to { transform: translateX(-50%); }
|
|
262
|
+
}
|
|
263
|
+
@media (prefers-reduced-motion: reduce) {
|
|
264
|
+
.sw-marquee-track { animation: none; }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Мобільний: менша висота й шрифт, фото збоку — зверху */
|
|
268
|
+
@media (max-width: 767.98px) {
|
|
269
|
+
.sw-h-compact { --sw-h: 90px; }
|
|
270
|
+
.sw-h-standard { --sw-h: 120px; }
|
|
271
|
+
.sw-h-tall { --sw-h: 160px; }
|
|
272
|
+
.sw-title { font-size: 19px; }
|
|
273
|
+
.sw-subtitle { font-size: 14px; }
|
|
274
|
+
.sw-split,
|
|
275
|
+
.sw-rev {
|
|
276
|
+
flex-direction: column;
|
|
277
|
+
}
|
|
278
|
+
.sw-split-img {
|
|
279
|
+
flex-basis: auto;
|
|
280
|
+
max-width: 100%;
|
|
281
|
+
height: 140px;
|
|
282
|
+
}
|
|
283
|
+
.sw-marquee-item { font-size: 14px; padding: 0 1.6rem; }
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
@@ -1,73 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useLayoutData } from './layoutData';
|
|
3
|
+
const { t } = useI18n();
|
|
4
|
+
const { phone, email, workingHours } = useLayoutData();
|
|
5
|
+
// Наскрізний банер тепер рендериться повністю (LayoutSitewideBanner) — тут його текст не дублюємо
|
|
6
|
+
</script>
|
|
7
|
+
|
|
1
8
|
<template>
|
|
2
|
-
|
|
9
|
+
<!-- Мобільний: лише телефон в один рядок -->
|
|
10
|
+
<div :class="['top-header-area border-bottom ptb-12', { 'd-none d-md-block': !phone }]">
|
|
3
11
|
<div class="container">
|
|
4
12
|
<v-row>
|
|
5
|
-
<v-col cols="12" lg="
|
|
13
|
+
<v-col cols="12" lg="8" md="8">
|
|
6
14
|
<ul
|
|
7
|
-
class="ps-0 mb-0 list-unstyled info-link d-flex align-items-center justify-content-center justify-content-md-start position-relative tops-1"
|
|
15
|
+
class="ps-0 mb-0 list-unstyled info-link d-flex flex-wrap align-items-center justify-content-center justify-content-md-start position-relative tops-1"
|
|
8
16
|
>
|
|
9
|
-
<li>
|
|
10
|
-
<
|
|
17
|
+
<li v-if="phone">
|
|
18
|
+
<a :href="`tel:${phone}`"><i class="ri-phone-line me-1"></i>{{ phone }}</a>
|
|
11
19
|
</li>
|
|
12
|
-
<li>
|
|
13
|
-
<
|
|
20
|
+
<li v-if="email" class="d-none d-md-block">
|
|
21
|
+
<a :href="`mailto:${email}`"><i class="ri-mail-line me-1"></i>{{ email }}</a>
|
|
14
22
|
</li>
|
|
15
|
-
<li>
|
|
16
|
-
<
|
|
17
|
-
</li>
|
|
18
|
-
</ul>
|
|
19
|
-
</v-col>
|
|
20
|
-
<v-col cols="12" lg="6" md="7">
|
|
21
|
-
<ul
|
|
22
|
-
class="ps-0 mb-0 list-unstyled info-link info-right d-flex align-items-center justify-content-center justify-content-md-end mt-2 mt-md-0"
|
|
23
|
-
>
|
|
24
|
-
<li>
|
|
25
|
-
<NuxtLink
|
|
26
|
-
to="/products"
|
|
27
|
-
class="text-danger d-flex align-items-center position-relative tops-1"
|
|
28
|
-
>
|
|
29
|
-
<i class="flaticon-flash-sale"></i>
|
|
30
|
-
<span>{{ t('nav.flashSale') }}</span>
|
|
31
|
-
</NuxtLink>
|
|
32
|
-
</li>
|
|
33
|
-
<li>
|
|
34
|
-
<div class="dropdown flag-wrap">
|
|
35
|
-
<button
|
|
36
|
-
class="dropdown-toggle fw-medium fs-14 p-0 border-0 bg-transparent flag"
|
|
37
|
-
@click="toggleDropdown"
|
|
38
|
-
:aria-pressed="show ? 'true' : 'false'"
|
|
39
|
-
:class="{ show: buttonShowState }"
|
|
40
|
-
>
|
|
41
|
-
<span class="me-1">{{ currentLocaleOption?.flag }}</span>
|
|
42
|
-
<span>{{ currentLocaleOption?.name }}</span>
|
|
43
|
-
</button>
|
|
44
|
-
<ul class="dropdown-menu" :class="{ toggler: show }">
|
|
45
|
-
<li>
|
|
46
|
-
<div v-if="locales.length > 0">
|
|
47
|
-
<a
|
|
48
|
-
class="dropdown-item d-flex align-items-center"
|
|
49
|
-
href="#"
|
|
50
|
-
v-for="loc in locales"
|
|
51
|
-
:key="loc.code"
|
|
52
|
-
@click.prevent="selectLocale(loc.code)"
|
|
53
|
-
>
|
|
54
|
-
<span>{{ loc.flag }}</span>
|
|
55
|
-
<span class="ms-2">{{ loc.name }}</span>
|
|
56
|
-
</a>
|
|
57
|
-
</div>
|
|
58
|
-
</li>
|
|
59
|
-
</ul>
|
|
60
|
-
</div>
|
|
61
|
-
</li>
|
|
62
|
-
<li>
|
|
63
|
-
<select
|
|
64
|
-
class="form-select p-0 border-0 currency fw-medium fs-14 text-secondary"
|
|
65
|
-
:aria-label="t('nav.currencySelect')"
|
|
66
|
-
>
|
|
67
|
-
<option selected>{{ t('nav.currencyUSD') }}</option>
|
|
68
|
-
<option value="1">{{ t('nav.currencyEUR') }}</option>
|
|
69
|
-
<option value="2">{{ t('nav.currencyYEN') }}</option>
|
|
70
|
-
</select>
|
|
23
|
+
<li v-if="workingHours" class="d-none d-md-block">
|
|
24
|
+
<span><i class="ri-time-line me-1"></i>{{ workingHours }}</span>
|
|
71
25
|
</li>
|
|
72
26
|
</ul>
|
|
73
27
|
</v-col>
|
|
@@ -75,23 +29,3 @@
|
|
|
75
29
|
</div>
|
|
76
30
|
</div>
|
|
77
31
|
</template>
|
|
78
|
-
|
|
79
|
-
<script setup lang="ts">
|
|
80
|
-
import { ref } from "vue";
|
|
81
|
-
|
|
82
|
-
const { t, locale, locales, setLocale, currentLocaleOption } = useI18n();
|
|
83
|
-
|
|
84
|
-
const show = ref(false);
|
|
85
|
-
const buttonShowState = ref(false);
|
|
86
|
-
|
|
87
|
-
const toggleDropdown = () => {
|
|
88
|
-
show.value = !show.value;
|
|
89
|
-
buttonShowState.value = !buttonShowState.value;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const selectLocale = (code: string) => {
|
|
93
|
-
setLocale(code);
|
|
94
|
-
show.value = false;
|
|
95
|
-
buttonShowState.value = false;
|
|
96
|
-
};
|
|
97
|
-
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { computed, ref, onMounted } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Спільні дані для шапки, меню та футера: беруться з конфігу магазину (CRM),
|
|
5
|
+
* а не з демо-контенту теми.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Пункти меню лише на існуючі сторінки
|
|
9
|
+
export const NAV_ITEMS = [
|
|
10
|
+
{ to: '/', key: 'layout.menu.home' },
|
|
11
|
+
{ to: '/products', key: 'layout.menu.catalog' },
|
|
12
|
+
{ to: '/categories', key: 'layout.menu.categories' },
|
|
13
|
+
{ to: '/about-us', key: 'pages.about.title' },
|
|
14
|
+
{ to: '/delivery', key: 'pages.delivery.title' },
|
|
15
|
+
{ to: '/contact', key: 'layout.menu.contact' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const SOCIAL_META: Record<string, { icon: string; label: string }> = {
|
|
19
|
+
facebook: { icon: 'ri-facebook-fill', label: 'Facebook' },
|
|
20
|
+
instagram: { icon: 'ri-instagram-line', label: 'Instagram' },
|
|
21
|
+
telegram: { icon: 'ri-telegram-fill', label: 'Telegram' },
|
|
22
|
+
youtube: { icon: 'ri-youtube-fill', label: 'YouTube' },
|
|
23
|
+
twitter: { icon: 'ri-twitter-x-line', label: 'X' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const useLayoutData = () => {
|
|
27
|
+
const { siteConfig } = useSiteConfig();
|
|
28
|
+
const cfg = computed<any>(() => siteConfig.value || {});
|
|
29
|
+
|
|
30
|
+
// Відносні media-шляхи → повна адреса медіа-домену (як mediaBase у template-grocery)
|
|
31
|
+
const mediaBase = String(
|
|
32
|
+
(useRuntimeConfig().public as any).mediaBase || 'https://media.crm-market.com.ua',
|
|
33
|
+
).replace(/\/$/, '');
|
|
34
|
+
const resolveMedia = (u?: string | null) => {
|
|
35
|
+
if (!u) return '';
|
|
36
|
+
if (/^(https?:)?\/\//.test(u) || u.startsWith('data:')) return u;
|
|
37
|
+
return mediaBase + (u.startsWith('/') ? u : '/' + u);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const siteName = computed<string>(() => cfg.value.siteName || '');
|
|
41
|
+
const siteDescription = computed<string>(() => cfg.value.siteDescription || '');
|
|
42
|
+
const logo = computed(() => resolveMedia(cfg.value.logoUrl));
|
|
43
|
+
const contact = computed<any>(() => cfg.value.contactInfo || {});
|
|
44
|
+
const phone = computed<string>(() => contact.value.phone || '');
|
|
45
|
+
const phones = computed<string[]>(() =>
|
|
46
|
+
[contact.value.phone, ...(contact.value.additionalPhones || [])].filter(Boolean),
|
|
47
|
+
);
|
|
48
|
+
const email = computed<string>(() => contact.value.email || '');
|
|
49
|
+
const address = computed<string>(() => contact.value.address || '');
|
|
50
|
+
const workingHours = computed<string>(() => contact.value.workingHours || '');
|
|
51
|
+
|
|
52
|
+
// Лише заповнені соцмережі
|
|
53
|
+
const socials = computed(() =>
|
|
54
|
+
Object.entries(contact.value.socialMedia || {})
|
|
55
|
+
.filter(([k, v]) => !!v && SOCIAL_META[k])
|
|
56
|
+
.map(([k, v]) => ({ key: k, url: String(v), ...SOCIAL_META[k] })),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Текст наскрізного банера (якщо увімкнений)
|
|
60
|
+
const bannerText = computed<string>(() => {
|
|
61
|
+
const b = cfg.value.sitewideBanner;
|
|
62
|
+
if (!b || b.enabled !== true) return '';
|
|
63
|
+
return b.title || (Array.isArray(b.marqueeItems) ? b.marqueeItems.filter(Boolean).join(' • ') : '');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Локальний wishlist (localStorage) — лише лічильник
|
|
67
|
+
const wishlistCount = ref(0);
|
|
68
|
+
onMounted(() => {
|
|
69
|
+
try {
|
|
70
|
+
const raw = localStorage.getItem('wishlist');
|
|
71
|
+
const arr = raw ? JSON.parse(raw) : [];
|
|
72
|
+
wishlistCount.value = Array.isArray(arr) ? arr.length : 0;
|
|
73
|
+
} catch {
|
|
74
|
+
wishlistCount.value = 0;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
siteName, siteDescription, logo, phone, phones, email, address,
|
|
80
|
+
workingHours, socials, bannerText, wishlistCount, resolveMedia,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
@@ -1,112 +1,8 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<span class="update">Last updated: February 2024</span>
|
|
6
|
-
<h3>Privacy policy</h3>
|
|
7
|
-
<p>
|
|
8
|
-
Turpis gravida turpis mauris magna in libero ac dis consequat.
|
|
9
|
-
Imperdiet ac semper quisque volutpat consectetur cursus non pretium
|
|
10
|
-
pharetra. Nisl duis blandit dui imperdiet nec id integer. Elit varius
|
|
11
|
-
cras arcu tempor integer in.
|
|
12
|
-
</p>
|
|
13
|
-
<p>
|
|
14
|
-
Quam nunc arcu nunc a lobortis convallis magnis dolor. Integer
|
|
15
|
-
malesuada vel mi eu elementum. Etiam eget urna quis dui amet facilisis
|
|
16
|
-
fringilla sed. Massa vitae accumsan viverra in morbi nec volutpat
|
|
17
|
-
aenean tortor. At nec sagittis eget placerat. Mi accumsan massa vitae
|
|
18
|
-
etiam nisi suspendisse condimentum elementum. Arcu varius id ipsum
|
|
19
|
-
arcu et diam non rutrum. Auctor elit malesuada non aenean posuere
|
|
20
|
-
mattis iaculis mauris nibh.
|
|
21
|
-
</p>
|
|
22
|
-
|
|
23
|
-
<div class="gap-20"></div>
|
|
24
|
-
|
|
25
|
-
<h3>What type of personal information do we collect?</h3>
|
|
26
|
-
<p>
|
|
27
|
-
Turpis gravida turpis mauris magna in libero ac dis consequat.
|
|
28
|
-
Imperdiet ac semper quisque volutpat consectetur cursus non pretium
|
|
29
|
-
pharetra. Nisl duis blandit dui imperdiet nec id integer. Elit varius
|
|
30
|
-
cras arcu tempor integer in. Quam nunc arcu nunc a lobortis convallis
|
|
31
|
-
magnis dolor. Integer malesuada vel mi eu elementum. Etiam eget urna
|
|
32
|
-
quis dui amet facilisis fringilla sed. Massa vitae accumsan viverra in
|
|
33
|
-
morbi nec volutpat aenean tortor. At nec sagittis eget placerat. Mi
|
|
34
|
-
accumsan massa vitae etiam nisi suspendisse condimentum elementum.
|
|
35
|
-
Arcu varius id ipsum arcu et diam non rutrum. Auctor elit malesuada
|
|
36
|
-
non aenean posuere mattis iaculis mauris nibh.
|
|
37
|
-
</p>
|
|
38
|
-
|
|
39
|
-
<div class="gap-20"></div>
|
|
40
|
-
|
|
41
|
-
<h3>How do we collect your personal data?</h3>
|
|
42
|
-
<p>
|
|
43
|
-
Turpis gravida turpis mauris magna in libero ac dis consequat.
|
|
44
|
-
Imperdiet ac semper quisque volutpat consectetur cursus non pretium
|
|
45
|
-
pharetra. Nisl duis blandit dui imperdiet nec id integer. Elit varius
|
|
46
|
-
cras arcu tempor integer in.
|
|
47
|
-
</p>
|
|
48
|
-
<p>
|
|
49
|
-
Quam nunc arcu nunc a lobortis convallis magnis dolor. Integer
|
|
50
|
-
malesuada vel mi eu elementum. Etiam eget urna quis dui amet facilisis
|
|
51
|
-
fringilla sed. Massa vitae accumsan viverra in morbi nec volutpat
|
|
52
|
-
aenean tortor. At nec sagittis eget placerat. Mi accumsan massa vitae
|
|
53
|
-
etiam nisi suspendisse condimentum elementum. Arcu varius id ipsum
|
|
54
|
-
arcu et diam non rutrum. Auctor elit malesuada non aenean posuere
|
|
55
|
-
mattis iaculis mauris nibh.
|
|
56
|
-
</p>
|
|
57
|
-
<ol>
|
|
58
|
-
<li>
|
|
59
|
-
Imperdiet ac semper quisque volutpat consectetur cursus non pretium
|
|
60
|
-
pharetra.
|
|
61
|
-
</li>
|
|
62
|
-
<li>Nisl duis blandit dui imperdiet nec id integer.</li>
|
|
63
|
-
<li>Quam nunc arcu nunc a lobortis convallis magnis dolor.</li>
|
|
64
|
-
<li>
|
|
65
|
-
Massa vitae accumsan viverra in morbi nec volutpat aenean tortor.
|
|
66
|
-
</li>
|
|
67
|
-
</ol>
|
|
68
|
-
|
|
69
|
-
<div class="gap-20"></div>
|
|
70
|
-
|
|
71
|
-
<h3>What are our legal bases for processing your data?</h3>
|
|
72
|
-
<p>
|
|
73
|
-
Turpis gravida turpis mauris magna in libero ac dis consequat.
|
|
74
|
-
Imperdiet ac semper quisque volutpat consectetur cursus non pretium
|
|
75
|
-
pharetra. Nisl duis blandit dui imperdiet nec id integer. Elit varius
|
|
76
|
-
cras arcu tempor integer in. Quam nunc arcu nunc a lobortis convallis
|
|
77
|
-
magnis dolor. Integer malesuada vel mi eu elementum. Etiam eget urna
|
|
78
|
-
quis dui amet facilisis fringilla sed. Massa vitae accumsan viverra in
|
|
79
|
-
morbi nec volutpat aenean tortor. At nec sagittis eget placerat. Mi
|
|
80
|
-
accumsan massa vitae etiam nisi suspendisse condimentum elementum.
|
|
81
|
-
Arcu varius id ipsum arcu et diam non rutrum. Auctor elit malesuada
|
|
82
|
-
non aenean posuere mattis iaculis mauris nibh.
|
|
83
|
-
</p>
|
|
84
|
-
|
|
85
|
-
<div class="gap-20"></div>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Політика конфіденційності: HTML продавця або дефолтний текст
|
|
3
|
+
const { pageHtml } = useStaticPages();
|
|
4
|
+
</script>
|
|
86
5
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Turpis gravida turpis mauris magna in libero ac dis consequat.
|
|
90
|
-
Imperdiet ac semper quisque volutpat consectetur cursus non pretium
|
|
91
|
-
pharetra. Nisl duis blandit dui imperdiet nec id integer. Elit varius
|
|
92
|
-
cras arcu tempor integer in.
|
|
93
|
-
</p>
|
|
94
|
-
<p>
|
|
95
|
-
Quam nunc arcu nunc a lobortis convallis magnis dolor. Integer
|
|
96
|
-
malesuada vel mi eu elementum. Etiam eget urna quis dui amet facilisis
|
|
97
|
-
fringilla sed. Massa vitae accumsan viverra in morbi nec volutpat
|
|
98
|
-
aenean tortor. At nec sagittis eget placerat. Mi accumsan massa vitae
|
|
99
|
-
etiam nisi suspendisse condimentum elementum. Arcu varius id ipsum
|
|
100
|
-
arcu et diam non rutrum. Auctor elit malesuada non aenean posuere
|
|
101
|
-
mattis iaculis mauris nibh.
|
|
102
|
-
</p>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
6
|
+
<template>
|
|
7
|
+
<AboutUsStaticContent :html="pageHtml('privacy')" />
|
|
106
8
|
</template>
|
|
107
|
-
|
|
108
|
-
<script>
|
|
109
|
-
export default {
|
|
110
|
-
name: "PrivacyPolicy",
|
|
111
|
-
};
|
|
112
|
-
</script>
|