@shopware/cms-base-layer 2.1.0 → 3.0.1
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 +93 -135
- package/app/app.config.ts +1 -0
- 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 +13 -9
- package/app/components/SwNewsletterForm.vue +4 -3
- package/app/components/SwProductAddToCart.vue +24 -7
- package/app/components/SwProductCard.vue +5 -0
- package/app/components/SwProductCardDetails.vue +27 -9
- package/app/components/SwProductCardImage.vue +52 -35
- 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 +23 -8
- package/app/components/SwProductListingFiltersHorizontal.vue +6 -3
- 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 +5 -3
- 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 +11 -4
- package/app/components/public/cms/CmsGenericElement.vue +8 -2
- package/app/components/public/cms/CmsNoComponent.vue +88 -8
- package/app/components/public/cms/CmsPage.md +1 -1
- package/app/components/public/cms/CmsPage.vue +5 -6
- package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +52 -0
- package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCenterText.vue +1 -0
- 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 +4 -1
- package/app/components/public/cms/block/CmsBlockImageCover.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageGallery.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +4 -1
- 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 +4 -1
- package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductHeading.vue +1 -0
- 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 +15 -34
- 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 +77 -29
- package/app/components/public/cms/element/CmsElementProductListing.vue +22 -7
- 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/useImagePlaceholder.ts +2 -2
- package/app/composables/useLcpImagePreload.test.ts +1 -0
- package/app/composables/useLcpImagePreload.ts +12 -6
- package/app/composables/useTypedAppConfig.ts +16 -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/index.d.ts +37 -29
- package/nuxt.config.ts +5 -0
- package/package.json +22 -25
- package/app/plugins/unocss-runtime.client.ts +0 -23
- package/uno.config.ts +0 -94
|
@@ -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
|
>
|
|
@@ -3,14 +3,12 @@ import type {
|
|
|
3
3
|
CmsElementImage,
|
|
4
4
|
CmsElementManufacturerLogo,
|
|
5
5
|
} from "@shopware/composables";
|
|
6
|
-
import {
|
|
7
|
-
buildCdnImageUrl,
|
|
8
|
-
buildUrlPrefix,
|
|
9
|
-
generateCdnSrcSet,
|
|
10
|
-
} from "@shopware/helpers";
|
|
6
|
+
import { buildUrlPrefix } from "@shopware/helpers";
|
|
11
7
|
import { useElementSize } from "@vueuse/core";
|
|
12
|
-
import { computed, defineAsyncComponent,
|
|
13
|
-
|
|
8
|
+
import { computed, defineAsyncComponent, useTemplateRef } from "vue";
|
|
9
|
+
|
|
10
|
+
import { useCmsElementImage, useUrlResolver } from "#imports";
|
|
11
|
+
|
|
14
12
|
import { isSpatial } from "../../../../helpers/media/isSpatial";
|
|
15
13
|
|
|
16
14
|
const props = defineProps<{
|
|
@@ -29,34 +27,17 @@ const {
|
|
|
29
27
|
mimeType,
|
|
30
28
|
} = useCmsElementImage(props.content);
|
|
31
29
|
|
|
32
|
-
const imageSizes = inject<string>("cms-image-sizes", "100vw");
|
|
33
|
-
const appConfig = useAppConfig();
|
|
34
|
-
|
|
35
30
|
const imageElement = useTemplateRef<HTMLImageElement>("imageElement");
|
|
36
31
|
const { width, height } = useElementSize(imageElement);
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
const srcSet = computed(
|
|
44
|
-
() =>
|
|
45
|
-
imageAttrs.value.srcset ||
|
|
46
|
-
generateCdnSrcSet(imageAttrs.value.src, undefined, cdnOptions.value),
|
|
47
|
-
);
|
|
33
|
+
function roundUp(num: number) {
|
|
34
|
+
return Math.ceil(num / 100) * 100;
|
|
35
|
+
}
|
|
48
36
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return buildCdnImageUrl(
|
|
54
|
-
imageAttrs.value.src,
|
|
55
|
-
{ width: width.value, height: height.value },
|
|
56
|
-
cdnOptions.value,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
return imageAttrs.value.src || "";
|
|
37
|
+
const imageSize = computed(() => {
|
|
38
|
+
const containerSize = Math.max(width.value || 0, height.value || 0);
|
|
39
|
+
if (!containerSize) return undefined;
|
|
40
|
+
return roundUp(containerSize * 2);
|
|
60
41
|
});
|
|
61
42
|
|
|
62
43
|
const imageComputedContainerAttrs = computed(() => {
|
|
@@ -110,7 +91,8 @@ const SwMedia3D = computed(() => {
|
|
|
110
91
|
ref="imageElement"
|
|
111
92
|
preset="productDetail"
|
|
112
93
|
loading="lazy"
|
|
113
|
-
:
|
|
94
|
+
:width="imageSize"
|
|
95
|
+
:height="imageSize"
|
|
114
96
|
:class="{
|
|
115
97
|
'w-full': !imageGallery,
|
|
116
98
|
'h-full': !imageGallery && ['cover', 'stretch'].includes(displayMode),
|
|
@@ -120,8 +102,7 @@ const SwMedia3D = computed(() => {
|
|
|
120
102
|
'object-contain': imageGallery || displayMode !== 'cover',
|
|
121
103
|
}"
|
|
122
104
|
:alt="imageAttrs.alt"
|
|
123
|
-
:src="
|
|
124
|
-
:srcset="srcSet"
|
|
105
|
+
:src="imageAttrs.src"
|
|
125
106
|
/>
|
|
126
107
|
</component>
|
|
127
108
|
</template>
|
|
@@ -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
|
|
@@ -5,6 +5,7 @@ import { getTranslatedProperty } from "@shopware/helpers";
|
|
|
5
5
|
import { defu } from "defu";
|
|
6
6
|
import { type Ref, computed, onMounted, ref } from "vue";
|
|
7
7
|
import xss from "xss";
|
|
8
|
+
|
|
8
9
|
import { useProduct, useShopwareContext, useUser } from "#imports";
|
|
9
10
|
import type { Schemas } from "#shopware";
|
|
10
11
|
|
|
@@ -89,28 +90,39 @@ onMounted(async () => {
|
|
|
89
90
|
</script>
|
|
90
91
|
|
|
91
92
|
<template>
|
|
92
|
-
<div
|
|
93
|
+
<div
|
|
94
|
+
class="w-full self-stretch inline-flex flex-col justify-start items-start gap-4"
|
|
95
|
+
>
|
|
93
96
|
<div class="self-stretch flex flex-col justify-center items-center">
|
|
94
97
|
<div
|
|
95
98
|
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)"
|
|
99
|
+
@click="toggleSection(1)"
|
|
100
|
+
>
|
|
97
101
|
<div class="flex-1 flex items-center gap-2.5">
|
|
98
|
-
<div
|
|
102
|
+
<div
|
|
103
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal"
|
|
104
|
+
>
|
|
99
105
|
{{ translations.product.description }}
|
|
100
106
|
</div>
|
|
101
107
|
</div>
|
|
102
108
|
<div class="w-6 h-6 relative">
|
|
103
109
|
<div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
|
|
104
|
-
<div
|
|
105
|
-
|
|
110
|
+
<div
|
|
111
|
+
class="i-carbon-chevron-down transition-transform duration-200"
|
|
112
|
+
:class="{ 'rotate-180': isSectionOpen(1) }"
|
|
113
|
+
></div>
|
|
106
114
|
</div>
|
|
107
115
|
</div>
|
|
108
116
|
</div>
|
|
109
117
|
</div>
|
|
110
118
|
<Transition name="accordion">
|
|
111
|
-
<div
|
|
119
|
+
<div
|
|
120
|
+
v-if="isSectionOpen(1)"
|
|
121
|
+
class="self-stretch flex flex-col justify-center items-center gap-2.5"
|
|
122
|
+
>
|
|
112
123
|
<div
|
|
113
|
-
class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
|
|
124
|
+
class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
|
|
125
|
+
>
|
|
114
126
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
115
127
|
<div v-html="description"></div>
|
|
116
128
|
</div>
|
|
@@ -119,43 +131,68 @@ onMounted(async () => {
|
|
|
119
131
|
<div class="self-stretch flex flex-col justify-center items-center">
|
|
120
132
|
<div
|
|
121
133
|
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)"
|
|
134
|
+
@click="toggleSection(2)"
|
|
135
|
+
>
|
|
123
136
|
<div class="flex-1 flex items-center gap-2.5">
|
|
124
|
-
<div
|
|
137
|
+
<div
|
|
138
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal"
|
|
139
|
+
>
|
|
125
140
|
{{ translations.product.reviews }} ({{ reviews.length }})
|
|
126
141
|
</div>
|
|
127
142
|
</div>
|
|
128
143
|
<div class="w-6 h-6 relative">
|
|
129
144
|
<div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
|
|
130
|
-
<div
|
|
131
|
-
|
|
145
|
+
<div
|
|
146
|
+
class="i-carbon-chevron-down transition-transform duration-200"
|
|
147
|
+
:class="{ 'rotate-180': isSectionOpen(2) }"
|
|
148
|
+
></div>
|
|
132
149
|
</div>
|
|
133
150
|
</div>
|
|
134
151
|
</div>
|
|
135
152
|
</div>
|
|
136
153
|
<Transition name="accordion">
|
|
137
|
-
<div
|
|
154
|
+
<div
|
|
155
|
+
v-if="isSectionOpen(2)"
|
|
156
|
+
class="self-stretch flex flex-col justify-center items-center gap-2.5"
|
|
157
|
+
>
|
|
138
158
|
<div
|
|
139
|
-
class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
|
|
140
|
-
|
|
159
|
+
class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
|
|
160
|
+
>
|
|
161
|
+
<SwProductReviews
|
|
162
|
+
v-if="product"
|
|
163
|
+
:product="product"
|
|
164
|
+
:reviews="reviews"
|
|
165
|
+
/>
|
|
141
166
|
<ClientOnly>
|
|
142
167
|
<SwProductReviewsForm
|
|
143
168
|
v-if="isLoggedIn && !reviewAdded && product"
|
|
144
169
|
:product-id="product.id"
|
|
145
170
|
@success="handleReviewAdded"
|
|
146
171
|
/>
|
|
147
|
-
<div
|
|
148
|
-
|
|
172
|
+
<div
|
|
173
|
+
v-else-if="!isLoggedIn && product"
|
|
174
|
+
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"
|
|
175
|
+
>
|
|
176
|
+
<div
|
|
177
|
+
class="w-5 h-5 text-surface-on-surface-variant flex-shrink-0"
|
|
178
|
+
>
|
|
149
179
|
<SwUserIcon :size="20" />
|
|
150
180
|
</div>
|
|
151
|
-
<span class="text-sm text-surface-on-surface-variant">{{
|
|
181
|
+
<span class="text-sm text-surface-on-surface-variant">{{
|
|
182
|
+
translations.product.messages.loginToReview
|
|
183
|
+
}}</span>
|
|
152
184
|
</div>
|
|
153
185
|
</ClientOnly>
|
|
154
|
-
<div
|
|
186
|
+
<div
|
|
187
|
+
v-if="reviewAdded"
|
|
188
|
+
class="mt-4 p-3 bg-surface-surface-container border border-states-success rounded-md flex gap-2 md:gap-3 items-center"
|
|
189
|
+
>
|
|
155
190
|
<div class="w-5 h-5 text-states-success flex-shrink-0">
|
|
156
191
|
<SwCheckmarkIcon :size="20" :filled="true" alt="Success" />
|
|
157
192
|
</div>
|
|
158
|
-
<span class="text-sm text-states-success">{{
|
|
193
|
+
<span class="text-sm text-states-success">{{
|
|
194
|
+
translations.product.messages.reviewAdded
|
|
195
|
+
}}</span>
|
|
159
196
|
</div>
|
|
160
197
|
</div>
|
|
161
198
|
</div>
|
|
@@ -163,32 +200,43 @@ onMounted(async () => {
|
|
|
163
200
|
<div class="self-stretch flex flex-col justify-center items-center">
|
|
164
201
|
<div
|
|
165
202
|
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)"
|
|
203
|
+
@click="toggleSection(3)"
|
|
204
|
+
>
|
|
167
205
|
<div class="flex-1 flex items-center gap-2.5">
|
|
168
|
-
<div
|
|
206
|
+
<div
|
|
207
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal"
|
|
208
|
+
>
|
|
169
209
|
Category
|
|
170
210
|
</div>
|
|
171
211
|
</div>
|
|
172
212
|
<div class="w-6 h-6 relative">
|
|
173
213
|
<div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
|
|
174
|
-
<div
|
|
175
|
-
|
|
214
|
+
<div
|
|
215
|
+
class="i-carbon-chevron-down transition-transform duration-200"
|
|
216
|
+
:class="{ 'rotate-180': isSectionOpen(3) }"
|
|
217
|
+
></div>
|
|
176
218
|
</div>
|
|
177
219
|
</div>
|
|
178
220
|
</div>
|
|
179
221
|
</div>
|
|
180
222
|
<Transition name="accordion">
|
|
181
|
-
<div
|
|
223
|
+
<div
|
|
224
|
+
v-if="isSectionOpen(3)"
|
|
225
|
+
class="self-stretch flex flex-col justify-center items-center gap-2.5"
|
|
226
|
+
>
|
|
182
227
|
<div
|
|
183
|
-
class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
|
|
228
|
+
class="self-stretch text-surface-on-surface text-base font-normal leading-normal"
|
|
229
|
+
>
|
|
184
230
|
<div v-if="product?.categories">
|
|
185
|
-
<div
|
|
231
|
+
<div
|
|
232
|
+
v-for="category in product.categories"
|
|
233
|
+
:key="category.id"
|
|
234
|
+
class="mb-2"
|
|
235
|
+
>
|
|
186
236
|
{{ category.name }}
|
|
187
237
|
</div>
|
|
188
238
|
</div>
|
|
189
|
-
<div v-else>
|
|
190
|
-
No categories available
|
|
191
|
-
</div>
|
|
239
|
+
<div v-else>No categories available</div>
|
|
192
240
|
</div>
|
|
193
241
|
</div>
|
|
194
242
|
</Transition>
|
|
@@ -3,8 +3,13 @@ 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
|
-
|
|
7
|
-
import {
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
useCategoryListing,
|
|
9
|
+
useCmsElementConfig,
|
|
10
|
+
useRoute,
|
|
11
|
+
useRouter,
|
|
12
|
+
} from "#imports";
|
|
8
13
|
import type { Schemas, operations } from "#shopware";
|
|
9
14
|
|
|
10
15
|
const props = defineProps<{
|
|
@@ -151,10 +156,17 @@ compareRouteQueryWithInitialListing();
|
|
|
151
156
|
|
|
152
157
|
<template>
|
|
153
158
|
<div class="max-w-2xl mx-auto lg:max-w-full">
|
|
154
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
v-if="!loading && getElements.length < 1"
|
|
161
|
+
class="text-center text-xl py-16 text-surface-on-surface-variant"
|
|
162
|
+
>
|
|
155
163
|
{{ translations.listing.noProducts }}
|
|
156
164
|
</div>
|
|
157
|
-
<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
|
+
>
|
|
158
170
|
<SwProductCard
|
|
159
171
|
v-for="product in getElements"
|
|
160
172
|
:key="product.id"
|
|
@@ -164,9 +176,12 @@ compareRouteQueryWithInitialListing();
|
|
|
164
176
|
class="w-full"
|
|
165
177
|
/>
|
|
166
178
|
</div>
|
|
167
|
-
<div
|
|
168
|
-
|
|
169
|
-
|
|
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" />
|
|
170
185
|
</div>
|
|
171
186
|
<SwProductListingPagination
|
|
172
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";
|
|
@@ -35,7 +35,11 @@ const handleLimitChange = (event: Event) => {
|
|
|
35
35
|
<div v-if="total > 0" class="flex flex-col gap-6 sm:gap-8 mt-6 sm:mt-8">
|
|
36
36
|
<!-- Pagination Controls -->
|
|
37
37
|
<div class="flex justify-center w-full">
|
|
38
|
-
<SwPagination
|
|
38
|
+
<SwPagination
|
|
39
|
+
:total="total"
|
|
40
|
+
:current="current"
|
|
41
|
+
@change-page="handlePageChange"
|
|
42
|
+
/>
|
|
39
43
|
</div>
|
|
40
44
|
|
|
41
45
|
<!-- Items per page selector -->
|
|
@@ -61,7 +65,9 @@ const handleLimitChange = (event: Event) => {
|
|
|
61
65
|
<option :value="30">30 {{ translations.listing.products }}</option>
|
|
62
66
|
<option :value="45">45 {{ translations.listing.products }}</option>
|
|
63
67
|
</select>
|
|
64
|
-
<div
|
|
68
|
+
<div
|
|
69
|
+
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
|
70
|
+
>
|
|
65
71
|
<SwChevronIcon direction="down" :size="16" />
|
|
66
72
|
</div>
|
|
67
73
|
</div>
|
|
@@ -18,14 +18,19 @@ provide("cms-section-layout", "sidebar");
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<template>
|
|
21
|
-
<div
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
<div
|
|
22
|
+
class="self-stretch flex flex-col lg:flex-row items-stretch gap-16"
|
|
23
|
+
:class="{
|
|
24
|
+
'px-6': fullWidth,
|
|
25
|
+
}"
|
|
26
|
+
>
|
|
27
|
+
<aside
|
|
28
|
+
:class="{
|
|
29
|
+
'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':
|
|
30
|
+
mobileBehavior !== 'hidden',
|
|
31
|
+
'hidden lg:block': mobileBehavior === 'hidden',
|
|
32
|
+
}"
|
|
33
|
+
>
|
|
29
34
|
<div v-for="cmsBlock in sidebarBlocks" :key="cmsBlock.id" class="w-full">
|
|
30
35
|
<CmsGenericBlock :content="cmsBlock" />
|
|
31
36
|
</div>
|
|
@@ -5,9 +5,14 @@ const placeholderSvg = useImagePlaceholder();
|
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<template>
|
|
8
|
-
<div
|
|
8
|
+
<div
|
|
9
|
+
role="status"
|
|
10
|
+
class="p-px flex flex-col justify-start items-start overflow-hidden"
|
|
11
|
+
>
|
|
9
12
|
<!-- Image skeleton -->
|
|
10
|
-
<div
|
|
13
|
+
<div
|
|
14
|
+
class="self-stretch min-h-[350px] relative flex items-center justify-center overflow-hidden aspect-square animate-pulse"
|
|
15
|
+
>
|
|
11
16
|
<img
|
|
12
17
|
:src="placeholderSvg"
|
|
13
18
|
alt=""
|
|
@@ -18,9 +23,15 @@ const placeholderSvg = useImagePlaceholder();
|
|
|
18
23
|
|
|
19
24
|
<!-- Details skeleton -->
|
|
20
25
|
<div class="w-full pt-4 animate-pulse">
|
|
21
|
-
<div
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
<div
|
|
27
|
+
class="h-4 bg-gray-200 rounded-full dark:bg-gray-700 w-3/4 mb-3"
|
|
28
|
+
></div>
|
|
29
|
+
<div
|
|
30
|
+
class="h-3 bg-gray-200 rounded-full dark:bg-gray-700 w-1/2 mb-4"
|
|
31
|
+
></div>
|
|
32
|
+
<div
|
|
33
|
+
class="h-5 bg-gray-200 rounded-full dark:bg-gray-700 w-20 mb-3"
|
|
34
|
+
></div>
|
|
24
35
|
<div class="h-10 bg-gray-200 rounded dark:bg-gray-700 w-full"></div>
|
|
25
36
|
</div>
|
|
26
37
|
<span class="sr-only">Loading...</span>
|