@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.
Files changed (139) 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 +67 -31
  57. package/composables/useCategoryImages.ts +45 -0
  58. package/composables/useCheckout.ts +302 -97
  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 -83
  64. package/composables/useRecent.ts +30 -0
  65. package/composables/useSanitize.ts +76 -0
  66. package/composables/useSiteConfig.ts +51 -47
  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/image.ts +0 -1
  95. package/utils/seo-urls.ts +70 -0
  96. package/utils/store-origin.ts +36 -0
  97. package/components/AboutUs/AboutUsTuan.vue +0 -25
  98. package/components/AboutUs/Statistics.vue +0 -42
  99. package/components/AboutUs/SubscribeToTheNewsletter.vue +0 -57
  100. package/components/AddAddress/index.vue +0 -68
  101. package/components/BestSellers/Products.vue +0 -1562
  102. package/components/BestSellers/RecentlyViewed.vue +0 -304
  103. package/components/ChangePassword/index.vue +0 -69
  104. package/components/Common/DashboardNavigation.vue +0 -35
  105. package/components/Dashboard/RecentOrder.vue +0 -173
  106. package/components/Dashboard/index.vue +0 -77
  107. package/components/EditAddress/index.vue +0 -117
  108. package/components/EditProfile/index.vue +0 -95
  109. package/components/FeaturedProduct/FeaturedProducts.vue +0 -304
  110. package/components/FeaturedProduct/Products.vue +0 -1562
  111. package/components/ForgotPassword/index.vue +0 -49
  112. package/components/Layout/FooterTwo.vue +0 -159
  113. package/components/Layout/LocationOption.vue +0 -178
  114. package/components/Layout/RTLSwitchBtn.vue +0 -40
  115. package/components/Login/index.vue +0 -89
  116. package/components/MyAccount/index.vue +0 -154
  117. package/components/NewArrivals/Products.vue +0 -1969
  118. package/components/NewArrivals/RecentlyViewed.vue +0 -304
  119. package/components/OrderDetails/index.vue +0 -75
  120. package/components/OrderHistory/index.vue +0 -195
  121. package/components/Products/RecentlyViewed.vue +0 -302
  122. package/components/Register/index.vue +0 -83
  123. package/components/TrendingProducts/FeaturedProducts.vue +0 -304
  124. package/components/TrendingProducts/Products.vue +0 -1564
  125. package/components/Wishlist/ProductQuantity.vue +0 -29
  126. package/layouts/inner.vue +0 -70
  127. package/locales/ar.json +0 -234
  128. package/locales/ru.json +0 -234
  129. package/pages/address.vue +0 -11
  130. package/pages/change-password.vue +0 -11
  131. package/pages/dashboard.vue +0 -11
  132. package/pages/edit-address.vue +0 -11
  133. package/pages/edit-profile.vue +0 -11
  134. package/pages/forgot-password.vue +0 -11
  135. package/pages/login.vue +0 -11
  136. package/pages/my-account.vue +0 -11
  137. package/pages/order-details.vue +0 -11
  138. package/pages/order-history.vue +0 -11
  139. package/pages/register.vue +0 -11
@@ -1,121 +1,8 @@
1
- <template>
2
- <div class="faq-area ptb-60">
3
- <div class="container">
4
- <div class="faq-content">
5
- <h2>Frequently Ask & Question</h2>
1
+ <script setup lang="ts">
2
+ // Питання та відповіді: HTML продавця або дефолтний текст
3
+ const { pageHtml } = useStaticPages();
4
+ </script>
6
5
 
7
- <div class="mb-40">
8
- <h3>How can I choose the right laptop for my needs?</h3>
9
- <p>
10
- Selecting the right laptop depends on your specific requirements.
11
- Consider factors such as intended use (work, gaming, or multimedia),
12
- processing power, storage capacity, and portability. Our
13
- knowledgeable staff can guide you based on your preferences and
14
- budget.
15
- </p>
16
- </div>
17
- <div class="mb-40">
18
- <h3>What's the difference between LED and OLED TVs?</h3>
19
- <p>
20
- LED TVs use a backlight to illuminate pixels, while OLED TVs have
21
- pixels that emit their own light. OLED TVs generally offer deeper
22
- blacks and more vibrant colors. The choice depends on your viewing
23
- preferences and budget. We can help you understand the differences
24
- and find the perfect TV for your space.
25
- </p>
26
- </div>
27
- <div class="mb-40">
28
- <h3>How can I protect my devices from malware and viruses?</h3>
29
- <p>
30
- Keeping your devices secure is crucial. Install reputable antivirus
31
- software, keep your operating system and applications up to date,
32
- and be cautious with email attachments and links. We offer a range
33
- of security solutions and can provide advice on best practices for
34
- maintaining a secure digital environment.
35
- </p>
36
- </div>
37
- <div class="mb-40">
38
- <h3>What's the best way to enhance home audio systems?</h3>
39
- <p>
40
- Improving your home audio experience can involve upgrading speakers,
41
- investing in a soundbar, or setting up a home theater system.
42
- Consider the size of your room, your preferred audio quality, and
43
- the type of content you enjoy. Our audio experts can recommend
44
- solutions tailored to your needs.
45
- </p>
46
- </div>
47
- <div class="mb-40">
48
- <h3>Do you offer tech support and repair services?</h3>
49
- <p>
50
- Yes, we provide tech support and repair services for a variety of
51
- devices. Whether you need assistance with software issues, hardware
52
- repairs, or upgrades, our skilled technicians are here to help.
53
- Contact us for information on our repair services and support
54
- options.
55
- </p>
56
- </div>
57
- <div class="mb-40">
58
- <h3>Do you offer online shopping and home delivery services?</h3>
59
- <p>
60
- Yes, we provide convenient online shopping options, and our home
61
- delivery service allows you to order groceries from the comfort of
62
- your home. Simply browse our online store, place your order, and
63
- have it delivered to your doorstep at your preferred time.
64
- </p>
65
- </div>
66
- <div class="mb-40">
67
- <h3>
68
- What measures do you take to ensure the freshness of your produce?
69
- </h3>
70
- <p>
71
- We prioritize the quality and freshness of our products. Our team
72
- follows stringent sourcing standards, and our produce is regularly
73
- restocked to ensure you receive the freshest fruits, vegetables, and
74
- other perishables.
75
- </p>
76
- </div>
77
- <div class="mb-40">
78
- <h3>
79
- Are there options for customers with dietary restrictions or
80
- preferences?
81
- </h3>
82
- <p>
83
- Absolutely! We strive to cater to various dietary needs. Our store
84
- offers a diverse range of products, including gluten-free, organic,
85
- and vegetarian options. Look for special labels or ask our staff for
86
- assistance in finding products that meet your specific requirements.
87
- </p>
88
- </div>
89
- <div class="mb-40">
90
- <h3>
91
- Do you have a loyalty program or discounts for regular customers?
92
- </h3>
93
- <p>
94
- Yes, we value our loyal customers. We offer a loyalty program where
95
- you can earn points with each purchase, and these points can be
96
- redeemed for discounts or special offers. Additionally, keep an eye
97
- out for exclusive promotions and discounts available to our regular
98
- shoppers.
99
- </p>
100
- </div>
101
- <div class="mb-0">
102
- <h3>
103
- How can I provide feedback or suggest products for your store?
104
- </h3>
105
- <p>
106
- We welcome your feedback and suggestions! You can share your
107
- thoughts with us through our website, customer service, or
108
- suggestion boxes in the store. Your input helps us improve our
109
- services and ensures we meet the needs of our valued customers.
110
- </p>
111
- </div>
112
- </div>
113
- </div>
114
- </div>
6
+ <template>
7
+ <AboutUsStaticContent :html="pageHtml('faq')" />
115
8
  </template>
116
-
117
- <script>
118
- export default {
119
- name: "FAQ",
120
- };
121
- </script>
@@ -1,23 +1,25 @@
1
+ <script setup lang="ts">
2
+ import { useLayoutData } from './layoutData';
3
+ const { t } = useI18n();
4
+ const { siteName } = useLayoutData();
5
+ </script>
6
+
1
7
  <template>
2
8
  <div class="copyright-area bg-white">
3
9
  <div class="container">
4
10
  <v-row class="align-items-center">
5
11
  <v-col cols="12" lg="6" md="6">
6
12
  <p class="mb-2 mb-md-0 text-center text-md-start">
7
- © <span>Tuan.</span> {{ t('footer.allRightsReserved') }}
8
- <a href="https://envytheme.com/" target="_blank">EnvyTheme</a>
13
+ © {{ new Date().getFullYear() }} {{ siteName }}
9
14
  </p>
10
15
  </v-col>
11
16
  <v-col cols="12" lg="6" md="6">
12
- <div class="text-center text-md-end">
13
- <img src="~/assets/images/payment.svg" alt="payment" />
14
- </div>
17
+ <ul class="list-unstyled d-flex flex-wrap justify-content-center justify-content-md-end gap-3 mb-0">
18
+ <li><NuxtLink to="/privacy-policy">{{ t('layout.footer.privacy') }}</NuxtLink></li>
19
+ <li><NuxtLink to="/terms-conditions">{{ t('layout.footer.terms') }}</NuxtLink></li>
20
+ </ul>
15
21
  </v-col>
16
22
  </v-row>
17
23
  </div>
18
24
  </div>
19
25
  </template>
20
-
21
- <script setup lang="ts">
22
- const { t } = useI18n();
23
- </script>
@@ -1,168 +1,81 @@
1
+ <script setup lang="ts">
2
+ import { useLayoutData } from './layoutData';
3
+ // Футер: дані магазину з конфігу CRM
4
+ const { t } = useI18n();
5
+ const { topLevelCategories } = useCategories();
6
+ const { siteName, siteDescription, phones, email, address, workingHours, socials } = useLayoutData();
7
+
8
+ const footerCategories = computed(() => topLevelCategories.value.slice(0, 6));
9
+ // Колонку «Контакти» показуємо, лише якщо є хоч одне значення
10
+ const hasContacts = computed(() => !!(phones.value.length || email.value || address.value || workingHours.value));
11
+ </script>
12
+
1
13
  <template>
2
14
  <div class="footer-area pb-60 bg-gray1">
3
15
  <div class="container">
4
16
  <div class="border-top pt-60 pb-35">
5
- <v-row class="justify-content-center">
6
- <v-col cols="12" lg="3" sm="6">
17
+ <v-row class="justify-content-start">
18
+ <v-col cols="12" lg="4" sm="6">
7
19
  <div class="single-footer-widget">
8
- <h3>{{ storeName }}</h3>
9
- <p v-if="siteConfig?.siteDescription">
10
- {{ siteConfig.siteDescription }}
11
- </p>
12
- <p v-else>
13
- {{ t('footer.storeDescription') }}
14
- </p>
15
-
16
- <div class="call-info" v-if="phone">
17
- <span class="title">{{ t('footer.gotQuestion') }}</span>
18
- <div class="d-flex align-items-center">
19
- <i class="flaticon-headphones-1"></i>
20
- <a :href="`tel:${phone}`">{{ phone }}</a>
21
- </div>
22
- </div>
23
- <div class="call-info" v-if="email">
24
- <a :href="`mailto:${email}`">{{ email }}</a>
25
- </div>
26
- <div class="mt-2" v-if="address">
27
- <small class="text-muted">{{ address }}</small>
28
- </div>
20
+ <h3>{{ siteName }}</h3>
21
+ <p v-if="siteDescription">{{ siteDescription }}</p>
22
+ <ul v-if="socials.length" class="ps-0 mb-0 mt-3 list-unstyled d-flex align-items-center social-link">
23
+ <li v-for="s in socials" :key="s.key">
24
+ <a :href="s.url" target="_blank" rel="noopener noreferrer" :aria-label="s.label">
25
+ <i :class="s.icon"></i>
26
+ </a>
27
+ </li>
28
+ </ul>
29
29
  </div>
30
30
  </v-col>
31
- <v-col cols="12" lg="3" sm="6">
31
+ <v-col cols="6" :lg="hasContacts ? 3 : 4" sm="6">
32
32
  <div class="single-footer-widget">
33
- <h3>{{ t('footer.popularCategories') }}</h3>
34
-
33
+ <h3>{{ t('layout.footer.categories') }}</h3>
35
34
  <ul class="ps-0 mb-0 list-unstyled import-link">
36
35
  <li v-for="category in footerCategories" :key="category.id">
37
- <NuxtLink :to="`/products?categoryId=${category.id}`">
38
- {{ category.name }}
39
- </NuxtLink>
36
+ <NuxtLink :to="categoryPath(category)">{{ category.name }}</NuxtLink>
40
37
  </li>
41
38
  <li v-if="footerCategories.length === 0">
42
- <NuxtLink to="/products">{{ t('nav.allProducts') }}</NuxtLink>
39
+ <NuxtLink to="/products">{{ t('layout.menu.catalog') }}</NuxtLink>
43
40
  </li>
44
41
  </ul>
45
42
  </div>
46
43
  </v-col>
47
- <v-col cols="12" lg="3" sm="6">
44
+ <v-col cols="6" :lg="hasContacts ? 2 : 4" sm="6">
48
45
  <div class="single-footer-widget">
49
- <h3>{{ t('footer.letUsHelp') }}</h3>
50
-
46
+ <h3>{{ t('layout.footer.info') }}</h3>
51
47
  <ul class="ps-0 mb-0 list-unstyled import-link">
52
- <li>
53
- <NuxtLink to="/dashboard">{{ t('account.yourAccount') }}</NuxtLink>
54
- </li>
55
- <li>
56
- <NuxtLink to="/order-history">{{ t('account.yourOrders') }}</NuxtLink>
57
- </li>
58
- <li>
59
- <NuxtLink to="/privacy-policy">{{ t('common.privacyPolicy') }}</NuxtLink>
60
- </li>
61
- <li>
62
- <NuxtLink to="/faq">{{ t('common.helpCenter') }}</NuxtLink>
63
- </li>
64
- <li>
65
- <NuxtLink to="/terms-conditions">{{ t('common.termsConditions') }}</NuxtLink>
66
- </li>
48
+ <li><NuxtLink to="/about-us">{{ t('layout.menu.about') }}</NuxtLink></li>
49
+ <li><NuxtLink to="/delivery">{{ t('pages.delivery.title') }}</NuxtLink></li>
50
+ <li><NuxtLink to="/returns">{{ t('pages.returns.title') }}</NuxtLink></li>
51
+ <li><NuxtLink to="/contact">{{ t('layout.menu.contact') }}</NuxtLink></li>
52
+ <li><NuxtLink to="/privacy-policy">{{ t('layout.footer.privacy') }}</NuxtLink></li>
53
+ <li><NuxtLink to="/terms-conditions">{{ t('layout.footer.terms') }}</NuxtLink></li>
67
54
  </ul>
68
55
  </div>
69
56
  </v-col>
70
- <v-col cols="12" lg="3" sm="6">
57
+ <v-col v-if="hasContacts" cols="12" lg="3" sm="6">
71
58
  <div class="single-footer-widget">
72
- <h3>{{ t('footer.getToKnowUs') }}</h3>
73
-
59
+ <h3>{{ t('layout.footer.contacts') }}</h3>
74
60
  <ul class="ps-0 mb-0 list-unstyled import-link">
75
- <li>
76
- <NuxtLink to="/about-us">{{ t('nav.aboutUs') }}</NuxtLink>
77
- </li>
78
- <li>
79
- <NuxtLink to="/contact">{{ t('nav.contact') }}</NuxtLink>
80
- </li>
81
- <li>
82
- <NuxtLink to="/categories">{{ t('nav.categories') }}</NuxtLink>
83
- </li>
84
- <li>
85
- <NuxtLink to="/products">{{ t('nav.allProducts') }}</NuxtLink>
86
- </li>
61
+ <li v-for="p in phones" :key="p"><a :href="`tel:${p}`">{{ p }}</a></li>
62
+ <li v-if="email"><a :href="`mailto:${email}`">{{ email }}</a></li>
63
+ <li v-if="address">{{ address }}</li>
64
+ <li v-if="workingHours">{{ workingHours }}</li>
87
65
  </ul>
88
66
  </div>
89
67
  </v-col>
90
68
  </v-row>
91
69
  </div>
92
- <v-row class="align-items-end justify-content-center">
93
- <v-col cols="12" lg="3" md="6">
94
- <div class="single-footer-bottom-item">
95
- <span class="title">{{ t('footer.subscribe') }}</span>
96
- <form class="subscribe-form position-relative" @submit.prevent>
97
- <input
98
- type="text"
99
- class="form-control"
100
- :placeholder="t('footer.enterEmail')"
101
- />
102
- <button
103
- class="btn btn-warning text-white position-absolute top-50 end-0 translate-middle-y"
104
- >
105
- {{ t('footer.submitNow') }}
106
- </button>
107
- </form>
108
- </div>
109
- </v-col>
110
- <v-col cols="12" lg="4" md="6">
111
- <div class="single-footer-bottom-item" v-if="hasSocialMedia">
112
- <ul
113
- class="ps-0 mb-0 list-unstyled d-flex align-items-center social-link justify-content-center"
114
- >
115
- <li>
116
- <span class="titles">{{ t('footer.followUs') }}:</span>
117
- </li>
118
- <li v-if="socialMedia?.facebook">
119
- <a :href="socialMedia.facebook" target="_blank">
120
- <i class="ri-facebook-fill"></i>
121
- </a>
122
- </li>
123
- <li v-if="socialMedia?.twitter">
124
- <a :href="socialMedia.twitter" target="_blank">
125
- <i class="ri-twitter-x-line"></i>
126
- </a>
127
- </li>
128
- <li v-if="socialMedia?.instagram">
129
- <a :href="socialMedia.instagram" target="_blank">
130
- <i class="ri-instagram-line"></i>
131
- </a>
132
- </li>
133
- </ul>
134
- </div>
135
- </v-col>
136
- <v-col cols="12" lg="5" md="6">
137
- <div class="single-footer-bottom-item">
138
- <p class="text-center text-lg-end mb-0 text-muted">
139
- &copy; {{ new Date().getFullYear() }} {{ storeName }}. {{ t('footer.allRightsReserved') }}.
140
- </p>
141
- </div>
142
- </v-col>
143
- </v-row>
144
70
  </div>
145
71
  </div>
146
72
  </template>
147
73
 
148
- <script setup lang="ts">
149
- import { computed } from "vue";
150
-
151
- const { t } = useI18n();
152
- const { siteConfig, siteName, contactInfo } = useSiteConfig();
153
- const { topLevelCategories } = useCategories();
154
-
155
- const storeName = computed(() => siteName.value || 'Our Store');
156
- const phone = computed(() => contactInfo.value?.phone);
157
- const email = computed(() => contactInfo.value?.email);
158
- const address = computed(() => contactInfo.value?.address);
159
- const socialMedia = computed(() => contactInfo.value?.socialMedia);
160
- const hasSocialMedia = computed(() => {
161
- const sm = socialMedia.value;
162
- return sm && (sm.facebook || sm.twitter || sm.instagram);
163
- });
164
-
165
- const footerCategories = computed(() =>
166
- topLevelCategories.value.slice(0, 6)
167
- );
168
- </script>
74
+ <style scoped>
75
+ /* Мобільний: колонки в один стовпчик з охайними відступами */
76
+ @media (max-width: 575.98px) {
77
+ .single-footer-widget { margin-bottom: 8px; }
78
+ .single-footer-widget p { overflow-wrap: anywhere; }
79
+ .single-footer-widget li { overflow-wrap: anywhere; }
80
+ }
81
+ </style>
@@ -0,0 +1,174 @@
1
+ <script setup lang="ts">
2
+ import stateStore from '~/utils/store';
3
+ import { useLayoutData } from './layoutData';
4
+ // Універсальна середня частина шапки; три варіанти лише передають props
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ withCategories?: boolean;
8
+ btnClass?: string;
9
+ areaClass?: string;
10
+ logoClass?: string;
11
+ searchClass?: string;
12
+ }>(),
13
+ { withCategories: true, btnClass: 'btn-warning', areaClass: '', logoClass: '', searchClass: '' },
14
+ );
15
+
16
+ const { t } = useI18n();
17
+ const router = useRouter();
18
+ const { topLevelCategories } = useCategories();
19
+ const { cart, itemCount, subtotal, isEmpty, removeFromCart } = useCart();
20
+ const { phone, wishlistCount, resolveMedia } = useLayoutData();
21
+
22
+ const searchQuery = ref('');
23
+ const catOpen = ref(false);
24
+ const cartOpen = ref(false);
25
+
26
+ // Пошук веде на каталог з параметром search
27
+ const handleSearch = () => {
28
+ const q = searchQuery.value.trim();
29
+ if (q) router.push(`/products?search=${encodeURIComponent(q)}`);
30
+ };
31
+ const money = (n: number) => `${n.toFixed(2)} ${t('layout.currency')}`;
32
+ </script>
33
+
34
+ <template>
35
+ <div :class="['middle-header-area ptb-20', areaClass]">
36
+ <div class="container">
37
+ <!-- Мобільний: ряд 1 — логотип + іконки + бургер, ряд 2 — пошук на всю ширину -->
38
+ <div class="hb-row d-flex flex-wrap align-items-center justify-content-between">
39
+ <div class="hb-left d-lg-flex align-items-center middle-header-left justify-content-lg-end">
40
+ <NuxtLink to="/" :class="['hb-logo middle-logo', logoClass]">
41
+ <LayoutHeaderLogo />
42
+ </NuxtLink>
43
+ <div class="hb-search d-flex">
44
+ <div v-if="props.withCategories" class="dropdown d-none d-lg-block">
45
+ <button
46
+ class="dropdown-toggle border border-2 bg-transparent all-categories"
47
+ :class="{ show: catOpen }"
48
+ @click="catOpen = !catOpen"
49
+ >
50
+ {{ t('layout.header.allCategories') }}
51
+ </button>
52
+ <ul class="dropdown-menu" :class="{ toggler: catOpen }">
53
+ <li v-for="category in topLevelCategories" :key="category.id">
54
+ <NuxtLink class="dropdown-item" :to="categoryPath(category)" @click="catOpen = false">
55
+ {{ category.name }}
56
+ </NuxtLink>
57
+ </li>
58
+ <li v-if="topLevelCategories.length === 0">
59
+ <span class="dropdown-item text-muted">{{ t('layout.header.noCategories') }}</span>
60
+ </li>
61
+ </ul>
62
+ </div>
63
+ <div :class="['position-relative hb-search-box', searchClass]">
64
+ <input
65
+ v-model="searchQuery"
66
+ type="text"
67
+ class="form-control border-2"
68
+ :placeholder="t('layout.header.searchPlaceholder')"
69
+ @keyup.enter="handleSearch"
70
+ />
71
+ <button
72
+ type="button"
73
+ :class="['position-absolute top-50 end-0 translate-middle-y btn fs-14 fw-medium text-white', btnClass]"
74
+ @click="handleSearch"
75
+ >
76
+ <i class="ri-search-line"></i>
77
+ <span class="d-none d-sm-inline-block">{{ t('layout.header.search') }}</span>
78
+ </button>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <ul class="hb-right d-flex align-items-center middle-header-right ps-0 mb-0 list-unstyled justify-content-end">
84
+ <li v-if="phone" class="info d-none d-lg-block">
85
+ <a :href="`tel:${phone}`" class="text-decoration-none">
86
+ <div class="d-flex align-items-center">
87
+ <div class="flex-shrink-0"><i class="ri-phone-line bg-danger-10"></i></div>
88
+ <div class="flex-grow-1 ms-10">
89
+ <span>{{ t('layout.header.hotline') }}</span>
90
+ <h3>{{ phone }}</h3>
91
+ </div>
92
+ </div>
93
+ </a>
94
+ </li>
95
+ <li class="buy-info tops-4 position-relative">
96
+ <NuxtLink to="/wishlist" class="text-decoration-none success-hover-bg" :aria-label="t('layout.header.wishlist')">
97
+ <div class="position-relative me-9">
98
+ <i class="ri-heart-line"></i>
99
+ <ClientOnly><span class="count bg-success">{{ wishlistCount }}</span><template #fallback><span class="count bg-success">0</span></template></ClientOnly>
100
+ </div>
101
+ </NuxtLink>
102
+ </li>
103
+ <li class="buy-info">
104
+ <div class="dropdown">
105
+ <a
106
+ class="text-decoration-none success-hover-bg"
107
+ href="javascript:void(0);"
108
+ :class="{ active: cartOpen }"
109
+ :aria-label="t('layout.header.cart')"
110
+ @click="cartOpen = !cartOpen"
111
+ >
112
+ <div class="position-relative me-9">
113
+ <i class="ri-shopping-cart-line"></i>
114
+ <ClientOnly><span class="count bg-success">{{ itemCount }}</span><template #fallback><span class="count bg-success">0</span></template></ClientOnly>
115
+ </div>
116
+ </a>
117
+ <ul class="dropdown-menu cart-dropdown dropdown-menu-end" :class="{ toggler: cartOpen }">
118
+ <!-- Вміст кошика з localStorage — лише на клієнті, щоб не було hydration-розбіжностей -->
119
+ <ClientOnly>
120
+ <li v-for="item in cart.items.slice(0, 3)" :key="item.productId" class="d-flex justify-content-between align-items-center cart-item">
121
+ <button class="p-0 border-0 bg-transparent delete-btn" :aria-label="t('layout.header.remove')" @click="removeFromCart(item.productId)">
122
+ <i class="ri-delete-bin-7-line"></i>
123
+ </button>
124
+ <NuxtLink class="dropdown-item d-flex align-items-center" :to="productPath({ id: item.productId, name: item.name })" @click="cartOpen = false">
125
+ <img v-if="item.image" :src="resolveMedia(item.image)" :alt="item.name" />
126
+ <div class="ms-3 d-inline-block text-truncate">
127
+ <span>{{ item.name }}</span>
128
+ <h3>{{ money(item.discountPrice || item.price) }}</h3>
129
+ </div>
130
+ </NuxtLink>
131
+ </li>
132
+ <li v-if="isEmpty" class="cart-item text-center text-muted py-3">{{ t('layout.header.cartEmpty') }}</li>
133
+ <template v-else>
134
+ <li class="d-flex justify-content-between align-items-center cart-item">
135
+ <h3 class="fs-15 mb-0">{{ t('layout.header.total') }}</h3>
136
+ <h3 class="fs-15 mb-0">{{ money(subtotal) }}</h3>
137
+ </li>
138
+ <li class="d-flex justify-content-between align-items-center cart-item">
139
+ <NuxtLink to="/cart" class="btn btn-primary fs-14 text-white" @click="cartOpen = false">{{ t('layout.header.viewCart') }}</NuxtLink>
140
+ <NuxtLink to="/checkout" class="btn btn-warning fs-14 text-white" @click="cartOpen = false">{{ t('layout.header.checkout') }}</NuxtLink>
141
+ </li>
142
+ </template>
143
+ </ClientOnly>
144
+ </ul>
145
+ </div>
146
+ </li>
147
+ <li class="d-lg-none hb-burger">
148
+ <button type="button" class="btn p-0 border-0 bg-transparent" :aria-label="t('layout.menu.toggle')" @click="stateStore.onChangeShow">
149
+ <i class="ri-menu-line"></i>
150
+ </button>
151
+ </li>
152
+ </ul>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </template>
157
+
158
+ <style scoped>
159
+ /* Мобільна розкладка шапки: від lg — оригінальна тема */
160
+ @media (max-width: 991.98px) {
161
+ .hb-left { display: contents; }
162
+ .hb-logo { order: 1; display: block; max-width: 45%; margin: 0 !important; flex: 0 1 auto; }
163
+ .hb-row { row-gap: 0; }
164
+ .hb-logo :deep(img) { max-height: 40px; width: auto; max-width: 100%; }
165
+ .hb-right { order: 2; gap: 14px; }
166
+ .hb-right :deep(.buy-info) { margin-right: 0 !important; }
167
+ .hb-search { order: 3; width: 100%; max-width: 100%; margin-top: 12px; }
168
+ .hb-search-box { width: 100%; }
169
+ .hb-search-box { flex: 1 1 auto; min-width: 0; }
170
+ .hb-search-box :deep(.form-control) { width: 100% !important; }
171
+ .hb-burger i { font-size: 28px; color: var(--primary-color); line-height: 1; }
172
+ .hb-right :deep(.cart-dropdown) { position: absolute; right: 0; max-width: calc(100vw - 32px); }
173
+ }
174
+ </style>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { useLayoutData } from './layoutData';
3
+ // Логотип магазину: картинка з конфігу або текст з назвою
4
+ defineProps<{ imgClass?: string }>();
5
+ const { logo, siteName } = useLayoutData();
6
+ </script>
7
+
8
+ <template>
9
+ <img v-if="logo" :src="logo" :alt="siteName" :class="imgClass" style="max-height: 48px; width: auto" />
10
+ <span v-else class="header-logo-text fs-4 fw-bold text-dark" :title="siteName">{{ siteName }}</span>
11
+ </template>
12
+
13
+
14
+ <style scoped>
15
+ /* Довга назва магазину не повинна ламати шапку: один рядок з трикрапкою */
16
+ .header-logo-text {
17
+ display: inline-block;
18
+ max-width: 260px;
19
+ white-space: nowrap;
20
+ overflow: hidden;
21
+ text-overflow: ellipsis;
22
+ vertical-align: middle;
23
+ }
24
+ @media (max-width: 575.98px) {
25
+ .header-logo-text { max-width: 170px; font-size: 1.1rem !important; }
26
+ }
27
+ </style>