@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
@@ -1,138 +1,162 @@
1
- <template>
2
- <div class="product-card">
3
- <div class="product-img position-relative">
4
- <NuxtLink :to="`/product-details?id=${product.id}`">
5
- <img
6
- :src="productImage"
7
- :alt="product.name"
8
- class="w-100 object-fit-cover"
9
- style="height: 200px;"
10
- />
11
- </NuxtLink>
12
- <div class="product-badges" v-if="hasDiscount">
13
- <span class="badge bg-danger">
14
- -{{ discountPercent }}%
15
- </span>
16
- </div>
17
- <div class="product-actions">
18
- <button
19
- class="btn btn-sm"
20
- :class="inCart ? 'btn-success' : 'btn-outline-primary'"
21
- @click="handleAddToCart"
22
- :title="inCart ? t('product.inCart') : t('product.addToCart')"
23
- >
24
- <i :class="inCart ? 'ri-check-line' : 'ri-shopping-cart-line'"></i>
25
- </button>
26
- </div>
27
- </div>
28
- <div class="product-info p-2">
29
- <NuxtLink :to="`/product-details?id=${product.id}`" class="text-decoration-none">
30
- <h6 class="product-name mb-1 text-truncate">{{ product.name }}</h6>
31
- </NuxtLink>
32
- <div class="product-rating mb-1" v-if="product.rating">
33
- <i
34
- v-for="star in 5"
35
- :key="star"
36
- :class="star <= Math.round(product.rating) ? 'ri-star-fill text-warning' : 'ri-star-line text-muted'"
37
- class="fs-12"
38
- ></i>
39
- <span class="text-muted fs-12 ms-1">({{ product.reviews || 0 }})</span>
40
- </div>
41
- <div class="product-price">
42
- <span v-if="hasDiscount" class="text-decoration-line-through text-muted me-1 fs-13">
43
- ${{ product.price.toFixed(2) }}
44
- </span>
45
- <span class="fw-bold" :class="hasDiscount ? 'text-danger' : ''">
46
- ${{ effectivePrice.toFixed(2) }}
47
- </span>
48
- </div>
49
- </div>
50
- </div>
51
- </template>
52
-
53
1
  <script setup lang="ts">
54
- import { computed } from "vue";
55
- import { resolveImageUrl } from "~/utils/image";
56
-
57
- const { t } = useI18n();
2
+ import { computed, onMounted, ref } from 'vue';
3
+ import { useState } from '#imports';
4
+ import {
5
+ getProductImage,
6
+ getProductPrice,
7
+ getProductOldPrice,
8
+ getProductDiscountLabel,
9
+ formatPrice,
10
+ type Product,
11
+ } from '../../composables/useProducts';
58
12
 
59
- const props = defineProps<{
60
- product: Record<string, any>;
61
- }>();
13
+ const props = defineProps<{ product: Product }>();
62
14
 
15
+ const { t } = useI18n();
63
16
  const { addToCart, isInCart } = useCart();
64
17
 
65
- const productImage = computed(() => {
66
- const images = props.product.images;
67
- if (images && images.length > 0) {
68
- return resolveImageUrl(images[0]);
69
- }
70
- return '';
71
- });
18
+ const WISHLIST_KEY = 'wishlist';
72
19
 
73
- const hasDiscount = computed(() =>
74
- props.product.discountPrice && props.product.discountPrice < props.product.price
75
- );
20
+ // Спільний між картками стан вішліста (id товарів), синхронізований з localStorage
21
+ const wishIds = useState<string[]>('wishlist-ids', () => []);
76
22
 
77
- const effectivePrice = computed(() =>
78
- hasDiscount.value ? props.product.discountPrice : props.product.price
79
- );
23
+ const readWishlist = (): any[] => {
24
+ try {
25
+ const arr = JSON.parse(localStorage.getItem(WISHLIST_KEY) || '[]');
26
+ return Array.isArray(arr) ? arr : [];
27
+ } catch {
28
+ return [];
29
+ }
30
+ };
80
31
 
81
- const discountPercent = computed(() => {
82
- if (!hasDiscount.value) return 0;
83
- return Math.round(((props.product.price - props.product.discountPrice) / props.product.price) * 100);
32
+ onMounted(() => {
33
+ wishIds.value = readWishlist().map((i) => (typeof i === 'string' ? i : i?.id));
84
34
  });
85
35
 
86
- const inCart = computed(() => isInCart(props.product.id));
36
+ const title = computed(() => props.product.title || props.product.name || '');
37
+ const image = computed(() => getProductImage(props.product));
38
+ // Фото не завантажилось — показуємо заглушку того ж розміру
39
+ const imageFailed = ref(false);
40
+ const price = computed(() => getProductPrice(props.product));
41
+ const oldPrice = computed(() => getProductOldPrice(props.product));
42
+ // «-20%» або «-50 ₴» (фіксована знижка) — залежно від типу акції
43
+ const discountLabel = computed(() => getProductDiscountLabel(props.product));
44
+ const link = computed(() => productPath(props.product));
45
+
46
+ // Немає в наявності: stock явно 0 або менше
47
+ const outOfStock = computed(() => typeof props.product.stock === 'number' && props.product.stock <= 0);
48
+ // Кошик живе в localStorage: до монтування показуємо «У кошик», щоб SSR і клієнт збігалися (без hydration mismatch)
49
+ const mounted = ref(false);
50
+ onMounted(() => { mounted.value = true; });
51
+ const inCart = computed(() => mounted.value && isInCart(props.product.id));
52
+ const inWishlist = computed(() => wishIds.value.includes(props.product.id));
87
53
 
88
54
  const handleAddToCart = () => {
89
- if (!inCart.value) {
90
- addToCart({
91
- productId: props.product.id,
92
- name: props.product.name,
93
- price: props.product.price,
94
- discountPrice: props.product.discountPrice,
95
- image: props.product.images?.[0] || '',
96
- });
97
- }
55
+ if (outOfStock.value) return;
56
+ // Кошик: price — ціна без знижки, discountPrice — акційна (salePrice)
57
+ addToCart({
58
+ productId: props.product.id,
59
+ name: title.value,
60
+ price: oldPrice.value || price.value,
61
+ discountPrice: oldPrice.value ? price.value : undefined,
62
+ image: image.value,
63
+ });
64
+ };
65
+
66
+ const toggleWishlist = () => {
67
+ const list = readWishlist().filter((i) => (typeof i === 'string' ? i : i?.id));
68
+ const exists = list.some((i) => (typeof i === 'string' ? i : i.id) === props.product.id);
69
+ const next = exists
70
+ ? list.filter((i) => (typeof i === 'string' ? i : i.id) !== props.product.id)
71
+ : [...list, { id: props.product.id, title: title.value, image: image.value, price: price.value }];
72
+ try {
73
+ localStorage.setItem(WISHLIST_KEY, JSON.stringify(next));
74
+ } catch { /* сховище недоступне — лишаємо лише стан у пам'яті */ }
75
+ wishIds.value = next.map((i) => (typeof i === 'string' ? i : i.id));
98
76
  };
99
77
  </script>
100
78
 
79
+ <template>
80
+ <div class="single-product-telemobile-item">
81
+ <div class="product-img position-relative">
82
+ <!-- Фіксоване співвідношення 1:1: картки з фото, без фото і з битим фото однакової висоти -->
83
+ <NuxtLink :to="link" class="d-block pc-media">
84
+ <img v-if="image && !imageFailed" :src="image" :alt="title" loading="lazy" @error="imageFailed = true" />
85
+ <div v-else class="pc-placeholder d-flex align-items-center justify-content-center bg-light">
86
+ <i class="ri-image-line fs-1 text-muted"></i>
87
+ </div>
88
+ </NuxtLink>
89
+ <div class="d-flex justify-content-between align-items-center position-absolute top-0 start-0 end-0">
90
+ <span v-if="discountLabel" class="minus-count">{{ discountLabel }}</span>
91
+ <span v-else></span>
92
+ <button
93
+ type="button"
94
+ class="border-0 wish-btn"
95
+ :aria-label="t('home.favorite')"
96
+ @click="toggleWishlist"
97
+ >
98
+ <i :class="inWishlist ? 'ri-heart-fill text-danger' : 'ri-heart-line'"></i>
99
+ </button>
100
+ </div>
101
+ </div>
102
+ <div class="product-content">
103
+ <NuxtLink :to="link" class="title pc-title">{{ title }}</NuxtLink>
104
+ <div class="d-flex align-items-center justify-content-between">
105
+ <h3>
106
+ {{ formatPrice(price) }}
107
+ <del v-if="oldPrice">{{ formatPrice(oldPrice) }}</del>
108
+ </h3>
109
+ <div v-if="outOfStock" class="d-flex sold-stock align-items-center">
110
+ <span class="stock text-danger">{{ t('home.outOfStock') }}</span>
111
+ </div>
112
+ </div>
113
+ <button
114
+ type="button"
115
+ class="btn btn-success text-white rounded-pill"
116
+ :disabled="outOfStock"
117
+ @click="handleAddToCart"
118
+ >
119
+ <i :class="inCart ? 'ri-check-line' : 'ri-shopping-cart-line'"></i>
120
+ <span>{{ inCart ? t('home.inCart') : t('home.addToCart') }}</span>
121
+ </button>
122
+ </div>
123
+ </div>
124
+ </template>
125
+
101
126
  <style scoped>
102
- .product-card {
103
- border: 1px solid #eee;
104
- border-radius: 8px;
127
+ .pc-media {
128
+ aspect-ratio: 1 / 1;
105
129
  overflow: hidden;
106
- transition: box-shadow 0.3s;
107
- background: #fff;
108
130
  }
109
- .product-card:hover {
110
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
131
+ .pc-media img {
132
+ width: 100%;
133
+ height: 100%;
134
+ object-fit: contain;
111
135
  }
112
- .product-img {
113
- overflow: hidden;
114
- background: #f8f9fa;
136
+ .pc-placeholder {
137
+ width: 100%;
138
+ height: 100%;
115
139
  }
116
- .product-badges {
117
- position: absolute;
118
- top: 8px;
119
- left: 8px;
120
- }
121
- .product-actions {
122
- position: absolute;
123
- top: 8px;
124
- right: 8px;
125
- opacity: 0;
126
- transition: opacity 0.3s;
127
- }
128
- .product-card:hover .product-actions {
129
- opacity: 1;
130
- }
131
- .product-name {
132
- font-size: 14px;
133
- color: #333;
140
+ /* Назва — максимум 2 рядки з обрізанням; висота завжди на 2 рядки, щоб сітка не «рвалась» */
141
+ .pc-title {
142
+ min-height: 2.6em;
143
+ line-height: 1.3;
144
+ display: -webkit-box;
145
+ -webkit-line-clamp: 2;
146
+ line-clamp: 2;
147
+ -webkit-box-orient: vertical;
148
+ overflow: hidden;
149
+ overflow-wrap: anywhere;
134
150
  }
135
- .product-name:hover {
136
- color: var(--primary-color, #0d6efd);
151
+ /* Компактна картка на мобільному */
152
+ @media (max-width: 575.98px) {
153
+ .single-product-telemobile-item { min-width: 0; }
154
+ .single-product-telemobile-item :deep(.product-content) { padding: 10px; }
155
+ .single-product-telemobile-item :deep(.product-content h3) { font-size: 15px; }
156
+ .single-product-telemobile-item :deep(.product-content del) { font-size: 12px; }
157
+ .single-product-telemobile-item :deep(.product-content .btn) {
158
+ width: 100%; padding: 6px 8px; font-size: 13px; white-space: nowrap;
159
+ }
160
+ .single-product-telemobile-item :deep(.product-content .title) { font-size: 14px; line-height: 1.3; }
137
161
  }
138
162
  </style>
@@ -5,7 +5,7 @@
5
5
  <v-col cols="12" lg="3" sm="6">
6
6
  <div class="d-flex align-items-center single-servives-item">
7
7
  <div class="flex-shrink-0">
8
- <img src="~/assets/images/shipped.png" alt="shipped" />
8
+ <img src="~/assets/images/shipped.png" :alt="t('services.fastDelivery')" />
9
9
  </div>
10
10
  <div class="flex-grow-1 ms-3">
11
11
  <h3>{{ t('services.fastDelivery') }}</h3>
@@ -16,7 +16,7 @@
16
16
  <v-col cols="12" lg="3" sm="6">
17
17
  <div class="d-flex align-items-center single-servives-item">
18
18
  <div class="flex-shrink-0">
19
- <img src="~/assets/images/credit.png" alt="shipped" />
19
+ <img src="~/assets/images/credit.png" :alt="t('services.securedPayment')" />
20
20
  </div>
21
21
  <div class="flex-grow-1 ms-3">
22
22
  <h3>{{ t('services.securedPayment') }}</h3>
@@ -27,7 +27,7 @@
27
27
  <v-col cols="12" lg="3" sm="6">
28
28
  <div class="d-flex align-items-center single-servives-item">
29
29
  <div class="flex-shrink-0">
30
- <img src="~/assets/images/wallet.png" alt="shipped" />
30
+ <img src="~/assets/images/wallet.png" :alt="t('services.moneyBack')" />
31
31
  </div>
32
32
  <div class="flex-grow-1 ms-3">
33
33
  <h3>{{ t('services.moneyBack') }}</h3>
@@ -38,7 +38,7 @@
38
38
  <v-col cols="12" lg="3" sm="6">
39
39
  <div class="d-flex align-items-center single-servives-item">
40
40
  <div class="flex-shrink-0">
41
- <img src="~/assets/images/headphones.png" alt="shipped" />
41
+ <img src="~/assets/images/headphones.png" :alt="t('services.support247')" />
42
42
  </div>
43
43
  <div class="flex-grow-1 ms-3">
44
44
  <h3>{{ t('services.support247') }}</h3>
@@ -1,86 +1,56 @@
1
+ <script setup lang="ts">
2
+ // Форма «Напишіть нам»
3
+ const { t } = useI18n();
4
+ const { form, loading, sent, error, submit } = useContactForm();
5
+ </script>
6
+
1
7
  <template>
2
8
  <div class="contact-area pb-60">
3
9
  <div class="container">
4
10
  <div class="authentication-content p-50 mw-100">
5
- <div class="row">
6
- <div class="col-lg-5 mb-4 mb-lg-0">
7
- <div class="login h-100 text-center">
8
- <img
9
- src="~/assets/images/contact-us.png"
10
- class="h-100 object-fit-cover"
11
- alt="contact-us"
12
- />
13
- </div>
14
- </div>
15
- <div class="col-lg-7">
16
- <div class="authentication-form form-wrap">
17
- <h3 class="title mb-2">{{ t('contact.getInTouch') }}</h3>
18
- <p>{{ t('contact.contactDescription') }}</p>
19
-
20
- <form>
21
- <div class="row">
22
- <div class="col-lg-6">
23
- <div class="form-group mb-4">
24
- <label class="label">{{ t('contact.yourName') }} *</label>
25
- <input
26
- type="text"
27
- class="form-control"
28
- :placeholder="t('contact.enterName')"
29
- />
30
- </div>
31
- </div>
32
- <div class="col-lg-6">
33
- <div class="form-group mb-4">
34
- <label class="label">{{ t('contact.yourEmail') }} *</label>
35
- <input
36
- type="email"
37
- class="form-control"
38
- :placeholder="t('contact.enterEmail')"
39
- />
40
- </div>
41
- </div>
42
- <div class="col-lg-12">
43
- <div class="form-group mb-4">
44
- <label class="label">{{ t('contact.yourSubject') }} *</label>
45
- <input
46
- type="text"
47
- class="form-control"
48
- :placeholder="t('contact.yourSubject')"
49
- />
50
- </div>
51
- </div>
52
- <div class="col-lg-12">
53
- <div class="form-group mb-4">
54
- <label class="label">{{ t('contact.yourMessage') }} *</label>
55
- <textarea
56
- class="form-control"
57
- :placeholder="t('contact.writeMessage')"
58
- cols="30"
59
- rows="5"
60
- ></textarea>
61
- </div>
62
- </div>
11
+ <div class="authentication-form form-wrap">
12
+ <h3 class="title mb-2">{{ t('pages.contact.formTitle') }}</h3>
13
+ <p>{{ t('pages.contact.formDesc') }}</p>
63
14
 
64
- <div class="col-lg-12">
65
- <div class="form-group mb-0">
66
- <button
67
- type="submit"
68
- class="btn btn-warning btn-pill text-white"
69
- >
70
- {{ t('footer.submitNow') }}
71
- </button>
72
- </div>
73
- </div>
15
+ <div v-if="sent" class="alert alert-success" role="status">{{ t('pages.contact.sent') }}</div>
16
+ <form v-else novalidate @submit.prevent="submit">
17
+ <div class="row">
18
+ <div class="col-lg-6">
19
+ <div class="form-group mb-4">
20
+ <label class="label" for="cf-name">{{ t('pages.contact.name') }} *</label>
21
+ <input id="cf-name" v-model="form.name" type="text" class="form-control" :placeholder="t('pages.contact.namePh')" />
22
+ </div>
23
+ </div>
24
+ <div class="col-lg-6">
25
+ <div class="form-group mb-4">
26
+ <label class="label" for="cf-email">{{ t('pages.contact.email') }} *</label>
27
+ <input id="cf-email" v-model="form.email" type="email" class="form-control" :placeholder="t('pages.contact.emailPh')" />
74
28
  </div>
75
- </form>
29
+ </div>
30
+ <div class="col-lg-6">
31
+ <div class="form-group mb-4">
32
+ <label class="label" for="cf-phone">{{ t('pages.contact.phone') }}</label>
33
+ <input id="cf-phone" v-model="form.phone" type="tel" class="form-control" :placeholder="t('pages.contact.phonePh')" />
34
+ </div>
35
+ </div>
36
+ <div class="col-lg-12">
37
+ <div class="form-group mb-4">
38
+ <label class="label" for="cf-msg">{{ t('pages.contact.message') }} *</label>
39
+ <textarea id="cf-msg" v-model="form.message" class="form-control" rows="5" :placeholder="t('pages.contact.messagePh')"></textarea>
40
+ </div>
41
+ </div>
42
+ <div v-if="error" class="col-lg-12">
43
+ <div class="alert alert-danger" role="alert">{{ error }}</div>
44
+ </div>
45
+ <div class="col-lg-12">
46
+ <button type="submit" class="btn btn-warning btn-pill text-white" :disabled="loading">
47
+ {{ loading ? t('pages.contact.sending') : t('pages.contact.send') }}
48
+ </button>
49
+ </div>
76
50
  </div>
77
- </div>
51
+ </form>
78
52
  </div>
79
53
  </div>
80
54
  </div>
81
55
  </div>
82
56
  </template>
83
-
84
- <script setup lang="ts">
85
- const { t } = useI18n();
86
- </script>
@@ -1,74 +1,48 @@
1
+ <script setup lang="ts">
2
+ // Контакти магазину з CRM: телефони, email, адреса, графік, соцмережі
3
+ import { useLayoutData } from '~/components/Layout/layoutData';
4
+
5
+ const { phones, email, address, workingHours, socials } = useLayoutData();
6
+ const { t } = useI18n();
7
+ const hasAny = computed(
8
+ () => phones.value.length || email.value || address.value || workingHours.value || socials.value.length,
9
+ );
10
+ </script>
11
+
1
12
  <template>
2
- <div class="contact-info-area pt-60 pb-35">
13
+ <div v-if="hasAny" class="contact-info-area pt-60 pb-35">
3
14
  <div class="container">
4
15
  <div class="row justify-content-center">
5
- <div class="col-lg-4 col-md-6">
6
- <div class="single-contact-info-item">
7
- <div class="d-sm-flex">
8
- <div class="flex-shrink-0">
9
- <i class="flaticon-placeholder"></i>
10
- </div>
11
- <div class="flex-grow-1 ms-sm-3 mt-3 mt-sm-0">
12
- <h3>United Kingdom</h3>
13
- <p>6391 Elgin St. Celina, Delaware 10299</p>
14
- <a href="#" class="tell">(208) 555-0112</a>
15
- <a
16
- href="mailto:support@tuaninfo.com"
17
- class="text-decoration-underline text-primary"
18
- >
19
- support@tuaninfo.com
20
- </a>
21
- </div>
22
- </div>
16
+ <div v-if="phones.length || email" class="col-lg-4 col-md-6 mb-4">
17
+ <div class="single-contact-info-item h-100">
18
+ <h3>{{ t('pages.contact.connect') }}</h3>
19
+ <a v-for="p in phones" :key="p" :href="`tel:${p}`" class="tell d-block">{{ p }}</a>
20
+ <a v-if="email" :href="`mailto:${email}`" class="text-decoration-underline text-primary">{{ email }}</a>
23
21
  </div>
24
22
  </div>
25
- <div class="col-lg-4 col-md-6">
26
- <div class="single-contact-info-item">
27
- <div class="d-sm-flex">
28
- <div class="flex-shrink-0">
29
- <i class="flaticon-placeholder"></i>
30
- </div>
31
- <div class="flex-grow-1 ms-sm-3 mt-3 mt-sm-0">
32
- <h3>Toronto Canada</h3>
33
- <p>1901 Thornridge Cir. Shiloh, Hawaii</p>
34
- <a href="tel:(252)555-0126" class="tell">(252) 555-0126</a>
35
- <a
36
- href="mailto:support@tuaninfo.com"
37
- class="text-decoration-underline text-primary"
38
- >
39
- support@tuaninfo.com
40
- </a>
41
- </div>
42
- </div>
23
+ <div v-if="address || workingHours" class="col-lg-4 col-md-6 mb-4">
24
+ <div class="single-contact-info-item h-100">
25
+ <h3>{{ t('pages.contact.addressHours') }}</h3>
26
+ <p v-if="address">{{ address }}</p>
27
+ <p v-if="workingHours" class="mb-0">{{ workingHours }}</p>
43
28
  </div>
44
29
  </div>
45
- <div class="col-lg-4 col-md-6">
46
- <div class="single-contact-info-item">
47
- <div class="d-sm-flex">
48
- <div class="flex-shrink-0">
49
- <i class="flaticon-placeholder"></i>
50
- </div>
51
- <div class="flex-grow-1 ms-sm-3 mt-3 mt-sm-0">
52
- <h3>Saint Barthélemy</h3>
53
- <p>4517 Washington Ave. Manchester</p>
54
- <a href="tel:(505)555-0125" class="tell">(505)555-0125</a>
55
- <a
56
- href="mailto:support@tuaninfo.com"
57
- class="text-decoration-underline text-primary"
58
- >
59
- support@tuaninfo.com
60
- </a>
61
- </div>
62
- </div>
30
+ <div v-if="socials.length" class="col-lg-4 col-md-6 mb-4">
31
+ <div class="single-contact-info-item h-100">
32
+ <h3>{{ t('pages.contact.socials') }}</h3>
33
+ <a
34
+ v-for="s in socials"
35
+ :key="s.key"
36
+ :href="s.url"
37
+ target="_blank"
38
+ rel="noopener noreferrer"
39
+ class="d-block text-primary"
40
+ >
41
+ <i :class="s.icon" class="me-1"></i>{{ s.label }}
42
+ </a>
63
43
  </div>
64
44
  </div>
65
45
  </div>
66
46
  </div>
67
47
  </div>
68
48
  </template>
69
-
70
- <script>
71
- export default {
72
- name: "ContactInfo",
73
- };
74
- </script>