@crm-market/template-shared 1.0.6 → 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 +67 -31
- package/composables/useCategoryImages.ts +45 -0
- package/composables/useCheckout.ts +302 -97
- 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 -83
- package/composables/useRecent.ts +30 -0
- package/composables/useSanitize.ts +76 -0
- package/composables/useSiteConfig.ts +51 -47
- 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/image.ts +0 -1
- 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
|
@@ -3,191 +3,58 @@
|
|
|
3
3
|
<div class="container">
|
|
4
4
|
<div class="row">
|
|
5
5
|
<div class="col-lg-6">
|
|
6
|
-
<
|
|
7
|
-
<v-tabs-window v-model="tab">
|
|
8
|
-
<v-tabs-window-item value="img1">
|
|
9
|
-
<div class="product-details-img">
|
|
10
|
-
<img src="~/assets/images/product-66.png" alt="product" />
|
|
11
|
-
</div>
|
|
12
|
-
</v-tabs-window-item>
|
|
13
|
-
<v-tabs-window-item value="img2">
|
|
14
|
-
<div class="product-details-img">
|
|
15
|
-
<img src="~/assets/images/product-67.png" alt="product" />
|
|
16
|
-
</div>
|
|
17
|
-
</v-tabs-window-item>
|
|
18
|
-
<v-tabs-window-item value="img3">
|
|
19
|
-
<div class="product-details-img">
|
|
20
|
-
<img src="~/assets/images/product-68.png" alt="product" />
|
|
21
|
-
</div>
|
|
22
|
-
</v-tabs-window-item>
|
|
23
|
-
<span class="discounts">-10%</span>
|
|
24
|
-
</v-tabs-window>
|
|
25
|
-
</v-card-text>
|
|
26
|
-
|
|
27
|
-
<v-tabs
|
|
28
|
-
v-model="tab"
|
|
29
|
-
bg-color="white"
|
|
30
|
-
class="products-tabs justify-content-center"
|
|
31
|
-
>
|
|
32
|
-
<v-tab value="img1">
|
|
33
|
-
<img src="~/assets/images/product-66.png" alt="product" />
|
|
34
|
-
</v-tab>
|
|
35
|
-
<v-tab value="img2">
|
|
36
|
-
<img src="~/assets/images/product-67.png" alt="product" />
|
|
37
|
-
</v-tab>
|
|
38
|
-
<v-tab value="img3">
|
|
39
|
-
<img src="~/assets/images/product-68.png" alt="product" />
|
|
40
|
-
</v-tab>
|
|
41
|
-
</v-tabs>
|
|
6
|
+
<ProductDetailsProductGallery :product="product" :discount="discountLabel" />
|
|
42
7
|
</div>
|
|
43
8
|
<div class="col-lg-6">
|
|
44
9
|
<div class="product-details-content">
|
|
45
|
-
<span class="stock">
|
|
10
|
+
<span v-if="outOfStock" class="stock text-danger">{{ t('product.outOfStock') }}</span>
|
|
11
|
+
<span v-else class="stock">
|
|
46
12
|
<i class="ri-check-line"></i>
|
|
47
13
|
{{ t('product.inStock') }}
|
|
48
14
|
</span>
|
|
49
|
-
<h3>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<i class="
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<span>(5 {{ t('product.reviews') }})</span>
|
|
60
|
-
</div>
|
|
61
|
-
<h2 class="price">$399 <del>$450</del></h2>
|
|
62
|
-
<div class="d-flex style-select">
|
|
63
|
-
<span>{{ t('product.style') }}:</span>
|
|
64
|
-
<h5>{{ typeText }}</h5>
|
|
65
|
-
</div>
|
|
66
|
-
<div class="select-style mb-4">
|
|
67
|
-
<input type="radio" class="btn-check" />
|
|
68
|
-
<label
|
|
69
|
-
class="btn mb-2 mb-sm-0 mr-1"
|
|
70
|
-
@click="setTypeTextToFirst"
|
|
71
|
-
:class="{ active: typeActiveButton === 'first' }"
|
|
72
|
-
>
|
|
73
|
-
Apple M1 Max Chip
|
|
74
|
-
</label>
|
|
75
|
-
<input type="radio" class="btn-check" />
|
|
76
|
-
<label
|
|
77
|
-
class="btn mb-2 mb-sm-0"
|
|
78
|
-
@click="setTypeTextToSecond"
|
|
79
|
-
:class="{ active: typeActiveButton === 'second' }"
|
|
80
|
-
>
|
|
81
|
-
Apple M1 Pro Chip
|
|
82
|
-
</label>
|
|
83
|
-
</div>
|
|
84
|
-
<div class="d-flex style-select">
|
|
85
|
-
<span>{{ t('product.capacity') }}:</span>
|
|
86
|
-
<h5>{{ capacityText }}</h5>
|
|
87
|
-
</div>
|
|
88
|
-
<div class="select-style mb-4">
|
|
89
|
-
<input type="radio" class="btn-check" />
|
|
90
|
-
<label
|
|
91
|
-
class="btn mb-2 mb-sm-0 mr-1"
|
|
92
|
-
@click="setCapacityTextToFirst"
|
|
93
|
-
:class="{ active: capacityActiveButton === 'first' }"
|
|
94
|
-
>
|
|
95
|
-
512 GB
|
|
96
|
-
</label>
|
|
15
|
+
<h3>{{ product.title }}</h3>
|
|
16
|
+
<h2 class="price">
|
|
17
|
+
{{ formatPrice(price) }}
|
|
18
|
+
<del v-if="oldPrice" class="ms-2">{{ formatPrice(oldPrice) }}</del>
|
|
19
|
+
<span v-if="product.unit" class="fs-6 fw-normal text-muted ms-1">/ {{ product.unit }}</span>
|
|
20
|
+
</h2>
|
|
21
|
+
<p v-if="discountEnd" class="pd-discount-end">
|
|
22
|
+
<i class="ri-time-line"></i>
|
|
23
|
+
{{ t('product.saleUntil', { date: discountEnd }) }}
|
|
24
|
+
</p>
|
|
97
25
|
|
|
98
|
-
<input type="radio" class="btn-check" />
|
|
99
|
-
<label
|
|
100
|
-
class="btn mb-2 mb-sm-0"
|
|
101
|
-
@click="setCapacityTextToSecond"
|
|
102
|
-
:class="{ active: capacityActiveButton === 'second' }"
|
|
103
|
-
>
|
|
104
|
-
1 TB
|
|
105
|
-
</label>
|
|
106
|
-
</div>
|
|
107
|
-
<div class="d-flex style-select">
|
|
108
|
-
<span>{{ t('product.color') }}:</span>
|
|
109
|
-
<h5>{{ colorText }}</h5>
|
|
110
|
-
</div>
|
|
111
|
-
<ul class="d-flex ps-0 mb-0 list-unstyled select-color gap-2 mb-4">
|
|
112
|
-
<li
|
|
113
|
-
class="color1"
|
|
114
|
-
@click="setColorTextToFirst"
|
|
115
|
-
:class="{ active: colorActiveButton === 'first' }"
|
|
116
|
-
></li>
|
|
117
|
-
<li
|
|
118
|
-
class="color2"
|
|
119
|
-
@click="setColorTextToSecond"
|
|
120
|
-
:class="{ active: colorActiveButton === 'second' }"
|
|
121
|
-
></li>
|
|
122
|
-
</ul>
|
|
123
26
|
<div class="d-flex align-items-center mb-4 gap-3">
|
|
124
|
-
<ProductDetailsProductQuantity />
|
|
125
|
-
<button class="btn btn-success text-white rounded-pill">
|
|
126
|
-
<i class="ri-shopping-cart-line"></i>
|
|
127
|
-
<span>{{ t('product.addToCart') }}</span>
|
|
27
|
+
<ProductDetailsProductQuantity v-model="quantity" />
|
|
28
|
+
<button type="button" class="btn btn-success text-white rounded-pill" :disabled="outOfStock" @click="handleAddToCart">
|
|
29
|
+
<i :class="inCart ? 'ri-check-line' : 'ri-shopping-cart-line'"></i>
|
|
30
|
+
<span>{{ inCart ? t('product.inCart') : t('product.addToCart') }}</span>
|
|
128
31
|
</button>
|
|
129
32
|
</div>
|
|
130
|
-
<button class="d-flex align-items-center gap-3 wishlist mb-3">
|
|
131
|
-
<i class="ri-heart-line"></i>
|
|
132
|
-
<span>{{ t('product.addToWishlist') }}</span>
|
|
133
|
-
</button>
|
|
134
|
-
<button class="d-flex align-items-center gap-3 wishlist">
|
|
135
|
-
<i class="ri-eye-line"></i>
|
|
136
|
-
<span><span>565</span> {{ t('product.viewingProduct') }}</span>
|
|
33
|
+
<button type="button" class="d-flex align-items-center gap-3 wishlist mb-3" @click="toggle(product)">
|
|
34
|
+
<i :class="inWishlist ? 'ri-heart-fill text-danger' : 'ri-heart-line'"></i>
|
|
35
|
+
<span>{{ inWishlist ? t('product.inWishlist') : t('product.addToWishlist') }}</span>
|
|
137
36
|
</button>
|
|
138
|
-
|
|
139
|
-
<li class="d-flex align-items-center">
|
|
140
|
-
<i class="ri-check-line"></i>
|
|
141
|
-
<span>{{ t('product.freeDelivery') }}</span>
|
|
142
|
-
</li>
|
|
143
|
-
<li class="d-flex align-items-center">
|
|
144
|
-
<i class="ri-check-line"></i>
|
|
145
|
-
<span>{{ t('product.moneyBack') }}</span>
|
|
146
|
-
</li>
|
|
147
|
-
<li class="d-flex align-items-center">
|
|
148
|
-
<i class="ri-check-line"></i>
|
|
149
|
-
<span>{{ t('product.returnPolicy') }}</span>
|
|
150
|
-
</li>
|
|
151
|
-
</ul>
|
|
37
|
+
|
|
152
38
|
<ul class="sku-list ps-0 mb-0 list-unstyled">
|
|
153
|
-
<li class="d-flex align-items-center">
|
|
39
|
+
<li v-if="product.sku" class="d-flex align-items-center">
|
|
154
40
|
<span class="title">{{ t('product.sku') }}:</span>
|
|
155
|
-
<span class="title2">
|
|
156
|
-
</li>
|
|
157
|
-
<li class="d-flex align-items-center">
|
|
158
|
-
<span class="title">{{ t('product.category') }}:</span>
|
|
159
|
-
<span class="title2">Computer</span>
|
|
160
|
-
</li>
|
|
161
|
-
<li class="d-flex align-items-center">
|
|
162
|
-
<span class="title">{{ t('nav.tags') }}:</span>
|
|
163
|
-
<span class="title2">laptop, macbook, PC</span>
|
|
164
|
-
</li>
|
|
165
|
-
</ul>
|
|
166
|
-
<ul
|
|
167
|
-
class="social-link ps-0 mb-0 list-unstyled d-flex gap-2 flex-wrap"
|
|
168
|
-
>
|
|
169
|
-
<li class="d-flex align-items-center me-2">
|
|
170
|
-
<span>{{ t('product.share') }}:</span>
|
|
171
|
-
</li>
|
|
172
|
-
<li class="d-flex align-items-center gap-2">
|
|
173
|
-
<a href="https://facebook.com/" target="_blank">
|
|
174
|
-
<i class="ri-facebook-fill"></i>
|
|
175
|
-
</a>
|
|
41
|
+
<span class="title2">{{ product.sku }}</span>
|
|
176
42
|
</li>
|
|
177
|
-
<li class="d-flex align-items-center
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
</a>
|
|
43
|
+
<li v-if="brandNames" class="d-flex align-items-center">
|
|
44
|
+
<span class="title">{{ t('product.brand') }}:</span>
|
|
45
|
+
<span class="title2">{{ brandNames }}</span>
|
|
181
46
|
</li>
|
|
182
|
-
<li class="d-flex align-items-center
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
47
|
+
<li v-if="product.categories?.length" class="d-flex align-items-center">
|
|
48
|
+
<span class="title">{{ t('product.category') }}:</span>
|
|
49
|
+
<span class="title2">
|
|
50
|
+
<template v-for="(cat, i) in product.categories" :key="cat.id">
|
|
51
|
+
<NuxtLink :to="categoryPath(cat)">{{ cat.name }}</NuxtLink><span v-if="i < product.categories.length - 1">, </span>
|
|
52
|
+
</template>
|
|
53
|
+
</span>
|
|
186
54
|
</li>
|
|
187
|
-
<li class="d-flex align-items-center
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
</a>
|
|
55
|
+
<li v-if="product.keywords?.length" class="d-flex align-items-center">
|
|
56
|
+
<span class="title">{{ t('product.tags') }}:</span>
|
|
57
|
+
<span class="title2">{{ product.keywords.join(', ') }}</span>
|
|
191
58
|
</li>
|
|
192
59
|
</ul>
|
|
193
60
|
</div>
|
|
@@ -196,75 +63,68 @@
|
|
|
196
63
|
|
|
197
64
|
<div class="pt-60"></div>
|
|
198
65
|
|
|
199
|
-
<ProductDetailsTab />
|
|
66
|
+
<ProductDetailsTab :product="product" />
|
|
200
67
|
</div>
|
|
201
68
|
</div>
|
|
202
69
|
</template>
|
|
203
70
|
|
|
204
|
-
<script lang="ts">
|
|
205
|
-
import {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const typeActiveButton = ref("first");
|
|
216
|
-
|
|
217
|
-
const setTypeTextToFirst = () => {
|
|
218
|
-
typeText.value = "Apple M1 Max Chip";
|
|
219
|
-
typeActiveButton.value = "first";
|
|
220
|
-
};
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
import { computed, onMounted, ref } from 'vue';
|
|
73
|
+
import {
|
|
74
|
+
formatPrice,
|
|
75
|
+
getProductDiscountEnd,
|
|
76
|
+
getProductDiscountLabel,
|
|
77
|
+
getProductOldPrice,
|
|
78
|
+
getProductPrice,
|
|
79
|
+
getProductImage,
|
|
80
|
+
type Product,
|
|
81
|
+
} from '../../composables/useProducts';
|
|
221
82
|
|
|
222
|
-
|
|
223
|
-
typeText.value = "Apple M1 Pro Chip";
|
|
224
|
-
typeActiveButton.value = "second";
|
|
225
|
-
};
|
|
83
|
+
const props = defineProps<{ product: Product }>();
|
|
226
84
|
|
|
227
|
-
|
|
228
|
-
|
|
85
|
+
const { t } = useI18n();
|
|
86
|
+
const { addToCart, isInCart } = useCart();
|
|
87
|
+
const { ids, load, toggle } = useWishlist();
|
|
229
88
|
|
|
230
|
-
|
|
231
|
-
capacityText.value = "512 GB";
|
|
232
|
-
capacityActiveButton.value = "first";
|
|
233
|
-
};
|
|
89
|
+
const quantity = ref(1);
|
|
234
90
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
91
|
+
const price = computed(() => getProductPrice(props.product));
|
|
92
|
+
const oldPrice = computed(() => getProductOldPrice(props.product));
|
|
93
|
+
// Бейдж «-20%» / «-50 ₴» (у галереї) і дата завершення акції
|
|
94
|
+
const discountLabel = computed(() => getProductDiscountLabel(props.product));
|
|
95
|
+
const discountEnd = computed(() => getProductDiscountEnd(props.product));
|
|
96
|
+
const outOfStock = computed(() => typeof props.product.stock === 'number' && props.product.stock <= 0);
|
|
97
|
+
const inCart = computed(() => isInCart(props.product.id));
|
|
98
|
+
const inWishlist = computed(() => ids.value.includes(props.product.id));
|
|
99
|
+
const brandNames = computed(() =>
|
|
100
|
+
(props.product.brands || []).map((b: any) => b?.name).filter(Boolean).join(', '),
|
|
101
|
+
);
|
|
239
102
|
|
|
240
|
-
|
|
241
|
-
|
|
103
|
+
const handleAddToCart = () => {
|
|
104
|
+
if (outOfStock.value) return;
|
|
105
|
+
addToCart(
|
|
106
|
+
{
|
|
107
|
+
productId: props.product.id,
|
|
108
|
+
name: props.product.title,
|
|
109
|
+
price: oldPrice.value || price.value,
|
|
110
|
+
discountPrice: oldPrice.value ? price.value : undefined,
|
|
111
|
+
image: getProductImage(props.product),
|
|
112
|
+
unit: props.product.unit,
|
|
113
|
+
},
|
|
114
|
+
quantity.value,
|
|
115
|
+
);
|
|
116
|
+
};
|
|
242
117
|
|
|
243
|
-
|
|
244
|
-
colorText.value = "Silver";
|
|
245
|
-
colorActiveButton.value = "first";
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const setColorTextToSecond = () => {
|
|
249
|
-
colorText.value = "Gray";
|
|
250
|
-
colorActiveButton.value = "second";
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
t,
|
|
255
|
-
typeText,
|
|
256
|
-
typeActiveButton,
|
|
257
|
-
setTypeTextToFirst,
|
|
258
|
-
setTypeTextToSecond,
|
|
259
|
-
capacityText,
|
|
260
|
-
capacityActiveButton,
|
|
261
|
-
setCapacityTextToFirst,
|
|
262
|
-
setCapacityTextToSecond,
|
|
263
|
-
colorText,
|
|
264
|
-
colorActiveButton,
|
|
265
|
-
setColorTextToFirst,
|
|
266
|
-
setColorTextToSecond,
|
|
267
|
-
};
|
|
268
|
-
},
|
|
269
|
-
});
|
|
118
|
+
onMounted(load);
|
|
270
119
|
</script>
|
|
120
|
+
|
|
121
|
+
<style scoped>
|
|
122
|
+
.pd-discount-end {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 6px;
|
|
126
|
+
margin: -6px 0 18px;
|
|
127
|
+
color: #c62828;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
@@ -10,7 +10,17 @@
|
|
|
10
10
|
md="3"
|
|
11
11
|
class="mb-4 mt-xxl-0 overflow-hidden"
|
|
12
12
|
>
|
|
13
|
-
|
|
13
|
+
<!-- Мобільний: кнопка «Фільтри» згортає/розгортає панель -->
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
class="btn btn-outline-primary w-100 d-md-none"
|
|
17
|
+
:aria-expanded="filtersOpen"
|
|
18
|
+
@click="filtersOpen = !filtersOpen"
|
|
19
|
+
>
|
|
20
|
+
<i class="ri-filter-3-line me-1"></i>{{ t('catalog.filters') }}
|
|
21
|
+
<i :class="filtersOpen ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'"></i>
|
|
22
|
+
</button>
|
|
23
|
+
<div :class="['sidebar product-sidebar', filtersOpen ? 'd-block mt-3' : 'd-none d-md-block']">
|
|
14
24
|
<!-- Categories filter -->
|
|
15
25
|
<div class="single-sidebar-widget">
|
|
16
26
|
<h3>{{ t('product.filterByCategories') }}</h3>
|
|
@@ -66,8 +76,8 @@
|
|
|
66
76
|
</button>
|
|
67
77
|
</div>
|
|
68
78
|
|
|
69
|
-
<!-- Sort -->
|
|
70
|
-
<div class="single-sidebar-widget">
|
|
79
|
+
<!-- Sort (на мобільному винесено над товари) -->
|
|
80
|
+
<div class="single-sidebar-widget d-none d-md-block">
|
|
71
81
|
<h3>{{ t('product.sortBy') }}</h3>
|
|
72
82
|
<select class="form-select form-select-sm" v-model="sortBy">
|
|
73
83
|
<option value="newest">{{ t('product.newest') }}</option>
|
|
@@ -81,6 +91,16 @@
|
|
|
81
91
|
|
|
82
92
|
<!-- Products grid -->
|
|
83
93
|
<v-col cols="12" xl="10" lg="9" md="9">
|
|
94
|
+
<!-- Мобільне сортування над списком -->
|
|
95
|
+
<div class="d-md-none mb-3">
|
|
96
|
+
<select class="form-select form-select-sm" v-model="sortBy" :aria-label="t('product.sortBy')">
|
|
97
|
+
<option value="newest">{{ t('product.newest') }}</option>
|
|
98
|
+
<option value="price-asc">{{ t('product.priceLowHigh') }}</option>
|
|
99
|
+
<option value="price-desc">{{ t('product.priceHighLow') }}</option>
|
|
100
|
+
<option value="name">{{ t('product.nameAZ') }}</option>
|
|
101
|
+
</select>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
84
104
|
<!-- Search info -->
|
|
85
105
|
<div class="d-flex align-items-center justify-content-between mb-3" v-if="searchQuery || selectedCategoryId">
|
|
86
106
|
<p class="mb-0 text-muted">
|
|
@@ -160,21 +180,32 @@ import { defineComponent, ref, computed, watch, onMounted } from "vue";
|
|
|
160
180
|
|
|
161
181
|
export default defineComponent({
|
|
162
182
|
name: "ProductsIndex",
|
|
163
|
-
|
|
183
|
+
props: {
|
|
184
|
+
// Категорія зі сторінки /category/<slug>-<id>: початковий фільтр, в URL не потрапляє
|
|
185
|
+
categoryId: { type: String, default: "" },
|
|
186
|
+
},
|
|
187
|
+
async setup(props) {
|
|
164
188
|
const { t } = useI18n();
|
|
165
189
|
const route = useRoute();
|
|
166
190
|
const router = useRouter();
|
|
167
|
-
|
|
191
|
+
// Локальний стан сторінки: глобальні products/loading з useProducts ділять інші секції, тому не підходять
|
|
192
|
+
const { fetchPage } = useProducts();
|
|
193
|
+
// useState: результат SSR передається клієнту без повторного запиту і без миготіння «0 товарів»
|
|
194
|
+
const products = useState<any[]>('catalog-products', () => []);
|
|
195
|
+
const loading = useState<boolean>('catalog-loading', () => true);
|
|
196
|
+
const loaded = useState<boolean>('catalog-loaded', () => false);
|
|
168
197
|
const { topLevelCategories, getCategoryById } = useCategories();
|
|
169
198
|
|
|
170
|
-
const selectedCategoryId = ref('');
|
|
199
|
+
const selectedCategoryId = ref(props.categoryId || '');
|
|
171
200
|
const minPrice = ref<number | undefined>(undefined);
|
|
172
201
|
const maxPrice = ref<number | undefined>(undefined);
|
|
173
202
|
const sortBy = ref('newest');
|
|
174
203
|
const searchQuery = ref('');
|
|
175
204
|
const currentPage = ref(1);
|
|
176
|
-
const totalProducts =
|
|
205
|
+
const totalProducts = useState<number>('catalog-total', () => 0);
|
|
177
206
|
const perPage = 20;
|
|
207
|
+
// Мобільна панель фільтрів: за замовчуванням згорнута
|
|
208
|
+
const filtersOpen = ref(false);
|
|
178
209
|
|
|
179
210
|
const selectedCategoryName = computed(() => {
|
|
180
211
|
if (!selectedCategoryId.value) return '';
|
|
@@ -201,18 +232,27 @@ export default defineComponent({
|
|
|
201
232
|
const sortField = sortParts[0] as any;
|
|
202
233
|
const sortOrder = sortParts[1] as 'asc' | 'desc' | undefined;
|
|
203
234
|
|
|
235
|
+
loading.value = true;
|
|
204
236
|
try {
|
|
205
|
-
const
|
|
237
|
+
const page = await fetchPage({
|
|
206
238
|
categoryId: selectedCategoryId.value || undefined,
|
|
207
239
|
minPrice: minPrice.value,
|
|
208
240
|
maxPrice: maxPrice.value,
|
|
209
241
|
search: searchQuery.value || undefined,
|
|
210
242
|
sortBy: sortField === 'price' ? 'price' : sortField === 'name' ? 'name' : 'newest',
|
|
211
243
|
sortOrder: sortOrder || (sortField === 'price' ? 'asc' : 'desc'),
|
|
212
|
-
|
|
213
|
-
|
|
244
|
+
limit: perPage,
|
|
245
|
+
offset: (currentPage.value - 1) * perPage,
|
|
246
|
+
} as any);
|
|
247
|
+
products.value = page.items;
|
|
248
|
+
totalProducts.value = page.total;
|
|
249
|
+
loaded.value = true;
|
|
214
250
|
} catch (e) {
|
|
215
251
|
console.error('Error loading products:', e);
|
|
252
|
+
products.value = [];
|
|
253
|
+
totalProducts.value = 0;
|
|
254
|
+
} finally {
|
|
255
|
+
loading.value = false;
|
|
216
256
|
}
|
|
217
257
|
};
|
|
218
258
|
|
|
@@ -242,18 +282,19 @@ export default defineComponent({
|
|
|
242
282
|
|
|
243
283
|
const syncToUrl = () => {
|
|
244
284
|
const query: any = {};
|
|
245
|
-
if (selectedCategoryId.value) query.categoryId = selectedCategoryId.value;
|
|
285
|
+
if (selectedCategoryId.value && !props.categoryId) query.categoryId = selectedCategoryId.value;
|
|
246
286
|
if (searchQuery.value) query.search = searchQuery.value;
|
|
247
287
|
if (minPrice.value) query.minPrice = minPrice.value;
|
|
248
288
|
if (maxPrice.value) query.maxPrice = maxPrice.value;
|
|
249
289
|
if (sortBy.value !== 'newest') query.sortBy = sortBy.value;
|
|
250
290
|
if (currentPage.value > 1) query.page = currentPage.value;
|
|
251
|
-
|
|
291
|
+
// На сторінці категорії лишаємось на її адресі (/category/<slug>-<id>?page=2)
|
|
292
|
+
router.replace({ path: props.categoryId ? route.path : '/products', query });
|
|
252
293
|
};
|
|
253
294
|
|
|
254
295
|
const syncFromUrl = () => {
|
|
255
296
|
const q = route.query;
|
|
256
|
-
if (q.categoryId) selectedCategoryId.value = q.categoryId as string;
|
|
297
|
+
if (q.categoryId && !props.categoryId) selectedCategoryId.value = q.categoryId as string;
|
|
257
298
|
if (q.search) searchQuery.value = q.search as string;
|
|
258
299
|
if (q.minPrice) minPrice.value = Number(q.minPrice);
|
|
259
300
|
if (q.maxPrice) maxPrice.value = Number(q.maxPrice);
|
|
@@ -262,16 +303,37 @@ export default defineComponent({
|
|
|
262
303
|
};
|
|
263
304
|
|
|
264
305
|
// Watch for category and sort changes
|
|
265
|
-
watch(selectedCategoryId, () =>
|
|
306
|
+
watch(selectedCategoryId, (id) => {
|
|
307
|
+
// На сторінці категорії інший вибір у фільтрі — перехід на адресу вибраної категорії (або в каталог)
|
|
308
|
+
if (props.categoryId && id !== props.categoryId) {
|
|
309
|
+
const cat = id ? getCategoryById(id) : undefined;
|
|
310
|
+
router.push(cat ? categoryPath(cat) : '/products');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
applyFilters();
|
|
314
|
+
});
|
|
266
315
|
watch(sortBy, () => applyFilters());
|
|
316
|
+
// Інша категорія в props — одразу очищаємо список, щоб не блимали товари попередньої
|
|
317
|
+
watch(() => props.categoryId, (id) => {
|
|
318
|
+
products.value = [];
|
|
319
|
+
loading.value = true;
|
|
320
|
+
if (selectedCategoryId.value === (id || '')) applyFilters();
|
|
321
|
+
else selectedCategoryId.value = id || ''; // далі підхопить watch(selectedCategoryId)
|
|
322
|
+
});
|
|
267
323
|
|
|
324
|
+
// SSR: рендеримо каталог одразу (SEO), клієнт лише гідрує; повторно вантажимо тільки при зміні фільтрів
|
|
325
|
+
// Хуки життєвого циклу реєструємо ДО першого await (вимога async setup)
|
|
268
326
|
onMounted(() => {
|
|
269
|
-
|
|
270
|
-
loadProducts();
|
|
327
|
+
if (!loaded.value) loadProducts();
|
|
271
328
|
});
|
|
329
|
+
syncFromUrl();
|
|
330
|
+
if (import.meta.server) {
|
|
331
|
+
await loadProducts();
|
|
332
|
+
}
|
|
272
333
|
|
|
273
334
|
return {
|
|
274
335
|
t,
|
|
336
|
+
filtersOpen,
|
|
275
337
|
products,
|
|
276
338
|
loading,
|
|
277
339
|
topLevelCategories,
|
|
@@ -1,112 +1,8 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<span class="update">Last updated: February 2024</span>
|
|
6
|
-
<h3>Terms & conditions</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>Intellectual property and acceptable use</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>Limitation of liability</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>Registration</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('terms')" />
|
|
106
8
|
</template>
|
|
107
|
-
|
|
108
|
-
<script>
|
|
109
|
-
export default {
|
|
110
|
-
name: "TermsConditions",
|
|
111
|
-
};
|
|
112
|
-
</script>
|