@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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { getCmsTranslate } from "@shopware/helpers";
|
|
13
13
|
import { defu } from "defu";
|
|
14
14
|
import { computed, ref, toRef } from "vue";
|
|
15
|
+
|
|
15
16
|
import {
|
|
16
17
|
useAddToCart,
|
|
17
18
|
useCartErrorParamsResolver,
|
|
@@ -50,6 +51,8 @@ type Translations = {
|
|
|
50
51
|
badges: {
|
|
51
52
|
topseller: string;
|
|
52
53
|
};
|
|
54
|
+
addToWishlist: string;
|
|
55
|
+
removeFromWishlist: string;
|
|
53
56
|
};
|
|
54
57
|
errors: {
|
|
55
58
|
[key: string]: string;
|
|
@@ -68,6 +71,8 @@ let translations: Translations = {
|
|
|
68
71
|
badges: {
|
|
69
72
|
topseller: "Tip",
|
|
70
73
|
},
|
|
74
|
+
addToWishlist: "Add to wishlist",
|
|
75
|
+
removeFromWishlist: "Remove from wishlist",
|
|
71
76
|
},
|
|
72
77
|
errors: {
|
|
73
78
|
"product-stock-reached":
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { BoxLayout } from "@shopware/composables";
|
|
3
3
|
import type { UrlRouteOutput } from "@shopware/helpers";
|
|
4
4
|
import { computed } from "vue";
|
|
5
|
+
|
|
5
6
|
import type { Schemas } from "#shopware";
|
|
6
7
|
|
|
7
8
|
type Translations = {
|
|
@@ -28,18 +29,24 @@ const props = defineProps<{
|
|
|
28
29
|
const isMinimalLayout = computed(() => props.layoutType === "minimal");
|
|
29
30
|
</script>
|
|
30
31
|
<template>
|
|
31
|
-
<div
|
|
32
|
+
<div
|
|
33
|
+
class="self-stretch p-2 flex flex-col justify-between items-start gap-4 flex-1"
|
|
34
|
+
>
|
|
32
35
|
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
33
36
|
<div class="self-stretch flex flex-col justify-start items-start gap-2">
|
|
34
37
|
<div class="self-stretch flex flex-col justify-start items-start gap-1">
|
|
35
|
-
<div
|
|
36
|
-
|
|
38
|
+
<div
|
|
39
|
+
v-if="productManufacturer"
|
|
40
|
+
class="self-stretch text-surface-on-surface text-sm font-bold leading-tight"
|
|
41
|
+
>
|
|
37
42
|
{{ productManufacturer }}
|
|
38
43
|
</div>
|
|
39
44
|
|
|
40
|
-
<RouterLink
|
|
45
|
+
<RouterLink
|
|
46
|
+
:to="productLink"
|
|
41
47
|
class="self-stretch text-surface-on-surface text-2xl font-normal font-serif leading-9 overflow-hidden line-clamp-2 break-words min-h-[4.5rem]"
|
|
42
|
-
data-testid="product-box-product-name-link"
|
|
48
|
+
data-testid="product-box-product-name-link"
|
|
49
|
+
>
|
|
43
50
|
{{ productName }}
|
|
44
51
|
</RouterLink>
|
|
45
52
|
</div>
|
|
@@ -54,13 +61,24 @@ const isMinimalLayout = computed(() => props.layoutType === "minimal");
|
|
|
54
61
|
/>
|
|
55
62
|
|
|
56
63
|
<!-- Price for standard layout -->
|
|
57
|
-
<SwListingProductPrice
|
|
64
|
+
<SwListingProductPrice
|
|
65
|
+
v-else
|
|
66
|
+
:product="product"
|
|
67
|
+
data-testid="product-box-product-price"
|
|
68
|
+
/>
|
|
58
69
|
</div>
|
|
59
70
|
|
|
60
71
|
<!-- CTA buttons only for non-minimal layout -->
|
|
61
72
|
<template v-if="!isMinimalLayout">
|
|
62
|
-
<SwBaseButton
|
|
63
|
-
|
|
73
|
+
<SwBaseButton
|
|
74
|
+
variant="primary"
|
|
75
|
+
v-if="!fromPrice"
|
|
76
|
+
size="medium"
|
|
77
|
+
:disabled="!product?.available"
|
|
78
|
+
block
|
|
79
|
+
data-testid="add-to-cart-button"
|
|
80
|
+
@click="addToCartProxy"
|
|
81
|
+
>
|
|
64
82
|
{{ translations.product.addToCart }}
|
|
65
83
|
</SwBaseButton>
|
|
66
84
|
|
|
@@ -71,4 +89,4 @@ const isMinimalLayout = computed(() => props.layoutType === "minimal");
|
|
|
71
89
|
</RouterLink>
|
|
72
90
|
</template>
|
|
73
91
|
</div>
|
|
74
|
-
</template>
|
|
92
|
+
</template>
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
isProductOnSale,
|
|
6
6
|
isProductTopSeller,
|
|
7
7
|
} from "@shopware/helpers";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {
|
|
8
|
+
import { computed, inject } from "vue";
|
|
9
|
+
|
|
10
|
+
import { useUser } from "#imports";
|
|
11
11
|
import type { Schemas } from "#shopware";
|
|
12
12
|
|
|
13
13
|
type Translations = {
|
|
@@ -15,6 +15,8 @@ type Translations = {
|
|
|
15
15
|
badges: {
|
|
16
16
|
topseller: string;
|
|
17
17
|
};
|
|
18
|
+
addToWishlist: string;
|
|
19
|
+
removeFromWishlist: string;
|
|
18
20
|
};
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -27,32 +29,13 @@ const props = defineProps<{
|
|
|
27
29
|
productLink: UrlRouteOutput;
|
|
28
30
|
}>();
|
|
29
31
|
|
|
30
|
-
const containerElement = useTemplateRef<HTMLDivElement>("containerElement");
|
|
31
|
-
const { width, height } = useElementSize(containerElement);
|
|
32
|
-
|
|
33
|
-
const DEFAULT_THUMBNAIL_SIZE = 10;
|
|
34
|
-
function roundUp(num: number) {
|
|
35
|
-
return num ? Math.ceil(num / 100) * 100 : DEFAULT_THUMBNAIL_SIZE;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
32
|
const coverSrcPath = computed(() => {
|
|
39
33
|
return (
|
|
40
|
-
|
|
41
|
-
props.product?.cover?.media
|
|
34
|
+
props.product?.cover?.media?.url ||
|
|
35
|
+
getSmallestThumbnailUrl(props.product?.cover?.media)
|
|
42
36
|
);
|
|
43
37
|
});
|
|
44
38
|
|
|
45
|
-
const imageModifiers = computed(() => {
|
|
46
|
-
// Use the larger dimension and apply 2x for high-DPI displays
|
|
47
|
-
// For square containers, width and height should be the same
|
|
48
|
-
const containerSize = Math.max(width.value || 0, height.value || 0);
|
|
49
|
-
const size = roundUp(containerSize * 2);
|
|
50
|
-
return {
|
|
51
|
-
width: size,
|
|
52
|
-
height: size,
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
|
|
56
39
|
const coverAlt = computed(() => {
|
|
57
40
|
return props.product?.cover?.media?.alt || props.product?.translated?.name;
|
|
58
41
|
});
|
|
@@ -60,31 +43,65 @@ const coverAlt = computed(() => {
|
|
|
60
43
|
const isOnSale = computed(() => isProductOnSale(props.product));
|
|
61
44
|
const isTopseller = computed(() => isProductTopSeller(props.product));
|
|
62
45
|
|
|
63
|
-
const
|
|
46
|
+
const { isLoggedIn } = useUser();
|
|
47
|
+
const loginModal = inject<{
|
|
48
|
+
open: (options?: { onSuccess?: () => void | Promise<void> }) => void;
|
|
49
|
+
} | null>("loginModal", null);
|
|
50
|
+
|
|
51
|
+
function handleWishlistClick() {
|
|
52
|
+
if (isLoggedIn.value || !loginModal) {
|
|
53
|
+
props.toggleWishlist();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
loginModal.open({ onSuccess: props.toggleWishlist });
|
|
57
|
+
}
|
|
64
58
|
</script>
|
|
65
59
|
|
|
66
60
|
<template>
|
|
67
|
-
<div
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
<div
|
|
62
|
+
class="self-stretch min-h-[350px] relative flex flex-col justify-start items-start overflow-hidden aspect-square"
|
|
63
|
+
>
|
|
64
|
+
<RouterLink
|
|
65
|
+
:to="productLink"
|
|
66
|
+
class="self-stretch h-full relative overflow-hidden"
|
|
67
|
+
>
|
|
68
|
+
<NuxtImg
|
|
69
|
+
preset="productCard"
|
|
70
70
|
class="w-full h-full absolute top-0 left-0 object-cover"
|
|
71
|
-
:
|
|
72
|
-
:
|
|
71
|
+
:src="coverSrcPath"
|
|
72
|
+
:alt="coverAlt"
|
|
73
|
+
width="400"
|
|
74
|
+
height="400"
|
|
75
|
+
densities="1x"
|
|
76
|
+
loading="lazy"
|
|
77
|
+
data-testid="product-box-img"
|
|
78
|
+
/>
|
|
73
79
|
</RouterLink>
|
|
74
80
|
|
|
75
|
-
<div
|
|
76
|
-
|
|
81
|
+
<div
|
|
82
|
+
v-if="isTopseller || isOnSale"
|
|
83
|
+
class="px-1.5 py-1 left-2 bottom-2 absolute bg-other-sale rounded inline-flex justify-center items-center"
|
|
84
|
+
>
|
|
77
85
|
<div class="text-states-on-error text-xs font-bold leading-none">
|
|
78
86
|
{{ translations.product.badges.topseller }}
|
|
79
87
|
</div>
|
|
80
88
|
</div>
|
|
81
89
|
|
|
82
90
|
<client-only>
|
|
83
|
-
<SwIconButton
|
|
91
|
+
<SwIconButton
|
|
92
|
+
type="secondary"
|
|
93
|
+
:aria-label="
|
|
94
|
+
isInWishlist
|
|
95
|
+
? translations.product.removeFromWishlist
|
|
96
|
+
: translations.product.addToWishlist
|
|
97
|
+
"
|
|
98
|
+
:disabled="isLoading"
|
|
84
99
|
class="w-10 h-10 right-4 top-4 absolute bg-brand-secondary rounded-full flex items-center justify-center"
|
|
85
|
-
data-testid="product-box-toggle-wishlist-button"
|
|
100
|
+
data-testid="product-box-toggle-wishlist-button"
|
|
101
|
+
@click="handleWishlistClick"
|
|
102
|
+
>
|
|
86
103
|
<SwWishlistIcon :filled="isInWishlist" />
|
|
87
104
|
</SwIconButton>
|
|
88
105
|
</client-only>
|
|
89
106
|
</div>
|
|
90
|
-
</template>
|
|
107
|
+
</template>
|
|
@@ -1,33 +1,56 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="inline-flex flex-col items-start justify-start self-stretch overflow-hidden p-px w-full"
|
|
4
|
+
aria-hidden="true"
|
|
5
|
+
>
|
|
4
6
|
<!-- image skeleton -->
|
|
5
|
-
<div
|
|
6
|
-
|
|
7
|
+
<div
|
|
8
|
+
class="relative flex h-80 flex-col items-start justify-start self-stretch overflow-hidden"
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
class="relative h-80 w-full bg-gray-200 dark:bg-gray-700 animate-pulse"
|
|
12
|
+
></div>
|
|
7
13
|
|
|
8
14
|
<div
|
|
9
|
-
class="absolute top-[281px] left-2 inline-flex items-center justify-center rounded bg-gray-300 dark:bg-gray-600 px-3 py-1 animate-pulse"
|
|
10
|
-
|
|
15
|
+
class="absolute top-[281px] left-2 inline-flex items-center justify-center rounded bg-gray-300 dark:bg-gray-600 px-3 py-1 animate-pulse"
|
|
16
|
+
>
|
|
17
|
+
<span
|
|
18
|
+
class="h-3 w-16 bg-gray-200 dark:bg-gray-700 rounded block"
|
|
19
|
+
></span>
|
|
11
20
|
</div>
|
|
12
21
|
|
|
13
|
-
<div
|
|
22
|
+
<div
|
|
23
|
+
class="absolute top-4 right-4 h-10 w-10 rounded-full bg-gray-300 dark:bg-gray-600 animate-pulse"
|
|
24
|
+
></div>
|
|
14
25
|
</div>
|
|
15
26
|
|
|
16
27
|
<!-- details skeleton -->
|
|
17
28
|
<div class="flex flex-col items-start justify-start gap-4 self-stretch p-2">
|
|
18
|
-
<div
|
|
29
|
+
<div
|
|
30
|
+
class="h-4 w-32 rounded bg-gray-200 dark:bg-gray-700 animate-pulse"
|
|
31
|
+
></div>
|
|
19
32
|
|
|
20
33
|
<div class="w-full">
|
|
21
|
-
<div
|
|
22
|
-
|
|
34
|
+
<div
|
|
35
|
+
class="h-7 w-3/4 rounded bg-gray-200 dark:bg-gray-700 mb-2 animate-pulse"
|
|
36
|
+
></div>
|
|
37
|
+
<div
|
|
38
|
+
class="h-7 w-1/2 rounded bg-gray-200 dark:bg-gray-700 animate-pulse"
|
|
39
|
+
></div>
|
|
23
40
|
</div>
|
|
24
41
|
|
|
25
|
-
<div
|
|
42
|
+
<div
|
|
43
|
+
class="h-6 w-24 rounded bg-gray-200 dark:bg-gray-700 animate-pulse"
|
|
44
|
+
></div>
|
|
26
45
|
|
|
27
46
|
<div class="flex w-full gap-3">
|
|
28
|
-
<div
|
|
29
|
-
|
|
47
|
+
<div
|
|
48
|
+
class="flex-1 h-10 rounded bg-gray-200 dark:bg-gray-700 animate-pulse"
|
|
49
|
+
></div>
|
|
50
|
+
<div
|
|
51
|
+
class="w-24 h-10 rounded bg-gray-200 dark:bg-gray-700 animate-pulse"
|
|
52
|
+
></div>
|
|
30
53
|
</div>
|
|
31
54
|
</div>
|
|
32
55
|
</div>
|
|
33
|
-
</template>
|
|
56
|
+
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts" generic="ListingFilter extends { code: string }">
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import type { Component } from "vue";
|
|
4
|
+
|
|
4
5
|
import SwFilterPriceVue from "./listing-filters/SwFilterPrice.vue";
|
|
5
6
|
import SwFilterPropertiesVue from "./listing-filters/SwFilterProperties.vue";
|
|
6
7
|
import SwFilterRatingVue from "./listing-filters/SwFilterRating.vue";
|
|
@@ -58,7 +59,10 @@ const filterComponent = computed<Component | undefined>(() => {
|
|
|
58
59
|
const handleSelectValue = ({
|
|
59
60
|
code,
|
|
60
61
|
value,
|
|
61
|
-
}: {
|
|
62
|
+
}: {
|
|
63
|
+
code: string;
|
|
64
|
+
value: string | number | boolean;
|
|
65
|
+
}) => {
|
|
62
66
|
emit("filter-change", { code, value });
|
|
63
67
|
};
|
|
64
68
|
</script>
|
|
@@ -7,8 +7,9 @@ import { useCmsTranslations } from "@shopware/composables";
|
|
|
7
7
|
import { defu } from "defu";
|
|
8
8
|
import { computed, reactive } from "vue";
|
|
9
9
|
import type { ComputedRef, UnwrapNestedRefs } from "vue";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import type { LocationQueryRaw } from "vue-router";
|
|
11
|
+
|
|
12
|
+
import { useCategoryListing, useRoute, useRouter } from "#imports";
|
|
12
13
|
import type { Schemas, operations } from "#shopware";
|
|
13
14
|
|
|
14
15
|
const props = defineProps<{
|
|
@@ -268,8 +269,11 @@ const handleRemoveFilterChip = async (chip: {
|
|
|
268
269
|
<!-- Filters Header -->
|
|
269
270
|
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
270
271
|
<div
|
|
271
|
-
class="flex flex-row items-center justify-between w-full mb-4 py-3 border-b border-outline-outline-variant"
|
|
272
|
-
|
|
272
|
+
class="flex flex-row items-center justify-between w-full mb-4 py-3 border-b border-outline-outline-variant"
|
|
273
|
+
>
|
|
274
|
+
<div
|
|
275
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal"
|
|
276
|
+
>
|
|
273
277
|
{{ translations.listing.filters }}
|
|
274
278
|
</div>
|
|
275
279
|
<SwSortDropdown
|
|
@@ -283,7 +287,9 @@ const handleRemoveFilterChip = async (chip: {
|
|
|
283
287
|
|
|
284
288
|
<!-- Filters List -->
|
|
285
289
|
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
286
|
-
<SwProductListingFilter
|
|
290
|
+
<SwProductListingFilter
|
|
291
|
+
v-for="filter in getInitialFilters"
|
|
292
|
+
:key="filter.id"
|
|
287
293
|
:filter="filter"
|
|
288
294
|
:selected-manufacturer="sidebarSelectedFilters.manufacturer"
|
|
289
295
|
:selected-properties="sidebarSelectedFilters.properties"
|
|
@@ -292,11 +298,20 @@ const handleRemoveFilterChip = async (chip: {
|
|
|
292
298
|
:selected-rating="sidebarSelectedFilters.rating"
|
|
293
299
|
:selected-shipping-free="sidebarSelectedFilters['shipping-free']"
|
|
294
300
|
@filter-change="handleFilterChange"
|
|
295
|
-
class="w-full"
|
|
301
|
+
class="w-full"
|
|
302
|
+
/>
|
|
296
303
|
<div v-if="showResetFiltersButton" class="w-full">
|
|
297
|
-
<SwBaseButton
|
|
304
|
+
<SwBaseButton
|
|
305
|
+
variant="primary"
|
|
306
|
+
size="medium"
|
|
307
|
+
block
|
|
308
|
+
@click="invokeCleanFilters"
|
|
309
|
+
type="button"
|
|
310
|
+
>
|
|
298
311
|
{{ translations.listing.resetFilters }}
|
|
299
|
-
<span
|
|
312
|
+
<span
|
|
313
|
+
class="w-6 h-6 i-carbon-close-filled inline-block align-middle ml-2"
|
|
314
|
+
></span>
|
|
300
315
|
</SwBaseButton>
|
|
301
316
|
</div>
|
|
302
317
|
</div>
|
|
@@ -7,8 +7,9 @@ import { useCmsTranslations } from "@shopware/composables";
|
|
|
7
7
|
import { defu } from "defu";
|
|
8
8
|
import { computed, reactive } from "vue";
|
|
9
9
|
import type { ComputedRef, UnwrapNestedRefs } from "vue";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import type { LocationQueryRaw } from "vue-router";
|
|
11
|
+
|
|
12
|
+
import { useCategoryListing, useRoute, useRouter } from "#imports";
|
|
12
13
|
import type { Schemas, operations } from "#shopware";
|
|
13
14
|
|
|
14
15
|
const props = defineProps<{
|
|
@@ -299,7 +300,9 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
299
300
|
type="button"
|
|
300
301
|
>
|
|
301
302
|
{{ translations.listing.resetFilters }}
|
|
302
|
-
<span
|
|
303
|
+
<span
|
|
304
|
+
class="w-5 h-5 i-carbon-close inline-block align-middle ml-1"
|
|
305
|
+
></span>
|
|
303
306
|
</SwBaseButton>
|
|
304
307
|
</div>
|
|
305
308
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { defu } from "defu";
|
|
3
3
|
import { computed, onMounted, ref, toRefs } from "vue";
|
|
4
|
+
|
|
4
5
|
import {
|
|
5
6
|
useCmsTranslations,
|
|
6
7
|
useProductReviews,
|
|
@@ -77,7 +78,10 @@ const formatDate = (date: string) => {
|
|
|
77
78
|
v-for="(review, index) in reviewsList"
|
|
78
79
|
:key="review.id"
|
|
79
80
|
class="pb-6"
|
|
80
|
-
:class="{
|
|
81
|
+
:class="{
|
|
82
|
+
'border-b border-surface-surface-container-highest':
|
|
83
|
+
index < reviewsList.length - 1,
|
|
84
|
+
}"
|
|
81
85
|
>
|
|
82
86
|
<div
|
|
83
87
|
v-if="review.createdAt"
|
|
@@ -87,19 +91,23 @@ const formatDate = (date: string) => {
|
|
|
87
91
|
<span>{{ formatDate(review.createdAt) }}</span>
|
|
88
92
|
</div>
|
|
89
93
|
<div
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
v-if="!review.status"
|
|
95
|
+
class="mt-2 text-3 p-2 bg-states-info-container text-states-on-info-container flex gap-2 items-center"
|
|
96
|
+
>
|
|
93
97
|
<div class="w-6 h-6 i-carbon-warning" />
|
|
94
98
|
{{ translations.product.reviewNotAccepted }}
|
|
95
99
|
</div>
|
|
96
|
-
<div
|
|
100
|
+
<div
|
|
101
|
+
class="cms-block-product-description-reviews__reviews-rating inline-flex items-center mt-2"
|
|
102
|
+
>
|
|
97
103
|
<SwProductRating
|
|
98
104
|
:rating="review.points ?? 0"
|
|
99
105
|
:star-size="20"
|
|
100
106
|
:show-count="false"
|
|
101
107
|
/>
|
|
102
|
-
<div
|
|
108
|
+
<div
|
|
109
|
+
class="cms-block-product-description-reviews__reviews-title font-semibold ml-2"
|
|
110
|
+
>
|
|
103
111
|
<p>{{ review.title }}</p>
|
|
104
112
|
</div>
|
|
105
113
|
</div>
|
|
@@ -7,6 +7,7 @@ import type { ValidationRuleWithoutParams } from "@vuelidate/core";
|
|
|
7
7
|
import { minLength, required } from "@vuelidate/validators";
|
|
8
8
|
import { defu } from "defu";
|
|
9
9
|
import { computed, reactive, ref } from "vue";
|
|
10
|
+
|
|
10
11
|
import { useShopwareContext } from "#imports";
|
|
11
12
|
|
|
12
13
|
const props = defineProps<{
|
|
@@ -154,7 +155,10 @@ const invokeRating = (value: number) => {
|
|
|
154
155
|
</script>
|
|
155
156
|
|
|
156
157
|
<template>
|
|
157
|
-
<form
|
|
158
|
+
<form
|
|
159
|
+
class="flex flex-col gap-4 md:gap-5 relative"
|
|
160
|
+
@submit.prevent="invokeSend"
|
|
161
|
+
>
|
|
158
162
|
<div
|
|
159
163
|
v-if="isLoading"
|
|
160
164
|
class="absolute inset-0 flex items-center justify-center z-10 bg-surface-surface/80 rounded-md"
|
|
@@ -171,7 +175,11 @@ const invokeRating = (value: number) => {
|
|
|
171
175
|
<span class="text-sm text-surface-on-surface-variant">{{
|
|
172
176
|
translations.product.reviewsForm.rating
|
|
173
177
|
}}</span>
|
|
174
|
-
<div
|
|
178
|
+
<div
|
|
179
|
+
class="flex flex-row gap-2"
|
|
180
|
+
role="group"
|
|
181
|
+
:aria-label="translations.product.reviewsForm.rating"
|
|
182
|
+
>
|
|
175
183
|
<SwStarIcon
|
|
176
184
|
v-for="index in state.rating || 0"
|
|
177
185
|
:key="`filled-${index}`"
|
|
@@ -217,7 +225,11 @@ const invokeRating = (value: number) => {
|
|
|
217
225
|
<SwExclamationIcon :size="20" />
|
|
218
226
|
</div>
|
|
219
227
|
<div class="flex-1">
|
|
220
|
-
<p
|
|
228
|
+
<p
|
|
229
|
+
v-for="(error, index) in errorMessages"
|
|
230
|
+
:key="index"
|
|
231
|
+
class="text-sm text-states-error"
|
|
232
|
+
>
|
|
221
233
|
{{ error.detail }}
|
|
222
234
|
</p>
|
|
223
235
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { defu } from "defu";
|
|
3
3
|
import { computed, useId } from "vue";
|
|
4
|
+
|
|
4
5
|
import { useCmsTranslations } from "#imports";
|
|
5
6
|
|
|
6
7
|
type Translations = {
|
|
@@ -63,7 +64,9 @@ const sizeClasses = {
|
|
|
63
64
|
};
|
|
64
65
|
</script>
|
|
65
66
|
<template>
|
|
66
|
-
<div
|
|
67
|
+
<div
|
|
68
|
+
class="rounded outline outline-1 outline-offset-[-1px] outline-outline-outline inline-flex"
|
|
69
|
+
>
|
|
67
70
|
<button
|
|
68
71
|
type="button"
|
|
69
72
|
:class="sizeClasses[size]"
|
|
@@ -73,9 +76,13 @@ const sizeClasses = {
|
|
|
73
76
|
>
|
|
74
77
|
-
|
|
75
78
|
</button>
|
|
76
|
-
<div
|
|
79
|
+
<div
|
|
80
|
+
class="bg-white border-l border-r border-outline-outline inline-flex flex-col justify-center items-center"
|
|
81
|
+
>
|
|
77
82
|
<!-- visually hidden label for screen readers -->
|
|
78
|
-
<label :for="inputId" class="sr-only">{{
|
|
83
|
+
<label :for="inputId" class="sr-only">{{
|
|
84
|
+
translations.form.quantitySelect.label
|
|
85
|
+
}}</label>
|
|
79
86
|
|
|
80
87
|
<input
|
|
81
88
|
:id="inputId"
|
|
@@ -31,14 +31,20 @@ translations = defu(useCmsTranslations(), translations) as Translations;
|
|
|
31
31
|
</script>
|
|
32
32
|
<template>
|
|
33
33
|
<div class="inline-flex justify-start items-center gap-2">
|
|
34
|
-
<div
|
|
34
|
+
<div
|
|
35
|
+
class="w-2 h-2 bg-states-success rounded-full"
|
|
36
|
+
v-if="availableStock > 0"
|
|
37
|
+
></div>
|
|
35
38
|
<div class="w-2 h-2 bg-states-error rounded-full" v-else></div>
|
|
36
|
-
<span v-if="availableStock >= minPurchase && deliveryTime"
|
|
37
|
-
deliveryTime?.name }}
|
|
39
|
+
<span v-if="availableStock >= minPurchase && deliveryTime"
|
|
40
|
+
>{{ translations.product.deliveryTime }} {{ deliveryTime?.name }}
|
|
38
41
|
</span>
|
|
39
|
-
<span
|
|
42
|
+
<span
|
|
43
|
+
v-else-if="availableStock < minPurchase && deliveryTime && restockTime"
|
|
44
|
+
>
|
|
40
45
|
{{ translations.product.deliveryTime }} {{ restockTime }}
|
|
41
|
-
{{ translations.product.days }} {{ deliveryTime?.name }}</span
|
|
46
|
+
{{ translations.product.days }} {{ deliveryTime?.name }}</span
|
|
47
|
+
>
|
|
42
48
|
<span v-else>{{ translations.product.noAvailable }}</span>
|
|
43
49
|
</div>
|
|
44
|
-
</template>
|
|
50
|
+
</template>
|
|
@@ -4,8 +4,8 @@ import { buildUrlPrefix, getProductRoute } from "@shopware/helpers";
|
|
|
4
4
|
import { defu } from "defu";
|
|
5
5
|
import { computed, ref, unref } from "vue";
|
|
6
6
|
import type { ComputedRef } from "vue";
|
|
7
|
-
|
|
8
|
-
import { useProductConfigurator, useUrlResolver } from "#imports";
|
|
7
|
+
|
|
8
|
+
import { useProductConfigurator, useRouter, useUrlResolver } from "#imports";
|
|
9
9
|
import type { Schemas } from "#shopware";
|
|
10
10
|
|
|
11
11
|
const { getUrlPrefix } = useUrlResolver();
|
|
@@ -85,7 +85,9 @@ const onHandleChange = async () => {
|
|
|
85
85
|
:key="optionGroup.id"
|
|
86
86
|
class="mt-6"
|
|
87
87
|
>
|
|
88
|
-
<div class="text-sm text-gray-900 font-medium">
|
|
88
|
+
<div class="text-sm text-gray-900 font-medium">
|
|
89
|
+
{{ optionGroup.name }}
|
|
90
|
+
</div>
|
|
89
91
|
<fieldset class="mt-4 flex-1">
|
|
90
92
|
<legend class="sr-only">
|
|
91
93
|
{{ translations.product.chooseA }} {{ optionGroup.name }}
|