@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.
- package/README.md +64 -58
- package/app/components/SwCategoryNavigation.vue +1 -0
- package/app/components/SwCategoryNavigationLink.vue +16 -8
- package/app/components/SwContactForm.vue +1 -0
- package/app/components/SwFilterChips.vue +1 -0
- package/app/components/SwListingProductPrice.vue +16 -4
- package/app/components/SwMedia3D.vue +1 -4
- package/app/components/SwNewsletterForm.vue +4 -3
- package/app/components/SwProductAddToCart.vue +24 -7
- package/app/components/SwProductCard.vue +20 -3
- package/app/components/SwProductCardDetails.vue +27 -9
- package/app/components/SwProductCardImage.vue +31 -12
- package/app/components/SwProductCardSkeleton.vue +37 -14
- package/app/components/SwProductGallery.vue +1 -0
- package/app/components/SwProductListingFilter.vue +5 -1
- package/app/components/SwProductListingFilters.vue +57 -76
- package/app/components/SwProductListingFiltersHorizontal.vue +40 -71
- package/app/components/SwProductPrice.vue +1 -0
- package/app/components/SwProductReviews.vue +14 -6
- package/app/components/SwProductReviewsForm.vue +15 -3
- package/app/components/SwProductUnits.vue +1 -0
- package/app/components/SwQuantitySelect.vue +10 -3
- package/app/components/SwSharedPrice.vue +1 -0
- package/app/components/SwSlider.vue +1 -0
- package/app/components/SwStockInfo.vue +12 -6
- package/app/components/SwVariantConfigurator.vue +4 -1
- package/app/components/listing-filters/SwFilterPrice.vue +79 -30
- package/app/components/listing-filters/SwFilterProperties.vue +40 -25
- package/app/components/listing-filters/SwFilterRating.vue +21 -9
- package/app/components/listing-filters/SwFilterShippingFree.vue +16 -8
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +1 -0
- package/app/components/public/cms/CmsGenericBlock.vue +3 -1
- package/app/components/public/cms/CmsGenericElement.vue +2 -1
- package/app/components/public/cms/CmsNoComponent.vue +2 -1
- package/app/components/public/cms/CmsPage.md +1 -1
- package/app/components/public/cms/CmsPage.vue +2 -0
- package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCenterText.vue +3 -1
- package/app/components/public/cms/block/CmsBlockCrossSelling.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCustomForm.vue +1 -0
- package/app/components/public/cms/block/CmsBlockForm.vue +1 -0
- package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImage.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +9 -2
- package/app/components/public/cms/block/CmsBlockImageCover.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +10 -3
- package/app/components/public/cms/block/CmsBlockImageGallery.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +10 -5
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +11 -3
- package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageSlider.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageText.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +4 -12
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +13 -4
- package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +11 -3
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +11 -4
- package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductHeading.vue +3 -1
- package/app/components/public/cms/block/CmsBlockProductListing.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +1 -0
- package/app/components/public/cms/block/CmsBlockText.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextHero.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +2 -4
- package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +1 -0
- package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +1 -0
- package/app/components/public/cms/element/CmsElementBuyBox.vue +90 -72
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +14 -3
- package/app/components/public/cms/element/CmsElementCrossSelling.vue +3 -1
- package/app/components/public/cms/element/CmsElementCustomForm.vue +1 -0
- package/app/components/public/cms/element/CmsElementForm.vue +1 -0
- package/app/components/public/cms/element/CmsElementHtml.vue +0 -1
- package/app/components/public/cms/element/CmsElementImage.vue +2 -1
- package/app/components/public/cms/element/CmsElementImageGallery.vue +6 -2
- package/app/components/public/cms/element/CmsElementManufacturerLogo.vue +1 -0
- package/app/components/public/cms/element/CmsElementProductBox.vue +1 -0
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +93 -36
- package/app/components/public/cms/element/CmsElementProductListing.vue +16 -5
- package/app/components/public/cms/element/CmsElementProductSlider.vue +6 -4
- package/app/components/public/cms/element/CmsElementText.vue +2 -0
- package/app/components/public/cms/element/CmsElementVimeoVideo.vue +1 -0
- package/app/components/public/cms/element/CmsElementYoutubeVideo.vue +1 -0
- package/app/components/public/cms/element/SwProductListingPagination.vue +8 -2
- package/app/components/public/cms/section/CmsSectionSidebar.vue +13 -8
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +16 -5
- package/app/components/ui/SwitchButton.vue +53 -14
- package/app/components/ui/WishlistIcon.vue +8 -2
- package/app/composables/useLcpImagePreload.test.ts +1 -0
- package/app/composables/useLcpImagePreload.ts +2 -0
- package/app/composables/useTypedAppConfig.ts +1 -0
- package/app/helpers/cms/getImageSizes.test.ts +1 -0
- package/app/helpers/html-to-vue/ast.ts +1 -0
- package/app/helpers/html-to-vue/getOptionsFromNode.test.ts +1 -0
- package/app/helpers/html-to-vue/renderToHtml.ts +1 -0
- package/app/helpers/html-to-vue/renderer.ts +1 -0
- package/app/providers/shopware.test.ts +1 -0
- package/app/utils/routeQuery.test.ts +65 -0
- package/app/utils/routeQuery.ts +26 -0
- package/app/utils/useSelectedListingFilters.test.ts +82 -0
- package/app/utils/useSelectedListingFilters.ts +105 -0
- package/nuxt.config.ts +24 -1
- 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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
50
|
-
|
|
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':
|
|
84
|
+
'border-b-3 border-brand-primary text-brand-primary':
|
|
85
|
+
currentTabIndex === index,
|
|
84
86
|
}"
|
|
85
87
|
@click="toggleTab(index)"
|
|
86
88
|
>
|
|
@@ -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
|
-
|
|
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'
|
|
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,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 =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
|
105
|
-
|
|
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
|
|
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
|
|
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
|
|
131
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
148
|
-
|
|
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">{{
|
|
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
|
|
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">{{
|
|
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
|
|
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
|
|
175
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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";
|