@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
|
@@ -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
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
20
|
+
// Спільний між картками стан вішліста (id товарів), синхронізований з localStorage
|
|
21
|
+
const wishIds = useState<string[]>('wishlist-ids', () => []);
|
|
76
22
|
|
|
77
|
-
const
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
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 (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
.
|
|
103
|
-
|
|
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
|
-
.
|
|
110
|
-
|
|
131
|
+
.pc-media img {
|
|
132
|
+
width: 100%;
|
|
133
|
+
height: 100%;
|
|
134
|
+
object-fit: contain;
|
|
111
135
|
}
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
.pc-placeholder {
|
|
137
|
+
width: 100%;
|
|
138
|
+
height: 100%;
|
|
115
139
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
6
|
-
<
|
|
7
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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>
|