@shopware/cms-base-layer 0.0.0-canary-20250116171244
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/LICENSE +21 -0
- package/README.md +144 -0
- package/components/SwCategoryNavigation.vue +44 -0
- package/components/SwCategoryNavigationLink.vue +57 -0
- package/components/SwContactForm.vue +392 -0
- package/components/SwListingProductPrice.vue +88 -0
- package/components/SwMedia3D.vue +34 -0
- package/components/SwNewsletterForm.vue +347 -0
- package/components/SwPagination.vue +106 -0
- package/components/SwProductAddToCart.vue +93 -0
- package/components/SwProductCard.vue +285 -0
- package/components/SwProductGallery.vue +39 -0
- package/components/SwProductListingFilter.vue +42 -0
- package/components/SwProductListingFilters.vue +292 -0
- package/components/SwProductPrice.vue +99 -0
- package/components/SwProductReviews.vue +99 -0
- package/components/SwProductUnits.vue +54 -0
- package/components/SwSharedPrice.vue +19 -0
- package/components/SwSlider.vue +328 -0
- package/components/SwVariantConfigurator.vue +116 -0
- package/components/listing-filters/SwFilterPrice.vue +160 -0
- package/components/listing-filters/SwFilterProperties.vue +123 -0
- package/components/listing-filters/SwFilterRating.vue +101 -0
- package/components/listing-filters/SwFilterShippingFree.vue +104 -0
- package/components/public/cms/CmsGenericBlock.md +27 -0
- package/components/public/cms/CmsGenericBlock.vue +63 -0
- package/components/public/cms/CmsGenericElement.md +31 -0
- package/components/public/cms/CmsGenericElement.vue +38 -0
- package/components/public/cms/CmsNoComponent.vue +27 -0
- package/components/public/cms/CmsPage.md +36 -0
- package/components/public/cms/CmsPage.vue +65 -0
- package/components/public/cms/block/CmsBlockCategoryNavigation.vue +16 -0
- package/components/public/cms/block/CmsBlockCenterText.vue +26 -0
- package/components/public/cms/block/CmsBlockCrossSelling.vue +15 -0
- package/components/public/cms/block/CmsBlockCustomForm.vue +17 -0
- package/components/public/cms/block/CmsBlockDefault.vue +14 -0
- package/components/public/cms/block/CmsBlockForm.vue +17 -0
- package/components/public/cms/block/CmsBlockGalleryBuybox.vue +25 -0
- package/components/public/cms/block/CmsBlockImage.vue +16 -0
- package/components/public/cms/block/CmsBlockImageBubbleRow.vue +32 -0
- package/components/public/cms/block/CmsBlockImageCover.vue +17 -0
- package/components/public/cms/block/CmsBlockImageFourColumn.vue +29 -0
- package/components/public/cms/block/CmsBlockImageGallery.vue +18 -0
- package/components/public/cms/block/CmsBlockImageHighlightRow.vue +27 -0
- package/components/public/cms/block/CmsBlockImageSimpleGrid.vue +24 -0
- package/components/public/cms/block/CmsBlockImageSlider.vue +17 -0
- package/components/public/cms/block/CmsBlockImageText.vue +19 -0
- package/components/public/cms/block/CmsBlockImageTextBubble.vue +51 -0
- package/components/public/cms/block/CmsBlockImageTextCover.vue +25 -0
- package/components/public/cms/block/CmsBlockImageTextGallery.vue +85 -0
- package/components/public/cms/block/CmsBlockImageTextRow.vue +43 -0
- package/components/public/cms/block/CmsBlockImageThreeColumn.vue +21 -0
- package/components/public/cms/block/CmsBlockImageThreeCover.vue +27 -0
- package/components/public/cms/block/CmsBlockImageTwoColumn.vue +25 -0
- package/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +15 -0
- package/components/public/cms/block/CmsBlockProductHeading.vue +26 -0
- package/components/public/cms/block/CmsBlockProductListing.vue +17 -0
- package/components/public/cms/block/CmsBlockProductSlider.vue +16 -0
- package/components/public/cms/block/CmsBlockProductThreeColumn.vue +22 -0
- package/components/public/cms/block/CmsBlockSidebarFilter.vue +17 -0
- package/components/public/cms/block/CmsBlockText.vue +15 -0
- package/components/public/cms/block/CmsBlockTextHero.vue +15 -0
- package/components/public/cms/block/CmsBlockTextOnImage.vue +20 -0
- package/components/public/cms/block/CmsBlockTextTeaser.vue +16 -0
- package/components/public/cms/block/CmsBlockTextTeaserSection.vue +21 -0
- package/components/public/cms/block/CmsBlockTextThreeColumn.vue +22 -0
- package/components/public/cms/block/CmsBlockTextTwoColumn.vue +28 -0
- package/components/public/cms/block/CmsBlockVimeoVideo.vue +17 -0
- package/components/public/cms/block/CmsBlockYoutubeVideo.vue +17 -0
- package/components/public/cms/element/CmsElementBuyBox.md +1 -0
- package/components/public/cms/element/CmsElementBuyBox.vue +190 -0
- package/components/public/cms/element/CmsElementCategoryNavigation.md +1 -0
- package/components/public/cms/element/CmsElementCategoryNavigation.vue +167 -0
- package/components/public/cms/element/CmsElementCrossSelling.md +1 -0
- package/components/public/cms/element/CmsElementCrossSelling.vue +106 -0
- package/components/public/cms/element/CmsElementCustomForm.md +1 -0
- package/components/public/cms/element/CmsElementCustomForm.vue +27 -0
- package/components/public/cms/element/CmsElementForm.md +1 -0
- package/components/public/cms/element/CmsElementForm.vue +27 -0
- package/components/public/cms/element/CmsElementImage.md +1 -0
- package/components/public/cms/element/CmsElementImage.vue +105 -0
- package/components/public/cms/element/CmsElementImageGallery.md +1 -0
- package/components/public/cms/element/CmsElementImageGallery.vue +249 -0
- package/components/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +53 -0
- package/components/public/cms/element/CmsElementImageSlider.md +1 -0
- package/components/public/cms/element/CmsElementImageSlider.vue +29 -0
- package/components/public/cms/element/CmsElementManufacturerLogo.md +1 -0
- package/components/public/cms/element/CmsElementManufacturerLogo.vue +11 -0
- package/components/public/cms/element/CmsElementProductBox.md +1 -0
- package/components/public/cms/element/CmsElementProductBox.vue +14 -0
- package/components/public/cms/element/CmsElementProductDescriptionReviews.md +1 -0
- package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +109 -0
- package/components/public/cms/element/CmsElementProductListing.md +1 -0
- package/components/public/cms/element/CmsElementProductListing.vue +245 -0
- package/components/public/cms/element/CmsElementProductName.md +1 -0
- package/components/public/cms/element/CmsElementProductName.vue +10 -0
- package/components/public/cms/element/CmsElementProductSlider.md +1 -0
- package/components/public/cms/element/CmsElementProductSlider.vue +80 -0
- package/components/public/cms/element/CmsElementSidebarFilter.md +1 -0
- package/components/public/cms/element/CmsElementSidebarFilter.vue +12 -0
- package/components/public/cms/element/CmsElementText.md +1 -0
- package/components/public/cms/element/CmsElementText.vue +186 -0
- package/components/public/cms/element/CmsElementVimeoVideo.md +1 -0
- package/components/public/cms/element/CmsElementVimeoVideo.vue +63 -0
- package/components/public/cms/element/CmsElementYoutubeVideo.md +1 -0
- package/components/public/cms/element/CmsElementYoutubeVideo.vue +43 -0
- package/components/public/cms/section/CmsSectionDefault.md +3 -0
- package/components/public/cms/section/CmsSectionDefault.vue +21 -0
- package/components/public/cms/section/CmsSectionSidebar.md +3 -0
- package/components/public/cms/section/CmsSectionSidebar.vue +49 -0
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +44 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +31 -0
- package/helpers/clientOnly.ts +11 -0
- package/helpers/html-to-vue/ast.ts +72 -0
- package/helpers/html-to-vue/getOptionsFromNode.test.ts +129 -0
- package/helpers/html-to-vue/getOptionsFromNode.ts +52 -0
- package/helpers/html-to-vue/renderToHtml.ts +45 -0
- package/helpers/html-to-vue/renderer.ts +56 -0
- package/helpers/media/isSpatial.ts +8 -0
- package/index.cjs +7 -0
- package/nuxt.config.ts +21 -0
- package/package.json +69 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementBuyBox } from "@shopware/composables";
|
|
3
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
import { computed } from "vue";
|
|
6
|
+
import {
|
|
7
|
+
useCmsElementConfig,
|
|
8
|
+
usePrice,
|
|
9
|
+
useProduct,
|
|
10
|
+
useProductPrice,
|
|
11
|
+
useSessionContext,
|
|
12
|
+
} from "#imports";
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
content: CmsElementBuyBox;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
type Translations = {
|
|
19
|
+
product: {
|
|
20
|
+
previously: string;
|
|
21
|
+
amount: string;
|
|
22
|
+
price: {
|
|
23
|
+
[key: string]: string;
|
|
24
|
+
};
|
|
25
|
+
to: string;
|
|
26
|
+
from: string;
|
|
27
|
+
content: string;
|
|
28
|
+
pricesIncl: string;
|
|
29
|
+
pricesExcl: string;
|
|
30
|
+
deliveryTime: string;
|
|
31
|
+
days: string;
|
|
32
|
+
noAvailable: string;
|
|
33
|
+
productNumber: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let translations: Translations = {
|
|
38
|
+
product: {
|
|
39
|
+
previously: "Previously",
|
|
40
|
+
amount: "Amount",
|
|
41
|
+
price: {
|
|
42
|
+
label: "Price",
|
|
43
|
+
to: "To",
|
|
44
|
+
from: "From",
|
|
45
|
+
},
|
|
46
|
+
to: "To",
|
|
47
|
+
from: "From",
|
|
48
|
+
content: "Content",
|
|
49
|
+
pricesIncl: "Prices incl. VAT plus shipping costs",
|
|
50
|
+
pricesExcl: "Prices excl. VAT plus shipping costs",
|
|
51
|
+
deliveryTime: "Available, delivery time",
|
|
52
|
+
days: "days",
|
|
53
|
+
noAvailable: "No longer available",
|
|
54
|
+
productNumber: "Product number",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
59
|
+
|
|
60
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
61
|
+
const alignment = computed(() => getConfigValue("alignment"));
|
|
62
|
+
|
|
63
|
+
const { taxState, currency } = useSessionContext();
|
|
64
|
+
|
|
65
|
+
const { product, changeVariant } = useProduct(
|
|
66
|
+
props.content.data.product,
|
|
67
|
+
props.content.data.configuratorSettings || [],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const { unitPrice, price, tierPrices, isListPrice } = useProductPrice(product);
|
|
71
|
+
const regulationPrice = computed(() => price.value?.regulationPrice?.price);
|
|
72
|
+
const { getFormattedPrice } = usePrice();
|
|
73
|
+
|
|
74
|
+
const referencePrice = computed(
|
|
75
|
+
() => product.value?.calculatedPrice?.referencePrice,
|
|
76
|
+
);
|
|
77
|
+
const productNumber = computed(() => product.value?.productNumber);
|
|
78
|
+
const purchaseUnit = computed(() => product.value?.purchaseUnit);
|
|
79
|
+
const unitName = computed(() => product.value?.unit?.name);
|
|
80
|
+
const availableStock = computed(() => product.value?.availableStock ?? 0);
|
|
81
|
+
const minPurchase = computed(() => product.value?.minPurchase ?? 0);
|
|
82
|
+
const deliveryTime = computed(() => product.value?.deliveryTime);
|
|
83
|
+
const restockTime = computed(() => product.value?.restockTime);
|
|
84
|
+
</script>
|
|
85
|
+
<template>
|
|
86
|
+
<div
|
|
87
|
+
v-if="product"
|
|
88
|
+
:class="{
|
|
89
|
+
'h-full flex flex-col': true,
|
|
90
|
+
'justify-start': alignment === 'flex-start',
|
|
91
|
+
'justify-end': alignment === 'flex-end',
|
|
92
|
+
'justify-center': alignment === 'center',
|
|
93
|
+
}"
|
|
94
|
+
>
|
|
95
|
+
<div>
|
|
96
|
+
<div v-if="tierPrices.length <= 1">
|
|
97
|
+
<SwSharedPrice
|
|
98
|
+
v-if="isListPrice"
|
|
99
|
+
class="text-1xl text-secondary-900 basis-2/6 justify-start line-through"
|
|
100
|
+
:value="price?.listPrice?.price"
|
|
101
|
+
/>
|
|
102
|
+
<SwSharedPrice
|
|
103
|
+
v-if="unitPrice"
|
|
104
|
+
class="text-3xl text-secondary-900 basis-2/6 justify-start"
|
|
105
|
+
:class="{
|
|
106
|
+
'text-red': isListPrice,
|
|
107
|
+
}"
|
|
108
|
+
:value="unitPrice"
|
|
109
|
+
/>
|
|
110
|
+
<div v-if="regulationPrice" class="text-xs flex text-secondary-500">
|
|
111
|
+
{{ translations.product.previously }}
|
|
112
|
+
<SwSharedPrice class="ml-1" :value="regulationPrice" />
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div v-else>
|
|
116
|
+
<table class="border-collapse table-auto w-full text-sm mb-8">
|
|
117
|
+
<thead>
|
|
118
|
+
<tr>
|
|
119
|
+
<th
|
|
120
|
+
class="border-b dark:border-secondary-600 font-medium p-4 pl-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left"
|
|
121
|
+
>
|
|
122
|
+
{{ translations.product.amount }}
|
|
123
|
+
</th>
|
|
124
|
+
|
|
125
|
+
<th
|
|
126
|
+
class="border-b dark:border-secondary-600 font-medium p-4 pr-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left"
|
|
127
|
+
>
|
|
128
|
+
{{ translations.product.price.label }}
|
|
129
|
+
</th>
|
|
130
|
+
</tr>
|
|
131
|
+
</thead>
|
|
132
|
+
<tbody class="bg-white dark:bg-secondary-800">
|
|
133
|
+
<tr v-for="(tierPrice, index) in tierPrices" :key="tierPrice.label">
|
|
134
|
+
<td
|
|
135
|
+
class="border-b border-secondary-100 dark:border-secondary-700 p-4 pl-8 font-medium text-secondary-500 dark:text-secondary-400"
|
|
136
|
+
>
|
|
137
|
+
<span v-if="index < tierPrices.length - 1">{{
|
|
138
|
+
translations.product.to
|
|
139
|
+
}}</span
|
|
140
|
+
><span v-else>{{ translations.product.from }}</span>
|
|
141
|
+
{{ tierPrice.quantity }}
|
|
142
|
+
</td>
|
|
143
|
+
<td
|
|
144
|
+
class="border-b border-secondary-100 dark:border-secondary-700 p-4 pr-8 font-medium text-current-500 dark:text-secondary-400"
|
|
145
|
+
>
|
|
146
|
+
{{ getFormattedPrice(tierPrice.unitPrice) }}
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
</tbody>
|
|
150
|
+
</table>
|
|
151
|
+
</div>
|
|
152
|
+
<div v-if="purchaseUnit && unitName" class="mt-1">
|
|
153
|
+
<span class="font-light"> {{ translations.product.content }}: </span>
|
|
154
|
+
<span class="font-light"> {{ purchaseUnit }} {{ unitName }} </span>
|
|
155
|
+
<span v-if="referencePrice" class="font-light">
|
|
156
|
+
{{ currency?.symbol }} {{ referencePrice?.price }} / /
|
|
157
|
+
{{ referencePrice?.referenceUnit }} {{ referencePrice?.unitName }}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
<span class="text-indigo-600">
|
|
161
|
+
<template v-if="taxState === 'gross'">
|
|
162
|
+
{{ translations.product.pricesIncl }}
|
|
163
|
+
</template>
|
|
164
|
+
<template v-else> {{ translations.product.pricesExcl }} </template>
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="mt-4">
|
|
168
|
+
<span v-if="availableStock >= minPurchase && deliveryTime"
|
|
169
|
+
>{{ translations.product.deliveryTime }} {{ deliveryTime?.name }}
|
|
170
|
+
</span>
|
|
171
|
+
<span
|
|
172
|
+
v-else-if="availableStock < minPurchase && deliveryTime && restockTime"
|
|
173
|
+
>
|
|
174
|
+
{{ translations.product.deliveryTime }} {{ restockTime }}
|
|
175
|
+
{{ translations.product.days }} {{ deliveryTime?.name }}</span
|
|
176
|
+
>
|
|
177
|
+
<span v-else>{{ translations.product.noAvailable }}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<SwVariantConfigurator @change="changeVariant" />
|
|
180
|
+
<SwProductAddToCart :product="product" />
|
|
181
|
+
<div class="mt-3 product-detail-ordernumber-container">
|
|
182
|
+
<span class="font-bold text-secondary-900">
|
|
183
|
+
{{ translations.product.productNumber }}:
|
|
184
|
+
</span>
|
|
185
|
+
<span>
|
|
186
|
+
{{ productNumber }}
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Load a navigation menu for current category
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
3
|
+
import { getTranslatedProperty } from "@shopware/helpers";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
import type { Ref } from "vue";
|
|
6
|
+
import { onMounted, ref } from "vue";
|
|
7
|
+
import { useCategory, useNavigation } from "#imports";
|
|
8
|
+
import type { Schemas } from "#shopware";
|
|
9
|
+
|
|
10
|
+
type Translations = {
|
|
11
|
+
listing: {
|
|
12
|
+
category: string;
|
|
13
|
+
subCategory: string;
|
|
14
|
+
subCategoryOf: string;
|
|
15
|
+
loading: string;
|
|
16
|
+
categories: string;
|
|
17
|
+
subCategories: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let translations: Translations = {
|
|
22
|
+
listing: {
|
|
23
|
+
category: "Category",
|
|
24
|
+
subCategory: "Sub-category",
|
|
25
|
+
subCategoryOf: "of",
|
|
26
|
+
loading: "Loading ...",
|
|
27
|
+
categories: "Categories",
|
|
28
|
+
subCategories: "Sub-categories",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
33
|
+
|
|
34
|
+
const { category: activeCategory } = useCategory();
|
|
35
|
+
const loading: Ref<boolean> = ref(true);
|
|
36
|
+
const flagAllowSubcategories: boolean = true; // could be passed maybe as a prop in the future
|
|
37
|
+
const categoryNavigation: Ref<Schemas["Category"][]> = ref([]);
|
|
38
|
+
|
|
39
|
+
const currentCategoryId = activeCategory.value?.id ?? "main-navigation";
|
|
40
|
+
const type = flagAllowSubcategories ? currentCategoryId : "main-navigation";
|
|
41
|
+
const { loadNavigationElements } = useNavigation({
|
|
42
|
+
type: type,
|
|
43
|
+
});
|
|
44
|
+
const removeChildrenIfNotActiveCategory = () => {
|
|
45
|
+
const navigation: Schemas["Category"][] = JSON.parse(
|
|
46
|
+
JSON.stringify(categoryNavigation.value),
|
|
47
|
+
);
|
|
48
|
+
return navigation?.map((navigationElement) => {
|
|
49
|
+
navigationElement.children =
|
|
50
|
+
activeCategory.value?.id === navigationElement.id
|
|
51
|
+
? navigationElement.children
|
|
52
|
+
: [];
|
|
53
|
+
return navigationElement;
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
onMounted(async () => {
|
|
58
|
+
// depth 0 means, we load only first level of categories, depth 1 means we load first and second level of categories ...
|
|
59
|
+
const depth = flagAllowSubcategories ? 0 : 1;
|
|
60
|
+
categoryNavigation.value = await loadNavigationElements({ depth });
|
|
61
|
+
if (!flagAllowSubcategories) {
|
|
62
|
+
categoryNavigation.value = removeChildrenIfNotActiveCategory();
|
|
63
|
+
}
|
|
64
|
+
loading.value = false;
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
<template>
|
|
68
|
+
<div>
|
|
69
|
+
<div
|
|
70
|
+
v-if="categoryNavigation && categoryNavigation.length"
|
|
71
|
+
class="cms-element-category-navigation max-w-screen-xl mx-auto"
|
|
72
|
+
>
|
|
73
|
+
<h2
|
|
74
|
+
v-if="categoryNavigation.length > 0 && !flagAllowSubcategories"
|
|
75
|
+
class="text-3xl tracking-tight text-secondary-900 m-0 px-5"
|
|
76
|
+
>
|
|
77
|
+
{{
|
|
78
|
+
categoryNavigation.length > 1
|
|
79
|
+
? translations.listing.categories
|
|
80
|
+
: translations.listing.category
|
|
81
|
+
}}
|
|
82
|
+
</h2>
|
|
83
|
+
<h2
|
|
84
|
+
v-if="categoryNavigation.length > 0 && flagAllowSubcategories"
|
|
85
|
+
class="text-3xl tracking-tight text-secondary-900 m-0 px-5"
|
|
86
|
+
>
|
|
87
|
+
{{
|
|
88
|
+
categoryNavigation.length > 1
|
|
89
|
+
? translations.listing.subCategories
|
|
90
|
+
: translations.listing.subCategory
|
|
91
|
+
}}
|
|
92
|
+
{{ translations.listing.subCategoryOf }}
|
|
93
|
+
{{ getTranslatedProperty(activeCategory, "name") }}
|
|
94
|
+
</h2>
|
|
95
|
+
<SwCategoryNavigation
|
|
96
|
+
:level="0"
|
|
97
|
+
:elements="categoryNavigation"
|
|
98
|
+
:active-category="activeCategory"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<div v-if="loading">
|
|
102
|
+
<div class="px-5">
|
|
103
|
+
<h2 class="text-3xl tracking-tight text-secondary-900 m-0 px-5 pl-0">
|
|
104
|
+
{{ translations.listing.loading }}
|
|
105
|
+
</h2>
|
|
106
|
+
<div
|
|
107
|
+
class="border border-secondary-200 shadow rounded-md p-4 max-w-screen-xl mx-auto"
|
|
108
|
+
>
|
|
109
|
+
<div class="animate-pulse flex space-x-4">
|
|
110
|
+
<div class="flex-1 space-y-6 py-1">
|
|
111
|
+
<div class="space-y-3">
|
|
112
|
+
<div class="grid grid-cols-3 gap-4">
|
|
113
|
+
<div class="h-2 bg-light-200 rounded col-span-2"></div>
|
|
114
|
+
<div class="h-2 bg-light-200 rounded col-span-1"></div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="h-2 bg-secondary-200 rounded"></div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="space-y-3">
|
|
119
|
+
<div class="grid grid-cols-3 gap-4">
|
|
120
|
+
<div class="h-2 bg-light-200 rounded col-span-1"></div>
|
|
121
|
+
<div class="h-2 bg-light-200 rounded col-span-2"></div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="h-2 bg-secondary-200 rounded"></div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="animate-pulse flex space-x-4">
|
|
128
|
+
<div class="flex-1 space-y-6 py-4">
|
|
129
|
+
<div class="space-y-3">
|
|
130
|
+
<div class="grid grid-cols-3 gap-4">
|
|
131
|
+
<div class="h-2 bg-light-200 rounded col-span-2"></div>
|
|
132
|
+
<div class="h-2 bg-light-200 rounded col-span-1"></div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="h-2 bg-secondary-200 rounded"></div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="space-y-3">
|
|
137
|
+
<div class="grid grid-cols-3 gap-4">
|
|
138
|
+
<div class="h-2 bg-light-200 rounded col-span-1"></div>
|
|
139
|
+
<div class="h-2 bg-light-200 rounded col-span-2"></div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="h-2 bg-secondary-200 rounded"></div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="animate-pulse flex space-x-4">
|
|
146
|
+
<div class="flex-1 space-y-6 pt-4">
|
|
147
|
+
<div class="space-y-3">
|
|
148
|
+
<div class="grid grid-cols-6 gap-4">
|
|
149
|
+
<div class="h-2 bg-light-200 rounded col-span-1"></div>
|
|
150
|
+
<div class="h-2 bg-light-200 rounded col-span-5"></div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="h-2 bg-secondary-200 rounded"></div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="space-y-3">
|
|
155
|
+
<div class="grid grid-cols-3 gap-4">
|
|
156
|
+
<div class="h-2 bg-light-200 rounded col-span-2"></div>
|
|
157
|
+
<div class="h-2 bg-light-200 rounded col-span-1"></div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="h-2 bg-secondary-200 rounded"></div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Render slider of the products from cross-selling setting of a product
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
CmsElementCrossSelling,
|
|
4
|
+
SliderElementConfig,
|
|
5
|
+
} from "@shopware/composables";
|
|
6
|
+
import { useElementSize } from "@vueuse/core";
|
|
7
|
+
import { computed, ref, useTemplateRef } from "vue";
|
|
8
|
+
import { useCmsElementConfig } from "#imports";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
content: CmsElementCrossSelling;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
15
|
+
const currentTabIndex = ref<number>(0);
|
|
16
|
+
const crossSellContainer = useTemplateRef("crossSellContainer");
|
|
17
|
+
const config = computed<SliderElementConfig>(() => ({
|
|
18
|
+
minHeight: {
|
|
19
|
+
value: "300px",
|
|
20
|
+
source: "static",
|
|
21
|
+
},
|
|
22
|
+
minWidth: {
|
|
23
|
+
value: "300px",
|
|
24
|
+
source: "static",
|
|
25
|
+
},
|
|
26
|
+
displayMode: {
|
|
27
|
+
value: "contain",
|
|
28
|
+
source: "static",
|
|
29
|
+
},
|
|
30
|
+
navigationDots: {
|
|
31
|
+
value: "",
|
|
32
|
+
source: "static",
|
|
33
|
+
},
|
|
34
|
+
navigationArrows: {
|
|
35
|
+
value: "outside",
|
|
36
|
+
source: "static",
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const crossSellCollections = computed(() => {
|
|
41
|
+
return (
|
|
42
|
+
props.content?.data?.crossSellings?.filter(
|
|
43
|
+
(collection) => !!collection.products.length,
|
|
44
|
+
) || []
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { width } = useElementSize(crossSellContainer);
|
|
49
|
+
const slidesToShow = computed(() => {
|
|
50
|
+
const minWidth = +(config.value.minWidth?.value.replace(/\D+/g, "") || 0);
|
|
51
|
+
return Math.floor(width.value / (minWidth * 1.2));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const toggleTab = (index: number) => {
|
|
55
|
+
if (currentTabIndex.value === index) return;
|
|
56
|
+
currentTabIndex.value = index;
|
|
57
|
+
};
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<div ref="crossSellContainer" class="cms-element-cross-selling">
|
|
62
|
+
<div class="flex gap-10 mb-5">
|
|
63
|
+
<a
|
|
64
|
+
v-for="(collection, index) of crossSellCollections"
|
|
65
|
+
:key="index"
|
|
66
|
+
class="transition text-lg font-bold text-secondary-700 cursor-pointer"
|
|
67
|
+
:class="{
|
|
68
|
+
'border-b-3 border-primary text-primary': currentTabIndex === index,
|
|
69
|
+
}"
|
|
70
|
+
@click="toggleTab(index)"
|
|
71
|
+
>
|
|
72
|
+
{{ collection.crossSelling.name }}
|
|
73
|
+
</a>
|
|
74
|
+
</div>
|
|
75
|
+
<transition name="fade" mode="out-in">
|
|
76
|
+
<SwSlider
|
|
77
|
+
v-if="crossSellCollections.length"
|
|
78
|
+
:config="config"
|
|
79
|
+
gap="1.25rem"
|
|
80
|
+
:slides-to-show="slidesToShow"
|
|
81
|
+
:slides-to-scroll="1"
|
|
82
|
+
:autoplay="false"
|
|
83
|
+
>
|
|
84
|
+
<SwProductCard
|
|
85
|
+
v-for="product of crossSellCollections[currentTabIndex].products"
|
|
86
|
+
:key="product.id"
|
|
87
|
+
class="w-[300px]"
|
|
88
|
+
:product="product"
|
|
89
|
+
:layout-type="getConfigValue('boxLayout')"
|
|
90
|
+
:display-mode="getConfigValue('displayMode')"
|
|
91
|
+
/>
|
|
92
|
+
</SwSlider>
|
|
93
|
+
</transition>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
<style scoped>
|
|
97
|
+
.fade-enter-active,
|
|
98
|
+
.fade-leave-active {
|
|
99
|
+
transition: all 0.2s ease;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.fade-enter-from,
|
|
103
|
+
.fade-leave-to {
|
|
104
|
+
opacity: 0;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a contact or newsletter sign up form
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementForm } from "@shopware/composables";
|
|
3
|
+
import { computed, defineAsyncComponent } from "vue";
|
|
4
|
+
import { useCmsElementConfig } from "#imports";
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
content: CmsElementForm;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
11
|
+
|
|
12
|
+
const FormComponent = computed(() => {
|
|
13
|
+
switch (getConfigValue("type")) {
|
|
14
|
+
case "newsletter":
|
|
15
|
+
return defineAsyncComponent(
|
|
16
|
+
() => import("../../../SwNewsletterForm.vue"),
|
|
17
|
+
);
|
|
18
|
+
default:
|
|
19
|
+
return defineAsyncComponent(() => import("../../../SwContactForm.vue"));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
<template>
|
|
24
|
+
<div class="cms-element-form">
|
|
25
|
+
<component :is="FormComponent" :content="content" />
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a contact or newsletter sign up form
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementForm } from "@shopware/composables";
|
|
3
|
+
import { computed, defineAsyncComponent } from "vue";
|
|
4
|
+
import { useCmsElementConfig } from "#imports";
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
content: CmsElementForm;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
11
|
+
|
|
12
|
+
const FormComponent = computed(() => {
|
|
13
|
+
switch (getConfigValue("type")) {
|
|
14
|
+
case "newsletter":
|
|
15
|
+
return defineAsyncComponent(
|
|
16
|
+
() => import("../../../SwNewsletterForm.vue"),
|
|
17
|
+
);
|
|
18
|
+
default:
|
|
19
|
+
return defineAsyncComponent(() => import("../../../SwContactForm.vue"));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
<template>
|
|
24
|
+
<div class="cms-element-form">
|
|
25
|
+
<component :is="FormComponent" :content="content" />
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display an image for provided media content. Including extra attributes like `srcset` and `alt`
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
CmsElementImage,
|
|
4
|
+
CmsElementManufacturerLogo,
|
|
5
|
+
} from "@shopware/composables";
|
|
6
|
+
import { buildUrlPrefix } from "@shopware/helpers";
|
|
7
|
+
import { useElementSize } from "@vueuse/core";
|
|
8
|
+
import { computed, defineAsyncComponent, useTemplateRef } from "vue";
|
|
9
|
+
import { useCmsElementImage, useUrlResolver } from "#imports";
|
|
10
|
+
import { ClientOnly } from "../../../../helpers/clientOnly";
|
|
11
|
+
import { isSpatial } from "../../../../helpers/media/isSpatial";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
content: CmsElementImage | CmsElementManufacturerLogo;
|
|
15
|
+
imageGallery?: boolean;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const { getUrlPrefix } = useUrlResolver();
|
|
19
|
+
const {
|
|
20
|
+
containerStyle,
|
|
21
|
+
displayMode,
|
|
22
|
+
imageContainerAttrs,
|
|
23
|
+
imageAttrs,
|
|
24
|
+
imageLink,
|
|
25
|
+
isVideoElement,
|
|
26
|
+
mimeType,
|
|
27
|
+
} = useCmsElementImage(props.content);
|
|
28
|
+
|
|
29
|
+
const DEFAULT_THUMBNAIL_SIZE = 10;
|
|
30
|
+
const imageElement = useTemplateRef("imageElement");
|
|
31
|
+
const { width, height } = useElementSize(imageElement);
|
|
32
|
+
|
|
33
|
+
function roundUp(num: number) {
|
|
34
|
+
return num ? Math.ceil(num / 100) * 100 : DEFAULT_THUMBNAIL_SIZE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const srcPath = computed(() => {
|
|
38
|
+
const biggestParam =
|
|
39
|
+
width.value > height.value
|
|
40
|
+
? `width=${roundUp(width.value)}`
|
|
41
|
+
: `height=${roundUp(height.value)}`;
|
|
42
|
+
return `${imageAttrs.value.src}?${biggestParam}&fit=crop,smart`;
|
|
43
|
+
});
|
|
44
|
+
const imageComputedContainerAttrs = computed(() => {
|
|
45
|
+
const imageAttrsCopy = Object.assign({}, imageContainerAttrs.value);
|
|
46
|
+
if (imageAttrsCopy?.href) {
|
|
47
|
+
imageAttrsCopy.href = buildUrlPrefix(
|
|
48
|
+
imageAttrsCopy.href,
|
|
49
|
+
getUrlPrefix(),
|
|
50
|
+
).path;
|
|
51
|
+
}
|
|
52
|
+
return imageAttrsCopy;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const SwMedia3D = computed(() => {
|
|
56
|
+
if (isSpatial(props.content.data.media)) {
|
|
57
|
+
return defineAsyncComponent(() => import("../../../SwMedia3D.vue"));
|
|
58
|
+
}
|
|
59
|
+
return "";
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
62
|
+
<template>
|
|
63
|
+
<!-- TODO: using a tag only works with externalLink, need to improve this element to deal with both internalLink & externalLink -->
|
|
64
|
+
<component
|
|
65
|
+
:is="imageLink.url ? 'a' : 'div'"
|
|
66
|
+
v-if="imageAttrs.src"
|
|
67
|
+
class="cms-element-image relative h-full w-full"
|
|
68
|
+
:class="{
|
|
69
|
+
'flex justify-center items-center': imageGallery,
|
|
70
|
+
}"
|
|
71
|
+
:style="containerStyle"
|
|
72
|
+
v-bind="imageComputedContainerAttrs"
|
|
73
|
+
>
|
|
74
|
+
<video
|
|
75
|
+
v-if="isVideoElement"
|
|
76
|
+
controls
|
|
77
|
+
:class="{
|
|
78
|
+
'h-full w-full': true,
|
|
79
|
+
'absolute inset-0': ['cover', 'stretch'].includes(displayMode),
|
|
80
|
+
'object-cover': displayMode === 'cover',
|
|
81
|
+
}"
|
|
82
|
+
>
|
|
83
|
+
<source :src="imageAttrs.src" :type="mimeType" />
|
|
84
|
+
Your browser does not support the video tag.
|
|
85
|
+
</video>
|
|
86
|
+
<ClientOnly v-else-if="isSpatial(props.content.data.media)">
|
|
87
|
+
<component :is="SwMedia3D" :src="props.content.data.media.url" />
|
|
88
|
+
</ClientOnly>
|
|
89
|
+
<img
|
|
90
|
+
v-else
|
|
91
|
+
ref="imageElement"
|
|
92
|
+
loading="lazy"
|
|
93
|
+
:class="{
|
|
94
|
+
'w-full h-full': !imageGallery,
|
|
95
|
+
'w-4/5': imageGallery,
|
|
96
|
+
'absolute inset-0': ['cover', 'stretch'].includes(displayMode),
|
|
97
|
+
'object-cover': displayMode === 'cover',
|
|
98
|
+
'object-contain': imageGallery,
|
|
99
|
+
}"
|
|
100
|
+
:alt="imageAttrs.alt"
|
|
101
|
+
:src="srcPath"
|
|
102
|
+
:srcset="imageAttrs.srcset"
|
|
103
|
+
/>
|
|
104
|
+
</component>
|
|
105
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Display a gallery for provided media. Handles a plain image and the spatial (3d) images.
|