@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.
Files changed (138) hide show
  1. package/assets/scss/_variables.scss +3 -3
  2. package/assets/scss/components/_header.scss +1 -1
  3. package/assets/scss/style.css +4 -4
  4. package/components/AboutUs/StaticContent.vue +32 -0
  5. package/components/Cart/CartEmpty.vue +42 -0
  6. package/components/Cart/CartItem.vue +145 -0
  7. package/components/Cart/CartSummary.vue +57 -0
  8. package/components/Cart/ProductQuantity.vue +23 -13
  9. package/components/Cart/index.vue +68 -150
  10. package/components/Categories/index.vue +88 -291
  11. package/components/Checkout/AddressFields.vue +74 -0
  12. package/components/Checkout/CheckoutForm.vue +75 -0
  13. package/components/Checkout/CityAutocomplete.vue +68 -0
  14. package/components/Checkout/DeliveryMethodCard.vue +66 -0
  15. package/components/Checkout/DeliveryOptions.vue +148 -0
  16. package/components/Checkout/OrderSummary.vue +116 -0
  17. package/components/Checkout/PaymentOptions.vue +76 -0
  18. package/components/Checkout/PickupPoints.vue +46 -0
  19. package/components/Checkout/PromoCode.vue +166 -0
  20. package/components/Checkout/index.vue +235 -173
  21. package/components/Checkout/types.ts +45 -0
  22. package/components/Common/PageBanner.vue +22 -20
  23. package/components/Common/ProductCard.vue +139 -115
  24. package/components/Common/Services.vue +4 -4
  25. package/components/Contact/ContactForm.vue +44 -74
  26. package/components/Contact/ContactInfo.vue +35 -61
  27. package/components/FAQ/index.vue +6 -119
  28. package/components/Layout/Copyright.vue +11 -9
  29. package/components/Layout/Footer.vue +49 -136
  30. package/components/Layout/HeaderBar.vue +174 -0
  31. package/components/Layout/HeaderLogo.vue +27 -0
  32. package/components/Layout/MiddleHeader.vue +1 -228
  33. package/components/Layout/MiddleHeaderThree.vue +1 -203
  34. package/components/Layout/MiddleHeaderTwo.vue +1 -239
  35. package/components/Layout/NavLinks.vue +23 -0
  36. package/components/Layout/Navbar.vue +38 -151
  37. package/components/Layout/NavbarStyleFour.vue +1 -332
  38. package/components/Layout/NavbarStyleThree.vue +1 -188
  39. package/components/Layout/NavbarStyleTwo.vue +1 -108
  40. package/components/Layout/Preloader.vue +5 -12
  41. package/components/Layout/ResponsiveNavbar.vue +32 -418
  42. package/components/Layout/SitewideBanner.vue +285 -0
  43. package/components/Layout/TopHeader.vue +17 -83
  44. package/components/Layout/layoutData.ts +83 -0
  45. package/components/PrivacyPolicy/index.vue +6 -110
  46. package/components/ProductDetails/ProductDetailsTab.vue +31 -321
  47. package/components/ProductDetails/ProductGallery.vue +45 -0
  48. package/components/ProductDetails/ProductQuantity.vue +8 -14
  49. package/components/ProductDetails/RecentlyViewed.vue +53 -289
  50. package/components/ProductDetails/RelatedProducts.vue +64 -0
  51. package/components/ProductDetails/index.vue +90 -230
  52. package/components/Products/index.vue +78 -16
  53. package/components/TermsConditions/index.vue +6 -110
  54. package/components/Wishlist/index.vue +62 -88
  55. package/composables/useCart.ts +133 -81
  56. package/composables/useCategories.ts +66 -29
  57. package/composables/useCategoryImages.ts +45 -0
  58. package/composables/useCheckout.ts +302 -95
  59. package/composables/useContactForm.ts +57 -0
  60. package/composables/useHomeContent.ts +220 -0
  61. package/composables/useI18n.ts +12 -24
  62. package/composables/useJsonLd.ts +162 -0
  63. package/composables/useProducts.ts +244 -81
  64. package/composables/useRecent.ts +30 -0
  65. package/composables/useSanitize.ts +76 -0
  66. package/composables/useSiteConfig.ts +51 -45
  67. package/composables/useStaticPages.ts +48 -0
  68. package/composables/useStoreOrigin.ts +32 -0
  69. package/composables/useTemplateSections.ts +51 -3
  70. package/composables/useThemeVars.ts +113 -0
  71. package/composables/useWishlist.ts +72 -0
  72. package/layouts/default.vue +80 -19
  73. package/locales/en.json +255 -86
  74. package/locales/uk.json +251 -82
  75. package/modules/template-schema.ts +35 -0
  76. package/nuxt.config.ts +8 -11
  77. package/package.json +1 -1
  78. package/pages/about-us.vue +9 -9
  79. package/pages/cart.vue +0 -1
  80. package/pages/categories.vue +0 -1
  81. package/pages/category/[slug].vue +84 -0
  82. package/pages/checkout.vue +0 -1
  83. package/pages/contact.vue +6 -6
  84. package/pages/delivery.vue +12 -0
  85. package/pages/faq.vue +6 -6
  86. package/pages/order-success.vue +113 -0
  87. package/pages/privacy-policy.vue +6 -6
  88. package/pages/product/[slug].vue +82 -0
  89. package/pages/product-details.vue +14 -6
  90. package/pages/products.vue +12 -1
  91. package/pages/returns.vue +12 -0
  92. package/pages/terms-conditions.vue +6 -6
  93. package/pages/wishlist.vue +2 -2
  94. package/utils/seo-urls.ts +70 -0
  95. package/utils/store-origin.ts +36 -0
  96. package/components/AboutUs/AboutUsTuan.vue +0 -25
  97. package/components/AboutUs/Statistics.vue +0 -42
  98. package/components/AboutUs/SubscribeToTheNewsletter.vue +0 -57
  99. package/components/AddAddress/index.vue +0 -68
  100. package/components/BestSellers/Products.vue +0 -1562
  101. package/components/BestSellers/RecentlyViewed.vue +0 -304
  102. package/components/ChangePassword/index.vue +0 -69
  103. package/components/Common/DashboardNavigation.vue +0 -35
  104. package/components/Dashboard/RecentOrder.vue +0 -173
  105. package/components/Dashboard/index.vue +0 -77
  106. package/components/EditAddress/index.vue +0 -117
  107. package/components/EditProfile/index.vue +0 -95
  108. package/components/FeaturedProduct/FeaturedProducts.vue +0 -304
  109. package/components/FeaturedProduct/Products.vue +0 -1562
  110. package/components/ForgotPassword/index.vue +0 -49
  111. package/components/Layout/FooterTwo.vue +0 -159
  112. package/components/Layout/LocationOption.vue +0 -178
  113. package/components/Layout/RTLSwitchBtn.vue +0 -40
  114. package/components/Login/index.vue +0 -89
  115. package/components/MyAccount/index.vue +0 -154
  116. package/components/NewArrivals/Products.vue +0 -1969
  117. package/components/NewArrivals/RecentlyViewed.vue +0 -304
  118. package/components/OrderDetails/index.vue +0 -75
  119. package/components/OrderHistory/index.vue +0 -195
  120. package/components/Products/RecentlyViewed.vue +0 -302
  121. package/components/Register/index.vue +0 -83
  122. package/components/TrendingProducts/FeaturedProducts.vue +0 -304
  123. package/components/TrendingProducts/Products.vue +0 -1564
  124. package/components/Wishlist/ProductQuantity.vue +0 -29
  125. package/layouts/inner.vue +0 -70
  126. package/locales/ar.json +0 -234
  127. package/locales/ru.json +0 -234
  128. package/pages/address.vue +0 -11
  129. package/pages/change-password.vue +0 -11
  130. package/pages/dashboard.vue +0 -11
  131. package/pages/edit-address.vue +0 -11
  132. package/pages/edit-profile.vue +0 -11
  133. package/pages/forgot-password.vue +0 -11
  134. package/pages/login.vue +0 -11
  135. package/pages/my-account.vue +0 -11
  136. package/pages/order-details.vue +0 -11
  137. package/pages/order-history.vue +0 -11
  138. 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
- <div class="top-header-area border-bottom ptb-12">
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="6" md="5">
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
- <NuxtLink to="/about-us">{{ t('nav.aboutUs') }}</NuxtLink>
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
- <NuxtLink to="/faq">{{ t('nav.faqs') }}</NuxtLink>
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
- <NuxtLink to="/contact">{{ t('nav.helpContact') }}</NuxtLink>
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
- <template>
2
- <div class="ptb-60">
3
- <div class="container">
4
- <div class="main-content-text">
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
- <h3>Security</h3>
88
- <p>
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>