@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.
- package/README.md +328 -13
- package/app/app.config.ts +7 -0
- package/app/assets/icons/check-circle.svg +3 -0
- package/app/assets/icons/checkmark.svg +3 -0
- package/app/assets/icons/chevron.svg +3 -0
- package/app/assets/icons/exclamation-circle.svg +3 -0
- package/app/assets/icons/star-empty.svg +3 -0
- package/app/assets/icons/star-filled.svg +3 -0
- package/app/assets/icons/user.svg +1 -0
- package/app/components/SwCategoryNavigation.vue +76 -0
- package/app/components/SwCategoryNavigationLink.vue +128 -0
- package/{components → app/components}/SwContactForm.vue +27 -27
- package/app/components/SwFilterChips.vue +144 -0
- package/app/components/SwListingProductPrice.vue +89 -0
- package/{components → app/components}/SwNewsletterForm.vue +45 -34
- package/{components → app/components}/SwPagination.vue +3 -5
- package/{components → app/components}/SwProductAddToCart.vue +22 -27
- package/app/components/SwProductCard.vue +170 -0
- package/app/components/SwProductCardDetails.vue +57 -0
- package/app/components/SwProductCardImage.vue +87 -0
- package/app/components/SwProductCardSkeleton.vue +33 -0
- package/app/components/SwProductListingFilter.vue +64 -0
- package/app/components/SwProductListingFilters.vue +308 -0
- package/{components → app/components}/SwProductReviews.vue +28 -13
- package/app/components/SwProductReviewsForm.vue +292 -0
- package/app/components/SwQuantitySelect.vue +106 -0
- package/{components → app/components}/SwSlider.vue +4 -4
- package/app/components/SwSortDropdown.vue +83 -0
- package/app/components/SwStockInfo.vue +44 -0
- package/{components → app/components}/SwVariantConfigurator.vue +1 -1
- package/app/components/listing-filters/SwFilterPrice.vue +214 -0
- package/app/components/listing-filters/SwFilterProperties.vue +113 -0
- package/app/components/listing-filters/SwFilterRating.vue +90 -0
- package/app/components/listing-filters/SwFilterShippingFree.vue +107 -0
- package/{components → app/components}/public/cms/CmsPage.vue +19 -4
- package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
- package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
- package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
- package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
- package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +30 -0
- package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +4 -4
- package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +3 -5
- package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
- package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
- package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
- package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
- package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
- package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
- package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
- package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
- package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
- package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
- package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +8 -2
- package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
- package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
- package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
- package/app/components/ui/BaseButton.vue +99 -0
- package/app/components/ui/BaseIcon.vue +15 -0
- package/app/components/ui/Checkbox.vue +49 -0
- package/app/components/ui/CheckmarkIcon.vue +23 -0
- package/app/components/ui/ChevronIcon.vue +37 -0
- package/app/components/ui/ExclamationIcon.vue +11 -0
- package/app/components/ui/IconButton.vue +32 -0
- package/app/components/ui/RadioButton.vue +26 -0
- package/app/components/ui/StarIcon.vue +18 -0
- package/app/components/ui/SwitchButton.vue +100 -0
- package/app/components/ui/UserIcon.vue +11 -0
- package/app/components/ui/WishlistIcon.vue +20 -0
- package/app/composables/useImagePlaceholder.ts +27 -0
- package/{helpers → app/helpers}/clientOnly.ts +5 -0
- package/app/providers/shopware.test.ts +213 -0
- package/app/providers/shopware.ts +107 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +2 -2
- package/index.d.ts +12 -0
- package/nuxt.config.ts +80 -6
- package/package.json +29 -21
- package/uno.config.ts +83 -0
- package/components/SwCategoryNavigation.vue +0 -44
- package/components/SwCategoryNavigationLink.vue +0 -57
- package/components/SwListingProductPrice.vue +0 -89
- package/components/SwProductCard.vue +0 -286
- package/components/SwProductListingFilter.vue +0 -42
- package/components/SwProductListingFilters.vue +0 -292
- package/components/listing-filters/SwFilterPrice.vue +0 -160
- package/components/listing-filters/SwFilterProperties.vue +0 -123
- package/components/listing-filters/SwFilterRating.vue +0 -101
- package/components/listing-filters/SwFilterShippingFree.vue +0 -104
- package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
- package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
- package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
- package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
- package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
- package/components/public/cms/element/CmsBlockHtml.md +0 -1
- package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
- package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
- package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
- package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
- package/components/public/cms/element/CmsElementProductName.vue +0 -10
- package/components/public/cms/section/CmsSectionSidebar.vue +0 -49
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
- /package/{components → app/components}/SwMedia3D.vue +0 -0
- /package/{components → app/components}/SwProductGallery.vue +0 -0
- /package/{components → app/components}/SwProductPrice.vue +0 -0
- /package/{components → app/components}/SwProductUnits.vue +0 -0
- /package/{components → app/components}/SwSharedPrice.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
- /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
- /package/{components → app/components}/public/cms/CmsPage.md +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
- /package/{components/public/cms/element → app/components/public/cms/block}/CmsBlockHtml.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
- /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
- /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 (
|
|
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:
|
|
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="
|
|
155
|
-
<div class="
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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: "
|
|
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="
|
|
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-
|
|
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(
|
|
79
|
-
|
|
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=
|
|
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
|
|
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="
|
|
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>
|