@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2023 Shopware
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# shopware/frontends - cms-base
|
|
2
|
+
|
|
3
|
+
[](https://npmjs.com/package/@shopware/cms-base-layer)
|
|
4
|
+
[](https://github.com/shopware/frontends/tree/main/packages/cms-base)
|
|
5
|
+
[](https://github.com/shopware/frontends/issues?q=is%3Aopen+is%3Aissue+label%3Acms-base)
|
|
6
|
+
[](#)
|
|
7
|
+
|
|
8
|
+
Nuxt [layer](https://nuxt.com/docs/getting-started/layers) that provides an implementation of all CMS components in Shopware [based on utility-classes](https://frontends.shopware.com/framework/styling.html) using atomic css syntax (UnoCss / Tailwind).
|
|
9
|
+
|
|
10
|
+
It is useful for projects that want to use the CMS components but design their own layout.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Vue components for [Shopping Experiences](https://www.shopware.com/en/products/shopping-experiences/) CMS
|
|
15
|
+
- CMS sections, blocks and elements styled using [Tailwind CSS](https://tailwindcss.com/) classes
|
|
16
|
+
- 🚀 Empowered by [@shopware/composables](https://www.npmjs.com/package/@shopware/composables)
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Install npm package:
|
|
21
|
+
|
|
22
|
+
<!-- automd:pm-install name="@shopware/cms-base-layer" dev -->
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
# ✨ Auto-detect
|
|
26
|
+
npx nypm install -D @shopware/cms-base-layer
|
|
27
|
+
|
|
28
|
+
# npm
|
|
29
|
+
npm install -D @shopware/cms-base-layer
|
|
30
|
+
|
|
31
|
+
# yarn
|
|
32
|
+
yarn add -D @shopware/cms-base-layer
|
|
33
|
+
|
|
34
|
+
# pnpm
|
|
35
|
+
pnpm install -D @shopware/cms-base-layer
|
|
36
|
+
|
|
37
|
+
# bun
|
|
38
|
+
bun install -D @shopware/cms-base-layer
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
<!-- /automd -->
|
|
42
|
+
|
|
43
|
+
Then, register the Nuxt layer in `nuxt.config.ts` file:
|
|
44
|
+
|
|
45
|
+
<!-- automd:file src="templates/vue-blank/nuxt.config.ts" code -->
|
|
46
|
+
|
|
47
|
+
```ts [nuxt.config.ts]
|
|
48
|
+
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
|
49
|
+
export default defineNuxtConfig({
|
|
50
|
+
extends: ["@shopware/composables/nuxt-layer", "@shopware/cms-base-layer"],
|
|
51
|
+
shopware: {
|
|
52
|
+
endpoint: "https://demo-frontends.shopware.store/store-api/",
|
|
53
|
+
accessToken: "SWSCBHFSNTVMAWNZDNFKSHLAYW",
|
|
54
|
+
},
|
|
55
|
+
modules: ["@shopware/nuxt-module"],
|
|
56
|
+
/**
|
|
57
|
+
* Commented because of the StackBlitz error
|
|
58
|
+
* Issue: https://github.com/shopware/frontends/issues/88
|
|
59
|
+
*/
|
|
60
|
+
typescript: {
|
|
61
|
+
// typeCheck: true,
|
|
62
|
+
strict: true,
|
|
63
|
+
},
|
|
64
|
+
telemetry: false,
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
<!-- /automd -->
|
|
69
|
+
|
|
70
|
+
## Basic usage
|
|
71
|
+
|
|
72
|
+
Since all CMS components are registered in your Nuxt application, you can now start using them in your template (no imports needed):
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
/* Vue component */
|
|
76
|
+
|
|
77
|
+
// response object can be a Product|Category|Landing Page response from Shopware 6 store-api containing a layout (cmsPage object) built using Shopping Experiences
|
|
78
|
+
<template>
|
|
79
|
+
<CmsPage v-if="response.cmsPage" :content="response.cmsPage"/>
|
|
80
|
+
</template>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> You can use default styling by installing/importing Tailwind CSS stylesheet in your project.
|
|
84
|
+
|
|
85
|
+
See a [short guide](https://frontends.shopware.com/getting-started/cms/content-pages.html#use-the-cms-base-package) how to use `cms-base` package in your project based on Nuxt v3.
|
|
86
|
+
|
|
87
|
+
## 📘 Available components
|
|
88
|
+
|
|
89
|
+
The list of available blocks and elements is [here](https://frontends.shopware.com/packages/cms-base.html#available-components).
|
|
90
|
+
|
|
91
|
+
## 🔄 Overwriting components
|
|
92
|
+
|
|
93
|
+
The procedure is:
|
|
94
|
+
|
|
95
|
+
- find a component in component's [list](https://frontends.shopware.com/packages/cms-base.html#available-components), using a [Vue devtools](https://devtools.vuejs.org/) or browsing the github [repository](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer/components)
|
|
96
|
+
- take its name
|
|
97
|
+
- create a file with the same name and place it into `~/components` dir in your nuxt project (or wherever according your nuxt config)
|
|
98
|
+
|
|
99
|
+
✅ Thanks to this, nuxt will take the component registered in your app instead of the one registered by this nuxt layer.
|
|
100
|
+
|
|
101
|
+
### Internal components
|
|
102
|
+
|
|
103
|
+
❗**Internal components are not a part of public API. Once overwritten you need to track the changes on your own.**
|
|
104
|
+
|
|
105
|
+
There is also a possibility to override the internal components, shared between public blocks and elements, the ones starting with `Sw` prefix, like [SwSlider.vue](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/components/SwSlider.vue) or [SwProductCard.vue](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/components/SwProductCard.vue).
|
|
106
|
+
|
|
107
|
+
An example: some components use `SwSharedPrice.vue` to show prices with corresponding currency for products in many places like product card, product details page and so on. In order to change the way how the price is displayed consistently - create a one component with a name `SwSharedPrice.vue` and that's it. The new component will be used everywhere where is "imported" (autoimported actually).
|
|
108
|
+
|
|
109
|
+
### ⚠️ `<RouterLink/>` components used
|
|
110
|
+
|
|
111
|
+
Some components use `RouterLink` component internally, available in [Vue Router](https://github.com/vuejs/router).
|
|
112
|
+
In order to parse CMS components correctly and avoid missing component warning, it's **highly recommended** to have **Vue Router installed** or **Nuxt router enabled** in your application.
|
|
113
|
+
|
|
114
|
+
## TypeScript support
|
|
115
|
+
|
|
116
|
+
All components are fully typed with TypeScript.
|
|
117
|
+
|
|
118
|
+
No additional packages needed to be installed.
|
|
119
|
+
|
|
120
|
+
## Links
|
|
121
|
+
|
|
122
|
+
- [📘 Documentation](https://frontends.shopware.com)
|
|
123
|
+
|
|
124
|
+
- [👥 Community](https://shopwarecommunity.slack.com) (`#composable-frontends`)
|
|
125
|
+
|
|
126
|
+
<!-- AUTO GENERATED CHANGELOG -->
|
|
127
|
+
|
|
128
|
+
## Changelog
|
|
129
|
+
|
|
130
|
+
Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
|
|
131
|
+
|
|
132
|
+
### Latest changes: 0.0.0-canary-20250116171244
|
|
133
|
+
|
|
134
|
+
### Minor Changes
|
|
135
|
+
|
|
136
|
+
- [#1602](https://github.com/shopware/frontends/pull/1602) [`bb7d1cb`](https://github.com/shopware/frontends/commit/bb7d1cbc4204ff1d48f77416f94f550bc235e5ed) Thanks [@patzick](https://github.com/patzick)! - Switch from `@shopware-pwa/helpers-next` to `@shopware/helpers` package.
|
|
137
|
+
|
|
138
|
+
- [#1602](https://github.com/shopware/frontends/pull/1602) [`bb7d1cb`](https://github.com/shopware/frontends/commit/bb7d1cbc4204ff1d48f77416f94f550bc235e5ed) Thanks [@patzick](https://github.com/patzick)! - Switch from `@shopware-pwa/cms-base` to `@shopware/cms-base-layer` package.
|
|
139
|
+
|
|
140
|
+
### Patch Changes
|
|
141
|
+
|
|
142
|
+
- Updated dependencies [[`bb7d1cb`](https://github.com/shopware/frontends/commit/bb7d1cbc4204ff1d48f77416f94f550bc235e5ed), [`bb7d1cb`](https://github.com/shopware/frontends/commit/bb7d1cbc4204ff1d48f77416f94f550bc235e5ed)]:
|
|
143
|
+
- @shopware/composables@0.0.0-canary-20250116171244
|
|
144
|
+
- @shopware/helpers@0.0.0-canary-20250116171244
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
activeCategory: Schemas["Category"];
|
|
7
|
+
elements: Schemas["Category"][];
|
|
8
|
+
level: number;
|
|
9
|
+
}>(),
|
|
10
|
+
{
|
|
11
|
+
level: 0,
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
function getHighlightCategory(navigationElement: Schemas["Category"]) {
|
|
16
|
+
return (
|
|
17
|
+
(props.activeCategory?.path || "").includes(navigationElement.id) ||
|
|
18
|
+
navigationElement.id === props.activeCategory?.id
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
<template>
|
|
23
|
+
<ul v-if="props.elements?.length" class="list-none m-0 px-5">
|
|
24
|
+
<li
|
|
25
|
+
v-for="(navigationElement, index) in props.elements"
|
|
26
|
+
:key="index"
|
|
27
|
+
:class="{
|
|
28
|
+
'border-b border-gray-200': props.level === 0,
|
|
29
|
+
}"
|
|
30
|
+
>
|
|
31
|
+
<SwCategoryNavigationLink
|
|
32
|
+
:navigation-element="navigationElement"
|
|
33
|
+
:is-highlighted="getHighlightCategory(navigationElement)"
|
|
34
|
+
:is-active="navigationElement.id === props.activeCategory?.id"
|
|
35
|
+
/>
|
|
36
|
+
<SwCategoryNavigation
|
|
37
|
+
v-if="navigationElement.children"
|
|
38
|
+
:elements="navigationElement.children"
|
|
39
|
+
:active-category="props.activeCategory"
|
|
40
|
+
:level="props.level + 1"
|
|
41
|
+
/>
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</template>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
buildUrlPrefix,
|
|
4
|
+
getCategoryRoute,
|
|
5
|
+
getTranslatedProperty,
|
|
6
|
+
urlIsAbsolute,
|
|
7
|
+
} from "@shopware/helpers";
|
|
8
|
+
import { computed } from "vue";
|
|
9
|
+
import { RouterLink } from "vue-router";
|
|
10
|
+
import { useUrlResolver } from "#imports";
|
|
11
|
+
import type { Schemas } from "#shopware";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
navigationElement: Schemas["Category"];
|
|
15
|
+
isActive?: boolean;
|
|
16
|
+
isHighlighted?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = defineProps<Props>();
|
|
20
|
+
const { getUrlPrefix } = useUrlResolver();
|
|
21
|
+
const url = computed(() => {
|
|
22
|
+
return buildUrlPrefix(
|
|
23
|
+
getCategoryRoute(props.navigationElement),
|
|
24
|
+
getUrlPrefix(),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
<template>
|
|
29
|
+
<div
|
|
30
|
+
class="flex items-center py-2 px-5 text-base rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 my-2"
|
|
31
|
+
>
|
|
32
|
+
<RouterLink
|
|
33
|
+
v-if="!urlIsAbsolute(url.path)"
|
|
34
|
+
:to="url"
|
|
35
|
+
:class="[
|
|
36
|
+
props.isHighlighted ? 'font-bold' : 'font-normal',
|
|
37
|
+
props.isActive ? 'text-indigo-600' : 'text-gray-900',
|
|
38
|
+
]"
|
|
39
|
+
>
|
|
40
|
+
<span>{{ getTranslatedProperty(navigationElement, "name") }}</span>
|
|
41
|
+
</RouterLink>
|
|
42
|
+
<a
|
|
43
|
+
v-else
|
|
44
|
+
:href="url.path"
|
|
45
|
+
:class="[
|
|
46
|
+
props.isHighlighted ? 'font-bold' : 'font-normal',
|
|
47
|
+
props.isActive ? 'text-indigo-600' : 'text-gray-900',
|
|
48
|
+
]"
|
|
49
|
+
:target="
|
|
50
|
+
navigationElement.externalLink || navigationElement.linkNewTab
|
|
51
|
+
? '_blank'
|
|
52
|
+
: ''
|
|
53
|
+
"
|
|
54
|
+
><span>{{ getTranslatedProperty(navigationElement, "name") }}</span></a
|
|
55
|
+
>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,392 @@
|
|
|
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 { email, minLength, required } from "@vuelidate/validators";
|
|
8
|
+
import { defu } from "defu";
|
|
9
|
+
import { computed, reactive, ref } from "vue";
|
|
10
|
+
import {
|
|
11
|
+
useCmsElementConfig,
|
|
12
|
+
useNavigationContext,
|
|
13
|
+
useSalutations,
|
|
14
|
+
useShopwareContext,
|
|
15
|
+
} from "#imports";
|
|
16
|
+
|
|
17
|
+
const props = defineProps<{
|
|
18
|
+
content: CmsElementForm;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
type Translations = {
|
|
22
|
+
form: {
|
|
23
|
+
salutation: string;
|
|
24
|
+
salutationPlaceholder: string;
|
|
25
|
+
firstName: string;
|
|
26
|
+
firstNamePlaceholder: string;
|
|
27
|
+
lastName: string;
|
|
28
|
+
lastNamePlaceholder: string;
|
|
29
|
+
email: string;
|
|
30
|
+
emailPlaceholder: string;
|
|
31
|
+
phone: string;
|
|
32
|
+
phonePlaceholder: string;
|
|
33
|
+
subject: string;
|
|
34
|
+
subjectPlaceholder: string;
|
|
35
|
+
comment: string;
|
|
36
|
+
commentPlaceholder: string;
|
|
37
|
+
privacy: string;
|
|
38
|
+
dataProtection: string;
|
|
39
|
+
submit: string;
|
|
40
|
+
messages: {
|
|
41
|
+
contactFormSuccess: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let translations: Translations = {
|
|
47
|
+
form: {
|
|
48
|
+
salutation: "Salutation",
|
|
49
|
+
salutationPlaceholder: "Enter salutation...",
|
|
50
|
+
firstName: "First name",
|
|
51
|
+
firstNamePlaceholder: "Enter first name...",
|
|
52
|
+
lastName: "Last name",
|
|
53
|
+
lastNamePlaceholder: "Enter last name...",
|
|
54
|
+
email: "Email address",
|
|
55
|
+
emailPlaceholder: "Enter email address...",
|
|
56
|
+
phone: "Phone number",
|
|
57
|
+
phonePlaceholder: "Enter phone number...",
|
|
58
|
+
subject: "Subject",
|
|
59
|
+
subjectPlaceholder: "Enter subject...",
|
|
60
|
+
comment: "Comment",
|
|
61
|
+
commentPlaceholder: "Enter comment...",
|
|
62
|
+
privacy: "Privacy",
|
|
63
|
+
dataProtection: "I have read the data protection information.",
|
|
64
|
+
submit: "Submit",
|
|
65
|
+
messages: {
|
|
66
|
+
contactFormSuccess:
|
|
67
|
+
"We have received your contact request and will process it as soon as possible.",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
73
|
+
|
|
74
|
+
const loading = ref<boolean>();
|
|
75
|
+
const formSent = ref<boolean>(false);
|
|
76
|
+
|
|
77
|
+
type ErrorMessages = ApiClientError<{
|
|
78
|
+
errors: ApiError[];
|
|
79
|
+
}>;
|
|
80
|
+
|
|
81
|
+
const errorMessages = ref<ErrorMessages[]>([]);
|
|
82
|
+
|
|
83
|
+
const { getSalutations } = useSalutations();
|
|
84
|
+
const { foreignKey } = useNavigationContext();
|
|
85
|
+
const { apiClient } = useShopwareContext();
|
|
86
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
87
|
+
|
|
88
|
+
const getConfirmationText = computed(
|
|
89
|
+
() =>
|
|
90
|
+
getConfigValue("confirmationText") ??
|
|
91
|
+
translations.form.messages.contactFormSuccess,
|
|
92
|
+
);
|
|
93
|
+
const getFormTitle = computed(() => getConfigValue("title") || "Contact");
|
|
94
|
+
const state = reactive({
|
|
95
|
+
salutationId: "",
|
|
96
|
+
firstName: "",
|
|
97
|
+
lastName: "",
|
|
98
|
+
email: "",
|
|
99
|
+
subject: "",
|
|
100
|
+
comment: "",
|
|
101
|
+
phone: "",
|
|
102
|
+
checkbox: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const rules = computed(() => ({
|
|
106
|
+
email: {
|
|
107
|
+
required,
|
|
108
|
+
email,
|
|
109
|
+
},
|
|
110
|
+
firstName: {
|
|
111
|
+
required,
|
|
112
|
+
minLength: minLength(3),
|
|
113
|
+
},
|
|
114
|
+
lastName: {
|
|
115
|
+
required,
|
|
116
|
+
minLength: minLength(3),
|
|
117
|
+
},
|
|
118
|
+
salutationId: {
|
|
119
|
+
required,
|
|
120
|
+
},
|
|
121
|
+
phone: {
|
|
122
|
+
required,
|
|
123
|
+
minLength: minLength(3),
|
|
124
|
+
},
|
|
125
|
+
subject: {
|
|
126
|
+
required,
|
|
127
|
+
minLength: minLength(3),
|
|
128
|
+
},
|
|
129
|
+
comment: {
|
|
130
|
+
required,
|
|
131
|
+
minLength: minLength(10),
|
|
132
|
+
},
|
|
133
|
+
checkbox: {
|
|
134
|
+
required,
|
|
135
|
+
isTrue: (value: boolean) => value === true,
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
const $v = useVuelidate(rules, state);
|
|
140
|
+
const invokeSubmit = async () => {
|
|
141
|
+
$v.value.$touch();
|
|
142
|
+
const valid = await $v.value.$validate();
|
|
143
|
+
if (valid) {
|
|
144
|
+
loading.value = true;
|
|
145
|
+
try {
|
|
146
|
+
await apiClient.invoke("sendContactMail post /contact-form", {
|
|
147
|
+
body: {
|
|
148
|
+
...state,
|
|
149
|
+
navigationId: foreignKey.value,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
formSent.value = true;
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (e instanceof ApiClientError) {
|
|
155
|
+
errorMessages.value = e.details.errors;
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
loading.value = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
</script>
|
|
163
|
+
<template>
|
|
164
|
+
<form class="w-full relative" @submit.prevent="invokeSubmit">
|
|
165
|
+
<div
|
|
166
|
+
v-if="loading"
|
|
167
|
+
class="absolute inset-0 flex items-center justify-center z-10 bg-white/50"
|
|
168
|
+
>
|
|
169
|
+
<div
|
|
170
|
+
class="h-15 w-15 i-carbon-progress-bar-round animate-spin c-gray-500"
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
<h3 class="pb-3 mb-10 border-b border-gray-300">
|
|
174
|
+
{{ getFormTitle }}
|
|
175
|
+
</h3>
|
|
176
|
+
<template v-if="!formSent">
|
|
177
|
+
<div class="grid grid-cols-12 gap-5">
|
|
178
|
+
<div class="col-span-4">
|
|
179
|
+
<label for="salutation">{{ translations.form.salutation }} *</label>
|
|
180
|
+
<select
|
|
181
|
+
id="salutation"
|
|
182
|
+
v-model="state.salutationId"
|
|
183
|
+
name="salutation"
|
|
184
|
+
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"
|
|
185
|
+
:class="[
|
|
186
|
+
$v.salutationId.$error
|
|
187
|
+
? 'border-red-600 focus:border-red-600'
|
|
188
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
189
|
+
]"
|
|
190
|
+
@blur="$v.salutationId.$touch()"
|
|
191
|
+
>
|
|
192
|
+
<option disabled selected value="">
|
|
193
|
+
{{ translations.form.salutationPlaceholder }}
|
|
194
|
+
</option>
|
|
195
|
+
<option
|
|
196
|
+
v-for="salutation in getSalutations"
|
|
197
|
+
:key="salutation.id"
|
|
198
|
+
:value="salutation.id"
|
|
199
|
+
>
|
|
200
|
+
{{ salutation.displayName }}
|
|
201
|
+
</option>
|
|
202
|
+
</select>
|
|
203
|
+
<span
|
|
204
|
+
v-if="$v.salutationId.$error"
|
|
205
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
206
|
+
>
|
|
207
|
+
{{ $v.salutationId.$errors[0].$message }}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="col-span-4">
|
|
211
|
+
<label for="first-name">{{ translations.form.firstName }} *</label>
|
|
212
|
+
<input
|
|
213
|
+
id="first-name"
|
|
214
|
+
v-model="state.firstName"
|
|
215
|
+
name="first-name"
|
|
216
|
+
type="text"
|
|
217
|
+
autocomplete="given-name"
|
|
218
|
+
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"
|
|
219
|
+
:class="[
|
|
220
|
+
$v.firstName.$error
|
|
221
|
+
? 'border-red-600 focus:border-red-600'
|
|
222
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
223
|
+
]"
|
|
224
|
+
:placeholder="translations.form.firstNamePlaceholder"
|
|
225
|
+
@blur="$v.firstName.$touch()"
|
|
226
|
+
/>
|
|
227
|
+
<span
|
|
228
|
+
v-if="$v.firstName.$error"
|
|
229
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
230
|
+
>
|
|
231
|
+
{{ $v.firstName.$errors[0].$message }}
|
|
232
|
+
</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="col-span-4">
|
|
235
|
+
<label for="last-name">{{ translations.form.lastName }} *</label>
|
|
236
|
+
<input
|
|
237
|
+
id="last-name"
|
|
238
|
+
v-model="state.lastName"
|
|
239
|
+
name="last-name"
|
|
240
|
+
type="text"
|
|
241
|
+
autocomplete="family-name"
|
|
242
|
+
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"
|
|
243
|
+
:class="[
|
|
244
|
+
$v.lastName.$error
|
|
245
|
+
? 'border-red-600 focus:border-red-600'
|
|
246
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
247
|
+
]"
|
|
248
|
+
:placeholder="translations.form.lastNamePlaceholder"
|
|
249
|
+
@blur="$v.lastName.$touch()"
|
|
250
|
+
/>
|
|
251
|
+
<span
|
|
252
|
+
v-if="$v.lastName.$error"
|
|
253
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
254
|
+
>
|
|
255
|
+
{{ $v.lastName.$errors[0].$message }}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="col-span-6">
|
|
259
|
+
<label for="email-address">{{ translations.form.email }} *</label>
|
|
260
|
+
<input
|
|
261
|
+
id="email-address"
|
|
262
|
+
v-model="state.email"
|
|
263
|
+
name="email"
|
|
264
|
+
type="email"
|
|
265
|
+
autocomplete="email"
|
|
266
|
+
:class="[
|
|
267
|
+
$v.email.$error
|
|
268
|
+
? 'border-red-600 focus:border-red-600'
|
|
269
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
270
|
+
]"
|
|
271
|
+
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"
|
|
272
|
+
:placeholder="translations.form.emailPlaceholder"
|
|
273
|
+
@blur="$v.email.$touch()"
|
|
274
|
+
/>
|
|
275
|
+
<span
|
|
276
|
+
v-if="$v.email.$error"
|
|
277
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
278
|
+
>
|
|
279
|
+
{{ $v.email.$errors[0].$message }}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
<div class="col-span-6">
|
|
283
|
+
<label for="phone">{{ translations.form.phone }} *</label>
|
|
284
|
+
<input
|
|
285
|
+
id="phone"
|
|
286
|
+
v-model="state.phone"
|
|
287
|
+
name="phone"
|
|
288
|
+
type="text"
|
|
289
|
+
autocomplete="phone"
|
|
290
|
+
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"
|
|
291
|
+
:class="[
|
|
292
|
+
$v.phone.$error
|
|
293
|
+
? 'border-red-600 focus:border-red-600'
|
|
294
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
295
|
+
]"
|
|
296
|
+
:placeholder="translations.form.phonePlaceholder"
|
|
297
|
+
@blur="$v.phone.$touch()"
|
|
298
|
+
/>
|
|
299
|
+
<span
|
|
300
|
+
v-if="$v.phone.$error"
|
|
301
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
302
|
+
>
|
|
303
|
+
{{ $v.phone.$errors[0].$message }}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="col-span-12">
|
|
307
|
+
<label for="subject">{{ translations.form.subject }} *</label>
|
|
308
|
+
<input
|
|
309
|
+
id="subject"
|
|
310
|
+
v-model="state.subject"
|
|
311
|
+
name="subject"
|
|
312
|
+
type="text"
|
|
313
|
+
autocomplete="subject"
|
|
314
|
+
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"
|
|
315
|
+
:class="[
|
|
316
|
+
$v.subject.$error
|
|
317
|
+
? 'border-red-600 focus:border-red-600'
|
|
318
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
319
|
+
]"
|
|
320
|
+
:placeholder="translations.form.subjectPlaceholder"
|
|
321
|
+
@blur="$v.subject.$touch()"
|
|
322
|
+
/>
|
|
323
|
+
<span
|
|
324
|
+
v-if="$v.subject.$error"
|
|
325
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
326
|
+
>
|
|
327
|
+
{{ $v.subject.$errors[0].$message }}
|
|
328
|
+
</span>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="col-span-12">
|
|
331
|
+
<label for="comment">{{ translations.form.comment }} *</label>
|
|
332
|
+
<textarea
|
|
333
|
+
id="comment"
|
|
334
|
+
v-model="state.comment"
|
|
335
|
+
name="comment"
|
|
336
|
+
type="text"
|
|
337
|
+
autocomplete="comment"
|
|
338
|
+
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"
|
|
339
|
+
:class="[
|
|
340
|
+
$v.comment.$error
|
|
341
|
+
? 'border-red-600 focus:border-red-600'
|
|
342
|
+
: 'border-gray-300 focus:border-indigo-500',
|
|
343
|
+
]"
|
|
344
|
+
:placeholder="translations.form.commentPlaceholder"
|
|
345
|
+
rows="5"
|
|
346
|
+
@blur="$v.comment.$touch()"
|
|
347
|
+
/>
|
|
348
|
+
<span
|
|
349
|
+
v-if="$v.comment.$error"
|
|
350
|
+
class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
|
|
351
|
+
>
|
|
352
|
+
{{ $v.comment.$errors[0].$message }}
|
|
353
|
+
</span>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="col-span-12">
|
|
356
|
+
<label>{{ translations.form.privacy }} *</label>
|
|
357
|
+
<div class="flex gap-3 items-start">
|
|
358
|
+
<input
|
|
359
|
+
id="privacy"
|
|
360
|
+
v-model="state.checkbox"
|
|
361
|
+
name="privacy"
|
|
362
|
+
type="checkbox"
|
|
363
|
+
class="mt-1 focus:ring-indigo-500 h-4 w-4 border text-indigo-600 rounded"
|
|
364
|
+
:class="[
|
|
365
|
+
$v.checkbox.$error ? 'border-red-600' : 'border-gray-300',
|
|
366
|
+
]"
|
|
367
|
+
/>
|
|
368
|
+
<div>
|
|
369
|
+
<label
|
|
370
|
+
:class="[$v.checkbox.$error ? 'text-red-600' : '']"
|
|
371
|
+
for="privacy"
|
|
372
|
+
>
|
|
373
|
+
{{ translations.form.dataProtection }}
|
|
374
|
+
</label>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="flex justify-end mt-10">
|
|
380
|
+
<button
|
|
381
|
+
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"
|
|
382
|
+
type="submit"
|
|
383
|
+
>
|
|
384
|
+
{{ translations.form.submit }}
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
</template>
|
|
388
|
+
<template v-else>
|
|
389
|
+
<p class="py-10 text-lg text-center">{{ getConfirmationText }}</p>
|
|
390
|
+
</template>
|
|
391
|
+
</form>
|
|
392
|
+
</template>
|