@shopware/cms-base-layer 1.5.1 → 2.0.0

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 (183) hide show
  1. package/README.md +330 -12
  2. package/app/app.config.ts +7 -0
  3. package/app/assets/icons/check-circle.svg +3 -0
  4. package/app/assets/icons/checkmark.svg +3 -0
  5. package/app/assets/icons/chevron.svg +3 -0
  6. package/app/assets/icons/exclamation-circle.svg +3 -0
  7. package/app/assets/icons/star-empty.svg +3 -0
  8. package/app/assets/icons/star-filled.svg +3 -0
  9. package/app/assets/icons/user.svg +1 -0
  10. package/app/components/SwCategoryNavigation.vue +76 -0
  11. package/app/components/SwCategoryNavigationLink.vue +128 -0
  12. package/{components → app/components}/SwContactForm.vue +27 -27
  13. package/app/components/SwFilterChips.vue +144 -0
  14. package/app/components/SwListingProductPrice.vue +89 -0
  15. package/{components → app/components}/SwNewsletterForm.vue +45 -34
  16. package/{components → app/components}/SwPagination.vue +3 -5
  17. package/{components → app/components}/SwProductAddToCart.vue +22 -27
  18. package/app/components/SwProductCard.vue +170 -0
  19. package/app/components/SwProductCardDetails.vue +57 -0
  20. package/app/components/SwProductCardImage.vue +87 -0
  21. package/app/components/SwProductCardSkeleton.vue +33 -0
  22. package/app/components/SwProductListingFilter.vue +64 -0
  23. package/app/components/SwProductListingFilters.vue +308 -0
  24. package/{components → app/components}/SwProductReviews.vue +28 -13
  25. package/app/components/SwProductReviewsForm.vue +292 -0
  26. package/app/components/SwQuantitySelect.vue +106 -0
  27. package/{components → app/components}/SwSlider.vue +4 -4
  28. package/app/components/SwSortDropdown.vue +83 -0
  29. package/app/components/SwStockInfo.vue +44 -0
  30. package/{components → app/components}/SwVariantConfigurator.vue +1 -1
  31. package/app/components/listing-filters/SwFilterPrice.vue +214 -0
  32. package/app/components/listing-filters/SwFilterProperties.vue +113 -0
  33. package/app/components/listing-filters/SwFilterRating.vue +90 -0
  34. package/app/components/listing-filters/SwFilterShippingFree.vue +107 -0
  35. package/{components → app/components}/public/cms/CmsPage.vue +19 -4
  36. package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
  37. package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
  38. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
  39. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
  40. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
  41. package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
  42. package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
  43. package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
  44. package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
  45. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
  46. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
  47. package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
  48. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
  49. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
  50. package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
  51. package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
  52. package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
  53. package/app/components/public/cms/block/CmsBlockTextOnImage.vue +30 -0
  54. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  55. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  56. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
  57. package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
  58. package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
  59. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  60. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
  61. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  62. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
  63. package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
  64. package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
  65. package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
  66. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  67. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
  68. package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
  69. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  70. package/app/components/ui/BaseButton.vue +99 -0
  71. package/app/components/ui/BaseIcon.vue +15 -0
  72. package/app/components/ui/Checkbox.vue +49 -0
  73. package/app/components/ui/CheckmarkIcon.vue +23 -0
  74. package/app/components/ui/ChevronIcon.vue +37 -0
  75. package/app/components/ui/ExclamationIcon.vue +11 -0
  76. package/app/components/ui/IconButton.vue +32 -0
  77. package/app/components/ui/RadioButton.vue +26 -0
  78. package/app/components/ui/StarIcon.vue +18 -0
  79. package/app/components/ui/SwitchButton.vue +100 -0
  80. package/app/components/ui/UserIcon.vue +11 -0
  81. package/app/components/ui/WishlistIcon.vue +20 -0
  82. package/app/composables/useImagePlaceholder.ts +27 -0
  83. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  84. package/app/providers/shopware.test.ts +213 -0
  85. package/app/providers/shopware.ts +107 -0
  86. package/dist/index.d.mts +3 -3
  87. package/dist/index.d.ts +3 -3
  88. package/dist/index.mjs +2 -2
  89. package/index.d.ts +12 -0
  90. package/nuxt.config.ts +80 -6
  91. package/package.json +29 -21
  92. package/uno.config.ts +83 -0
  93. package/components/SwCategoryNavigation.vue +0 -44
  94. package/components/SwCategoryNavigationLink.vue +0 -57
  95. package/components/SwListingProductPrice.vue +0 -89
  96. package/components/SwProductCard.vue +0 -286
  97. package/components/SwProductListingFilter.vue +0 -42
  98. package/components/SwProductListingFilters.vue +0 -292
  99. package/components/listing-filters/SwFilterPrice.vue +0 -160
  100. package/components/listing-filters/SwFilterProperties.vue +0 -123
  101. package/components/listing-filters/SwFilterRating.vue +0 -101
  102. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  103. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  104. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  105. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  106. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  107. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  108. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  109. package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
  110. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  111. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  112. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  113. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  114. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  115. package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
  116. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  117. /package/{components → app/components}/SwMedia3D.vue +0 -0
  118. /package/{components → app/components}/SwProductGallery.vue +0 -0
  119. /package/{components → app/components}/SwProductPrice.vue +0 -0
  120. /package/{components → app/components}/SwProductUnits.vue +0 -0
  121. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  122. /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
  123. /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
  124. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  125. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  126. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  127. /package/{components → app/components}/public/cms/CmsPage.md +0 -0
  128. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  129. /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
  130. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  131. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  132. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  133. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  134. /package/{components → app/components}/public/cms/block/CmsBlockHtml.vue +0 -0
  135. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  136. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  137. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  138. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  139. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  140. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  141. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  142. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  143. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  144. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  145. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +0 -0
  146. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  147. /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +0 -0
  148. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  150. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  151. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  152. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  153. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  154. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  155. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  156. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  157. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  158. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  159. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  160. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  161. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  162. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  163. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  164. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  165. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  166. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  167. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  168. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  169. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +0 -0
  176. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  177. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  178. /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
  179. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  180. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
  181. /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
  182. /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
  183. /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
@@ -1,286 +0,0 @@
1
- <script setup lang="ts">
2
- import { ApiClientError } from "@shopware/api-client";
3
- import type { BoxLayout, DisplayMode } from "@shopware/composables";
4
- import { useCmsTranslations } from "@shopware/composables";
5
- import {
6
- buildUrlPrefix,
7
- getProductFromPrice,
8
- getProductName,
9
- getProductRoute,
10
- getSmallestThumbnailUrl,
11
- } from "@shopware/helpers";
12
- import { getCmsTranslate } from "@shopware/helpers";
13
- import { useElementSize } from "@vueuse/core";
14
- import { defu } from "defu";
15
- import { computed, ref, toRefs, useTemplateRef } from "vue";
16
- import { RouterLink } from "vue-router";
17
- import {
18
- useAddToCart,
19
- useCartErrorParamsResolver,
20
- useCartNotification,
21
- useNotifications,
22
- useProductWishlist,
23
- useUrlResolver,
24
- } from "#imports";
25
- import type { Schemas } from "#shopware";
26
-
27
- const { pushSuccess, pushError } = useNotifications();
28
- const { getErrorsCodes } = useCartNotification();
29
- const { resolveCartError } = useCartErrorParamsResolver();
30
-
31
- const props = withDefaults(
32
- defineProps<{
33
- product: Schemas["Product"];
34
- layoutType?: BoxLayout;
35
- isProductListing?: boolean;
36
- displayMode?: DisplayMode;
37
- }>(),
38
- {
39
- layoutType: "standard",
40
- displayMode: "standard",
41
- isProductListing: false,
42
- },
43
- );
44
-
45
- type Translations = {
46
- product: {
47
- addedToWishlist: string;
48
- removedFromTheWishlist: string;
49
- reason: string;
50
- cannotAddToWishlist: string;
51
- addedToCart: string;
52
- addToCart: string;
53
- details: string;
54
- badges: {
55
- topseller: string;
56
- };
57
- };
58
- errors: {
59
- [key: string]: string;
60
- };
61
- };
62
-
63
- let translations: Translations = {
64
- product: {
65
- addedToWishlist: "has been added to wishlist.",
66
- removedFromTheWishlist: "has been removed from wishlist.",
67
- reason: "Reason",
68
- cannotAddToWishlist: "cannot be added to wishlist.",
69
- addedToCart: "has been added to cart.",
70
- addToCart: "Add to cart",
71
- details: "Details",
72
- badges: {
73
- topseller: "Tip",
74
- },
75
- },
76
- errors: {
77
- "product-stock-reached":
78
- "The product {name} is only available {quantity} times",
79
- },
80
- };
81
-
82
- translations = defu(useCmsTranslations(), translations) as Translations;
83
-
84
- const { product } = toRefs(props);
85
-
86
- const { addToCart, isInCart, count } = useAddToCart(product);
87
-
88
- const { addToWishlist, removeFromWishlist, isInWishlist } = useProductWishlist(
89
- product.value.id,
90
- );
91
- const isLoading = ref(false);
92
-
93
- const toggleWishlistProduct = async () => {
94
- isLoading.value = true;
95
-
96
- try {
97
- if (!isInWishlist.value) {
98
- await addToWishlist();
99
- pushSuccess(
100
- `${props.product?.translated.name} ${translations.product.addedToWishlist}`,
101
- );
102
- } else {
103
- await removeFromWishlist();
104
- pushError(
105
- `${props.product?.translated.name} ${translations.product.removedFromTheWishlist}`,
106
- );
107
- }
108
- } catch (error) {
109
- if (error instanceof ApiClientError) {
110
- const reason = error.details.errors?.[0]?.detail
111
- ? `${translations.product.reason}: ${error.details.errors?.[0]?.detail}`
112
- : "";
113
- return pushError(
114
- `${props.product?.translated.name} ${translations.product.cannotAddToWishlist}\n${reason}`,
115
- {
116
- timeout: 5000,
117
- },
118
- );
119
- }
120
- } finally {
121
- isLoading.value = false;
122
- }
123
- };
124
-
125
- const addToCartProxy = async () => {
126
- await addToCart();
127
- const errors = getErrorsCodes();
128
- for (const element of errors) {
129
- const { messageKey, params } = resolveCartError(element);
130
- if (translations.errors[messageKey])
131
- pushError(getCmsTranslate(translations.errors[messageKey], params));
132
- }
133
-
134
- if (!errors.length)
135
- pushSuccess(
136
- `${props.product?.translated.name} ${translations.product.addedToCart}`,
137
- );
138
- };
139
-
140
- const fromPrice = getProductFromPrice(props.product);
141
- const { getUrlPrefix } = useUrlResolver();
142
-
143
- const imageElement = useTemplateRef("imageElement");
144
- const { height } = useElementSize(imageElement);
145
-
146
- const DEFAULT_THUMBNAIL_SIZE = 10;
147
- function roundUp(num: number) {
148
- return num ? Math.ceil(num / 100) * 100 : DEFAULT_THUMBNAIL_SIZE;
149
- }
150
-
151
- const srcPath = computed(() => {
152
- return `${getSmallestThumbnailUrl(
153
- product.value?.cover?.media,
154
- )}?&height=${roundUp(height.value)}&fit=cover`;
155
- });
156
- </script>
157
-
158
- <template>
159
- <div
160
- class="sw-product-card group relative max-w-full inline-block max-w-sm bg-white border border-gray-200 rounded-md shadow transform transition duration-300 hover:scale-101"
161
- data-testid="product-box"
162
- >
163
- <div
164
- :class="[
165
- 'w-full rounded-md overflow-hidden hover:opacity-75',
166
- layoutType === 'image' ? 'h-80' : 'h-60',
167
- ]"
168
- >
169
- <div class="absolute top-5 -left-1 z-10">
170
- <span
171
- v-if="product.markAsTopseller"
172
- class="bg-[#FFBD5D] px-2.5 py-1.5 color-black text-xl"
173
- >{{ translations.product.badges.topseller }}</span
174
- >
175
- </div>
176
- <RouterLink
177
- :to="buildUrlPrefix(getProductRoute(product), getUrlPrefix())"
178
- class="overflow-hidden"
179
- >
180
- <img
181
- ref="imageElement"
182
- loading="lazy"
183
- :src="srcPath"
184
- :alt="getProductName({ product }) || ''"
185
- class="transform transition duration-400 hover:scale-120"
186
- :class="{
187
- 'w-full h-full': true,
188
- 'object-cover':
189
- displayMode === 'cover' ||
190
- (displayMode === 'standard' && layoutType === 'image'),
191
- 'object-contain': displayMode === 'contain',
192
- 'object-scale-down':
193
- displayMode === 'standard' && layoutType !== 'image',
194
- }"
195
- data-testid="product-box-img"
196
- />
197
- </RouterLink>
198
- </div>
199
- <button
200
- aria-label="Add to wishlist"
201
- type="button"
202
- :disabled="isLoading"
203
- class="absolute bg-transparent top-2 right-2 hover:animate-count-infinite hover:animate-heart-beat"
204
- data-testid="product-box-toggle-wishlist-button"
205
- @click="toggleWishlistProduct"
206
- >
207
- <client-only>
208
- <div
209
- v-if="isInWishlist"
210
- class="h-9 w-9 i-carbon-favorite-filled c-red-500"
211
- data-testid="product-box-wishlist-icon-in"
212
- ></div>
213
- <div
214
- v-else
215
- class="h-9 w-9 i-carbon-favorite c-black"
216
- data-testid="product-box-wishlist-icon-not-in"
217
- ></div>
218
- <template #placeholder>
219
- <div class="h-9 w-9 i-carbon-favorite"></div>
220
- </template>
221
- </client-only>
222
- </button>
223
- <div class="h-8 mx-4 my-2">
224
- <p
225
- v-for="option in product?.options"
226
- :key="option.id"
227
- class="items-center line-clamp-2 rounded-md text-xs font-medium text-gray-600 mt-3"
228
- >
229
- {{ option.group.name }}:
230
- <span class="font-bold">{{ option.name }} </span>
231
- </p>
232
- </div>
233
- <div class="px-4 pb-4">
234
- <RouterLink
235
- class="line-clamp-2"
236
- :to="buildUrlPrefix(getProductRoute(product), getUrlPrefix())"
237
- data-testid="product-box-product-name-link"
238
- >
239
- <div
240
- class="text-xl font-semibold tracking-tight text-gray-900 dark:text-white min-h-60px"
241
- >
242
- {{ getProductName({ product }) }}
243
- </div>
244
- </RouterLink>
245
-
246
- <SwListingProductPrice
247
- :product="product"
248
- class="ml-auto"
249
- data-testid="product-box-product-price"
250
- />
251
-
252
- <div>
253
- <button
254
- v-if="!fromPrice"
255
- type="button"
256
- class="w-full justify-center my-8 md-m-0 py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transform transition duration-400 flex"
257
- :class="{
258
- 'text-white bg-blue-600 hover:bg-blue-700': !isInCart,
259
- 'text-gray-600 bg-gray-100': isInCart,
260
- 'opacity-50 cursor-not-allowed': !product.available,
261
- }"
262
- data-testid="add-to-cart-button"
263
- :disabled="!product.available"
264
- @click="addToCartProxy"
265
- >
266
- {{ translations.product.addToCart }}
267
- <div v-if="isInCart" class="flex ml-2">
268
- <div class="w-5 h-5 i-carbon-shopping-bag text-gray-600" />
269
- {{ count }}
270
- </div>
271
- </button>
272
- <RouterLink
273
- v-else
274
- :to="buildUrlPrefix(getProductRoute(product), getUrlPrefix())"
275
- class=""
276
- >
277
- <div
278
- class="text-center justify-center w-full md:w-auto my-8 md-m-0 py-2 px-3 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-black hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transform transition duration-400"
279
- >
280
- <span data-testid="product-box-product-show-details">Details</span>
281
- </div>
282
- </RouterLink>
283
- </div>
284
- </div>
285
- </div>
286
- </template>
@@ -1,42 +0,0 @@
1
- <script setup lang="ts" generic="ListingFilter extends { code: string }">
2
- import type { Component } from "vue";
3
- import SwFilterPriceVue from "./listing-filters/SwFilterPrice.vue";
4
- import SwFilterPropertiesVue from "./listing-filters/SwFilterProperties.vue";
5
- import SwFilterRatingVue from "./listing-filters/SwFilterRating.vue";
6
- import SwFilterShippingFreeVue from "./listing-filters/SwFilterShippingFree.vue";
7
-
8
- const props = defineProps<{
9
- filter: ListingFilter;
10
- selectedFilters?: {
11
- [key: string]: unknown;
12
- };
13
- }>();
14
-
15
- const emit = defineEmits<{
16
- selectFilterValue: [{ code: string; value: string | number | boolean }];
17
- }>();
18
-
19
- const cmsMap = () => {
20
- const map: {
21
- [key: string]: Component;
22
- } = {
23
- manufacturer: SwFilterPropertiesVue,
24
- properties: SwFilterPropertiesVue,
25
- price: SwFilterPriceVue,
26
- rating: SwFilterRatingVue,
27
- "shipping-free": SwFilterShippingFreeVue,
28
- };
29
-
30
- return map[props.filter?.code];
31
- };
32
- </script>
33
- <template>
34
- <div>
35
- <component
36
- :is="cmsMap()"
37
- :filter="filter"
38
- :selected-filters="selectedFilters"
39
- @select-value="emit('selectFilterValue', $event)"
40
- />
41
- </div>
42
- </template>
@@ -1,292 +0,0 @@
1
- <script setup lang="ts">
2
- import type {
3
- CmsElementProductListing,
4
- CmsElementSidebarFilter,
5
- } from "@shopware/composables";
6
- import { useCmsTranslations } from "@shopware/composables";
7
- import { onClickOutside } from "@vueuse/core";
8
- import { defu } from "defu";
9
- import { computed, provide, reactive, ref, useTemplateRef } from "vue";
10
- import type { ComputedRef, UnwrapNestedRefs } from "vue";
11
- import { type LocationQueryRaw, useRoute, useRouter } from "vue-router";
12
- import { useCategoryListing } from "#imports";
13
- import type { Schemas, operations } from "#shopware";
14
-
15
- const props = defineProps<{
16
- content: CmsElementProductListing | CmsElementSidebarFilter;
17
- listingType?: string;
18
- }>();
19
-
20
- type Translations = {
21
- listing: {
22
- filters: string;
23
- sort: string;
24
- resetFilters: string;
25
- };
26
- };
27
-
28
- let translations: Translations = {
29
- listing: {
30
- filters: "Filters",
31
- sort: "Sort",
32
- resetFilters: "Reset filters",
33
- },
34
- };
35
-
36
- translations = defu(useCmsTranslations(), translations) as Translations;
37
-
38
- const route = useRoute();
39
- const router = useRouter();
40
-
41
- const isSortMenuOpen = ref(false);
42
- const {
43
- changeCurrentSortingOrder,
44
- filtersToQuery,
45
- getCurrentFilters,
46
- getCurrentSortingOrder,
47
- getInitialFilters,
48
- getSortingOrders,
49
- search,
50
- } = useCategoryListing();
51
-
52
- const sidebarSelectedFilters: UnwrapNestedRefs<{
53
- [key: string]: Set<string> | string | number | boolean | undefined;
54
- }> = reactive<{
55
- manufacturer: Set<string>;
56
- properties: Set<string>;
57
- "min-price": string | number | undefined;
58
- "max-price": string | number | undefined;
59
- rating: string | number | undefined;
60
- "shipping-free": boolean | undefined;
61
- }>({
62
- manufacturer: new Set(),
63
- properties: new Set(),
64
- "min-price": undefined,
65
- "max-price": undefined,
66
- rating: undefined,
67
- "shipping-free": undefined,
68
- });
69
-
70
- const showResetFiltersButton = computed<boolean>(() => {
71
- if (
72
- selectedOptionIds.value.length !== 0 ||
73
- sidebarSelectedFilters["max-price"] ||
74
- sidebarSelectedFilters["min-price"] ||
75
- sidebarSelectedFilters.rating ||
76
- sidebarSelectedFilters["shipping-free"]
77
- ) {
78
- return true;
79
- }
80
-
81
- return false;
82
- });
83
-
84
- const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
85
- computed(() => ({
86
- manufacturer: [
87
- ...(sidebarSelectedFilters.manufacturer as Set<string>),
88
- ]?.join("|"),
89
- properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
90
- "|",
91
- ),
92
- "min-price": sidebarSelectedFilters["min-price"] as number,
93
- "max-price": sidebarSelectedFilters["max-price"] as number,
94
- order: getCurrentSortingOrder.value as string,
95
- "shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
96
- rating: sidebarSelectedFilters.rating as number,
97
- search: "",
98
- limit: route.query.limit ? Number(route.query.limit) : 15,
99
- }));
100
-
101
- for (const param in route.query) {
102
- if (Object.prototype.hasOwnProperty.call(sidebarSelectedFilters, param)) {
103
- if (
104
- sidebarSelectedFilters[param] &&
105
- typeof sidebarSelectedFilters[param] === "object"
106
- ) {
107
- const elements = (route.query[param] as unknown as string).split("|");
108
- for (const element of elements) {
109
- sidebarSelectedFilters[param].add(element);
110
- }
111
- } else {
112
- const queryValue = route.query[param];
113
- if (queryValue !== null && !Array.isArray(queryValue)) {
114
- sidebarSelectedFilters[param] = queryValue;
115
- }
116
- }
117
- }
118
- }
119
-
120
- const onOptionSelectToggle = async ({
121
- code,
122
- value,
123
- }: {
124
- code: string;
125
- value: string | number | boolean;
126
- }) => {
127
- if (!["properties", "manufacturer"].includes(code)) {
128
- sidebarSelectedFilters[code] = value;
129
- } else {
130
- const filterSet = sidebarSelectedFilters[code] as Set<string>;
131
- const stringValue = String(value);
132
- if (filterSet?.has(stringValue)) {
133
- filterSet.delete(stringValue);
134
- } else {
135
- filterSet?.add(stringValue);
136
- }
137
- }
138
-
139
- await executeSearch();
140
- };
141
-
142
- const executeSearch = async () => {
143
- await search(searchCriteriaForRequest.value);
144
- const query = filtersToQuery(searchCriteriaForRequest.value);
145
- const { limit: _, ...queryWithoutLimit } = query; // remove limit from query
146
- await router.push({
147
- query: queryWithoutLimit as LocationQueryRaw,
148
- });
149
- };
150
-
151
- const clearFilters = () => {
152
- (sidebarSelectedFilters.manufacturer as Set<string>).clear();
153
- (sidebarSelectedFilters.properties as Set<string>).clear();
154
- sidebarSelectedFilters["min-price"] = undefined;
155
- sidebarSelectedFilters["max-price"] = undefined;
156
- sidebarSelectedFilters.rating = undefined;
157
- sidebarSelectedFilters["shipping-free"] = undefined;
158
- };
159
-
160
- const currentSortingOrder = computed({
161
- get: (): string => getCurrentSortingOrder.value || "",
162
- set: async (order: string): Promise<void> => {
163
- await router.push({
164
- query: {
165
- ...route.query,
166
- order,
167
- },
168
- });
169
-
170
- await changeCurrentSortingOrder(order, {
171
- ...(route.query as unknown as operations["searchPage post /search"]["body"]),
172
- limit: route.query.limit ? Number(route.query.limit) : 15,
173
- });
174
- },
175
- });
176
-
177
- const selectedOptionIds = computed(() => [
178
- ...(sidebarSelectedFilters.properties as Set<string>),
179
- ...(sidebarSelectedFilters.manufacturer as Set<string>),
180
- ]);
181
- provide("selectedOptionIds", selectedOptionIds);
182
-
183
- async function invokeCleanFilters() {
184
- clearFilters();
185
- await executeSearch();
186
- }
187
- const isDefaultSidebarFilter =
188
- props.content.type === "sidebar-filter" &&
189
- props.content.config?.boxLayout?.value === "standard";
190
- const dropdownElement = useTemplateRef("dropdownElement");
191
- onClickOutside(dropdownElement, () => {
192
- isSortMenuOpen.value = false;
193
- });
194
-
195
- const handleSortingClick = (key: string) => {
196
- currentSortingOrder.value = key;
197
- isSortMenuOpen.value = false;
198
- };
199
- </script>
200
- <template>
201
- <div class="bg-white">
202
- <div class="mx-auto m-0" :class="{ 'px-5': isDefaultSidebarFilter }">
203
- <ClientOnly>
204
- <div
205
- class="relative flex items-baseline justify-between pt-6 pb-6 border-b border-gray-200"
206
- >
207
- <div class="text-4xl tracking-tight text-gray-900">
208
- {{ translations.listing.filters }}
209
- </div>
210
-
211
- <div ref="dropdownElement" class="flex items-center">
212
- <div class="relative inline-block text-left">
213
- <div>
214
- <button
215
- type="button"
216
- @click="isSortMenuOpen = !isSortMenuOpen"
217
- class="group inline-flex justify-center bg-transparent text-base font-medium text-gray-700 hover:text-gray-900"
218
- id="menu-button"
219
- aria-expanded="false"
220
- aria-haspopup="true"
221
- >
222
- {{ translations.listing.sort }}
223
- <div
224
- class="i-carbon-chevron-down h-5 w-5 ml-1"
225
- :class="{ hidden: isSortMenuOpen }"
226
- ></div>
227
- <div
228
- class="i-carbon-chevron-up h-5 w-5 ml-1"
229
- :class="{ hidden: !isSortMenuOpen }"
230
- ></div>
231
- </button>
232
- </div>
233
- <div
234
- :class="[isSortMenuOpen ? 'absolute' : 'hidden']"
235
- class="origin-top-left left-0 lg:origin-top-right lg:right-0 lg:left-auto mt-2 w-40 rounded-md shadow-2xl bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-1000"
236
- role="menu"
237
- aria-orientation="vertical"
238
- aria-labelledby="menu-button"
239
- tabindex="-1"
240
- >
241
- <div class="py-1" role="none">
242
- <button
243
- v-for="sorting in getSortingOrders"
244
- :key="sorting.key"
245
- @click="handleSortingClick(sorting.key)"
246
- :class="[
247
- sorting.key === getCurrentSortingOrder
248
- ? 'font-medium text-gray-900'
249
- : 'text-gray-500',
250
- ]"
251
- class="block px-4 py-2 text-sm bg-transparent"
252
- role="menuitem"
253
- tabindex="-1"
254
- >
255
- {{ sorting.label }}
256
- </button>
257
- </div>
258
- </div>
259
- </div>
260
- </div>
261
- </div>
262
-
263
- <div class="flex flex-wrap" v-if="getInitialFilters.length">
264
- <div
265
- v-for="filter in getInitialFilters"
266
- :key="`${filter?.id || filter?.code}`"
267
- class="mb-2 w-full"
268
- >
269
- <SwProductListingFilter
270
- @select-filter-value="onOptionSelectToggle"
271
- :selected-filters="getCurrentFilters"
272
- :filter="filter"
273
- class="relative"
274
- />
275
- </div>
276
- <div v-if="showResetFiltersButton" class="mx-auto mt-4 mb-2">
277
- <button
278
- class="w-full justify-center py-2 px-6 border border-transparent shadow-sm text-md font-medium rounded-md text-white bg-black hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
279
- @click="invokeCleanFilters"
280
- type="button"
281
- >
282
- {{ translations.listing.resetFilters
283
- }}<span
284
- class="w-6 h-6 i-carbon-close-filled inline-block align-middle ml-2"
285
- ></span>
286
- </button>
287
- </div>
288
- </div>
289
- </ClientOnly>
290
- </div>
291
- </div>
292
- </template>