@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
@@ -0,0 +1,217 @@
1
+ <script setup lang="ts">
2
+ import type { CmsElementProductDescriptionReviews } from "@shopware/composables";
3
+ import { useCmsTranslations } from "@shopware/composables";
4
+ import { getTranslatedProperty } from "@shopware/helpers";
5
+ import { defu } from "defu";
6
+ import { type Ref, computed, onMounted, ref } from "vue";
7
+ import xss from "xss";
8
+ import { useProduct, useShopwareContext, useUser } from "#imports";
9
+ import type { Schemas } from "#shopware";
10
+
11
+ const props = defineProps<{
12
+ content: CmsElementProductDescriptionReviews;
13
+ }>();
14
+
15
+ type Translations = {
16
+ product: {
17
+ description: string;
18
+ reviews: string;
19
+ messages: {
20
+ reviewAdded: string;
21
+ loginToReview: string;
22
+ };
23
+ };
24
+ };
25
+
26
+ let translations: Translations = {
27
+ product: {
28
+ description: "Description",
29
+ reviews: "Reviews",
30
+ messages: {
31
+ reviewAdded: "Thank you for submitting your review",
32
+ loginToReview: "Please log in to write a review",
33
+ },
34
+ },
35
+ };
36
+ translations = defu(useCmsTranslations(), translations) as Translations;
37
+
38
+ const openSections = ref<Set<number>>(new Set([1]));
39
+ const { product } = useProduct(props.content.data?.product);
40
+
41
+ const description = computed(() =>
42
+ xss(getTranslatedProperty(product.value, "description")),
43
+ );
44
+
45
+ const toggleSection = (sectionNumber: number) => {
46
+ if (openSections.value.has(sectionNumber)) {
47
+ openSections.value.delete(sectionNumber);
48
+ } else {
49
+ openSections.value.add(sectionNumber);
50
+ }
51
+ };
52
+
53
+ const isSectionOpen = (sectionNumber: number) => {
54
+ return openSections.value.has(sectionNumber);
55
+ };
56
+
57
+ const reviews: Ref<Schemas["ProductReview"][]> = ref([]);
58
+ const { apiClient } = useShopwareContext();
59
+ const { isLoggedIn } = useUser();
60
+ const reviewAdded = ref(false);
61
+
62
+ const fetchReviews = async () => {
63
+ try {
64
+ const reviewsResponse = await apiClient.invoke(
65
+ "readProductReviews post /product/{productId}/reviews",
66
+ {
67
+ pathParams: { productId: product.value.id },
68
+ },
69
+ );
70
+ reviews.value = reviewsResponse.data.elements || [];
71
+ } catch (error) {
72
+ console.error("Failed to fetch reviews:", error);
73
+ // Keep existing reviews if fetch fails
74
+ }
75
+ };
76
+
77
+ const handleReviewAdded = () => {
78
+ reviewAdded.value = true;
79
+ fetchReviews();
80
+ };
81
+
82
+ onMounted(async () => {
83
+ if (props.content.data?.reviews?.elements?.length) {
84
+ reviews.value = props.content.data.reviews.elements;
85
+ } else {
86
+ await fetchReviews();
87
+ }
88
+ });
89
+ </script>
90
+
91
+ <template>
92
+ <div class="w-full self-stretch inline-flex flex-col justify-start items-start gap-4">
93
+ <div class="self-stretch flex flex-col justify-center items-center">
94
+ <div
95
+ 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)">
97
+ <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">
99
+ {{ translations.product.description }}
100
+ </div>
101
+ </div>
102
+ <div class="w-6 h-6 relative">
103
+ <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>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <Transition name="accordion">
111
+ <div v-if="isSectionOpen(1)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
112
+ <div
113
+ class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
114
+ <!-- eslint-disable-next-line vue/no-v-html -->
115
+ <div v-html="description"></div>
116
+ </div>
117
+ </div>
118
+ </Transition>
119
+ <div class="self-stretch flex flex-col justify-center items-center">
120
+ <div
121
+ 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)">
123
+ <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">
125
+ {{ translations.product.reviews }} ({{ reviews.length }})
126
+ </div>
127
+ </div>
128
+ <div class="w-6 h-6 relative">
129
+ <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>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ <Transition name="accordion">
137
+ <div v-if="isSectionOpen(2)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
138
+ <div
139
+ class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
140
+ <SwProductReviews v-if="product" :product="product" :reviews="reviews" />
141
+ <ClientOnly>
142
+ <SwProductReviewsForm
143
+ v-if="isLoggedIn && !reviewAdded && product"
144
+ :product-id="product.id"
145
+ @success="handleReviewAdded"
146
+ />
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">
149
+ <SwUserIcon :size="20" />
150
+ </div>
151
+ <span class="text-sm text-surface-on-surface-variant">{{ translations.product.messages.loginToReview }}</span>
152
+ </div>
153
+ </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">
155
+ <div class="w-5 h-5 text-states-success flex-shrink-0">
156
+ <SwCheckmarkIcon :size="20" :filled="true" alt="Success" />
157
+ </div>
158
+ <span class="text-sm text-states-success">{{ translations.product.messages.reviewAdded }}</span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </Transition>
163
+ <div class="self-stretch flex flex-col justify-center items-center">
164
+ <div
165
+ 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)">
167
+ <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">
169
+ Category
170
+ </div>
171
+ </div>
172
+ <div class="w-6 h-6 relative">
173
+ <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>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ <Transition name="accordion">
181
+ <div v-if="isSectionOpen(3)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
182
+ <div
183
+ class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
184
+ <div v-if="product?.categories">
185
+ <div v-for="category in product.categories" :key="category.id" class="mb-2">
186
+ {{ category.name }}
187
+ </div>
188
+ </div>
189
+ <div v-else>
190
+ No categories available
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </Transition>
195
+ </div>
196
+ </template>
197
+ <style scoped>
198
+ .accordion-enter-active,
199
+ .accordion-leave-active {
200
+ transition: all 0.3s ease;
201
+ overflow: hidden;
202
+ }
203
+
204
+ .accordion-enter-from,
205
+ .accordion-leave-to {
206
+ max-height: 0;
207
+ opacity: 0;
208
+ transform: translateY(-10px);
209
+ }
210
+
211
+ .accordion-enter-to,
212
+ .accordion-leave-from {
213
+ max-height: 500px;
214
+ opacity: 1;
215
+ transform: translateY(0);
216
+ }
217
+ </style>
@@ -14,7 +14,7 @@ const props = defineProps<{
14
14
  const defaultLimit = 15;
15
15
  const defaultPage = 1;
16
16
  const defaultOrder = "name-asc";
17
- const productListElement = useTemplateRef("productListElement");
17
+ const productListElement = useTemplateRef<HTMLDivElement>("productListElement");
18
18
 
19
19
  type Translations = {
20
20
  listing: {
@@ -87,13 +87,11 @@ const changePage = async (page: number) => {
87
87
  productListElement.value?.scrollIntoView({ behavior: "smooth" });
88
88
  };
89
89
 
90
- const changeLimit = async (limit: Event) => {
91
- const select = limit.target as HTMLSelectElement;
92
-
90
+ const changeLimit = async (newLimit: number) => {
93
91
  await router.push({
94
92
  query: {
95
93
  ...route.query,
96
- limit: select.value,
94
+ limit: newLimit,
97
95
  p: defaultPage,
98
96
  },
99
97
  });
@@ -151,95 +149,26 @@ compareRouteQueryWithInitialListing();
151
149
  </script>
152
150
 
153
151
  <template>
154
- <div class="bg-white">
155
- <div class="max-w-2xl mx-auto lg:max-w-full">
156
- <div class="mt-6">
157
- <div
158
- v-if="!loading"
159
- ref="productListElement"
160
- class="flex justify-center flex-wrap p-4 md:p-6 lg:p-8 productListElement"
161
- >
162
- <SwProductCard
163
- v-for="product in getElements"
164
- :key="product.id"
165
- :product="product"
166
- :is-product-listing="isProductListing"
167
- class="p-4 border rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 ease-in-out w-full lg:w-3/7 2xl:w-7/24 mr-0 sm:mr-8 mb-8"
168
- />
169
- </div>
170
- <div
171
- v-if="loading"
172
- data-testid="loading"
173
- class="flex justify-center flex-wrap p-4 md:p-6 lg:p-8"
174
- >
175
- <ProductCardSkeleton
176
- v-for="index in limit"
177
- :key="index"
178
- class="w-full mb-8 sm:w-3/7 lg:w-2/7 2xl:w-7/24 mr-0 sm:mr-8 mb-8"
179
- />
180
- </div>
181
- <div
182
- class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 lg:gap-8 p-4 md:p-6 lg:p-8"
183
- >
184
- <div class="text-center place-self-center">
185
- <SwPagination
186
- :total="getTotalPagesCount"
187
- :current="Number(getCurrentPage)"
188
- @change-page="changePage"
189
- />
190
- </div>
191
- <div class="text-center place-self-center mt-2 lg:mt-0">
192
- <div
193
- class="inline-block align-top text-center md:text-left"
194
- data-testid="listing-pagination-limit-box"
195
- >
196
- <label
197
- for="limit"
198
- class="inline mr-4"
199
- data-testid="listing-pagination-limit-label"
200
- >{{ translations.listing.perPage }}</label
201
- >
202
- <select
203
- id="limit"
204
- v-model="limit"
205
- name="limitchoices"
206
- class="inline appearance-none bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
207
- data-testid="listing-pagination-limit-select"
208
- @change="changeLimit"
209
- >
210
- <option :value="1">1 {{ translations.listing.product }}</option>
211
- <option :value="15">
212
- 15 {{ translations.listing.products }}
213
- </option>
214
- <option :value="30">
215
- 30 {{ translations.listing.products }}
216
- </option>
217
- <option :value="45">
218
- 45 {{ translations.listing.products }}
219
- </option>
220
- </select>
221
- <div
222
- class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
223
- >
224
- <svg
225
- class="fill-current h-4 w-4"
226
- xmlns="http://www.w3.org/2000/svg"
227
- viewBox="0 0 20 20"
228
- >
229
- <path
230
- d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
231
- />
232
- </svg>
233
- </div>
234
- </div>
235
- </div>
236
- </div>
237
- </div>
238
- <!-- <div v-else>
239
- <h2 class="mx-auto text-center">
240
- {{ translations.listing.noProducts }}
241
- </h2>
242
- </div> -->
152
+ <div class="max-w-2xl mx-auto lg:max-w-full">
153
+ <div v-if="!loading && getElements.length < 1" class="text-center text-xl py-16 text-surface-on-surface-variant">
154
+ {{ translations.listing.noProducts }}
155
+ </div>
156
+ <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">
157
+ <SwProductCard v-for="product in getElements" :key="product.id" :product="product"
158
+ :is-product-listing="isProductListing" class="w-full" />
159
+ </div>
160
+ <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">
161
+ <ProductCardSkeleton v-for="index in limit" :key="index"
162
+ class="w-full" />
243
163
  </div>
164
+ <SwProductListingPagination
165
+ v-if="!loading"
166
+ v-model:limit="limit"
167
+ :total="getTotalPagesCount"
168
+ :current="Number(getCurrentPage)"
169
+ :translations="translations"
170
+ @change-page="changePage"
171
+ @change-limit="changeLimit"
172
+ />
244
173
  </div>
245
174
  </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import type { CmsElementProductName } from "@shopware/composables";
3
+
4
+ defineProps<{
5
+ content: CmsElementProductName;
6
+ }>();
7
+ </script>
8
+ <template>
9
+ <!-- there is no css config coming from API for this element so we don't need to merge -->
10
+ <CmsElementText :content="content as any" class="self-stretch text-surface-on-surface text-4xl font-normal font-serif leading-[60px]" />
11
+ </template>
@@ -12,12 +12,12 @@ const props = defineProps<{
12
12
  }>();
13
13
  const { getConfigValue } = useCmsElementConfig(props.content);
14
14
 
15
- const productSlider = useTemplateRef("productSlider");
15
+ const productSlider = useTemplateRef<HTMLDivElement>("productSlider");
16
16
  const slidesToShow = ref<number>();
17
17
  const products = computed(() => props.content?.data?.products ?? []);
18
18
  const config: ComputedRef<SliderElementConfig> = computed(() => ({
19
19
  minHeight: {
20
- value: "300px",
20
+ value: "450px",
21
21
  source: "static",
22
22
  },
23
23
  verticalAlign: {
@@ -55,10 +55,10 @@ const border = computed(() => getConfigValue("border"));
55
55
  </script>
56
56
  <template>
57
57
  <div ref="productSlider" class="cms-element-product-slider">
58
- <h3 v-if="title" class="mb-5 text-lg font-bold text-secondary-700">
58
+ <h3 v-if="title" class="pl-6 pb-6 text-center md:text-left text-surface-on-surface">
59
59
  {{ title }}
60
60
  </h3>
61
- <div :class="{ 'py-5 border border-secondary-300': border }">
61
+ <div :class="{ 'py-5 border border-outline-outline-variant': border }">
62
62
  <SwSlider
63
63
  :config="config"
64
64
  gap="1.25rem"
@@ -75,8 +75,14 @@ const CmsTextRender = defineComponent({
75
75
  "rounded-md inline-block my-2 py-2 px-4 border border-transparent text-sm font-medium focus:outline-none disabled:opacity-75";
76
76
 
77
77
  _class = node.attrs.class
78
- .replace("btn-secondary", `${btnClass} bg-dark text-white`)
79
- .replace("btn-primary", `${btnClass} bg-primary text-white`);
78
+ .replace(
79
+ "btn-secondary",
80
+ `${btnClass} bg-brand-secondary text-brand-on-secondary hover:bg-brand-secondary-hover`,
81
+ )
82
+ .replace(
83
+ "btn-primary",
84
+ `${btnClass} bg-brand-primary text-brand-on-primary hover:bg-brand-primary-hover`,
85
+ );
80
86
  }
81
87
 
82
88
  return createElement(
@@ -15,7 +15,7 @@ const config = computed(() => ({
15
15
  loop: getConfigValue("loop")
16
16
  ? `loop=1&playlist=${getConfigValue("videoID")}&`
17
17
  : "",
18
- showControls: getConfigValue("showControls") ? "controls=0&" : "",
18
+ showControls: getConfigValue("showControls") ? "controls=1&" : "controls=0&",
19
19
  start:
20
20
  Number.parseInt(getConfigValue("start")) !== 0
21
21
  ? `start=${getConfigValue("start")}&`
@@ -27,7 +27,13 @@ const config = computed(() => ({
27
27
  disableKeyboard: "disablekb=1",
28
28
  }));
29
29
 
30
- const videoUrl = `https://www.youtube-nocookie.com/embed/\
30
+ const YOUTUBE_URL = "https://www.youtube.com/embed/";
31
+ const YOUTUBE_NOCOOKIE_URL = "https://www.youtube-nocookie.com/embed/";
32
+ const videoDomain = getConfigValue("advancedPrivacyMode")
33
+ ? YOUTUBE_NOCOOKIE_URL
34
+ : YOUTUBE_URL;
35
+
36
+ const videoUrl = `${videoDomain}\
31
37
  ${config.value.videoID}?\
32
38
  ${config.value.relatedVideos}\
33
39
  ${config.value.loop}\
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ type Translations = {
3
+ listing: {
4
+ perPage: string;
5
+ product: string;
6
+ products: string;
7
+ };
8
+ };
9
+
10
+ defineProps<{
11
+ total: number;
12
+ current: number;
13
+ limit: number;
14
+ translations: Translations;
15
+ }>();
16
+
17
+ const emit = defineEmits<{
18
+ changePage: [page: number];
19
+ changeLimit: [limit: number];
20
+ }>();
21
+
22
+ const limitModel = defineModel<number>("limit", { required: true });
23
+
24
+ const handlePageChange = (page: number) => {
25
+ emit("changePage", page);
26
+ };
27
+
28
+ const handleLimitChange = (event: Event) => {
29
+ const select = event.target as HTMLSelectElement;
30
+ emit("changeLimit", Number(select.value));
31
+ };
32
+ </script>
33
+
34
+ <template>
35
+ <div v-if="total > 0" class="flex flex-col gap-6 sm:gap-8 mt-6 sm:mt-8">
36
+ <!-- Pagination Controls -->
37
+ <div class="flex justify-center w-full">
38
+ <SwPagination :total="total" :current="current" @change-page="handlePageChange" />
39
+ </div>
40
+
41
+ <!-- Items per page selector -->
42
+ <div class="flex justify-center items-center gap-3 sm:gap-4">
43
+ <label
44
+ for="limit"
45
+ class="text-sm sm:text-base text-surface-on-surface"
46
+ data-testid="listing-pagination-limit-label"
47
+ >
48
+ {{ translations.listing.perPage }}
49
+ </label>
50
+ <div class="relative">
51
+ <select
52
+ id="limit"
53
+ v-model="limitModel"
54
+ name="limitchoices"
55
+ class="appearance-none bg-surface-surface border border-outline-outline hover:border-outline-outline-primary focus:border-outline-outline-primary focus:ring-2 focus:ring-outline-outline-primary focus:ring-opacity-20 px-4 py-2 pr-10 rounded-md text-sm sm:text-base text-surface-on-surface cursor-pointer transition-colors"
56
+ data-testid="listing-pagination-limit-select"
57
+ @change="handleLimitChange"
58
+ >
59
+ <option :value="1">1 {{ translations.listing.product }}</option>
60
+ <option :value="15">15 {{ translations.listing.products }}</option>
61
+ <option :value="30">30 {{ translations.listing.products }}</option>
62
+ <option :value="45">45 {{ translations.listing.products }}</option>
63
+ </select>
64
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
65
+ <SwChevronIcon direction="down" :size="16" />
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </template>
@@ -10,7 +10,7 @@ const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(props.content);
10
10
  </script>
11
11
 
12
12
  <template>
13
- <div class="cms-section-default" :class="cssClasses" :styles="layoutStyles">
13
+ <div class="my-4" :class="cssClasses" :style="layoutStyles as any">
14
14
  <CmsGenericBlock
15
15
  v-for="cmsBlock in content.blocks"
16
16
  :key="cmsBlock.id"
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { useCmsSection } from "@shopware/composables";
3
+ import type { CmsSectionSidebar } from "@shopware/composables";
4
+ import { computed } from "vue";
5
+
6
+ const props = defineProps<{
7
+ content: CmsSectionSidebar;
8
+ }>();
9
+ const { getPositionContent, section } = useCmsSection(props.content);
10
+
11
+ const sidebarBlocks = getPositionContent("sidebar");
12
+ const mainBlocks = getPositionContent("main");
13
+ const mobileBehavior = computed(() => props.content.mobileBehavior);
14
+ const fullWidth = computed(() => section.sizingMode === "full_width");
15
+ </script>
16
+
17
+ <template>
18
+ <div class="self-stretch flex flex-col lg:flex-row items-stretch gap-16" :class="{
19
+ 'px-6': fullWidth,
20
+ }">
21
+ <aside :class="{
22
+ 'w-full lg:w-72 xl:w-80 flex-shrink-0 bg-surface-surface flex flex-col justify-start items-stretch gap-4 lg:sticky lg:top-20 px-4 lg:px-0':
23
+ mobileBehavior !== 'hidden',
24
+ 'hidden lg:block': mobileBehavior === 'hidden',
25
+ }">
26
+ <div v-for="cmsBlock in sidebarBlocks" :key="cmsBlock.id" class="w-full">
27
+ <CmsGenericBlock :content="cmsBlock" />
28
+ </div>
29
+ </aside>
30
+ <main class="flex-1 flex flex-col justify-start items-stretch gap-20">
31
+ <div v-for="cmsBlock in mainBlocks" :key="cmsBlock.id" class="w-full">
32
+ <CmsGenericBlock :content="cmsBlock" />
33
+ </div>
34
+ </main>
35
+ </div>
36
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { useImagePlaceholder } from "#imports";
3
+
4
+ const placeholderSvg = useImagePlaceholder();
5
+ </script>
6
+
7
+ <template>
8
+ <div role="status" class="p-px flex flex-col justify-start items-start overflow-hidden">
9
+ <!-- Image skeleton -->
10
+ <div class="self-stretch min-h-[350px] relative flex items-center justify-center overflow-hidden aspect-square animate-pulse">
11
+ <img
12
+ :src="placeholderSvg"
13
+ alt=""
14
+ aria-hidden="true"
15
+ class="w-full h-full object-cover"
16
+ />
17
+ </div>
18
+
19
+ <!-- Details skeleton -->
20
+ <div class="w-full pt-4 animate-pulse">
21
+ <div class="h-4 bg-gray-200 rounded-full dark:bg-gray-700 w-3/4 mb-3"></div>
22
+ <div class="h-3 bg-gray-200 rounded-full dark:bg-gray-700 w-1/2 mb-4"></div>
23
+ <div class="h-5 bg-gray-200 rounded-full dark:bg-gray-700 w-20 mb-3"></div>
24
+ <div class="h-10 bg-gray-200 rounded dark:bg-gray-700 w-full"></div>
25
+ </div>
26
+ <span class="sr-only">Loading...</span>
27
+ </div>
28
+ </template>