@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,43 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ CmsElementImageGallery,
4
+ SliderElementConfig,
5
+ } from "@shopware/composables";
6
+ import { ref, watch } from "vue";
7
+ import type { Schemas } from "#shopware";
8
+
9
+ const { product, config = {} } = defineProps<{
10
+ product: Schemas["Product"];
11
+ config?: Partial<SliderElementConfig>;
12
+ }>();
13
+
14
+ const defaultConfig: SliderElementConfig = {
15
+ minHeight: { value: "300px", source: "static" },
16
+ navigationArrows: { value: "inside", source: "static" },
17
+ navigationDots: { value: "inside", source: "static" },
18
+ };
19
+
20
+ const content = ref<CmsElementImageGallery>();
21
+
22
+ watch(
23
+ [() => product, () => config],
24
+ ([currentProduct, currentConfig]) => {
25
+ content.value = {
26
+ config: {
27
+ ...defaultConfig,
28
+ ...currentConfig,
29
+ },
30
+ data: {
31
+ sliderItems: currentProduct.media,
32
+ },
33
+ } as CmsElementImageGallery;
34
+ },
35
+ {
36
+ immediate: true,
37
+ },
38
+ );
39
+ </script>
40
+
41
+ <template>
42
+ <CmsElementImageGallery v-if="content" :content="content" />
43
+ </template>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts" generic="ListingFilter extends { code: string }">
2
+ import { computed } from "vue";
3
+ import type { Component } from "vue";
4
+ import SwFilterPriceVue from "./listing-filters/SwFilterPrice.vue";
5
+ import SwFilterPropertiesVue from "./listing-filters/SwFilterProperties.vue";
6
+ import SwFilterRatingVue from "./listing-filters/SwFilterRating.vue";
7
+ import SwFilterShippingFreeVue from "./listing-filters/SwFilterShippingFree.vue";
8
+
9
+ const {
10
+ filter,
11
+ selectedManufacturer,
12
+ selectedProperties,
13
+ selectedMinPrice,
14
+ selectedMaxPrice,
15
+ selectedRating,
16
+ selectedShippingFree,
17
+ displayMode = "accordion",
18
+ } = defineProps<{
19
+ filter: ListingFilter;
20
+ selectedManufacturer: Set<string>;
21
+ selectedProperties: Set<string>;
22
+ selectedMinPrice: number | undefined;
23
+ selectedMaxPrice: number | undefined;
24
+ selectedRating: number | undefined;
25
+ selectedShippingFree: boolean | undefined;
26
+ displayMode?: "accordion" | "dropdown";
27
+ }>();
28
+
29
+ const emit = defineEmits<{
30
+ "filter-change": [{ code: string; value: string | number | boolean }];
31
+ }>();
32
+
33
+ const transformedFilters = computed(() => ({
34
+ price: {
35
+ min: selectedMinPrice,
36
+ max: selectedMaxPrice,
37
+ },
38
+ rating: selectedRating,
39
+ "shipping-free": selectedShippingFree,
40
+ manufacturer: [...selectedManufacturer],
41
+ properties: [...selectedProperties],
42
+ }));
43
+
44
+ const filterComponent = computed<Component | undefined>(() => {
45
+ const componentMap: Record<string, Component> = {
46
+ manufacturer: SwFilterPropertiesVue,
47
+ price: SwFilterPriceVue,
48
+ rating: SwFilterRatingVue,
49
+ "shipping-free": SwFilterShippingFreeVue,
50
+ };
51
+
52
+ return (
53
+ componentMap[filter.code] ||
54
+ ("options" in filter ? SwFilterPropertiesVue : undefined)
55
+ );
56
+ });
57
+
58
+ const handleSelectValue = ({
59
+ code,
60
+ value,
61
+ }: { code: string; value: string | number | boolean }) => {
62
+ emit("filter-change", { code, value });
63
+ };
64
+ </script>
65
+ <template>
66
+ <div>
67
+ <component
68
+ :is="filterComponent"
69
+ :filter="filter"
70
+ :selected-filters="transformedFilters"
71
+ :display-mode="displayMode"
72
+ @select-value="handleSelectValue"
73
+ />
74
+ </div>
75
+ </template>
@@ -0,0 +1,304 @@
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
+ getCurrentFilters,
52
+ getCurrentSortingOrder,
53
+ getInitialFilters,
54
+ getSortingOrders,
55
+ search,
56
+ } = useCategoryListing();
57
+
58
+ const sidebarSelectedFilters: UnwrapNestedRefs<FilterState> =
59
+ reactive<FilterState>({
60
+ manufacturer: new Set(),
61
+ properties: new Set(),
62
+ "min-price": undefined,
63
+ "max-price": undefined,
64
+ rating: undefined,
65
+ "shipping-free": undefined,
66
+ });
67
+
68
+ const showResetFiltersButton = computed<boolean>(() => {
69
+ if (
70
+ sidebarSelectedFilters.manufacturer.size !== 0 ||
71
+ sidebarSelectedFilters.properties.size !== 0 ||
72
+ sidebarSelectedFilters["max-price"] ||
73
+ sidebarSelectedFilters["min-price"] ||
74
+ sidebarSelectedFilters.rating ||
75
+ sidebarSelectedFilters["shipping-free"]
76
+ ) {
77
+ return true;
78
+ }
79
+
80
+ return false;
81
+ });
82
+
83
+ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
84
+ computed(() => ({
85
+ manufacturer: [
86
+ ...(sidebarSelectedFilters.manufacturer as Set<string>),
87
+ ]?.join("|"),
88
+ properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
89
+ "|",
90
+ ),
91
+ "min-price": sidebarSelectedFilters["min-price"] as number,
92
+ "max-price": sidebarSelectedFilters["max-price"] as number,
93
+ order: getCurrentSortingOrder.value as string,
94
+ "shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
95
+ rating: sidebarSelectedFilters.rating as number,
96
+ search: "",
97
+ limit: route.query.limit ? Number(route.query.limit) : 15,
98
+ }));
99
+
100
+ for (const param in route.query) {
101
+ if (param in sidebarSelectedFilters) {
102
+ const queryValue = route.query[param];
103
+
104
+ // Skip arrays
105
+ if (Array.isArray(queryValue)) continue;
106
+
107
+ if (["manufacturer", "properties"].includes(param)) {
108
+ if (typeof queryValue === "string") {
109
+ const elements = queryValue.split("|");
110
+ const targetSet = sidebarSelectedFilters[
111
+ param as keyof FilterState
112
+ ] as Set<string>;
113
+ for (const element of elements) {
114
+ targetSet.add(element);
115
+ }
116
+ }
117
+ } else if (queryValue && typeof queryValue === "string") {
118
+ // Fix: Use specific property assignments instead of generic keyof
119
+ if (param === "min-price") {
120
+ const numValue = Number(queryValue);
121
+ if (!Number.isNaN(numValue)) {
122
+ sidebarSelectedFilters["min-price"] = numValue;
123
+ }
124
+ } else if (param === "max-price") {
125
+ const numValue = Number(queryValue);
126
+ if (!Number.isNaN(numValue)) {
127
+ sidebarSelectedFilters["max-price"] = numValue;
128
+ }
129
+ } else if (param === "rating") {
130
+ const numValue = Number(queryValue);
131
+ if (!Number.isNaN(numValue)) {
132
+ sidebarSelectedFilters.rating = numValue;
133
+ }
134
+ } else if (param === "shipping-free") {
135
+ sidebarSelectedFilters["shipping-free"] = queryValue === "true";
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ const handleFilterChange = async (event: {
142
+ code: string;
143
+ value: string | number | boolean;
144
+ }) => {
145
+ try {
146
+ const { code, value } = event;
147
+
148
+ if (code === "manufacturer" || code === "properties") {
149
+ const filterSet = sidebarSelectedFilters[code];
150
+ const stringValue = String(value);
151
+
152
+ if (filterSet.has(stringValue)) {
153
+ filterSet.delete(stringValue);
154
+ } else {
155
+ filterSet.add(stringValue);
156
+ }
157
+ } else if (code === "min-price" || code === "max-price") {
158
+ sidebarSelectedFilters[code] =
159
+ typeof value === "number" ? value : Number(value);
160
+ } else if (code === "rating") {
161
+ sidebarSelectedFilters.rating = Number(value);
162
+ } else if (code === "shipping-free") {
163
+ sidebarSelectedFilters["shipping-free"] = Boolean(value);
164
+ }
165
+
166
+ await executeSearch();
167
+ } catch (error) {
168
+ console.error("Filter update failed:", error);
169
+ }
170
+ };
171
+
172
+ const executeSearch = async () => {
173
+ try {
174
+ await search(searchCriteriaForRequest.value);
175
+
176
+ // Build query directly from searchCriteriaForRequest which already has pipe-separated strings
177
+ const criteria = searchCriteriaForRequest.value;
178
+ const query: Record<string, unknown> = {};
179
+
180
+ if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
181
+ if (criteria.properties) query.properties = criteria.properties;
182
+ if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
183
+ if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
184
+ if (criteria.rating) query.rating = criteria.rating;
185
+ if (criteria["shipping-free"])
186
+ query["shipping-free"] = criteria["shipping-free"];
187
+ if (criteria.order) query.order = criteria.order;
188
+
189
+ await router.push({
190
+ query: query as LocationQueryRaw,
191
+ });
192
+ } catch (error) {
193
+ console.error("Search execution failed:", error);
194
+ }
195
+ };
196
+
197
+ const clearFilters = () => {
198
+ (sidebarSelectedFilters.manufacturer as Set<string>).clear();
199
+ (sidebarSelectedFilters.properties as Set<string>).clear();
200
+ sidebarSelectedFilters["min-price"] = undefined;
201
+ sidebarSelectedFilters["max-price"] = undefined;
202
+ sidebarSelectedFilters.rating = undefined;
203
+ sidebarSelectedFilters["shipping-free"] = undefined;
204
+ };
205
+
206
+ const currentSortingOrder = computed({
207
+ get: (): string => getCurrentSortingOrder.value || "",
208
+ set: async (order: string): Promise<void> => {
209
+ try {
210
+ await router.push({
211
+ query: {
212
+ ...route.query,
213
+ order,
214
+ },
215
+ });
216
+
217
+ await changeCurrentSortingOrder(order, {
218
+ ...(route.query as unknown as operations["searchPage post /search"]["body"]),
219
+ limit: route.query.limit ? Number(route.query.limit) : 15,
220
+ });
221
+ } catch (error) {
222
+ console.error("Sorting order change failed:", error);
223
+ }
224
+ },
225
+ });
226
+
227
+ async function invokeCleanFilters() {
228
+ try {
229
+ clearFilters();
230
+ await executeSearch();
231
+ } catch (error) {
232
+ console.error("Clear filters failed:", error);
233
+ }
234
+ }
235
+
236
+ const handleSortChange = (sortKey: string) => {
237
+ currentSortingOrder.value = sortKey;
238
+ };
239
+
240
+ const handleRemoveFilterChip = async (chip: {
241
+ code: string;
242
+ value: string | number;
243
+ }) => {
244
+ if (chip.code === "properties" || chip.code === "manufacturer") {
245
+ const filterSet = sidebarSelectedFilters[chip.code] as Set<string>;
246
+ filterSet.delete(String(chip.value));
247
+ } else if (chip.code === "price") {
248
+ sidebarSelectedFilters["min-price"] = undefined;
249
+ sidebarSelectedFilters["max-price"] = undefined;
250
+ } else if (chip.code === "rating") {
251
+ sidebarSelectedFilters.rating = undefined;
252
+ } else if (chip.code === "shipping-free") {
253
+ sidebarSelectedFilters["shipping-free"] = undefined;
254
+ }
255
+
256
+ await executeSearch();
257
+ };
258
+ </script>
259
+ <template>
260
+ <div>
261
+ <!-- Active Filter Chips -->
262
+ <SwFilterChips
263
+ :filters="sidebarSelectedFilters"
264
+ :available-filters="getInitialFilters"
265
+ @remove="handleRemoveFilterChip"
266
+ />
267
+
268
+ <!-- Filters Header -->
269
+ <div class="self-stretch flex flex-col justify-start items-start gap-4">
270
+ <div
271
+ class="flex flex-row items-center justify-between w-full mb-4 py-3 border-b border-outline-outline-variant">
272
+ <div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
273
+ {{ translations.listing.filters }}
274
+ </div>
275
+ <SwSortDropdown
276
+ :sort-options="getSortingOrders ?? []"
277
+ :current-sort="getCurrentSortingOrder ?? ''"
278
+ :label="translations.listing.sort"
279
+ @sort-change="handleSortChange"
280
+ />
281
+ </div>
282
+ </div>
283
+
284
+ <!-- Filters List -->
285
+ <div class="self-stretch flex flex-col justify-start items-start gap-4">
286
+ <SwProductListingFilter v-for="filter in getInitialFilters" :key="filter.id"
287
+ :filter="filter"
288
+ :selected-manufacturer="sidebarSelectedFilters.manufacturer"
289
+ :selected-properties="sidebarSelectedFilters.properties"
290
+ :selected-min-price="sidebarSelectedFilters['min-price']"
291
+ :selected-max-price="sidebarSelectedFilters['max-price']"
292
+ :selected-rating="sidebarSelectedFilters.rating"
293
+ :selected-shipping-free="sidebarSelectedFilters['shipping-free']"
294
+ @filter-change="handleFilterChange"
295
+ class="w-full" />
296
+ <div v-if="showResetFiltersButton" class="w-full">
297
+ <SwBaseButton variant="primary" size="medium" block @click="invokeCleanFilters" type="button">
298
+ {{ translations.listing.resetFilters }}
299
+ <span class="w-6 h-6 i-carbon-close-filled inline-block align-middle ml-2"></span>
300
+ </SwBaseButton>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </template>