@crm-market/template-shared 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/assets/scss/_variables.scss +3 -3
  2. package/assets/scss/components/_header.scss +1 -1
  3. package/assets/scss/style.css +4 -4
  4. package/components/AboutUs/StaticContent.vue +32 -0
  5. package/components/Cart/CartEmpty.vue +42 -0
  6. package/components/Cart/CartItem.vue +145 -0
  7. package/components/Cart/CartSummary.vue +57 -0
  8. package/components/Cart/ProductQuantity.vue +23 -13
  9. package/components/Cart/index.vue +68 -150
  10. package/components/Categories/index.vue +88 -291
  11. package/components/Checkout/AddressFields.vue +74 -0
  12. package/components/Checkout/CheckoutForm.vue +75 -0
  13. package/components/Checkout/CityAutocomplete.vue +68 -0
  14. package/components/Checkout/DeliveryMethodCard.vue +66 -0
  15. package/components/Checkout/DeliveryOptions.vue +148 -0
  16. package/components/Checkout/OrderSummary.vue +116 -0
  17. package/components/Checkout/PaymentOptions.vue +76 -0
  18. package/components/Checkout/PickupPoints.vue +46 -0
  19. package/components/Checkout/PromoCode.vue +166 -0
  20. package/components/Checkout/index.vue +235 -173
  21. package/components/Checkout/types.ts +45 -0
  22. package/components/Common/PageBanner.vue +22 -20
  23. package/components/Common/ProductCard.vue +139 -115
  24. package/components/Common/Services.vue +4 -4
  25. package/components/Contact/ContactForm.vue +44 -74
  26. package/components/Contact/ContactInfo.vue +35 -61
  27. package/components/FAQ/index.vue +6 -119
  28. package/components/Layout/Copyright.vue +11 -9
  29. package/components/Layout/Footer.vue +49 -136
  30. package/components/Layout/HeaderBar.vue +174 -0
  31. package/components/Layout/HeaderLogo.vue +27 -0
  32. package/components/Layout/MiddleHeader.vue +1 -228
  33. package/components/Layout/MiddleHeaderThree.vue +1 -203
  34. package/components/Layout/MiddleHeaderTwo.vue +1 -239
  35. package/components/Layout/NavLinks.vue +23 -0
  36. package/components/Layout/Navbar.vue +38 -151
  37. package/components/Layout/NavbarStyleFour.vue +1 -332
  38. package/components/Layout/NavbarStyleThree.vue +1 -188
  39. package/components/Layout/NavbarStyleTwo.vue +1 -108
  40. package/components/Layout/Preloader.vue +5 -12
  41. package/components/Layout/ResponsiveNavbar.vue +32 -418
  42. package/components/Layout/SitewideBanner.vue +285 -0
  43. package/components/Layout/TopHeader.vue +17 -83
  44. package/components/Layout/layoutData.ts +83 -0
  45. package/components/PrivacyPolicy/index.vue +6 -110
  46. package/components/ProductDetails/ProductDetailsTab.vue +31 -321
  47. package/components/ProductDetails/ProductGallery.vue +45 -0
  48. package/components/ProductDetails/ProductQuantity.vue +8 -14
  49. package/components/ProductDetails/RecentlyViewed.vue +53 -289
  50. package/components/ProductDetails/RelatedProducts.vue +64 -0
  51. package/components/ProductDetails/index.vue +90 -230
  52. package/components/Products/index.vue +78 -16
  53. package/components/TermsConditions/index.vue +6 -110
  54. package/components/Wishlist/index.vue +62 -88
  55. package/composables/useCart.ts +133 -81
  56. package/composables/useCategories.ts +66 -29
  57. package/composables/useCategoryImages.ts +45 -0
  58. package/composables/useCheckout.ts +302 -95
  59. package/composables/useContactForm.ts +57 -0
  60. package/composables/useHomeContent.ts +220 -0
  61. package/composables/useI18n.ts +12 -24
  62. package/composables/useJsonLd.ts +162 -0
  63. package/composables/useProducts.ts +244 -81
  64. package/composables/useRecent.ts +30 -0
  65. package/composables/useSanitize.ts +76 -0
  66. package/composables/useSiteConfig.ts +51 -45
  67. package/composables/useStaticPages.ts +48 -0
  68. package/composables/useStoreOrigin.ts +32 -0
  69. package/composables/useTemplateSections.ts +51 -3
  70. package/composables/useThemeVars.ts +113 -0
  71. package/composables/useWishlist.ts +72 -0
  72. package/layouts/default.vue +80 -19
  73. package/locales/en.json +255 -86
  74. package/locales/uk.json +251 -82
  75. package/modules/template-schema.ts +35 -0
  76. package/nuxt.config.ts +8 -11
  77. package/package.json +1 -1
  78. package/pages/about-us.vue +9 -9
  79. package/pages/cart.vue +0 -1
  80. package/pages/categories.vue +0 -1
  81. package/pages/category/[slug].vue +84 -0
  82. package/pages/checkout.vue +0 -1
  83. package/pages/contact.vue +6 -6
  84. package/pages/delivery.vue +12 -0
  85. package/pages/faq.vue +6 -6
  86. package/pages/order-success.vue +113 -0
  87. package/pages/privacy-policy.vue +6 -6
  88. package/pages/product/[slug].vue +82 -0
  89. package/pages/product-details.vue +14 -6
  90. package/pages/products.vue +12 -1
  91. package/pages/returns.vue +12 -0
  92. package/pages/terms-conditions.vue +6 -6
  93. package/pages/wishlist.vue +2 -2
  94. package/utils/seo-urls.ts +70 -0
  95. package/utils/store-origin.ts +36 -0
  96. package/components/AboutUs/AboutUsTuan.vue +0 -25
  97. package/components/AboutUs/Statistics.vue +0 -42
  98. package/components/AboutUs/SubscribeToTheNewsletter.vue +0 -57
  99. package/components/AddAddress/index.vue +0 -68
  100. package/components/BestSellers/Products.vue +0 -1562
  101. package/components/BestSellers/RecentlyViewed.vue +0 -304
  102. package/components/ChangePassword/index.vue +0 -69
  103. package/components/Common/DashboardNavigation.vue +0 -35
  104. package/components/Dashboard/RecentOrder.vue +0 -173
  105. package/components/Dashboard/index.vue +0 -77
  106. package/components/EditAddress/index.vue +0 -117
  107. package/components/EditProfile/index.vue +0 -95
  108. package/components/FeaturedProduct/FeaturedProducts.vue +0 -304
  109. package/components/FeaturedProduct/Products.vue +0 -1562
  110. package/components/ForgotPassword/index.vue +0 -49
  111. package/components/Layout/FooterTwo.vue +0 -159
  112. package/components/Layout/LocationOption.vue +0 -178
  113. package/components/Layout/RTLSwitchBtn.vue +0 -40
  114. package/components/Login/index.vue +0 -89
  115. package/components/MyAccount/index.vue +0 -154
  116. package/components/NewArrivals/Products.vue +0 -1969
  117. package/components/NewArrivals/RecentlyViewed.vue +0 -304
  118. package/components/OrderDetails/index.vue +0 -75
  119. package/components/OrderHistory/index.vue +0 -195
  120. package/components/Products/RecentlyViewed.vue +0 -302
  121. package/components/Register/index.vue +0 -83
  122. package/components/TrendingProducts/FeaturedProducts.vue +0 -304
  123. package/components/TrendingProducts/Products.vue +0 -1564
  124. package/components/Wishlist/ProductQuantity.vue +0 -29
  125. package/layouts/inner.vue +0 -70
  126. package/locales/ar.json +0 -234
  127. package/locales/ru.json +0 -234
  128. package/pages/address.vue +0 -11
  129. package/pages/change-password.vue +0 -11
  130. package/pages/dashboard.vue +0 -11
  131. package/pages/edit-address.vue +0 -11
  132. package/pages/edit-profile.vue +0 -11
  133. package/pages/forgot-password.vue +0 -11
  134. package/pages/login.vue +0 -11
  135. package/pages/my-account.vue +0 -11
  136. package/pages/order-details.vue +0 -11
  137. package/pages/order-history.vue +0 -11
  138. package/pages/register.vue +0 -11
@@ -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;
@@ -153,7 +153,7 @@
153
153
  }
154
154
  }
155
155
  .btn {
156
- font-family: "Satoshi-Bold";
156
+ font-family: var(--fontFamilyBold);
157
157
  padding: 10px 23px;
158
158
  i {
159
159
  margin-right: 5px;
@@ -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: "Satoshi-Bold";
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="decrement">
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 type="text" size="25" v-model="internalValue" class="count" />
8
- <div @click="increment">
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
- import { ref } from "vue";
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 internalValue = ref(0);
19
-
20
- const increment = () => {
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 decrement = () => {
25
- if (internalValue.value > 0) {
26
- internalValue.value--;
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
- <div class="table-responsive cart-table">
5
- <table class="table align-middle">
6
- <thead>
7
- <tr>
8
- <th scope="col">{{ t('product.product') }}</th>
9
- <th scope="col">{{ t('product.name') }}</th>
10
- <th scope="col">{{ t('product.unitPrice') }}</th>
11
- <th scope="col">{{ t('product.quantity') }}</th>
12
- <th scope="col">{{ t('product.subtotal') }}</th>
13
- <th scope="col">{{ t('product.remove') }}</th>
14
- </tr>
15
- </thead>
16
- <tbody>
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
- </td>
78
- </tr>
79
- <tr>
80
- <td>
81
- <NuxtLink to="/product-details">
82
- <img
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
- </td>
110
- </tr>
111
- </tbody>
112
- </table>
113
- </div>
114
- <div class="row justify-content-between mt-4">
115
- <div class="col-xxl-4 col-lg-6">
116
- <div class="d-md-flex coupon-wrap">
117
- <input
118
- type="text"
119
- class="form-control w-100"
120
- :placeholder="t('cart.coupon')"
121
- />
122
- <button
123
- type="submit"
124
- class="btn btn-warning text-white mt-3 mt-md-0 w-100"
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
- <div class="total-cart">
143
- <h3>{{ t('cart.cartTotals') }}</h3>
144
- <ul>
145
- <li class="d-flex justify-content-between">
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>