@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
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
--gray4: #fcfcfc;
|
|
19
19
|
--boxShadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
|
20
20
|
--transition: all ease 0.5s;
|
|
21
|
-
--fontFamilyRegular: "Satoshi-Regular";
|
|
22
|
-
--fontFamilyMedium: "Satoshi-Medium";
|
|
23
|
-
--fontFamilyBold: "Satoshi-Bold";
|
|
21
|
+
--fontFamilyRegular: "Satoshi-Regular", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
22
|
+
--fontFamilyMedium: "Satoshi-Medium", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
23
|
+
--fontFamilyBold: "Satoshi-Bold", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
24
24
|
--fontSize13: 13px;
|
|
25
25
|
--fontSize14: 14px;
|
|
26
26
|
--fontSize15: 15px;
|
package/assets/scss/style.css
CHANGED
|
@@ -28,9 +28,9 @@ This files table contents are outlined below>>>>>
|
|
|
28
28
|
--gray4: #fcfcfc;
|
|
29
29
|
--boxShadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
|
30
30
|
--transition: all ease 0.5s;
|
|
31
|
-
--fontFamilyRegular: "Satoshi-Regular";
|
|
32
|
-
--fontFamilyMedium: "Satoshi-Medium";
|
|
33
|
-
--fontFamilyBold: "Satoshi-Bold";
|
|
31
|
+
--fontFamilyRegular: "Satoshi-Regular", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
32
|
+
--fontFamilyMedium: "Satoshi-Medium", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
33
|
+
--fontFamilyBold: "Satoshi-Bold", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
34
34
|
--fontSize13: 13px;
|
|
35
35
|
--fontSize14: 14px;
|
|
36
36
|
--fontSize15: 15px;
|
|
@@ -1003,7 +1003,7 @@ Page-navigation Area Style
|
|
|
1003
1003
|
font-size: 14px;
|
|
1004
1004
|
}
|
|
1005
1005
|
.middle-header-left .btn {
|
|
1006
|
-
font-family:
|
|
1006
|
+
font-family: var(--fontFamilyBold);
|
|
1007
1007
|
padding: 10px 23px;
|
|
1008
1008
|
}
|
|
1009
1009
|
.middle-header-left .btn i {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Контентний блок статичної сторінки: показує лише санітизований HTML продавця
|
|
3
|
+
import { sanitizeHtml } from '~/composables/useSanitize';
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ html: string }>();
|
|
6
|
+
const safeHtml = computed(() => sanitizeHtml(props.html));
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div class="ptb-60">
|
|
11
|
+
<div class="container">
|
|
12
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
13
|
+
<div class="main-content-text static-content" v-html="safeHtml"></div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<style lang="scss" scoped>
|
|
19
|
+
.static-content {
|
|
20
|
+
:deep(ul) {
|
|
21
|
+
list-style: disc;
|
|
22
|
+
padding-left: 20px;
|
|
23
|
+
li { padding-left: 0; }
|
|
24
|
+
}
|
|
25
|
+
:deep(ol) {
|
|
26
|
+
padding-left: 20px;
|
|
27
|
+
li { padding-left: 0; margin-bottom: 10px; }
|
|
28
|
+
}
|
|
29
|
+
:deep(img) { max-width: 100%; height: auto; }
|
|
30
|
+
:deep(a) { text-decoration: underline; }
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="cart-empty">
|
|
4
|
+
<div class="cart-empty__icon" aria-hidden="true"><i class="ri-shopping-cart-2-line"></i></div>
|
|
5
|
+
<h3>{{ t('cart.empty') }}</h3>
|
|
6
|
+
<p class="text-muted mb-4">{{ t('cart.emptyText') }}</p>
|
|
7
|
+
<NuxtLink to="/products" class="btn btn-warning text-white">{{ t('cart.toCatalog') }}</NuxtLink>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<section v-if="suggested.length" class="mt-5">
|
|
11
|
+
<h4 class="mb-4">{{ t('cart.suggested') }}</h4>
|
|
12
|
+
<div class="row g-3">
|
|
13
|
+
<div v-for="p in suggested" :key="p.id" class="col-6 col-md-4 col-lg-2">
|
|
14
|
+
<CommonProductCard :product="p" />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</section>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
// Порожній кошик + рекомендації, щоб не було глухого кута
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
const { fetchFeatured, fetchPage } = useProducts();
|
|
25
|
+
|
|
26
|
+
const suggested = ref<any[]>([]);
|
|
27
|
+
onMounted(async () => {
|
|
28
|
+
let list = await fetchFeatured(6);
|
|
29
|
+
if (!list.length) list = (await fetchPage({ limit: 6, sortBy: 'newest' } as any)).items;
|
|
30
|
+
suggested.value = list.slice(0, 6);
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style scoped lang="scss">
|
|
35
|
+
.cart-empty {
|
|
36
|
+
text-align: center; padding: 48px 16px; border: 1px solid var(--primary-color); border-radius: 12px; background: #fff;
|
|
37
|
+
}
|
|
38
|
+
.cart-empty__icon {
|
|
39
|
+
width: 84px; height: 84px; margin: 0 auto 16px; border-radius: 50%; background: rgba(74, 141, 22, 0.1);
|
|
40
|
+
color: var(--primary-color); font-size: 40px; display: flex; align-items: center; justify-content: center;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cart-line">
|
|
3
|
+
<NuxtLink :to="link" class="cart-line__img" tabindex="-1" aria-hidden="true">
|
|
4
|
+
<img v-if="item.image" :src="cartImageUrl(item.image)" :alt="item.name" loading="lazy" />
|
|
5
|
+
<span v-else class="cart-line__noimg"><i class="ri-image-line"></i></span>
|
|
6
|
+
</NuxtLink>
|
|
7
|
+
|
|
8
|
+
<div class="cart-line__main">
|
|
9
|
+
<NuxtLink :to="link" class="cart-line__title">{{ item.name }}</NuxtLink>
|
|
10
|
+
<div class="cart-line__unit">
|
|
11
|
+
<span class="cart-line__price">{{ formatUah(unitPrice(item)) }}</span>
|
|
12
|
+
<span v-if="hasDiscount" class="cart-line__old">{{ formatUah(item.price) }}</span>
|
|
13
|
+
<span v-if="item.unit" class="text-muted"> / {{ item.unit }}</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div v-if="(item.minQty || 1) > 1" class="cart-line__min text-muted">
|
|
16
|
+
{{ t('cart.minQty', { n: item.minQty }) }}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="cart-line__qty" role="group" :aria-label="t('product.quantity')">
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
class="cart-line__step"
|
|
24
|
+
:aria-label="t('cart.decrease')"
|
|
25
|
+
:disabled="item.quantity <= min"
|
|
26
|
+
@click="change(-1)"
|
|
27
|
+
>
|
|
28
|
+
<i class="ri-subtract-line"></i>
|
|
29
|
+
</button>
|
|
30
|
+
<input
|
|
31
|
+
class="cart-line__input"
|
|
32
|
+
type="text"
|
|
33
|
+
inputmode="numeric"
|
|
34
|
+
:value="item.quantity"
|
|
35
|
+
:aria-label="t('product.quantity')"
|
|
36
|
+
@change="onInput"
|
|
37
|
+
/>
|
|
38
|
+
<button type="button" class="cart-line__step" :aria-label="t('cart.increase')" @click="change(1)">
|
|
39
|
+
<i class="ri-add-line"></i>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="cart-line__sum">{{ formatUah(unitPrice(item) * item.quantity) }}</div>
|
|
44
|
+
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
class="cart-line__remove"
|
|
48
|
+
:aria-label="`${t('product.remove')}: ${item.name}`"
|
|
49
|
+
@click="emit('remove', item.productId)"
|
|
50
|
+
>
|
|
51
|
+
<i class="ri-delete-bin-line"></i>
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup lang="ts">
|
|
57
|
+
// Одна позиція кошика (рядок на десктопі, компактна картка на мобільному)
|
|
58
|
+
const props = defineProps<{ item: any }>();
|
|
59
|
+
const emit = defineEmits<{
|
|
60
|
+
(e: 'update', id: string, qty: number): void;
|
|
61
|
+
(e: 'remove', id: string): void;
|
|
62
|
+
}>();
|
|
63
|
+
|
|
64
|
+
const { t } = useI18n();
|
|
65
|
+
const { unitPrice } = useCart();
|
|
66
|
+
|
|
67
|
+
const link = computed(() => productPath({ id: props.item.productId, name: props.item.name }));
|
|
68
|
+
const min = computed(() => props.item.minQty || 1);
|
|
69
|
+
const hasDiscount = computed(() => !!props.item.discountPrice && props.item.discountPrice < props.item.price);
|
|
70
|
+
|
|
71
|
+
const change = (delta: number) => {
|
|
72
|
+
const next = props.item.quantity + delta;
|
|
73
|
+
if (next >= min.value) emit('update', props.item.productId, next);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onInput = (e: Event) => {
|
|
77
|
+
const el = e.target as HTMLInputElement;
|
|
78
|
+
const n = parseInt(el.value, 10);
|
|
79
|
+
const next = Number.isFinite(n) ? Math.max(min.value, n) : min.value;
|
|
80
|
+
el.value = String(next);
|
|
81
|
+
emit('update', props.item.productId, next);
|
|
82
|
+
};
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<style scoped lang="scss">
|
|
86
|
+
.cart-line {
|
|
87
|
+
display: grid;
|
|
88
|
+
grid-template-columns: 96px minmax(0, 1fr) auto 110px 44px;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 16px;
|
|
91
|
+
padding: 14px 16px;
|
|
92
|
+
background: #fff;
|
|
93
|
+
border: 1px solid #e7e7e7;
|
|
94
|
+
border-radius: 12px;
|
|
95
|
+
}
|
|
96
|
+
.cart-line__img {
|
|
97
|
+
width: 96px; height: 96px; border-radius: 10px; overflow: hidden; background: #f6f7f5; display: block;
|
|
98
|
+
img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
99
|
+
}
|
|
100
|
+
.cart-line__noimg { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #aaa; font-size: 28px; }
|
|
101
|
+
.cart-line__main { min-width: 0; }
|
|
102
|
+
.cart-line__title {
|
|
103
|
+
color: #1f2a1a; font-weight: 600; font-size: 16px; line-height: 1.3; text-decoration: none;
|
|
104
|
+
display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
105
|
+
&:hover, &:focus-visible { color: var(--primary-color); }
|
|
106
|
+
}
|
|
107
|
+
.cart-line__unit { margin-top: 4px; font-size: 14px; }
|
|
108
|
+
.cart-line__price { font-weight: 600; color: var(--primary-color); }
|
|
109
|
+
.cart-line__old { margin-left: 6px; color: #999; text-decoration: line-through; font-size: 13px; }
|
|
110
|
+
.cart-line__min { font-size: 12px; margin-top: 2px; }
|
|
111
|
+
.cart-line__qty {
|
|
112
|
+
display: inline-flex; align-items: center; border: 1px solid #d9d9d9; border-radius: 10px; overflow: hidden; background: #fff;
|
|
113
|
+
}
|
|
114
|
+
.cart-line__step {
|
|
115
|
+
width: 40px; height: 40px; border: 0; background: #f6f7f5; color: #1f2a1a; font-size: 18px; cursor: pointer;
|
|
116
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
117
|
+
&:hover:not(:disabled) { background: var(--primary-color); color: #fff; }
|
|
118
|
+
&:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
119
|
+
}
|
|
120
|
+
.cart-line__input {
|
|
121
|
+
width: 48px; height: 40px; border: 0; text-align: center; font-weight: 600; background: transparent; padding: 0;
|
|
122
|
+
}
|
|
123
|
+
.cart-line__step:focus-visible, .cart-line__input:focus-visible, .cart-line__remove:focus-visible {
|
|
124
|
+
outline: 2px solid var(--primary-color); outline-offset: -2px;
|
|
125
|
+
}
|
|
126
|
+
.cart-line__sum { text-align: right; font-weight: 700; font-size: 17px; color: #1f2a1a; white-space: nowrap; }
|
|
127
|
+
.cart-line__remove {
|
|
128
|
+
width: 44px; height: 44px; border: 0; border-radius: 10px; background: transparent; color: #c0392b; font-size: 20px; cursor: pointer;
|
|
129
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
130
|
+
&:hover { background: #fdecea; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@media (max-width: 767.98px) {
|
|
134
|
+
.cart-line {
|
|
135
|
+
grid-template-columns: 84px minmax(0, 1fr) 44px;
|
|
136
|
+
grid-template-areas: 'img main remove' 'qty qty sum';
|
|
137
|
+
gap: 10px 12px; padding: 12px;
|
|
138
|
+
}
|
|
139
|
+
.cart-line__img { grid-area: img; width: 84px; height: 84px; }
|
|
140
|
+
.cart-line__main { grid-area: main; }
|
|
141
|
+
.cart-line__remove { grid-area: remove; align-self: start; }
|
|
142
|
+
.cart-line__qty { grid-area: qty; justify-self: start; }
|
|
143
|
+
.cart-line__sum { grid-area: sum; font-size: 16px; color: var(--primary-color); }
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside class="cart-summary" :aria-label="t('cart.cartTotals')">
|
|
3
|
+
<h3 class="cart-summary__title">{{ t('cart.cartTotals') }}</h3>
|
|
4
|
+
<ul class="cart-summary__list">
|
|
5
|
+
<li><span>{{ t('cart.itemsCount') }}</span><strong>{{ itemCount }}</strong></li>
|
|
6
|
+
<li><span>{{ t('cart.itemsSum') }}</span><strong>{{ formatUah(subtotal + totalDiscount) }}</strong></li>
|
|
7
|
+
<li v-if="totalDiscount > 0" class="cart-summary__discount">
|
|
8
|
+
<span>{{ t('cart.itemsDiscount') }}</span><strong>−{{ formatUah(totalDiscount) }}</strong>
|
|
9
|
+
</li>
|
|
10
|
+
<li>
|
|
11
|
+
<span>{{ t('cart.shipping') }}</span>
|
|
12
|
+
<span class="text-muted text-end">{{ t('cart.shippingAtCheckout') }}</span>
|
|
13
|
+
</li>
|
|
14
|
+
</ul>
|
|
15
|
+
<div class="cart-summary__total">
|
|
16
|
+
<span>{{ t('cart.grandTotal') }}</span>
|
|
17
|
+
<strong>{{ formatUah(subtotal) }}</strong>
|
|
18
|
+
</div>
|
|
19
|
+
<p class="cart-summary__hint"><i class="ri-coupon-3-line"></i> {{ t('cart.promoHint') }}</p>
|
|
20
|
+
<NuxtLink to="/checkout" class="btn btn-warning text-white w-100 cart-summary__cta">
|
|
21
|
+
{{ t('cart.proceedToCheckout') }}
|
|
22
|
+
</NuxtLink>
|
|
23
|
+
<NuxtLink to="/products" class="cart-summary__continue">{{ t('cart.continue') }}</NuxtLink>
|
|
24
|
+
</aside>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
// Підсумок кошика: кількість, сума, знижка, CTA
|
|
29
|
+
const { t } = useI18n();
|
|
30
|
+
const { itemCount, subtotal, totalDiscount } = useCart();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<style scoped lang="scss">
|
|
34
|
+
.cart-summary {
|
|
35
|
+
padding: 24px; background: #fff; border: 1px solid var(--primary-color); border-radius: 12px;
|
|
36
|
+
position: sticky; top: 90px;
|
|
37
|
+
}
|
|
38
|
+
.cart-summary__title { margin-bottom: 16px; font-size: 20px; }
|
|
39
|
+
.cart-summary__list {
|
|
40
|
+
list-style: none; padding: 0; margin: 0 0 12px;
|
|
41
|
+
li { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px dashed #e7e7e7; font-size: 15px; }
|
|
42
|
+
}
|
|
43
|
+
.cart-summary__discount { color: var(--primary-color); }
|
|
44
|
+
.cart-summary__total {
|
|
45
|
+
display: flex; justify-content: space-between; align-items: baseline; margin: 12px 0 18px; font-size: 18px;
|
|
46
|
+
strong { font-size: 26px; color: var(--primary-color); }
|
|
47
|
+
}
|
|
48
|
+
.cart-summary__hint { margin: -6px 0 14px; font-size: 13px; color: #57636c; i { color: var(--primary-color); } }
|
|
49
|
+
.cart-summary__cta { padding: 14px 20px; font-size: 18px; font-weight: 600; }
|
|
50
|
+
.cart-summary__continue {
|
|
51
|
+
display: block; text-align: center; margin-top: 14px; padding: 10px; color: var(--primary-color); text-decoration: none; font-weight: 500;
|
|
52
|
+
&:hover, &:focus-visible { text-decoration: underline; }
|
|
53
|
+
}
|
|
54
|
+
@media (max-width: 991.98px) {
|
|
55
|
+
.cart-summary { position: static; }
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="product-quantity">
|
|
3
3
|
<div class="add-to-cart-counter">
|
|
4
|
-
<div @click="
|
|
5
|
-
<input type="button" class="minusBtn" value="-" />
|
|
4
|
+
<div @click="change(-1)">
|
|
5
|
+
<input type="button" class="minusBtn" value="-" :disabled="modelValue <= min" />
|
|
6
6
|
</div>
|
|
7
|
-
<input
|
|
8
|
-
|
|
7
|
+
<input
|
|
8
|
+
type="text"
|
|
9
|
+
size="25"
|
|
10
|
+
class="count"
|
|
11
|
+
inputmode="numeric"
|
|
12
|
+
:value="modelValue"
|
|
13
|
+
@change="onInput"
|
|
14
|
+
/>
|
|
15
|
+
<div @click="change(1)">
|
|
9
16
|
<input type="button" class="plusBtn" value="+" />
|
|
10
17
|
</div>
|
|
11
18
|
</div>
|
|
@@ -13,17 +20,20 @@
|
|
|
13
20
|
</template>
|
|
14
21
|
|
|
15
22
|
<script lang="ts" setup>
|
|
16
|
-
|
|
23
|
+
// Лічильник кількості: v-model + мінімум (для прайс-листа може бути > 1)
|
|
24
|
+
const props = withDefaults(defineProps<{ modelValue: number; min?: number }>(), { min: 1 });
|
|
25
|
+
const emit = defineEmits<{ (e: 'update:modelValue', value: number): void }>();
|
|
17
26
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
internalValue.value++;
|
|
27
|
+
const change = (delta: number) => {
|
|
28
|
+
const next = props.modelValue + delta;
|
|
29
|
+
if (next >= props.min) emit('update:modelValue', next);
|
|
22
30
|
};
|
|
23
31
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
const onInput = (e: Event) => {
|
|
33
|
+
const el = e.target as HTMLInputElement;
|
|
34
|
+
const n = parseInt(el.value, 10);
|
|
35
|
+
const next = Number.isFinite(n) ? Math.max(props.min, n) : props.min;
|
|
36
|
+
el.value = String(next);
|
|
37
|
+
emit('update:modelValue', next);
|
|
28
38
|
};
|
|
29
39
|
</script>
|
|
@@ -1,165 +1,83 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="ptb-60">
|
|
3
3
|
<div class="container">
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<tr>
|
|
18
|
-
<td>
|
|
19
|
-
<NuxtLink to="/product-details">
|
|
20
|
-
<img
|
|
21
|
-
src="~/assets/images/product-6.png"
|
|
22
|
-
class="cart-product"
|
|
23
|
-
alt="product"
|
|
24
|
-
/>
|
|
25
|
-
</NuxtLink>
|
|
26
|
-
</td>
|
|
27
|
-
<td>
|
|
28
|
-
<NuxtLink to="/product-details" class="title">
|
|
29
|
-
Multiplatform Wireless Noise-Cancelling Gaming Headset
|
|
30
|
-
</NuxtLink>
|
|
31
|
-
</td>
|
|
32
|
-
<td>
|
|
33
|
-
<span class="price">$79</span>
|
|
34
|
-
</td>
|
|
35
|
-
<td>
|
|
36
|
-
<CartProductQuantity />
|
|
37
|
-
</td>
|
|
38
|
-
<td>
|
|
39
|
-
<span class="price">$79</span>
|
|
40
|
-
</td>
|
|
41
|
-
<td>
|
|
42
|
-
<button class="border-0 p-0 bg-transparent remove">
|
|
43
|
-
<i class="ri-delete-bin-line"></i>
|
|
44
|
-
</button>
|
|
45
|
-
</td>
|
|
46
|
-
</tr>
|
|
47
|
-
<tr>
|
|
48
|
-
<td>
|
|
49
|
-
<NuxtLink to="/product-details">
|
|
50
|
-
<img
|
|
51
|
-
src="~/assets/images/product-7.png"
|
|
52
|
-
class="cart-product"
|
|
53
|
-
alt="product"
|
|
54
|
-
/>
|
|
55
|
-
</NuxtLink>
|
|
56
|
-
</td>
|
|
57
|
-
<td>
|
|
58
|
-
<NuxtLink to="/product-details" class="title">
|
|
59
|
-
GPS Smartwatch with Bright Touchscreen Display
|
|
60
|
-
</NuxtLink>
|
|
61
|
-
</td>
|
|
62
|
-
<td>
|
|
63
|
-
<span class="price">$119</span>
|
|
64
|
-
</td>
|
|
65
|
-
<td>
|
|
66
|
-
<div class="product-quantity">
|
|
67
|
-
<CartProductQuantity />
|
|
68
|
-
</div>
|
|
69
|
-
</td>
|
|
70
|
-
<td>
|
|
71
|
-
<span class="price">$119</span>
|
|
72
|
-
</td>
|
|
73
|
-
<td>
|
|
74
|
-
<button class="border-0 p-0 bg-transparent remove">
|
|
75
|
-
<i class="ri-delete-bin-line"></i>
|
|
4
|
+
<ClientOnly>
|
|
5
|
+
<CartEmpty v-if="isEmpty" />
|
|
6
|
+
|
|
7
|
+
<div v-else class="row g-4">
|
|
8
|
+
<div class="col-lg-8">
|
|
9
|
+
<!-- Шапка списку -->
|
|
10
|
+
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
|
11
|
+
<h2 class="cart-title mb-0">
|
|
12
|
+
{{ t('cart.cart') }} <span class="cart-title__count">{{ cart.items.length }}</span>
|
|
13
|
+
</h2>
|
|
14
|
+
<div v-if="!confirmClear">
|
|
15
|
+
<button type="button" class="cart-clear" @click="confirmClear = true">
|
|
16
|
+
<i class="ri-delete-bin-line"></i> {{ t('cart.clear') }}
|
|
76
17
|
</button>
|
|
77
|
-
</
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
src="~/assets/images/product-8.png"
|
|
84
|
-
class="cart-product"
|
|
85
|
-
alt="product"
|
|
86
|
-
/>
|
|
87
|
-
</NuxtLink>
|
|
88
|
-
</td>
|
|
89
|
-
<td>
|
|
90
|
-
<NuxtLink to="/product-details" class="title">
|
|
91
|
-
SAMSUNG 32-Inch Class QLED 4K Q60C Series Quantum HDR
|
|
92
|
-
</NuxtLink>
|
|
93
|
-
</td>
|
|
94
|
-
<td>
|
|
95
|
-
<span class="price">$397</span>
|
|
96
|
-
</td>
|
|
97
|
-
<td>
|
|
98
|
-
<div class="product-quantity">
|
|
99
|
-
<CartProductQuantity />
|
|
100
|
-
</div>
|
|
101
|
-
</td>
|
|
102
|
-
<td>
|
|
103
|
-
<span class="price">$397</span>
|
|
104
|
-
</td>
|
|
105
|
-
<td>
|
|
106
|
-
<button class="border-0 p-0 bg-transparent remove">
|
|
107
|
-
<i class="ri-delete-bin-line"></i>
|
|
18
|
+
</div>
|
|
19
|
+
<div v-else class="cart-confirm" role="alert">
|
|
20
|
+
<span>{{ t('cart.clearConfirm') }}</span>
|
|
21
|
+
<button type="button" class="btn btn-sm btn-danger" @click="doClear">{{ t('cart.yes') }}</button>
|
|
22
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" @click="confirmClear = false">
|
|
23
|
+
{{ t('cart.cancel') }}
|
|
108
24
|
</button>
|
|
109
|
-
</
|
|
110
|
-
</
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
>
|
|
126
|
-
{{ t('cart.applyCoupon') }}
|
|
127
|
-
<i class="flaticon-up-right-arrow"></i>
|
|
128
|
-
</button>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="d-flex flex-column gap-3">
|
|
29
|
+
<CartItem
|
|
30
|
+
v-for="item in cart.items"
|
|
31
|
+
:key="item.productId"
|
|
32
|
+
:item="item"
|
|
33
|
+
@update="updateQuantity"
|
|
34
|
+
@remove="removeFromCart"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="col-lg-4">
|
|
40
|
+
<CartSummary />
|
|
129
41
|
</div>
|
|
130
42
|
</div>
|
|
131
|
-
<div class="col-xxl-4 col-lg-6 text-end">
|
|
132
|
-
<button
|
|
133
|
-
type="submit"
|
|
134
|
-
class="btn btn-warning text-white w-100 mt-3 mt-lg-0"
|
|
135
|
-
>
|
|
136
|
-
{{ t('cart.updatePrice') }}
|
|
137
|
-
<i class="flaticon-up-right-arrow"></i>
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
43
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<span class="title">{{ t('cart.subtotal') }}</span>
|
|
147
|
-
<span class="price">$595</span>
|
|
148
|
-
</li>
|
|
149
|
-
<li class="d-flex justify-content-between">
|
|
150
|
-
<span class="title">{{ t('cart.total') }}</span>
|
|
151
|
-
<span class="price">$595</span>
|
|
152
|
-
</li>
|
|
153
|
-
</ul>
|
|
154
|
-
<NuxtLink to="/checkout" class="btn btn-warning text-white">
|
|
155
|
-
{{ t('cart.proceedToCheckout') }}
|
|
156
|
-
<i class="flaticon-up-right-arrow"></i>
|
|
157
|
-
</NuxtLink>
|
|
158
|
-
</div>
|
|
44
|
+
<template #fallback>
|
|
45
|
+
<div class="text-center py-5 text-muted">{{ t('cart.loading') }}</div>
|
|
46
|
+
</template>
|
|
47
|
+
</ClientOnly>
|
|
159
48
|
</div>
|
|
160
49
|
</div>
|
|
161
50
|
</template>
|
|
162
51
|
|
|
163
52
|
<script setup lang="ts">
|
|
164
53
|
const { t } = useI18n();
|
|
54
|
+
const { cart, isEmpty, updateQuantity, removeFromCart, clearCart, syncPrices } = useCart();
|
|
55
|
+
const { fetchProductsByIds } = useProducts();
|
|
56
|
+
|
|
57
|
+
// Свіжі ціни/знижки для товарів, що вже лежать у кошику
|
|
58
|
+
onMounted(async () => {
|
|
59
|
+
const ids = cart.value.items.map((i) => i.productId);
|
|
60
|
+
if (ids.length && !cart.value.priceListId) syncPrices(await fetchProductsByIds(ids).catch(() => []));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Inline-підтвердження очищення (без window.confirm)
|
|
64
|
+
const confirmClear = ref(false);
|
|
65
|
+
const doClear = () => {
|
|
66
|
+
clearCart();
|
|
67
|
+
confirmClear.value = false;
|
|
68
|
+
};
|
|
165
69
|
</script>
|
|
70
|
+
|
|
71
|
+
<style scoped lang="scss">
|
|
72
|
+
.cart-title { font-size: 26px; }
|
|
73
|
+
.cart-title__count {
|
|
74
|
+
display: inline-block; min-width: 28px; padding: 0 8px; margin-left: 6px; border-radius: 14px; font-size: 15px;
|
|
75
|
+
line-height: 28px; text-align: center; background: var(--primary-color); color: #fff; vertical-align: middle;
|
|
76
|
+
}
|
|
77
|
+
.cart-clear {
|
|
78
|
+
min-height: 40px; padding: 0 14px; border: 1px solid #e7e7e7; border-radius: 10px; background: #fff; color: #c0392b; cursor: pointer;
|
|
79
|
+
&:hover { background: #fdecea; }
|
|
80
|
+
&:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
|
|
81
|
+
}
|
|
82
|
+
.cart-confirm { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; .btn { min-height: 40px; } }
|
|
83
|
+
</style>
|