@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,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCmsTranslations, useProductPrice } from "@shopware/composables";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { toRefs } from "vue";
|
|
5
|
+
import type { Schemas } from "#shopware";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
product: Schemas["Product"];
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
type Translations = {
|
|
12
|
+
listing: {
|
|
13
|
+
variantsFrom: string;
|
|
14
|
+
previously: string;
|
|
15
|
+
to: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let translations: Translations = {
|
|
20
|
+
listing: {
|
|
21
|
+
variantsFrom: "variants from",
|
|
22
|
+
previously: "previously",
|
|
23
|
+
from: "from",
|
|
24
|
+
to: "to",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
29
|
+
|
|
30
|
+
const { product } = toRefs(props);
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
price,
|
|
34
|
+
unitPrice,
|
|
35
|
+
displayFromVariants,
|
|
36
|
+
displayFrom,
|
|
37
|
+
isListPrice,
|
|
38
|
+
regulationPrice,
|
|
39
|
+
} = useProductPrice(product);
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div :id="product.id">
|
|
44
|
+
<SwSharedPrice
|
|
45
|
+
v-if="isListPrice"
|
|
46
|
+
class="text-l text-gray-900 basis-2/6 justify-end line-through"
|
|
47
|
+
:value="price?.listPrice?.price"
|
|
48
|
+
/>
|
|
49
|
+
<template v-if="!isListPrice">
|
|
50
|
+
<div class="h-6"><!-- placeholder --></div>
|
|
51
|
+
</template>
|
|
52
|
+
<SwSharedPrice
|
|
53
|
+
v-if="displayFromVariants"
|
|
54
|
+
class="text-xl text-gray-900 basis-2/6 justify-end"
|
|
55
|
+
:value="displayFromVariants"
|
|
56
|
+
>
|
|
57
|
+
<template #beforePrice
|
|
58
|
+
><span v-if="displayFromVariants" class="text-sm">{{
|
|
59
|
+
translations.listing.variantsFrom
|
|
60
|
+
}}</span></template
|
|
61
|
+
>
|
|
62
|
+
</SwSharedPrice>
|
|
63
|
+
<SwSharedPrice
|
|
64
|
+
class="text-gray-900 basis-2/6"
|
|
65
|
+
:class="{
|
|
66
|
+
'text-red-600 font-bold': isListPrice,
|
|
67
|
+
'justify-end text-xl':
|
|
68
|
+
regulationPrice || !regulationPrice || !displayFromVariants,
|
|
69
|
+
}"
|
|
70
|
+
:value="unitPrice"
|
|
71
|
+
>
|
|
72
|
+
<template #beforePrice
|
|
73
|
+
><span v-if="displayFrom || displayFromVariants" class="text-sm">{{
|
|
74
|
+
translations.listing.from
|
|
75
|
+
}}</span></template
|
|
76
|
+
>
|
|
77
|
+
</SwSharedPrice>
|
|
78
|
+
<template v-if="regulationPrice">
|
|
79
|
+
<div class="flex gap-2 justify-end text-gray-500 text-3.5 mb-2">
|
|
80
|
+
{{ translations.listing.previously }}
|
|
81
|
+
<SharedPrice :value="regulationPrice" />
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
<template v-if="!regulationPrice">
|
|
85
|
+
<div class="h-7"><!-- placeholder --></div>
|
|
86
|
+
</template>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { OrbitControls, useGLTF } from "@tresjs/cientos";
|
|
3
|
+
import { TresCanvas } from "@tresjs/core";
|
|
4
|
+
import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from "three";
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
src: string;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const gl = {
|
|
11
|
+
clearColor: "#FFF",
|
|
12
|
+
shadows: true,
|
|
13
|
+
alpha: false,
|
|
14
|
+
shadowMapType: BasicShadowMap,
|
|
15
|
+
outputColorSpace: SRGBColorSpace,
|
|
16
|
+
toneMapping: NoToneMapping,
|
|
17
|
+
windowSize: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { scene: model } = await useGLTF(props.src);
|
|
21
|
+
</script>
|
|
22
|
+
<template>
|
|
23
|
+
<TresCanvas v-bind="gl">
|
|
24
|
+
<TresPerspectiveCamera
|
|
25
|
+
:args="[75, 1, 0.1, 2000]"
|
|
26
|
+
:position="[0, 0, 500]"
|
|
27
|
+
:look-at="[0, 0, 0]"
|
|
28
|
+
/>
|
|
29
|
+
<OrbitControls />
|
|
30
|
+
<primitive :object="model" />
|
|
31
|
+
<TresDirectionalLight :position="[3, 3, 3]" :intensity="1" />
|
|
32
|
+
<TresAmbientLight :intensity="2" />
|
|
33
|
+
</TresCanvas>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ApiClientError } from "@shopware/api-client";
|
|
3
|
+
import type { ApiError } from "@shopware/api-client";
|
|
4
|
+
import type { CmsElementForm } from "@shopware/composables";
|
|
5
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
6
|
+
import { useVuelidate } from "@vuelidate/core";
|
|
7
|
+
import type { ValidationRuleWithoutParams } from "@vuelidate/core";
|
|
8
|
+
import { email, required } from "@vuelidate/validators";
|
|
9
|
+
import { defu } from "defu";
|
|
10
|
+
import { computed, reactive, ref } from "vue";
|
|
11
|
+
import { useCmsElementConfig, useNewsletter, useSalutations } from "#imports";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
content: CmsElementForm;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
type Translations = {
|
|
18
|
+
form: {
|
|
19
|
+
subscribeLabel: string;
|
|
20
|
+
unsubscribeLabel: string;
|
|
21
|
+
action: string;
|
|
22
|
+
email: string;
|
|
23
|
+
emailPlaceholder: string;
|
|
24
|
+
salutation: string;
|
|
25
|
+
salutationPlaceholder: string;
|
|
26
|
+
firstName: string;
|
|
27
|
+
firstNamePlaceholder: string;
|
|
28
|
+
lastName: string;
|
|
29
|
+
lastNamePlaceholder: string;
|
|
30
|
+
privacy: string;
|
|
31
|
+
privacyLabel: string;
|
|
32
|
+
submit: string;
|
|
33
|
+
newsletterBenefits: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let translations: Translations = {
|
|
38
|
+
form: {
|
|
39
|
+
subscribeLabel: "Subscribe to newsletter",
|
|
40
|
+
unsubscribeLabel: "Unsubscribe from newsletter",
|
|
41
|
+
action: "Action",
|
|
42
|
+
email: "Email address",
|
|
43
|
+
emailPlaceholder: "Enter email address...",
|
|
44
|
+
salutation: "Salutation",
|
|
45
|
+
salutationPlaceholder: "Enter salutation...",
|
|
46
|
+
firstName: "First name",
|
|
47
|
+
firstNamePlaceholder: "Enter first name...",
|
|
48
|
+
lastName: "Last name",
|
|
49
|
+
lastNamePlaceholder: "Enter last name...",
|
|
50
|
+
privacy: "Privacy",
|
|
51
|
+
privacyLabel: "I have read the data protection information.",
|
|
52
|
+
submit: "Submit",
|
|
53
|
+
newsletterBenefits:
|
|
54
|
+
"Be aware of upcoming sales and events.Receive gifts and special offers!",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
59
|
+
|
|
60
|
+
const loading = ref<boolean>();
|
|
61
|
+
const formSent = ref<boolean>(false);
|
|
62
|
+
const errorMessages = ref<ApiError[]>([]);
|
|
63
|
+
const subscriptionOptions: {
|
|
64
|
+
label: string;
|
|
65
|
+
value: "subscribe" | "unsubscribe";
|
|
66
|
+
}[] = [
|
|
67
|
+
{
|
|
68
|
+
label: translations.form.subscribeLabel,
|
|
69
|
+
value: "subscribe",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: translations.form.unsubscribeLabel,
|
|
73
|
+
value: "unsubscribe",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
const { getSalutations } = useSalutations();
|
|
77
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
78
|
+
const { newsletterSubscribe, newsletterUnsubscribe } = useNewsletter();
|
|
79
|
+
|
|
80
|
+
const getFormTitle = computed(() => getConfigValue("title"));
|
|
81
|
+
const state = reactive({
|
|
82
|
+
option: subscriptionOptions[0].value,
|
|
83
|
+
salutationId: "",
|
|
84
|
+
firstName: "",
|
|
85
|
+
lastName: "",
|
|
86
|
+
email: "",
|
|
87
|
+
checkbox: false,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
type Rules = {
|
|
91
|
+
email: {
|
|
92
|
+
required: ValidationRuleWithoutParams;
|
|
93
|
+
email: ValidationRuleWithoutParams;
|
|
94
|
+
};
|
|
95
|
+
checkbox: {
|
|
96
|
+
required: ValidationRuleWithoutParams;
|
|
97
|
+
isTrue: (value: boolean) => boolean;
|
|
98
|
+
};
|
|
99
|
+
firstName: {
|
|
100
|
+
required: ValidationRuleWithoutParams;
|
|
101
|
+
minLength: number;
|
|
102
|
+
};
|
|
103
|
+
lastName: {
|
|
104
|
+
required: ValidationRuleWithoutParams;
|
|
105
|
+
minLength: number;
|
|
106
|
+
};
|
|
107
|
+
salutationId: {
|
|
108
|
+
required: ValidationRuleWithoutParams;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const rules = computed(() => {
|
|
112
|
+
let temp: Partial<Rules> = {
|
|
113
|
+
email: {
|
|
114
|
+
required,
|
|
115
|
+
email,
|
|
116
|
+
},
|
|
117
|
+
checkbox: {
|
|
118
|
+
required,
|
|
119
|
+
isTrue: (value: boolean) => value === true,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
if (state.option === "subscribe") {
|
|
123
|
+
temp = {
|
|
124
|
+
...temp,
|
|
125
|
+
firstName: {
|
|
126
|
+
required,
|
|
127
|
+
minLength: 3,
|
|
128
|
+
},
|
|
129
|
+
lastName: {
|
|
130
|
+
required,
|
|
131
|
+
minLength: 3,
|
|
132
|
+
},
|
|
133
|
+
salutationId: {
|
|
134
|
+
required,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return temp;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const $v = useVuelidate(rules, state);
|
|
142
|
+
const invokeSubmit = async () => {
|
|
143
|
+
$v.value.$touch();
|
|
144
|
+
const valid = await $v.value.$validate();
|
|
145
|
+
if (valid) {
|
|
146
|
+
loading.value = true;
|
|
147
|
+
try {
|
|
148
|
+
if (state.option === "subscribe") {
|
|
149
|
+
await newsletterSubscribe({
|
|
150
|
+
...state,
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
await newsletterUnsubscribe(state.email);
|
|
154
|
+
}
|
|
155
|
+
formSent.value = true;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (e instanceof ApiClientError) {
|
|
158
|
+
errorMessages.value = e.details.errors;
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
loading.value = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
</script>
|
|
166
|
+
<template>
|
|
167
|
+
<form class="w-full relative" @submit.prevent="invokeSubmit">
|
|
168
|
+
<div
|
|
169
|
+
v-if="loading"
|
|
170
|
+
class="absolute inset-0 flex items-center justify-center z-10 bg-white/50"
|
|
171
|
+
>
|
|
172
|
+
<div
|
|
173
|
+
class="h-15 w-15 i-carbon-progress-bar-round animate-spin c-gray-500"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
<h3 class="pb-3 mb-10 border-b border-gray-300">
|
|
177
|
+
{{
|
|
178
|
+
getFormTitle
|
|
179
|
+
? getFormTitle
|
|
180
|
+
: state.option === "subscribe"
|
|
181
|
+
? translations.form.subscribeLabel
|
|
182
|
+
: translations.form.unsubscribeLabel
|
|
183
|
+
}}
|
|
184
|
+
</h3>
|
|
185
|
+
<template v-if="!formSent">
|
|
186
|
+
<div class="grid grid-cols-12 gap-5">
|
|
187
|
+
<div class="col-span-12">
|
|
188
|
+
<label for="option">{{ translations.form.action }} *</label>
|
|
189
|
+
<select
|
|
190
|
+
id="option"
|
|
191
|
+
v-model="state.option"
|
|
192
|
+
name="option"
|
|
193
|
+
class="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
|
|
194
|
+
>
|
|
195
|
+
<option
|
|
196
|
+
v-for="subscription in subscriptionOptions"
|
|
197
|
+
:key="subscription.value"
|
|
198
|
+
:value="subscription.value"
|
|
199
|
+
>
|
|
200
|
+
{{ subscription.label }}
|
|
201
|
+
</option>
|
|
202
|
+
</select>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="col-span-12">
|
|
205
|
+
<label for="email-address">{{ translations.form.email }} *</label>
|
|
206
|
+
<input
|
|
207
|
+
id="email-address"
|
|
208
|
+
v-model="state.email"
|
|
209
|
+
name="email"
|
|
210
|
+
type="email"
|
|
211
|
+
autocomplete="email"
|
|
212
|
+
:class="[
|
|
213
|
+
$v.email?.$error
|
|
214
|
+
? 'border-red-600 focus:border-red-600'
|
|
215
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
216
|
+
]"
|
|
217
|
+
class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
|
|
218
|
+
:placeholder="translations.form.emailPlaceholder"
|
|
219
|
+
@blur="$v.email?.$touch()"
|
|
220
|
+
/>
|
|
221
|
+
<span
|
|
222
|
+
v-if="$v.email?.$error"
|
|
223
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
224
|
+
>
|
|
225
|
+
{{ $v.email?.$errors[0].$message }}
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
<div v-if="state.option === 'subscribe'" class="col-span-4">
|
|
229
|
+
<label for="salutation">{{ translations.form.salutation }} *</label>
|
|
230
|
+
<select
|
|
231
|
+
id="salutation"
|
|
232
|
+
v-model="state.salutationId"
|
|
233
|
+
name="salutation"
|
|
234
|
+
class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
|
|
235
|
+
:class="[
|
|
236
|
+
$v.salutationId?.$error
|
|
237
|
+
? 'border-red-600 focus:border-red-600'
|
|
238
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
239
|
+
]"
|
|
240
|
+
@blur="$v.salutationId?.$touch()"
|
|
241
|
+
>
|
|
242
|
+
<option disabled selected value="">
|
|
243
|
+
{{ translations.form.salutationPlaceholder }}
|
|
244
|
+
</option>
|
|
245
|
+
<option
|
|
246
|
+
v-for="salutation in getSalutations"
|
|
247
|
+
:key="salutation.id"
|
|
248
|
+
:value="salutation.id"
|
|
249
|
+
>
|
|
250
|
+
{{ salutation.displayName }}
|
|
251
|
+
</option>
|
|
252
|
+
</select>
|
|
253
|
+
<span
|
|
254
|
+
v-if="$v.salutationId?.$error"
|
|
255
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
256
|
+
>
|
|
257
|
+
{{ $v.salutationId?.$errors[0].$message }}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
<div v-if="state.option === 'subscribe'" class="col-span-4">
|
|
261
|
+
<label for="first-name">{{ translations.form.firstName }} *</label>
|
|
262
|
+
<input
|
|
263
|
+
id="first-name"
|
|
264
|
+
v-model="state.firstName"
|
|
265
|
+
name="first-name"
|
|
266
|
+
type="text"
|
|
267
|
+
autocomplete="given-name"
|
|
268
|
+
class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
|
|
269
|
+
:class="[
|
|
270
|
+
$v.firstName?.$error
|
|
271
|
+
? 'border-red-600 focus:border-red-600'
|
|
272
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
273
|
+
]"
|
|
274
|
+
:placeholder="translations.form.firstNamePlaceholder"
|
|
275
|
+
@blur="$v.firstName?.$touch()"
|
|
276
|
+
/>
|
|
277
|
+
<span
|
|
278
|
+
v-if="$v.firstName?.$error"
|
|
279
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
280
|
+
>
|
|
281
|
+
{{ $v.firstName?.$errors[0].$message }}
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
<div v-if="state.option === 'subscribe'" class="col-span-4">
|
|
285
|
+
<label for="last-name">{{ translations.form.lastName }} *</label>
|
|
286
|
+
<input
|
|
287
|
+
id="last-name"
|
|
288
|
+
v-model="state.lastName"
|
|
289
|
+
name="last-name"
|
|
290
|
+
type="text"
|
|
291
|
+
autocomplete="family-name"
|
|
292
|
+
class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
|
|
293
|
+
:class="[
|
|
294
|
+
$v.lastName?.$error
|
|
295
|
+
? 'border-red-600 focus:border-red-600'
|
|
296
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
297
|
+
]"
|
|
298
|
+
:placeholder="translations.form.lastNamePlaceholder"
|
|
299
|
+
@blur="$v.lastName?.$touch()"
|
|
300
|
+
/>
|
|
301
|
+
<span
|
|
302
|
+
v-if="$v.lastName?.$error"
|
|
303
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
304
|
+
>
|
|
305
|
+
{{ $v.lastName?.$errors[0].$message }}
|
|
306
|
+
</span>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="col-span-12">
|
|
309
|
+
<label>{{ translations.form.privacy }} *</label>
|
|
310
|
+
<div class="flex gap-3 items-start">
|
|
311
|
+
<input
|
|
312
|
+
id="privacy"
|
|
313
|
+
v-model="state.checkbox"
|
|
314
|
+
name="privacy"
|
|
315
|
+
type="checkbox"
|
|
316
|
+
class="mt-1 focus:ring-indigo-500 h-4 w-4 border text-indigo-600 rounded"
|
|
317
|
+
:class="[
|
|
318
|
+
$v.checkbox?.$error ? 'border-red-600' : 'border-gray-300',
|
|
319
|
+
]"
|
|
320
|
+
/>
|
|
321
|
+
<div>
|
|
322
|
+
<label
|
|
323
|
+
:class="[$v.checkbox?.$error ? 'text-red-600' : '']"
|
|
324
|
+
for="privacy"
|
|
325
|
+
>
|
|
326
|
+
{{ translations.form.privacyLabel }}
|
|
327
|
+
</label>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="flex justify-end mt-10">
|
|
333
|
+
<button
|
|
334
|
+
class="group relative flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-75"
|
|
335
|
+
type="submit"
|
|
336
|
+
>
|
|
337
|
+
{{ translations.form.submit }}
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
</template>
|
|
341
|
+
<template v-else>
|
|
342
|
+
<p class="py-10 text-lg text-center">
|
|
343
|
+
{{ translations.form.newsletterBenefits }}
|
|
344
|
+
</p>
|
|
345
|
+
</template>
|
|
346
|
+
</form>
|
|
347
|
+
</template>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
total: number;
|
|
7
|
+
current: number;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
type Translations = {
|
|
11
|
+
listing: {
|
|
12
|
+
previous: string;
|
|
13
|
+
next: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let translations: Translations = {
|
|
18
|
+
listing: {
|
|
19
|
+
previous: "Previous",
|
|
20
|
+
next: "Next",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
25
|
+
|
|
26
|
+
defineEmits<(e: "changePage", page: number) => void>();
|
|
27
|
+
</script>
|
|
28
|
+
<template>
|
|
29
|
+
<nav
|
|
30
|
+
class="relative z-0 inline-flex rounded-md shadow-sm space-x-px"
|
|
31
|
+
aria-label="Pagination"
|
|
32
|
+
>
|
|
33
|
+
<button
|
|
34
|
+
v-if="current - 1 >= 2"
|
|
35
|
+
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-secondary-300 bg-white text-sm font-medium text-secondary-500 hover:bg-secondary-50"
|
|
36
|
+
@click="$emit('changePage', current - 1)"
|
|
37
|
+
>
|
|
38
|
+
<span class="sr-only">{{ translations.listing.previous }}</span>
|
|
39
|
+
<!-- Heroicon name: solid/chevron-left -->
|
|
40
|
+
<div class="w-5 h-5 i-carbon-chevron-left" />
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
v-if="current > 2"
|
|
44
|
+
class="bg-white border-secondary-300 text-secondary-500 hover:bg-secondary-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium"
|
|
45
|
+
@click="$emit('changePage', 1)"
|
|
46
|
+
>
|
|
47
|
+
<span class="sr-only">Page </span>1
|
|
48
|
+
</button>
|
|
49
|
+
<span
|
|
50
|
+
v-if="current - 1 > 2"
|
|
51
|
+
class="relative inline-flex items-center px-4 py-2 border border-secondary-300 bg-white text-sm font-medium text-secondary-700"
|
|
52
|
+
>
|
|
53
|
+
...
|
|
54
|
+
</span>
|
|
55
|
+
<button
|
|
56
|
+
v-if="current > 1"
|
|
57
|
+
class="bg-white border-secondary-300 text-secondary-500 hover:bg-secondary-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium"
|
|
58
|
+
:class="[current == 2 ? 'rounded-l-md border border-secondary-300' : '']"
|
|
59
|
+
@click="$emit('changePage', current - 1)"
|
|
60
|
+
>
|
|
61
|
+
<span class="sr-only">Page </span>{{ current - 1 }}
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
aria-current="page"
|
|
65
|
+
class="bg-indigo-50 border-indigo-500 text-indigo-600 relative inline-flex items-center px-4 py-2 border text-sm font-medium"
|
|
66
|
+
:class="[
|
|
67
|
+
current - 1 >= 1 ? '' : 'rounded-l-md border border-secondary-300',
|
|
68
|
+
total == current ? 'rounded-r-md border border-secondary-300' : '',
|
|
69
|
+
]"
|
|
70
|
+
>
|
|
71
|
+
<span class="sr-only">Page </span>{{ current }}
|
|
72
|
+
</button>
|
|
73
|
+
<button
|
|
74
|
+
v-if="current < total"
|
|
75
|
+
class="bg-white border-secondary-300 text-secondary-500 hover:bg-secondary-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium"
|
|
76
|
+
:class="[
|
|
77
|
+
total == current + 1 ? 'rounded-r-md border border-secondary-300' : '',
|
|
78
|
+
]"
|
|
79
|
+
@click="$emit('changePage', current + 1)"
|
|
80
|
+
>
|
|
81
|
+
<span class="sr-only">Page </span>{{ current + 1 }}
|
|
82
|
+
</button>
|
|
83
|
+
<span
|
|
84
|
+
v-if="total - current > 2"
|
|
85
|
+
class="relative inline-flex items-center px-4 py-2 border border-secondary-300 bg-white text-sm font-medium text-secondary-700"
|
|
86
|
+
>
|
|
87
|
+
...
|
|
88
|
+
</span>
|
|
89
|
+
<button
|
|
90
|
+
v-if="total - current > 1"
|
|
91
|
+
class="bg-white border-secondary-300 text-secondary-500 hover:bg-secondary-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium"
|
|
92
|
+
@click="$emit('changePage', total)"
|
|
93
|
+
>
|
|
94
|
+
{{ total }}
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
v-if="total > current + 1"
|
|
98
|
+
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-secondary-300 bg-white text-sm font-medium text-secondary-500 hover:bg-secondary-50"
|
|
99
|
+
@click="$emit('changePage', current + 1)"
|
|
100
|
+
>
|
|
101
|
+
<span class="sr-only">{{ translations.listing.next }}</span>
|
|
102
|
+
<!-- Heroicon name: solid/chevron-right -->
|
|
103
|
+
<div class="w-5 h-5 i-carbon-chevron-right" />
|
|
104
|
+
</button>
|
|
105
|
+
</nav>
|
|
106
|
+
</template>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
3
|
+
import { getCmsTranslate } from "@shopware/helpers";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
import { toRefs } from "vue";
|
|
6
|
+
import {
|
|
7
|
+
useAddToCart,
|
|
8
|
+
useCartErrorParamsResolver,
|
|
9
|
+
useCartNotification,
|
|
10
|
+
useNotifications,
|
|
11
|
+
} from "#imports";
|
|
12
|
+
import type { Schemas } from "#shopware";
|
|
13
|
+
|
|
14
|
+
const { pushSuccess, pushError } = useNotifications();
|
|
15
|
+
const { getErrorsCodes } = useCartNotification();
|
|
16
|
+
const { resolveCartError } = useCartErrorParamsResolver();
|
|
17
|
+
const props = defineProps<{
|
|
18
|
+
product: Schemas["Product"];
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
type Translations = {
|
|
22
|
+
product: {
|
|
23
|
+
addedToCart: string;
|
|
24
|
+
qty: string;
|
|
25
|
+
addToCart: string;
|
|
26
|
+
};
|
|
27
|
+
errors: {
|
|
28
|
+
[key: string]: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let translations: Translations = {
|
|
33
|
+
product: {
|
|
34
|
+
addedToCart: "has been added to cart.",
|
|
35
|
+
qty: "Qty",
|
|
36
|
+
addToCart: "Add to cart",
|
|
37
|
+
},
|
|
38
|
+
errors: {
|
|
39
|
+
"product-stock-reached":
|
|
40
|
+
"The product {name} is only available {quantity} times",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
45
|
+
|
|
46
|
+
const { product } = toRefs(props);
|
|
47
|
+
const { addToCart, quantity } = useAddToCart(product);
|
|
48
|
+
|
|
49
|
+
const addToCartProxy = async () => {
|
|
50
|
+
await addToCart();
|
|
51
|
+
const errors = getErrorsCodes();
|
|
52
|
+
for (const element of errors) {
|
|
53
|
+
const { messageKey, params } = resolveCartError(element);
|
|
54
|
+
pushError(getCmsTranslate(translations.errors[messageKey], params));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!errors.length)
|
|
58
|
+
pushSuccess(
|
|
59
|
+
`${props.product?.translated.name} ${translations.product.addedToCart}`,
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<div class="flex flex-row mt-10">
|
|
66
|
+
<div class="basis-1/4 relative -top-6">
|
|
67
|
+
<label for="qty" class="text-sm">{{ translations.product.qty }}</label>
|
|
68
|
+
<input
|
|
69
|
+
id="qty"
|
|
70
|
+
v-model="quantity"
|
|
71
|
+
type="number"
|
|
72
|
+
:min="product.minPurchase || 1"
|
|
73
|
+
:max="product.calculatedMaxPurchase"
|
|
74
|
+
:step="product.purchaseSteps || 1"
|
|
75
|
+
class="border rounded-md py-2 px-4 border-solid border-1 border-cyan-600 w-full mt-4"
|
|
76
|
+
data-testid="product-quantity"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="basis-3/4 ml-4">
|
|
80
|
+
<button
|
|
81
|
+
:disabled="!product.available"
|
|
82
|
+
class="py-2 px-6 w-full mt-4 bg-gradient-to-r from-cyan-500 to-blue-500 transition ease-in-out hover:bg-gradient-to-l duration-300 cursor-pointer border border-transparent rounded-md flex items-center justify-center text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
83
|
+
:class="{
|
|
84
|
+
'opacity-50 cursor-not-allowed': !product.available,
|
|
85
|
+
}"
|
|
86
|
+
data-testid="add-to-cart-button"
|
|
87
|
+
@click="addToCartProxy"
|
|
88
|
+
>
|
|
89
|
+
🛍 {{ translations.product.addToCart }}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|