@shopware/cms-base-layer 1.5.0 → 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 (184) hide show
  1. package/README.md +328 -13
  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/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +4 -4
  55. package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +3 -5
  56. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  57. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  58. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
  59. package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
  60. package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
  61. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  62. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
  63. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  64. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
  65. package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
  66. package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
  67. package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
  68. package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +8 -2
  69. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  70. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
  71. package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
  72. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  73. package/app/components/ui/BaseButton.vue +99 -0
  74. package/app/components/ui/BaseIcon.vue +15 -0
  75. package/app/components/ui/Checkbox.vue +49 -0
  76. package/app/components/ui/CheckmarkIcon.vue +23 -0
  77. package/app/components/ui/ChevronIcon.vue +37 -0
  78. package/app/components/ui/ExclamationIcon.vue +11 -0
  79. package/app/components/ui/IconButton.vue +32 -0
  80. package/app/components/ui/RadioButton.vue +26 -0
  81. package/app/components/ui/StarIcon.vue +18 -0
  82. package/app/components/ui/SwitchButton.vue +100 -0
  83. package/app/components/ui/UserIcon.vue +11 -0
  84. package/app/components/ui/WishlistIcon.vue +20 -0
  85. package/app/composables/useImagePlaceholder.ts +27 -0
  86. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  87. package/app/providers/shopware.test.ts +213 -0
  88. package/app/providers/shopware.ts +107 -0
  89. package/dist/index.d.mts +3 -3
  90. package/dist/index.d.ts +3 -3
  91. package/dist/index.mjs +2 -2
  92. package/index.d.ts +12 -0
  93. package/nuxt.config.ts +80 -6
  94. package/package.json +29 -21
  95. package/uno.config.ts +83 -0
  96. package/components/SwCategoryNavigation.vue +0 -44
  97. package/components/SwCategoryNavigationLink.vue +0 -57
  98. package/components/SwListingProductPrice.vue +0 -89
  99. package/components/SwProductCard.vue +0 -286
  100. package/components/SwProductListingFilter.vue +0 -42
  101. package/components/SwProductListingFilters.vue +0 -292
  102. package/components/listing-filters/SwFilterPrice.vue +0 -160
  103. package/components/listing-filters/SwFilterProperties.vue +0 -123
  104. package/components/listing-filters/SwFilterRating.vue +0 -101
  105. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  106. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  107. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  108. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  109. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  110. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  111. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  112. package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
  113. package/components/public/cms/element/CmsBlockHtml.md +0 -1
  114. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  115. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  116. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  117. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  118. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  119. package/components/public/cms/section/CmsSectionSidebar.vue +0 -49
  120. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  121. /package/{components → app/components}/SwMedia3D.vue +0 -0
  122. /package/{components → app/components}/SwProductGallery.vue +0 -0
  123. /package/{components → app/components}/SwProductPrice.vue +0 -0
  124. /package/{components → app/components}/SwProductUnits.vue +0 -0
  125. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  126. /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
  127. /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
  128. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  129. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  130. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  131. /package/{components → app/components}/public/cms/CmsPage.md +0 -0
  132. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  133. /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
  134. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  135. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  136. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  137. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  138. /package/{components/public/cms/element → app/components/public/cms/block}/CmsBlockHtml.vue +0 -0
  139. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  140. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  141. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  142. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  143. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  144. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  145. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  146. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  147. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  148. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  150. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  151. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  152. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  153. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  154. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  155. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  156. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  157. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  158. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  159. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  160. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  161. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  162. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  163. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  164. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  165. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  166. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  167. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  168. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  169. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  176. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  177. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  178. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  179. /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
  180. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  181. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
  182. /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
  183. /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
  184. /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
@@ -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>
@@ -1,160 +0,0 @@
1
- <script
2
- setup
3
- lang="ts"
4
- generic="
5
- ListingFilter extends {
6
- code: string;
7
- min?: number;
8
- max?: number;
9
- label: string;
10
- }
11
- "
12
- >
13
- import { useCmsTranslations } from "@shopware/composables";
14
- import { onClickOutside, useDebounceFn } from "@vueuse/core";
15
- import { defu } from "defu";
16
- import { onMounted, reactive, ref, watch } from "vue";
17
- import type { Schemas } from "#shopware";
18
-
19
- const emits =
20
- defineEmits<
21
- (e: "select-value", value: { code: string; value: unknown }) => void
22
- >();
23
-
24
- const props = defineProps<{
25
- filter: ListingFilter;
26
- selectedFilters: Schemas["ProductListingResult"]["currentFilters"];
27
- }>();
28
-
29
- type Translations = {
30
- listing: {
31
- min: string;
32
- max: string;
33
- };
34
- };
35
- let translations: Translations = {
36
- listing: {
37
- min: "Min",
38
- max: "Max",
39
- },
40
- };
41
- translations = defu(useCmsTranslations(), translations) as Translations;
42
-
43
- const prices = reactive<{ min: number; max: number }>({
44
- min: 0,
45
- max: 0,
46
- });
47
-
48
- onMounted(() => {
49
- prices.min = Math.floor(
50
- props.selectedFilters.price.min ?? props.filter?.min ?? 0,
51
- );
52
- prices.max = Math.floor(
53
- props.selectedFilters.price.max ?? props.filter?.max ?? 0,
54
- );
55
- });
56
-
57
- const isFilterVisible = ref<boolean>(false);
58
- const toggle = () => {
59
- isFilterVisible.value = !isFilterVisible.value;
60
- };
61
-
62
- const dropdownElement = ref(null);
63
- onClickOutside(dropdownElement, () => {
64
- isFilterVisible.value = false;
65
- });
66
-
67
- function onMinPriceChange(newPrice: number, oldPrice: number) {
68
- if (newPrice === oldPrice || oldPrice === 0) return;
69
- emits("select-value", {
70
- code: "min-price",
71
- value: newPrice,
72
- });
73
- }
74
- const debounceMinPriceUpdate = useDebounceFn(onMinPriceChange, 500);
75
- watch(() => prices.min, debounceMinPriceUpdate);
76
-
77
- function onMaxPriceChange(newPrice: number, oldPrice: number) {
78
- if (newPrice === oldPrice || oldPrice === 0) return;
79
- emits("select-value", {
80
- code: "max-price",
81
- value: newPrice,
82
- });
83
- }
84
- const debounceMaxPriceUpdate = useDebounceFn(onMaxPriceChange, 500);
85
- watch(() => prices.max, debounceMaxPriceUpdate);
86
- </script>
87
-
88
- <template>
89
- <div class="border-b border-gray-200 py-6 px-5">
90
- <h3 class="-my-3 flow-root">
91
- <button
92
- type="button"
93
- class="flex w-full items-center justify-between bg-white py-2 text-base text-gray-400 hover:text-gray-500"
94
- @click="toggle"
95
- >
96
- <span class="font-medium text-gray-900 text-left">{{
97
- props.filter.label
98
- }}</span>
99
- <span class="ml-6 flex items-center">
100
- <i
101
- :class="[
102
- !isFilterVisible
103
- ? 'i-carbon-chevron-down'
104
- : 'i-carbon-chevron-up',
105
- ]"
106
- />
107
- </span>
108
- </button>
109
- </h3>
110
-
111
- <transition name="fade" mode="out-in">
112
- <div v-show="isFilterVisible" class="space-y-6 mt-5">
113
- <div class="mt-2 flex">
114
- <div class="w-1/2 flex rounded-md mr-4">
115
- <span
116
- class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm"
117
- >
118
- {{ translations.listing.min }}
119
- </span>
120
- <input
121
- id="min-price"
122
- v-model="prices.min"
123
- step="1"
124
- type="number"
125
- name="min-price"
126
- class="pl-2 focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border border-gray-300"
127
- :placeholder="prices.min?.toString()"
128
- />
129
- </div>
130
- <div class="w-1/2 flex rounded-md">
131
- <span
132
- class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm"
133
- >
134
- {{ translations.listing.max }}
135
- </span>
136
- <input
137
- id="max-price"
138
- v-model="prices.max"
139
- type="number"
140
- name="max-price"
141
- class="pl-2 focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border border-gray-300"
142
- :placeholder="prices.max?.toString()"
143
- />
144
- </div>
145
- </div>
146
- </div>
147
- </transition>
148
- </div>
149
- </template>
150
- <style scoped>
151
- .fade-enter-active,
152
- .fade-leave-active {
153
- transition: all 0.2s ease;
154
- }
155
-
156
- .fade-enter-from,
157
- .fade-leave-to {
158
- opacity: 0;
159
- }
160
- </style>
@@ -1,123 +0,0 @@
1
- <script
2
- setup
3
- lang="ts"
4
- generic="
5
- ListingFilter extends {
6
- code: string;
7
- label: string;
8
- name: string;
9
- options: Array<Schemas['PropertyGroupOption']>;
10
- entities: Array<Schemas['ProductManufacturer']>;
11
- }
12
- "
13
- >
14
- import { getTranslatedProperty } from "@shopware/helpers";
15
- import { inject, ref } from "vue";
16
- import type { Schemas } from "#shopware";
17
-
18
- const props = defineProps<{
19
- filter: ListingFilter;
20
- }>();
21
-
22
- const emits =
23
- defineEmits<
24
- (e: "select-value", value: { code: string; value: unknown }) => void
25
- >();
26
- const selectedOptionIds = inject<string[]>("selectedOptionIds");
27
- const isFilterVisible = ref<boolean>(false);
28
- const toggle = () => {
29
- isFilterVisible.value = !isFilterVisible.value;
30
- };
31
- </script>
32
-
33
- <template>
34
- <div class="border-b border-gray-200 py-6 px-5">
35
- <h2 class="-my-3 flow-root">
36
- <button
37
- type="button"
38
- class="flex w-full items-center justify-between bg-white py-2 text-base text-gray-400 hover:text-gray-500"
39
- @click="toggle"
40
- >
41
- <span class="font-medium text-gray-900 text-left">{{
42
- props.filter.label
43
- }}</span>
44
- <span class="ml-6 flex items-center">
45
- <i
46
- :class="[
47
- !isFilterVisible
48
- ? 'i-carbon-chevron-down'
49
- : 'i-carbon-chevron-up',
50
- ]"
51
- />
52
- </span>
53
- </button>
54
- </h2>
55
- <transition name="fade" mode="out-in">
56
- <div v-show="isFilterVisible" :id="props.filter.code" class="pt-6">
57
- <fieldset class="space-y-4">
58
- <legend class="sr-only">{{ props.filter.name }}</legend>
59
- <div
60
- v-for="option in props.filter.options || props.filter.entities"
61
- :key="`${option.id}-${selectedOptionIds?.includes(option.id)}`"
62
- class="flex items-center"
63
- >
64
- <input
65
- :id="`filter-mobile-${props.filter.code}-${option.id}`"
66
- :checked="selectedOptionIds?.includes(option.id)"
67
- :name="props.filter.name"
68
- :value="option.name"
69
- :aria-label="`${option.name} filter`"
70
- type="checkbox"
71
- class="h-4 w-4 border-gray-300 rounded text-indigo-600 focus:ring-indigo-500"
72
- @change="
73
- emits('select-value', {
74
- code: props.filter.code,
75
- value: option.id,
76
- })
77
- "
78
- />
79
-
80
- <div v-if="option.media?.url">
81
- <img
82
- loading="lazy"
83
- class="ml-2 h-4 w-4"
84
- :src="option.media.url"
85
- :alt="option.media.translated.alt || ''"
86
- :class="{
87
- 'border-blue border-2': selectedOptionIds?.includes(
88
- option.id,
89
- ),
90
- }"
91
- />
92
- </div>
93
- <div
94
- v-else-if="option.colorHexCode"
95
- class="ml-2 h-4 w-4"
96
- :style="`background-color: ${option.colorHexCode}`"
97
- :class="{
98
- 'border-blue border-2': selectedOptionIds?.includes(option.id),
99
- }"
100
- />
101
- <label
102
- :for="`filter-mobile-${props.filter.code}-${option.id}`"
103
- class="ml-3 text-gray-600"
104
- >
105
- {{ getTranslatedProperty(option, "name") }}
106
- </label>
107
- </div>
108
- </fieldset>
109
- </div>
110
- </transition>
111
- </div>
112
- </template>
113
- <style scoped>
114
- .fade-enter-active,
115
- .fade-leave-active {
116
- transition: all 0.2s ease;
117
- }
118
-
119
- .fade-enter-from,
120
- .fade-leave-to {
121
- opacity: 0;
122
- }
123
- </style>
@@ -1,101 +0,0 @@
1
- <script
2
- setup
3
- lang="ts"
4
- generic="
5
- ListingFilter extends {
6
- code: string;
7
- label: string;
8
- }
9
- "
10
- >
11
- import { computed, ref } from "vue";
12
- import type { Schemas } from "#shopware";
13
-
14
- const emits =
15
- defineEmits<
16
- (e: "select-value", value: { code: string; value: unknown }) => void
17
- >();
18
-
19
- const props = defineProps<{
20
- filter: ListingFilter;
21
- selectedFilters: Schemas["ProductListingResult"]["currentFilters"];
22
- }>();
23
- const isHoverActive = ref(false);
24
- const hoveredIndex = ref(0);
25
- const displayedScore = computed(() =>
26
- isHoverActive.value ? hoveredIndex.value : props.selectedFilters?.rating || 0,
27
- );
28
-
29
- const hoverRating = (key: number) => {
30
- hoveredIndex.value = key;
31
- isHoverActive.value = true;
32
- };
33
- const onChangeRating = () => {
34
- const newValue =
35
- props.selectedFilters?.rating !== hoveredIndex.value
36
- ? hoveredIndex.value
37
- : undefined;
38
- emits("select-value", { code: props.filter?.code, value: newValue });
39
- };
40
-
41
- const isFilterVisible = ref<boolean>(false);
42
- const toggle = () => {
43
- isFilterVisible.value = !isFilterVisible.value;
44
- };
45
- </script>
46
-
47
- <template>
48
- <div class="border-b border-gray-200 py-6 px-5">
49
- <h3 class="-my-3 flow-root">
50
- <button
51
- type="button"
52
- class="flex w-full items-center justify-between bg-white py-2 text-base text-gray-400 hover:text-gray-500"
53
- @click="toggle"
54
- >
55
- <span class="font-medium text-gray-900 text-left">{{
56
- props.filter.label
57
- }}</span>
58
- <span class="ml-6 flex items-center">
59
- <i
60
- :class="[
61
- !isFilterVisible
62
- ? 'i-carbon-chevron-down'
63
- : 'i-carbon-chevron-up',
64
- ]"
65
- />
66
- </span>
67
- </button>
68
- </h3>
69
- <transition name="fade" mode="out-in">
70
- <div v-show="isFilterVisible">
71
- <div class="space-y-6 mt-4">
72
- <div class="flex">
73
- <div
74
- v-for="i in 5"
75
- :key="i"
76
- class="h-6 w-6 c-yellow-500"
77
- :class="{
78
- 'i-carbon-star-filled': displayedScore >= i,
79
- 'i-carbon-star': displayedScore < i,
80
- }"
81
- @mouseleave="isHoverActive = false"
82
- @click="onChangeRating()"
83
- @mouseover="hoverRating(i)"
84
- />
85
- </div>
86
- </div>
87
- </div>
88
- </transition>
89
- </div>
90
- </template>
91
- <style scoped>
92
- .fade-enter-active,
93
- .fade-leave-active {
94
- transition: all 0.2s ease;
95
- }
96
-
97
- .fade-enter-from,
98
- .fade-leave-to {
99
- opacity: 0;
100
- }
101
- </style>