@shopware/cms-base-layer 3.0.0 → 3.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 (107) hide show
  1. package/README.md +64 -58
  2. package/app/components/SwCategoryNavigation.vue +1 -0
  3. package/app/components/SwCategoryNavigationLink.vue +16 -8
  4. package/app/components/SwContactForm.vue +1 -0
  5. package/app/components/SwFilterChips.vue +1 -0
  6. package/app/components/SwListingProductPrice.vue +16 -4
  7. package/app/components/SwMedia3D.vue +1 -4
  8. package/app/components/SwNewsletterForm.vue +4 -3
  9. package/app/components/SwProductAddToCart.vue +24 -7
  10. package/app/components/SwProductCard.vue +20 -3
  11. package/app/components/SwProductCardDetails.vue +27 -9
  12. package/app/components/SwProductCardImage.vue +31 -12
  13. package/app/components/SwProductCardSkeleton.vue +37 -14
  14. package/app/components/SwProductGallery.vue +1 -0
  15. package/app/components/SwProductListingFilter.vue +5 -1
  16. package/app/components/SwProductListingFilters.vue +57 -76
  17. package/app/components/SwProductListingFiltersHorizontal.vue +40 -71
  18. package/app/components/SwProductPrice.vue +1 -0
  19. package/app/components/SwProductReviews.vue +14 -6
  20. package/app/components/SwProductReviewsForm.vue +15 -3
  21. package/app/components/SwProductUnits.vue +1 -0
  22. package/app/components/SwQuantitySelect.vue +10 -3
  23. package/app/components/SwSharedPrice.vue +1 -0
  24. package/app/components/SwSlider.vue +1 -0
  25. package/app/components/SwStockInfo.vue +12 -6
  26. package/app/components/SwVariantConfigurator.vue +4 -1
  27. package/app/components/listing-filters/SwFilterPrice.vue +79 -30
  28. package/app/components/listing-filters/SwFilterProperties.vue +40 -25
  29. package/app/components/listing-filters/SwFilterRating.vue +21 -9
  30. package/app/components/listing-filters/SwFilterShippingFree.vue +16 -8
  31. package/app/components/public/cms/CmsBlockSpatialViewer.vue +1 -0
  32. package/app/components/public/cms/CmsGenericBlock.vue +3 -1
  33. package/app/components/public/cms/CmsGenericElement.vue +2 -1
  34. package/app/components/public/cms/CmsNoComponent.vue +2 -1
  35. package/app/components/public/cms/CmsPage.md +1 -1
  36. package/app/components/public/cms/CmsPage.vue +2 -0
  37. package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +1 -0
  38. package/app/components/public/cms/block/CmsBlockCenterText.vue +3 -1
  39. package/app/components/public/cms/block/CmsBlockCrossSelling.vue +1 -0
  40. package/app/components/public/cms/block/CmsBlockCustomForm.vue +1 -0
  41. package/app/components/public/cms/block/CmsBlockForm.vue +1 -0
  42. package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -1
  43. package/app/components/public/cms/block/CmsBlockImage.vue +1 -0
  44. package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +9 -2
  45. package/app/components/public/cms/block/CmsBlockImageCover.vue +1 -0
  46. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +10 -3
  47. package/app/components/public/cms/block/CmsBlockImageGallery.vue +1 -0
  48. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +10 -5
  49. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +11 -3
  50. package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +1 -0
  51. package/app/components/public/cms/block/CmsBlockImageSlider.vue +1 -0
  52. package/app/components/public/cms/block/CmsBlockImageText.vue +4 -1
  53. package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +4 -1
  54. package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -1
  55. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +4 -12
  56. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +13 -4
  57. package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +1 -0
  58. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +11 -3
  59. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +11 -4
  60. package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +1 -0
  61. package/app/components/public/cms/block/CmsBlockProductHeading.vue +3 -1
  62. package/app/components/public/cms/block/CmsBlockProductListing.vue +1 -0
  63. package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +1 -0
  64. package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +1 -0
  65. package/app/components/public/cms/block/CmsBlockText.vue +1 -0
  66. package/app/components/public/cms/block/CmsBlockTextHero.vue +1 -0
  67. package/app/components/public/cms/block/CmsBlockTextOnImage.vue +2 -4
  68. package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +1 -0
  69. package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -1
  70. package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +1 -0
  71. package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +1 -0
  72. package/app/components/public/cms/element/CmsElementBuyBox.vue +90 -72
  73. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +14 -3
  74. package/app/components/public/cms/element/CmsElementCrossSelling.vue +3 -1
  75. package/app/components/public/cms/element/CmsElementCustomForm.vue +1 -0
  76. package/app/components/public/cms/element/CmsElementForm.vue +1 -0
  77. package/app/components/public/cms/element/CmsElementHtml.vue +0 -1
  78. package/app/components/public/cms/element/CmsElementImage.vue +2 -1
  79. package/app/components/public/cms/element/CmsElementImageGallery.vue +6 -2
  80. package/app/components/public/cms/element/CmsElementManufacturerLogo.vue +1 -0
  81. package/app/components/public/cms/element/CmsElementProductBox.vue +1 -0
  82. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +93 -36
  83. package/app/components/public/cms/element/CmsElementProductListing.vue +16 -5
  84. package/app/components/public/cms/element/CmsElementProductSlider.vue +6 -4
  85. package/app/components/public/cms/element/CmsElementText.vue +2 -0
  86. package/app/components/public/cms/element/CmsElementVimeoVideo.vue +1 -0
  87. package/app/components/public/cms/element/CmsElementYoutubeVideo.vue +1 -0
  88. package/app/components/public/cms/element/SwProductListingPagination.vue +8 -2
  89. package/app/components/public/cms/section/CmsSectionSidebar.vue +13 -8
  90. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +16 -5
  91. package/app/components/ui/SwitchButton.vue +53 -14
  92. package/app/components/ui/WishlistIcon.vue +8 -2
  93. package/app/composables/useLcpImagePreload.test.ts +1 -0
  94. package/app/composables/useLcpImagePreload.ts +2 -0
  95. package/app/composables/useTypedAppConfig.ts +1 -0
  96. package/app/helpers/cms/getImageSizes.test.ts +1 -0
  97. package/app/helpers/html-to-vue/ast.ts +1 -0
  98. package/app/helpers/html-to-vue/getOptionsFromNode.test.ts +1 -0
  99. package/app/helpers/html-to-vue/renderToHtml.ts +1 -0
  100. package/app/helpers/html-to-vue/renderer.ts +1 -0
  101. package/app/providers/shopware.test.ts +1 -0
  102. package/app/utils/routeQuery.test.ts +65 -0
  103. package/app/utils/routeQuery.ts +26 -0
  104. package/app/utils/useSelectedListingFilters.test.ts +82 -0
  105. package/app/utils/useSelectedListingFilters.ts +105 -0
  106. package/nuxt.config.ts +24 -1
  107. package/package.json +22 -24
@@ -3,6 +3,7 @@ import type { CmsElementBuyBox } from "@shopware/composables";
3
3
  import { useCmsTranslations } from "@shopware/composables";
4
4
  import { defu } from "defu";
5
5
  import { computed } from "vue";
6
+
6
7
  import {
7
8
  useCmsElementConfig,
8
9
  usePrice,
@@ -67,79 +68,96 @@ const unitName = computed(() => product.value?.unit?.name);
67
68
  const productName = computed(() => product.value?.translated?.name || "");
68
69
  </script>
69
70
  <template>
70
- <div v-if="product" :class="{
71
- 'h-full w-full flex flex-col': true,
72
- 'justify-start': alignment === 'flex-start',
73
- 'justify-end': alignment === 'flex-end',
74
- 'justify-center': alignment === 'center',
75
- }">
76
- <div class="self-stretch inline-flex flex-col justify-start items-start gap-8 mt-4 min-w-0">
77
- <div
78
- class="md:hidden self-stretch text-surface-on-surface text-4xl font-normal font-serif leading-[60px]">
79
- {{ productName }}</div>
80
-
81
- <div v-if="tierPrices.length <= 1">
82
- <SwSharedPrice v-if="hasListPrice"
83
- class="text-1xl text-secondary-900 basis-2/6 justify-start line-through"
84
- :value="price?.listPrice?.price" />
85
- <SwSharedPrice v-if="unitPrice"
86
- class="text-surface-on-surface text-base font-bold leading-normal"
87
- :class="{
88
- 'text-red': hasListPrice,
89
- }" :value="unitPrice" />
90
- <div v-if="regulationPrice" class="text-xs flex text-secondary-500">
91
- {{ translations.product.previously }}
92
- <SwSharedPrice class="ml-1" :value="regulationPrice" />
93
- </div>
94
- </div>
95
- <div v-else>
96
- <table class="border-collapse table-auto w-full text-sm mb-8">
97
- <thead>
98
- <tr>
99
- <th
100
- class="border-b dark:border-secondary-600 font-medium p-4 pl-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left">
101
- {{ translations.product.amount }}
102
- </th>
71
+ <div
72
+ v-if="product"
73
+ :class="{
74
+ 'h-full w-full flex flex-col': true,
75
+ 'justify-start': alignment === 'flex-start',
76
+ 'justify-end': alignment === 'flex-end',
77
+ 'justify-center': alignment === 'center',
78
+ }"
79
+ >
80
+ <div
81
+ class="self-stretch inline-flex flex-col justify-start items-start gap-8 mt-4 min-w-0"
82
+ >
83
+ <div
84
+ class="md:hidden self-stretch text-surface-on-surface text-4xl font-normal font-serif leading-[60px]"
85
+ >
86
+ {{ productName }}
87
+ </div>
103
88
 
104
- <th
105
- class="border-b dark:border-secondary-600 font-medium p-4 pr-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left">
106
- {{ translations.product.price.label }}
107
- </th>
108
- </tr>
109
- </thead>
110
- <tbody class="bg-white dark:bg-secondary-800">
111
- <tr v-for="(tierPrice, index) in tierPrices" :key="tierPrice.label">
112
- <td
113
- class="border-b border-secondary-100 dark:border-secondary-700 p-4 pl-8 font-medium text-secondary-500 dark:text-secondary-400">
114
- <span v-if="index < tierPrices.length - 1">{{
115
- translations.product.to
116
- }}</span><span v-else>{{ translations.product.from }}</span>
117
- {{ tierPrice.quantity }}
118
- </td>
119
- <td
120
- class="border-b border-secondary-100 dark:border-secondary-700 p-4 pr-8 font-medium text-current-500 dark:text-secondary-400">
121
- {{ getFormattedPrice(tierPrice.unitPrice) }}
122
- </td>
123
- </tr>
124
- </tbody>
125
- </table>
126
- </div>
127
- <div v-if="purchaseUnit && unitName" class="mt-1">
128
- <span class="font-light"> {{ translations.product.content }}: </span>
129
- <span class="font-light"> {{ purchaseUnit }} {{ unitName }} </span>
130
- <span v-if="referencePrice" class="font-light">
131
- {{ currency?.symbol }} {{ referencePrice?.price }} / /
132
- {{ referencePrice?.referenceUnit }} {{ referencePrice?.unitName }}
133
- </span>
134
- </div>
135
- <span class="text-brand-primary">
136
- <template v-if="taxState === 'gross'">
137
- {{ translations.product.pricesIncl }}
138
- </template>
139
- <template v-else> {{ translations.product.pricesExcl }} </template>
140
- </span>
141
- <SwVariantConfigurator @change="changeVariant" />
142
- <SwProductAddToCart :product="product" />
89
+ <div v-if="tierPrices.length <= 1">
90
+ <SwSharedPrice
91
+ v-if="hasListPrice"
92
+ class="text-1xl text-secondary-900 basis-2/6 justify-start line-through"
93
+ :value="price?.listPrice?.price"
94
+ />
95
+ <SwSharedPrice
96
+ v-if="unitPrice"
97
+ class="text-surface-on-surface text-base font-bold leading-normal"
98
+ :class="{
99
+ 'text-red': hasListPrice,
100
+ }"
101
+ :value="unitPrice"
102
+ />
103
+ <div v-if="regulationPrice" class="text-xs flex text-secondary-500">
104
+ {{ translations.product.previously }}
105
+ <SwSharedPrice class="ml-1" :value="regulationPrice" />
143
106
  </div>
107
+ </div>
108
+ <div v-else>
109
+ <table class="border-collapse table-auto w-full text-sm mb-8">
110
+ <thead>
111
+ <tr>
112
+ <th
113
+ class="border-b dark:border-secondary-600 font-medium p-4 pl-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left"
114
+ >
115
+ {{ translations.product.amount }}
116
+ </th>
117
+
118
+ <th
119
+ class="border-b dark:border-secondary-600 font-medium p-4 pr-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left"
120
+ >
121
+ {{ translations.product.price.label }}
122
+ </th>
123
+ </tr>
124
+ </thead>
125
+ <tbody class="bg-white dark:bg-secondary-800">
126
+ <tr v-for="(tierPrice, index) in tierPrices" :key="tierPrice.label">
127
+ <td
128
+ class="border-b border-secondary-100 dark:border-secondary-700 p-4 pl-8 font-medium text-secondary-500 dark:text-secondary-400"
129
+ >
130
+ <span v-if="index < tierPrices.length - 1">{{
131
+ translations.product.to
132
+ }}</span
133
+ ><span v-else>{{ translations.product.from }}</span>
134
+ {{ tierPrice.quantity }}
135
+ </td>
136
+ <td
137
+ class="border-b border-secondary-100 dark:border-secondary-700 p-4 pr-8 font-medium text-current-500 dark:text-secondary-400"
138
+ >
139
+ {{ getFormattedPrice(tierPrice.unitPrice) }}
140
+ </td>
141
+ </tr>
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ <div v-if="purchaseUnit && unitName" class="mt-1">
146
+ <span class="font-light"> {{ translations.product.content }}: </span>
147
+ <span class="font-light"> {{ purchaseUnit }} {{ unitName }} </span>
148
+ <span v-if="referencePrice" class="font-light">
149
+ {{ currency?.symbol }} {{ referencePrice?.price }} / /
150
+ {{ referencePrice?.referenceUnit }} {{ referencePrice?.unitName }}
151
+ </span>
152
+ </div>
153
+ <span class="text-brand-primary">
154
+ <template v-if="taxState === 'gross'">
155
+ {{ translations.product.pricesIncl }}
156
+ </template>
157
+ <template v-else> {{ translations.product.pricesExcl }} </template>
158
+ </span>
159
+ <SwVariantConfigurator @change="changeVariant" />
160
+ <SwProductAddToCart :product="product" />
144
161
  </div>
162
+ </div>
145
163
  </template>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { Ref } from "vue";
3
3
  import { onMounted, ref } from "vue";
4
+
4
5
  import { useCategory, useNavigation } from "#imports";
5
6
  import type { Schemas } from "#shopware";
6
7
 
@@ -39,15 +40,25 @@ onMounted(async () => {
39
40
  </script>
40
41
  <template>
41
42
  <ClientOnly>
42
- <div v-if="!loading && categoryNavigation && categoryNavigation.length" class="self-stretch inline-flex flex-col justify-start items-start gap-3">
43
+ <div
44
+ v-if="!loading && categoryNavigation && categoryNavigation.length"
45
+ class="self-stretch inline-flex flex-col justify-start items-start gap-3"
46
+ >
43
47
  <SwCategoryNavigation
44
48
  :level="0"
45
49
  :elements="categoryNavigation"
46
50
  :active-category="activeCategory"
47
51
  />
48
52
  </div>
49
- <div v-else-if="loading" class="self-stretch flex flex-col justify-start items-start gap-4 animate-pulse">
50
- <div v-for="i in 3" :key="i" class="w-full h-12 bg-surface-surface-container rounded"></div>
53
+ <div
54
+ v-else-if="loading"
55
+ class="self-stretch flex flex-col justify-start items-start gap-4 animate-pulse"
56
+ >
57
+ <div
58
+ v-for="i in 3"
59
+ :key="i"
60
+ class="w-full h-12 bg-surface-surface-container rounded"
61
+ ></div>
51
62
  </div>
52
63
  </ClientOnly>
53
64
  </template>
@@ -5,6 +5,7 @@ import type {
5
5
  } from "@shopware/composables";
6
6
  import { useElementSize } from "@vueuse/core";
7
7
  import { computed, inject, ref, useTemplateRef } from "vue";
8
+
8
9
  import { useCmsElementConfig } from "#imports";
9
10
 
10
11
  const props = defineProps<{
@@ -80,7 +81,8 @@ const toggleTab = (index: number) => {
80
81
  :key="index"
81
82
  class="transition text-lg font-semibold text-surface-on-surface-variant cursor-pointer"
82
83
  :class="{
83
- 'border-b-3 border-brand-primary text-brand-primary': currentTabIndex === index,
84
+ 'border-b-3 border-brand-primary text-brand-primary':
85
+ currentTabIndex === index,
84
86
  }"
85
87
  @click="toggleTab(index)"
86
88
  >
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsElementForm } from "@shopware/composables";
3
3
  import { computed, defineAsyncComponent } from "vue";
4
+
4
5
  import { useCmsElementConfig } from "#imports";
5
6
 
6
7
  const props = defineProps<{
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsElementForm } from "@shopware/composables";
3
3
  import { computed, defineAsyncComponent } from "vue";
4
+
4
5
  import { useCmsElementConfig } from "#imports";
5
6
 
6
7
  const props = defineProps<{
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsElementHtml } from "@shopware/composables";
3
-
4
3
  import { getCmsLayoutConfiguration } from "@shopware/helpers";
5
4
  import { h } from "vue";
6
5
 
@@ -6,7 +6,9 @@ import type {
6
6
  import { buildUrlPrefix } from "@shopware/helpers";
7
7
  import { useElementSize } from "@vueuse/core";
8
8
  import { computed, defineAsyncComponent, useTemplateRef } from "vue";
9
+
9
10
  import { useCmsElementImage, useUrlResolver } from "#imports";
11
+
10
12
  import { isSpatial } from "../../../../helpers/media/isSpatial";
11
13
 
12
14
  const props = defineProps<{
@@ -90,7 +92,6 @@ const SwMedia3D = computed(() => {
90
92
  preset="productDetail"
91
93
  loading="lazy"
92
94
  :width="imageSize"
93
- :height="imageSize"
94
95
  :class="{
95
96
  'w-full': !imageGallery,
96
97
  'h-full': !imageGallery && ['cover', 'stretch'].includes(displayMode),
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsElementImageGallery } from "@shopware/composables";
3
3
  import { computed, defineAsyncComponent, ref } from "vue";
4
+
4
5
  import { useCmsElementConfig, useImagePlaceholder } from "#imports";
6
+
5
7
  import { isSpatial } from "../../../../helpers/media/isSpatial";
6
8
 
7
9
  // Load SwMedia3D only on client-side to avoid SSR issues with three.js packages
@@ -102,7 +104,7 @@ function onTouchEnd() {
102
104
  v-if="currentImage && isSpatial(currentImage)"
103
105
  :key="currentImage.url + '-3d'"
104
106
  class="w-full h-full relative"
105
- :style="{ minHeight }"
107
+ :style="{ minHeight }"
106
108
  >
107
109
  <client-only>
108
110
  <SwMedia3DAsync :src="currentImage.url" />
@@ -180,7 +182,9 @@ function onTouchEnd() {
180
182
  v-if="mediaGallery.length > 1 && navigationDots !== 'none'"
181
183
  :class="[
182
184
  'flex justify-center items-center gap-2',
183
- navigationDots === 'outside' ? 'mt-4' : 'absolute bottom-4 left-1/2 transform -translate-x-1/2',
185
+ navigationDots === 'outside'
186
+ ? 'mt-4'
187
+ : 'absolute bottom-4 left-1/2 transform -translate-x-1/2',
184
188
  ]"
185
189
  >
186
190
  <button
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsElementManufacturerLogo } from "@shopware/composables";
3
+
3
4
  import CmsElementImage from "./CmsElementImage.vue";
4
5
 
5
6
  defineProps<{
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsElementProductBox } from "@shopware/composables";
3
3
  import { computed } from "vue";
4
+
4
5
  import { useCmsElementConfig } from "#imports";
5
6
 
6
7
  const props = defineProps<{
@@ -1,10 +1,12 @@
1
1
  <script setup lang="ts">
2
+ import { encodeForQuery } from "@shopware/api-client/helpers";
2
3
  import type { CmsElementProductDescriptionReviews } from "@shopware/composables";
3
4
  import { useCmsTranslations } from "@shopware/composables";
4
5
  import { getTranslatedProperty } from "@shopware/helpers";
5
6
  import { defu } from "defu";
6
7
  import { type Ref, computed, onMounted, ref } from "vue";
7
8
  import xss from "xss";
9
+
8
10
  import { useProduct, useShopwareContext, useUser } from "#imports";
9
11
  import type { Schemas } from "#shopware";
10
12
 
@@ -55,18 +57,26 @@ const isSectionOpen = (sectionNumber: number) => {
55
57
  };
56
58
 
57
59
  const reviews: Ref<Schemas["ProductReview"][]> = ref([]);
58
- const { apiClient } = useShopwareContext();
60
+ const { apiClient, cacheableReads } = useShopwareContext();
59
61
  const { isLoggedIn } = useUser();
60
62
  const reviewAdded = ref(false);
61
63
 
62
64
  const fetchReviews = async () => {
63
65
  try {
64
- const reviewsResponse = await apiClient.invoke(
65
- "readProductReviews post /product/{productId}/reviews",
66
- {
67
- pathParams: { productId: product.value.id },
68
- },
69
- );
66
+ const reviewsResponse = cacheableReads
67
+ ? await apiClient.invoke(
68
+ "readProductReviewsGet get /product/{productId}/reviews",
69
+ {
70
+ pathParams: { productId: product.value.id },
71
+ query: { _criteria: encodeForQuery({}) },
72
+ },
73
+ )
74
+ : await apiClient.invoke(
75
+ "readProductReviews post /product/{productId}/reviews",
76
+ {
77
+ pathParams: { productId: product.value.id },
78
+ },
79
+ );
70
80
  reviews.value = reviewsResponse.data.elements || [];
71
81
  } catch (error) {
72
82
  console.error("Failed to fetch reviews:", error);
@@ -89,28 +99,39 @@ onMounted(async () => {
89
99
  </script>
90
100
 
91
101
  <template>
92
- <div class="w-full self-stretch inline-flex flex-col justify-start items-start gap-4">
102
+ <div
103
+ class="w-full self-stretch inline-flex flex-col justify-start items-start gap-4"
104
+ >
93
105
  <div class="self-stretch flex flex-col justify-center items-center">
94
106
  <div
95
107
  class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1 cursor-pointer hover:bg-surface-surface-variant transition-colors"
96
- @click="toggleSection(1)">
108
+ @click="toggleSection(1)"
109
+ >
97
110
  <div class="flex-1 flex items-center gap-2.5">
98
- <div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
111
+ <div
112
+ class="flex-1 text-surface-on-surface text-base font-bold leading-normal"
113
+ >
99
114
  {{ translations.product.description }}
100
115
  </div>
101
116
  </div>
102
117
  <div class="w-6 h-6 relative">
103
118
  <div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
104
- <div class="i-carbon-chevron-down transition-transform duration-200"
105
- :class="{ 'rotate-180': isSectionOpen(1) }"></div>
119
+ <div
120
+ class="i-carbon-chevron-down transition-transform duration-200"
121
+ :class="{ 'rotate-180': isSectionOpen(1) }"
122
+ ></div>
106
123
  </div>
107
124
  </div>
108
125
  </div>
109
126
  </div>
110
127
  <Transition name="accordion">
111
- <div v-if="isSectionOpen(1)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
128
+ <div
129
+ v-if="isSectionOpen(1)"
130
+ class="self-stretch flex flex-col justify-center items-center gap-2.5"
131
+ >
112
132
  <div
113
- class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
133
+ class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
134
+ >
114
135
  <!-- eslint-disable-next-line vue/no-v-html -->
115
136
  <div v-html="description"></div>
116
137
  </div>
@@ -119,43 +140,68 @@ onMounted(async () => {
119
140
  <div class="self-stretch flex flex-col justify-center items-center">
120
141
  <div
121
142
  class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1 cursor-pointer hover:bg-surface-surface-variant transition-colors"
122
- @click="toggleSection(2)">
143
+ @click="toggleSection(2)"
144
+ >
123
145
  <div class="flex-1 flex items-center gap-2.5">
124
- <div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
146
+ <div
147
+ class="flex-1 text-surface-on-surface text-base font-bold leading-normal"
148
+ >
125
149
  {{ translations.product.reviews }} ({{ reviews.length }})
126
150
  </div>
127
151
  </div>
128
152
  <div class="w-6 h-6 relative">
129
153
  <div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
130
- <div class="i-carbon-chevron-down transition-transform duration-200"
131
- :class="{ 'rotate-180': isSectionOpen(2) }"></div>
154
+ <div
155
+ class="i-carbon-chevron-down transition-transform duration-200"
156
+ :class="{ 'rotate-180': isSectionOpen(2) }"
157
+ ></div>
132
158
  </div>
133
159
  </div>
134
160
  </div>
135
161
  </div>
136
162
  <Transition name="accordion">
137
- <div v-if="isSectionOpen(2)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
163
+ <div
164
+ v-if="isSectionOpen(2)"
165
+ class="self-stretch flex flex-col justify-center items-center gap-2.5"
166
+ >
138
167
  <div
139
- class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
140
- <SwProductReviews v-if="product" :product="product" :reviews="reviews" />
168
+ class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
169
+ >
170
+ <SwProductReviews
171
+ v-if="product"
172
+ :product="product"
173
+ :reviews="reviews"
174
+ />
141
175
  <ClientOnly>
142
176
  <SwProductReviewsForm
143
177
  v-if="isLoggedIn && !reviewAdded && product"
144
178
  :product-id="product.id"
145
179
  @success="handleReviewAdded"
146
180
  />
147
- <div v-else-if="!isLoggedIn && product" class="mt-4 p-3 bg-surface-surface-container border border-surface-on-surface-variant rounded-md flex gap-2 md:gap-3 items-center">
148
- <div class="w-5 h-5 text-surface-on-surface-variant flex-shrink-0">
181
+ <div
182
+ v-else-if="!isLoggedIn && product"
183
+ class="mt-4 p-3 bg-surface-surface-container border border-surface-on-surface-variant rounded-md flex gap-2 md:gap-3 items-center"
184
+ >
185
+ <div
186
+ class="w-5 h-5 text-surface-on-surface-variant flex-shrink-0"
187
+ >
149
188
  <SwUserIcon :size="20" />
150
189
  </div>
151
- <span class="text-sm text-surface-on-surface-variant">{{ translations.product.messages.loginToReview }}</span>
190
+ <span class="text-sm text-surface-on-surface-variant">{{
191
+ translations.product.messages.loginToReview
192
+ }}</span>
152
193
  </div>
153
194
  </ClientOnly>
154
- <div v-if="reviewAdded" class="mt-4 p-3 bg-surface-surface-container border border-states-success rounded-md flex gap-2 md:gap-3 items-center">
195
+ <div
196
+ v-if="reviewAdded"
197
+ class="mt-4 p-3 bg-surface-surface-container border border-states-success rounded-md flex gap-2 md:gap-3 items-center"
198
+ >
155
199
  <div class="w-5 h-5 text-states-success flex-shrink-0">
156
200
  <SwCheckmarkIcon :size="20" :filled="true" alt="Success" />
157
201
  </div>
158
- <span class="text-sm text-states-success">{{ translations.product.messages.reviewAdded }}</span>
202
+ <span class="text-sm text-states-success">{{
203
+ translations.product.messages.reviewAdded
204
+ }}</span>
159
205
  </div>
160
206
  </div>
161
207
  </div>
@@ -163,32 +209,43 @@ onMounted(async () => {
163
209
  <div class="self-stretch flex flex-col justify-center items-center">
164
210
  <div
165
211
  class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1 cursor-pointer hover:bg-surface-surface-variant transition-colors"
166
- @click="toggleSection(3)">
212
+ @click="toggleSection(3)"
213
+ >
167
214
  <div class="flex-1 flex items-center gap-2.5">
168
- <div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
215
+ <div
216
+ class="flex-1 text-surface-on-surface text-base font-bold leading-normal"
217
+ >
169
218
  Category
170
219
  </div>
171
220
  </div>
172
221
  <div class="w-6 h-6 relative">
173
222
  <div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
174
- <div class="i-carbon-chevron-down transition-transform duration-200"
175
- :class="{ 'rotate-180': isSectionOpen(3) }"></div>
223
+ <div
224
+ class="i-carbon-chevron-down transition-transform duration-200"
225
+ :class="{ 'rotate-180': isSectionOpen(3) }"
226
+ ></div>
176
227
  </div>
177
228
  </div>
178
229
  </div>
179
230
  </div>
180
231
  <Transition name="accordion">
181
- <div v-if="isSectionOpen(3)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
232
+ <div
233
+ v-if="isSectionOpen(3)"
234
+ class="self-stretch flex flex-col justify-center items-center gap-2.5"
235
+ >
182
236
  <div
183
- class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
237
+ class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
238
+ >
184
239
  <div v-if="product?.categories">
185
- <div v-for="category in product.categories" :key="category.id" class="mb-2">
240
+ <div
241
+ v-for="category in product.categories"
242
+ :key="category.id"
243
+ class="mb-2"
244
+ >
186
245
  {{ category.name }}
187
246
  </div>
188
247
  </div>
189
- <div v-else>
190
- No categories available
191
- </div>
248
+ <div v-else>No categories available</div>
192
249
  </div>
193
250
  </div>
194
251
  </Transition>
@@ -3,6 +3,7 @@ import type { CmsElementProductListing } from "@shopware/composables";
3
3
  import { useCmsTranslations } from "@shopware/composables";
4
4
  import { defu } from "defu";
5
5
  import { computed, ref, useTemplateRef, watch } from "vue";
6
+
6
7
  import {
7
8
  useCategoryListing,
8
9
  useCmsElementConfig,
@@ -155,10 +156,17 @@ compareRouteQueryWithInitialListing();
155
156
 
156
157
  <template>
157
158
  <div class="max-w-2xl mx-auto lg:max-w-full">
158
- <div v-if="!loading && getElements.length < 1" class="text-center text-xl py-16 text-surface-on-surface-variant">
159
+ <div
160
+ v-if="!loading && getElements.length < 1"
161
+ class="text-center text-xl py-16 text-surface-on-surface-variant"
162
+ >
159
163
  {{ translations.listing.noProducts }}
160
164
  </div>
161
- <div v-if="!loading" ref="productListElement" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 auto-rows-fr gap-x-4 sm:gap-x-6 lg:gap-x-8 gap-y-8 sm:gap-y-12 lg:gap-y-16">
165
+ <div
166
+ v-if="!loading"
167
+ ref="productListElement"
168
+ class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 auto-rows-fr gap-x-4 sm:gap-x-6 lg:gap-x-8 gap-y-8 sm:gap-y-12 lg:gap-y-16"
169
+ >
162
170
  <SwProductCard
163
171
  v-for="product in getElements"
164
172
  :key="product.id"
@@ -168,9 +176,12 @@ compareRouteQueryWithInitialListing();
168
176
  class="w-full"
169
177
  />
170
178
  </div>
171
- <div v-if="loading" data-testid="loading" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 auto-rows-fr gap-x-4 sm:gap-x-6 lg:gap-x-8 gap-y-8 sm:gap-y-12 lg:gap-y-16">
172
- <ProductCardSkeleton v-for="index in limit" :key="index"
173
- class="w-full" />
179
+ <div
180
+ v-if="loading"
181
+ data-testid="loading"
182
+ class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 auto-rows-fr gap-x-4 sm:gap-x-6 lg:gap-x-8 gap-y-8 sm:gap-y-12 lg:gap-y-16"
183
+ >
184
+ <ProductCardSkeleton v-for="index in limit" :key="index" class="w-full" />
174
185
  </div>
175
186
  <SwProductListingPagination
176
187
  v-if="!loading"
@@ -6,6 +6,7 @@ import type {
6
6
  import { useElementSize } from "@vueuse/core";
7
7
  import { computed, inject, useTemplateRef } from "vue";
8
8
  import type { CSSProperties, ComputedRef } from "vue";
9
+
9
10
  import { useCmsElementConfig } from "#imports";
10
11
 
11
12
  const props = defineProps<{
@@ -70,11 +71,12 @@ const hasVerticalAlignment = computed(
70
71
  );
71
72
  </script>
72
73
  <template>
73
- <div
74
- :style="hasVerticalAlignment ? verticalAlignStyle : undefined"
75
- >
74
+ <div :style="hasVerticalAlignment ? verticalAlignStyle : undefined">
76
75
  <div ref="productSlider" class="cms-element-product-slider">
77
- <h3 v-if="title" class="pl-6 pb-6 text-center md:text-left text-surface-on-surface">
76
+ <h3
77
+ v-if="title"
78
+ class="pl-6 pb-6 text-center md:text-left text-surface-on-surface"
79
+ >
78
80
  {{ title }}
79
81
  </h3>
80
82
  <div :class="{ 'py-5 border border-outline-outline-variant': border }">
@@ -3,7 +3,9 @@ import type { CmsElementText } from "@shopware/composables";
3
3
  import { decodeHTML } from "entities";
4
4
  import { computed, defineComponent, getCurrentInstance, h } from "vue";
5
5
  import type { CSSProperties, VNode, VNodeArrayChildren } from "vue";
6
+
6
7
  import { useCmsElementConfig, useUrlResolver } from "#imports";
8
+
7
9
  import { getOptionsFromNode } from "../../../../helpers/html-to-vue/getOptionsFromNode";
8
10
  import type { NodeObject } from "../../../../helpers/html-to-vue/getOptionsFromNode";
9
11
  import { renderHtml } from "../../../../helpers/html-to-vue/renderToHtml";
@@ -2,6 +2,7 @@
2
2
  import type { CmsElementVimeoVideo } from "@shopware/composables";
3
3
  import { ref } from "vue";
4
4
  import type { Ref } from "vue";
5
+
5
6
  import { useCmsElementConfig } from "#imports";
6
7
 
7
8
  const props = defineProps<{