@shopware/cms-base-layer 1.5.1 → 2.1.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 (198) hide show
  1. package/README.md +398 -12
  2. package/app/app.config.ts +18 -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 +83 -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/SwFilterDropdown.vue +54 -0
  15. package/app/components/SwListingProductPrice.vue +89 -0
  16. package/{components → app/components}/SwMedia3D.vue +4 -2
  17. package/{components → app/components}/SwNewsletterForm.vue +45 -34
  18. package/{components → app/components}/SwPagination.vue +3 -5
  19. package/{components → app/components}/SwProductAddToCart.vue +22 -27
  20. package/app/components/SwProductCard.vue +169 -0
  21. package/app/components/SwProductCardDetails.vue +74 -0
  22. package/app/components/SwProductCardImage.vue +90 -0
  23. package/app/components/SwProductCardSkeleton.vue +33 -0
  24. package/app/components/SwProductGallery.vue +43 -0
  25. package/app/components/SwProductListingFilter.vue +75 -0
  26. package/app/components/SwProductListingFilters.vue +304 -0
  27. package/app/components/SwProductListingFiltersHorizontal.vue +306 -0
  28. package/{components → app/components}/SwProductPrice.vue +3 -3
  29. package/app/components/SwProductRating.vue +40 -0
  30. package/{components → app/components}/SwProductReviews.vue +25 -23
  31. package/app/components/SwProductReviewsForm.vue +292 -0
  32. package/{components → app/components}/SwProductUnits.vue +10 -15
  33. package/app/components/SwQuantitySelect.vue +103 -0
  34. package/{components → app/components}/SwSlider.vue +154 -55
  35. package/app/components/SwSortDropdown.vue +87 -0
  36. package/app/components/SwStockInfo.vue +44 -0
  37. package/{components → app/components}/SwVariantConfigurator.vue +13 -12
  38. package/app/components/listing-filters/SwFilterPrice.vue +219 -0
  39. package/app/components/listing-filters/SwFilterProperties.vue +120 -0
  40. package/app/components/listing-filters/SwFilterRating.vue +99 -0
  41. package/app/components/listing-filters/SwFilterShippingFree.vue +114 -0
  42. package/app/components/public/cms/CmsBlockSpatialViewer.vue +94 -0
  43. package/app/components/public/cms/CmsGenericBlock.md +42 -0
  44. package/{components → app/components}/public/cms/CmsGenericBlock.vue +15 -1
  45. package/{components → app/components}/public/cms/CmsPage.md +19 -2
  46. package/{components → app/components}/public/cms/CmsPage.vue +30 -5
  47. package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +1 -1
  48. package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
  49. package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
  50. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
  51. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
  52. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
  53. package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
  54. package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
  55. package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
  56. package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
  57. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
  58. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
  59. package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
  60. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
  61. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
  62. package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
  63. package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
  64. package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
  65. package/{components → app/components}/public/cms/block/CmsBlockTextOnImage.vue +8 -5
  66. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  67. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  68. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +22 -6
  69. package/{components → app/components}/public/cms/element/CmsElementImage.vue +58 -21
  70. package/app/components/public/cms/element/CmsElementImageGallery.vue +225 -0
  71. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  72. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +8 -1
  73. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  74. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +31 -95
  75. package/app/components/public/cms/element/CmsElementProductName.vue +16 -0
  76. package/app/components/public/cms/element/CmsElementProductSlider.vue +101 -0
  77. package/app/components/public/cms/element/CmsElementSidebarFilter.vue +20 -0
  78. package/{components → app/components}/public/cms/element/CmsElementText.vue +17 -12
  79. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  80. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +2 -2
  81. package/app/components/public/cms/section/CmsSectionSidebar.vue +39 -0
  82. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  83. package/app/components/ui/BaseButton.vue +102 -0
  84. package/app/components/ui/BaseIcon.vue +15 -0
  85. package/app/components/ui/Checkbox.vue +49 -0
  86. package/app/components/ui/CheckmarkIcon.vue +23 -0
  87. package/app/components/ui/ChevronIcon.vue +34 -0
  88. package/app/components/ui/ExclamationIcon.vue +11 -0
  89. package/app/components/ui/IconButton.vue +32 -0
  90. package/app/components/ui/RadioButton.vue +26 -0
  91. package/app/components/ui/StarIcon.vue +18 -0
  92. package/app/components/ui/SwitchButton.vue +100 -0
  93. package/app/components/ui/UserIcon.vue +11 -0
  94. package/app/components/ui/WishlistIcon.vue +15 -0
  95. package/app/composables/useImagePlaceholder.ts +27 -0
  96. package/app/composables/useLcpImagePreload.test.ts +229 -0
  97. package/app/composables/useLcpImagePreload.ts +39 -0
  98. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  99. package/app/helpers/cms/findFirstCmsImageUrl.ts +86 -0
  100. package/app/helpers/cms/getImageSizes.test.ts +50 -0
  101. package/app/helpers/cms/getImageSizes.ts +36 -0
  102. package/app/helpers/html-to-vue/ast.ts +106 -0
  103. package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +1 -1
  104. package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +7 -11
  105. package/app/helpers/html-to-vue/renderer.ts +116 -0
  106. package/app/plugins/unocss-runtime.client.ts +23 -0
  107. package/app/providers/shopware.test.ts +213 -0
  108. package/app/providers/shopware.ts +107 -0
  109. package/dist/index.d.mts +3 -3
  110. package/dist/index.d.ts +3 -3
  111. package/dist/index.mjs +2 -2
  112. package/index.d.ts +36 -0
  113. package/nuxt.config.ts +100 -6
  114. package/package.json +33 -23
  115. package/uno.config.ts +94 -0
  116. package/components/SwCategoryNavigation.vue +0 -44
  117. package/components/SwCategoryNavigationLink.vue +0 -57
  118. package/components/SwListingProductPrice.vue +0 -89
  119. package/components/SwProductCard.vue +0 -286
  120. package/components/SwProductGallery.vue +0 -39
  121. package/components/SwProductListingFilter.vue +0 -42
  122. package/components/SwProductListingFilters.vue +0 -292
  123. package/components/listing-filters/SwFilterPrice.vue +0 -160
  124. package/components/listing-filters/SwFilterProperties.vue +0 -123
  125. package/components/listing-filters/SwFilterRating.vue +0 -101
  126. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  127. package/components/public/cms/CmsGenericBlock.md +0 -27
  128. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  129. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  130. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  131. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  132. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  133. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  134. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  135. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  136. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  137. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  138. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  139. package/components/public/cms/element/CmsElementProductSlider.vue +0 -80
  140. package/components/public/cms/element/CmsElementSidebarFilter.vue +0 -12
  141. package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
  142. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  143. package/helpers/html-to-vue/ast.ts +0 -72
  144. package/helpers/html-to-vue/renderer.ts +0 -56
  145. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  146. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  147. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  148. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  150. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  151. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  152. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  153. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  154. /package/{components → app/components}/public/cms/block/CmsBlockHtml.vue +0 -0
  155. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  156. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  157. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  158. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  159. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  160. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  161. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  162. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  163. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  164. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  165. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +0 -0
  166. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  167. /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +0 -0
  168. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  169. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  176. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  177. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  178. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  179. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  180. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  181. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  182. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  183. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  184. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  185. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  186. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  187. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  188. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  189. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  190. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  191. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  192. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  193. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  194. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +0 -0
  195. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  196. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  197. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  198. /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
@@ -0,0 +1,306 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ CmsElementProductListing,
4
+ CmsElementSidebarFilter,
5
+ } from "@shopware/composables";
6
+ import { useCmsTranslations } from "@shopware/composables";
7
+ import { defu } from "defu";
8
+ import { computed, reactive } from "vue";
9
+ import type { ComputedRef, UnwrapNestedRefs } from "vue";
10
+ import { type LocationQueryRaw, useRoute, useRouter } from "vue-router";
11
+ import { useCategoryListing } from "#imports";
12
+ import type { Schemas, operations } from "#shopware";
13
+
14
+ const props = defineProps<{
15
+ content: CmsElementProductListing | CmsElementSidebarFilter;
16
+ listingType?: string;
17
+ }>();
18
+
19
+ type Translations = {
20
+ listing: {
21
+ filters: string;
22
+ sort: string;
23
+ resetFilters: string;
24
+ };
25
+ };
26
+
27
+ type FilterState = {
28
+ manufacturer: Set<string>;
29
+ properties: Set<string>;
30
+ "min-price": number | undefined;
31
+ "max-price": number | undefined;
32
+ rating: number | undefined;
33
+ "shipping-free": boolean | undefined;
34
+ };
35
+
36
+ let translations: Translations = {
37
+ listing: {
38
+ filters: "Filters",
39
+ sort: "Sort",
40
+ resetFilters: "Reset filters",
41
+ },
42
+ };
43
+
44
+ translations = defu(useCmsTranslations(), translations) as Translations;
45
+
46
+ const route = useRoute();
47
+ const router = useRouter();
48
+
49
+ const {
50
+ changeCurrentSortingOrder,
51
+ getCurrentSortingOrder,
52
+ getInitialFilters,
53
+ getSortingOrders,
54
+ search,
55
+ } = useCategoryListing();
56
+
57
+ const sidebarSelectedFilters: UnwrapNestedRefs<FilterState> =
58
+ reactive<FilterState>({
59
+ manufacturer: new Set(),
60
+ properties: new Set(),
61
+ "min-price": undefined,
62
+ "max-price": undefined,
63
+ rating: undefined,
64
+ "shipping-free": undefined,
65
+ });
66
+
67
+ const showResetFiltersButton = computed<boolean>(() => {
68
+ if (
69
+ sidebarSelectedFilters.manufacturer.size !== 0 ||
70
+ sidebarSelectedFilters.properties.size !== 0 ||
71
+ sidebarSelectedFilters["max-price"] ||
72
+ sidebarSelectedFilters["min-price"] ||
73
+ sidebarSelectedFilters.rating ||
74
+ sidebarSelectedFilters["shipping-free"]
75
+ ) {
76
+ return true;
77
+ }
78
+
79
+ return false;
80
+ });
81
+
82
+ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
83
+ computed(() => ({
84
+ manufacturer: [
85
+ ...(sidebarSelectedFilters.manufacturer as Set<string>),
86
+ ]?.join("|"),
87
+ properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
88
+ "|",
89
+ ),
90
+ "min-price": sidebarSelectedFilters["min-price"] as number,
91
+ "max-price": sidebarSelectedFilters["max-price"] as number,
92
+ order: getCurrentSortingOrder.value as string,
93
+ "shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
94
+ rating: sidebarSelectedFilters.rating as number,
95
+ search: "",
96
+ limit: route.query.limit ? Number(route.query.limit) : 15,
97
+ }));
98
+
99
+ for (const param in route.query) {
100
+ if (param in sidebarSelectedFilters) {
101
+ const queryValue = route.query[param];
102
+
103
+ // Skip arrays
104
+ if (Array.isArray(queryValue)) continue;
105
+
106
+ if (["manufacturer", "properties"].includes(param)) {
107
+ if (typeof queryValue === "string") {
108
+ const elements = queryValue.split("|");
109
+ const targetSet = sidebarSelectedFilters[
110
+ param as keyof FilterState
111
+ ] as Set<string>;
112
+ for (const element of elements) {
113
+ targetSet.add(element);
114
+ }
115
+ }
116
+ } else if (queryValue && typeof queryValue === "string") {
117
+ // Fix: Use specific property assignments instead of generic keyof
118
+ if (param === "min-price") {
119
+ const numValue = Number(queryValue);
120
+ if (!Number.isNaN(numValue)) {
121
+ sidebarSelectedFilters["min-price"] = numValue;
122
+ }
123
+ } else if (param === "max-price") {
124
+ const numValue = Number(queryValue);
125
+ if (!Number.isNaN(numValue)) {
126
+ sidebarSelectedFilters["max-price"] = numValue;
127
+ }
128
+ } else if (param === "rating") {
129
+ const numValue = Number(queryValue);
130
+ if (!Number.isNaN(numValue)) {
131
+ sidebarSelectedFilters.rating = numValue;
132
+ }
133
+ } else if (param === "shipping-free") {
134
+ sidebarSelectedFilters["shipping-free"] = queryValue === "true";
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ const handleFilterChange = async (event: {
141
+ code: string;
142
+ value: string | number | boolean;
143
+ }) => {
144
+ try {
145
+ const { code, value } = event;
146
+
147
+ if (code === "manufacturer" || code === "properties") {
148
+ const filterSet = sidebarSelectedFilters[code];
149
+ const stringValue = String(value);
150
+
151
+ if (filterSet.has(stringValue)) {
152
+ filterSet.delete(stringValue);
153
+ } else {
154
+ filterSet.add(stringValue);
155
+ }
156
+ } else if (code === "min-price" || code === "max-price") {
157
+ sidebarSelectedFilters[code] =
158
+ typeof value === "number" ? value : Number(value);
159
+ } else if (code === "rating") {
160
+ sidebarSelectedFilters.rating = Number(value);
161
+ } else if (code === "shipping-free") {
162
+ sidebarSelectedFilters["shipping-free"] = Boolean(value);
163
+ }
164
+
165
+ await executeSearch();
166
+ } catch (error) {
167
+ console.error("Filter update failed:", error);
168
+ }
169
+ };
170
+
171
+ const executeSearch = async () => {
172
+ try {
173
+ await search(searchCriteriaForRequest.value);
174
+
175
+ // Build query directly from searchCriteriaForRequest which already has pipe-separated strings
176
+ const criteria = searchCriteriaForRequest.value;
177
+ const query: Record<string, unknown> = {};
178
+
179
+ if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
180
+ if (criteria.properties) query.properties = criteria.properties;
181
+ if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
182
+ if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
183
+ if (criteria.rating) query.rating = criteria.rating;
184
+ if (criteria["shipping-free"])
185
+ query["shipping-free"] = criteria["shipping-free"];
186
+ if (criteria.order) query.order = criteria.order;
187
+
188
+ await router.push({
189
+ query: query as LocationQueryRaw,
190
+ });
191
+ } catch (error) {
192
+ console.error("Search execution failed:", error);
193
+ }
194
+ };
195
+
196
+ const clearFilters = () => {
197
+ (sidebarSelectedFilters.manufacturer as Set<string>).clear();
198
+ (sidebarSelectedFilters.properties as Set<string>).clear();
199
+ sidebarSelectedFilters["min-price"] = undefined;
200
+ sidebarSelectedFilters["max-price"] = undefined;
201
+ sidebarSelectedFilters.rating = undefined;
202
+ sidebarSelectedFilters["shipping-free"] = undefined;
203
+ };
204
+
205
+ const currentSortingOrder = computed({
206
+ get: (): string => getCurrentSortingOrder.value || "",
207
+ set: async (order: string): Promise<void> => {
208
+ try {
209
+ await router.push({
210
+ query: {
211
+ ...route.query,
212
+ order,
213
+ },
214
+ });
215
+
216
+ await changeCurrentSortingOrder(order, {
217
+ ...(route.query as unknown as operations["searchPage post /search"]["body"]),
218
+ limit: route.query.limit ? Number(route.query.limit) : 15,
219
+ });
220
+ } catch (error) {
221
+ console.error("Sorting order change failed:", error);
222
+ }
223
+ },
224
+ });
225
+
226
+ async function invokeCleanFilters() {
227
+ try {
228
+ clearFilters();
229
+ await executeSearch();
230
+ } catch (error) {
231
+ console.error("Clear filters failed:", error);
232
+ }
233
+ }
234
+
235
+ const handleSortChange = (sortKey: string) => {
236
+ currentSortingOrder.value = sortKey;
237
+ };
238
+
239
+ // Helper to check if a filter has active selections
240
+ const hasActiveFilter = (filter: { code: string }) => {
241
+ if (filter.code === "manufacturer") {
242
+ return sidebarSelectedFilters.manufacturer.size > 0;
243
+ }
244
+ if (filter.code === "price") {
245
+ return (
246
+ sidebarSelectedFilters["min-price"] !== undefined ||
247
+ sidebarSelectedFilters["max-price"] !== undefined
248
+ );
249
+ }
250
+ if (filter.code === "rating") {
251
+ return sidebarSelectedFilters.rating !== undefined;
252
+ }
253
+ if (filter.code === "shipping-free") {
254
+ return sidebarSelectedFilters["shipping-free"] === true;
255
+ }
256
+ // Properties filter - check if any property from this filter group is selected
257
+ return sidebarSelectedFilters.properties.size > 0;
258
+ };
259
+ </script>
260
+
261
+ <template>
262
+ <div>
263
+ <!-- Horizontal Filters Row -->
264
+ <div class="flex flex-wrap items-center justify-start gap-4 z-10">
265
+ <!-- Filter dropdowns -->
266
+ <SwFilterDropdown
267
+ v-for="filter in getInitialFilters"
268
+ :key="filter.id"
269
+ :label="filter.label"
270
+ :is-active="hasActiveFilter(filter)"
271
+ >
272
+ <SwProductListingFilter
273
+ :filter="filter"
274
+ display-mode="dropdown"
275
+ :selected-manufacturer="sidebarSelectedFilters.manufacturer"
276
+ :selected-properties="sidebarSelectedFilters.properties"
277
+ :selected-min-price="sidebarSelectedFilters['min-price']"
278
+ :selected-max-price="sidebarSelectedFilters['max-price']"
279
+ :selected-rating="sidebarSelectedFilters.rating"
280
+ :selected-shipping-free="sidebarSelectedFilters['shipping-free']"
281
+ @filter-change="handleFilterChange"
282
+ />
283
+ </SwFilterDropdown>
284
+
285
+ <!-- Sort dropdown -->
286
+ <SwSortDropdown
287
+ :sort-options="getSortingOrders ?? []"
288
+ :current-sort="getCurrentSortingOrder ?? ''"
289
+ :label="translations.listing.sort"
290
+ @sort-change="handleSortChange"
291
+ />
292
+
293
+ <!-- Reset filters button -->
294
+ <SwBaseButton
295
+ v-if="showResetFiltersButton"
296
+ variant="ghost"
297
+ size="medium"
298
+ @click="invokeCleanFilters"
299
+ type="button"
300
+ >
301
+ {{ translations.listing.resetFilters }}
302
+ <span class="w-5 h-5 i-carbon-close inline-block align-middle ml-1"></span>
303
+ </SwBaseButton>
304
+ </div>
305
+ </div>
306
+ </template>
@@ -37,7 +37,7 @@ translations = defu(useCmsTranslations(), translations) as Translations;
37
37
 
38
38
  const { product } = toRefs(props);
39
39
 
40
- const { unitPrice, price, tierPrices, isListPrice } = useProductPrice(product);
40
+ const { unitPrice, price, tierPrices, hasListPrice } = useProductPrice(product);
41
41
  const { getFormattedPrice } = usePrice();
42
42
  </script>
43
43
 
@@ -45,7 +45,7 @@ const { getFormattedPrice } = usePrice();
45
45
  <div>
46
46
  <div v-if="!tierPrices.length">
47
47
  <SwSharedPrice
48
- v-if="isListPrice"
48
+ v-if="hasListPrice"
49
49
  class="text-1xl text-gray-900 basis-2/6 justify-end line-through"
50
50
  :value="price?.listPrice?.price"
51
51
  />
@@ -53,7 +53,7 @@ const { getFormattedPrice } = usePrice();
53
53
  v-if="unitPrice"
54
54
  class="text-3xl text-gray-900 basis-2/6 justify-end"
55
55
  :class="{
56
- 'text-red': isListPrice,
56
+ 'text-red': hasListPrice,
57
57
  }"
58
58
  :value="unitPrice"
59
59
  />
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ const {
5
+ rating = 0,
6
+ reviewCount = 0,
7
+ starSize = 16,
8
+ showCount = true,
9
+ } = defineProps<{
10
+ rating: number;
11
+ reviewCount?: number;
12
+ starSize?: number;
13
+ showCount?: boolean;
14
+ }>();
15
+
16
+ const filledStars = computed(() => Math.round(rating));
17
+ </script>
18
+
19
+ <template>
20
+ <div class="flex items-center">
21
+ <div
22
+ class="flex items-center gap-1.5"
23
+ role="img"
24
+ :aria-label="`${rating} out of 5 stars`"
25
+ >
26
+ <SwStarIcon
27
+ v-for="i in 5"
28
+ :key="`star-${i}`"
29
+ :filled="i <= filledStars"
30
+ :size="starSize"
31
+ />
32
+ </div>
33
+ <span
34
+ v-if="showCount && reviewCount > 0"
35
+ class="ml-1 text-surface-on-surface-variant text-base leading-normal"
36
+ >
37
+ ({{ reviewCount }})
38
+ </span>
39
+ </div>
40
+ </template>
@@ -17,6 +17,7 @@ type Translations = {
17
17
  product: {
18
18
  noReviews: string;
19
19
  reviewNotAccepted: string;
20
+ reviewFeedback: string;
20
21
  };
21
22
  };
22
23
 
@@ -24,6 +25,7 @@ let translations: Translations = {
24
25
  product: {
25
26
  noReviews: "No reviews yet.",
26
27
  reviewNotAccepted: "Your review has not been approved yet",
28
+ reviewFeedback: "Shop feedback",
27
29
  },
28
30
  };
29
31
 
@@ -64,48 +66,48 @@ const formatDate = (date: string) => {
64
66
  <template>
65
67
  <div
66
68
  v-if="loadingReviews"
67
- class="absolute inset-0 flex items-center justify-center z-10 bg-white/75"
69
+ class="absolute inset-0 flex items-center justify-center z-10 bg-surface-surface/75"
68
70
  >
69
71
  <div
70
- class="h-15 w-15 i-carbon-progress-bar-round animate-spin c-gray-500"
72
+ class="h-15 w-15 i-carbon-progress-bar-round animate-spin text-outline-outline"
71
73
  />
72
74
  </div>
73
- <div v-else-if="reviewsList.length">
74
- <div v-for="review in reviews" :key="review.id">
75
+ <div v-else-if="reviewsList.length" class="flex flex-col gap-6">
76
+ <div
77
+ v-for="(review, index) in reviewsList"
78
+ :key="review.id"
79
+ class="pb-6"
80
+ :class="{ 'border-b border-surface-surface-container-highest': index < reviewsList.length - 1 }"
81
+ >
75
82
  <div
76
83
  v-if="review.createdAt"
77
- class="cms-block-product-description-reviews__reviews-time mt-3 text-gray-600 text-sm"
84
+ class="cms-block-product-description-reviews__reviews-time text-surface-on-surface-variant text-sm"
78
85
  >
86
+ <span v-if="review.externalUser">{{ review.externalUser }} - </span>
79
87
  <span>{{ formatDate(review.createdAt) }}</span>
80
88
  </div>
81
89
  <div
82
90
  v-if="!review.status"
83
- class="mt-2 text-3 p-2 bg-[#d4f0f5] flex gap-2 items-center"
91
+ class="mt-2 text-3 p-2 bg-states-info-container text-states-on-info-container flex gap-2 items-center"
84
92
  >
85
93
  <div class="w-6 h-6 i-carbon-warning" />
86
94
  {{ translations.product.reviewNotAccepted }}
87
95
  </div>
88
- <div
89
- class="cms-block-product-description-reviews__reviews-rating inline-flex items-center mt-2"
90
- >
91
- <div
92
- v-for="_ in review.points"
93
- :key="`filled-star-${_}`"
94
- class="w-5 h-5 i-carbon-star-filled"
95
- ></div>
96
- <div
97
- v-for="_ in 5 - (review.points || 0)"
98
- :key="`empty-star-${_}`"
99
- class="w-5 h-5 i-carbon-star"
100
- ></div>
101
- <div
102
- class="cms-block-product-description-reviews__reviews-title font-semibold ml-2"
103
- >
96
+ <div class="cms-block-product-description-reviews__reviews-rating inline-flex items-center mt-2">
97
+ <SwProductRating
98
+ :rating="review.points ?? 0"
99
+ :star-size="20"
100
+ :show-count="false"
101
+ />
102
+ <div class="cms-block-product-description-reviews__reviews-title font-semibold ml-2">
104
103
  <p>{{ review.title }}</p>
105
104
  </div>
106
105
  </div>
107
106
  <div class="cms-block-product-description-reviews__reviews-content mt-2">
108
- <span>{{ review.content }}</span>
107
+ <p class="break-words">{{ review.content }}</p>
108
+ <p v-if="review.comment" class="text-surface-on-surface-variant mt-2">
109
+ - {{ translations.product.reviewFeedback }}: {{ review.comment }}
110
+ </p>
109
111
  </div>
110
112
  </div>
111
113
  </div>