@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/components/SwCategoryNavigation.vue +44 -0
  4. package/components/SwCategoryNavigationLink.vue +57 -0
  5. package/components/SwContactForm.vue +392 -0
  6. package/components/SwListingProductPrice.vue +88 -0
  7. package/components/SwMedia3D.vue +34 -0
  8. package/components/SwNewsletterForm.vue +347 -0
  9. package/components/SwPagination.vue +106 -0
  10. package/components/SwProductAddToCart.vue +93 -0
  11. package/components/SwProductCard.vue +285 -0
  12. package/components/SwProductGallery.vue +39 -0
  13. package/components/SwProductListingFilter.vue +42 -0
  14. package/components/SwProductListingFilters.vue +292 -0
  15. package/components/SwProductPrice.vue +99 -0
  16. package/components/SwProductReviews.vue +99 -0
  17. package/components/SwProductUnits.vue +54 -0
  18. package/components/SwSharedPrice.vue +19 -0
  19. package/components/SwSlider.vue +328 -0
  20. package/components/SwVariantConfigurator.vue +116 -0
  21. package/components/listing-filters/SwFilterPrice.vue +160 -0
  22. package/components/listing-filters/SwFilterProperties.vue +123 -0
  23. package/components/listing-filters/SwFilterRating.vue +101 -0
  24. package/components/listing-filters/SwFilterShippingFree.vue +104 -0
  25. package/components/public/cms/CmsGenericBlock.md +27 -0
  26. package/components/public/cms/CmsGenericBlock.vue +63 -0
  27. package/components/public/cms/CmsGenericElement.md +31 -0
  28. package/components/public/cms/CmsGenericElement.vue +38 -0
  29. package/components/public/cms/CmsNoComponent.vue +27 -0
  30. package/components/public/cms/CmsPage.md +36 -0
  31. package/components/public/cms/CmsPage.vue +65 -0
  32. package/components/public/cms/block/CmsBlockCategoryNavigation.vue +16 -0
  33. package/components/public/cms/block/CmsBlockCenterText.vue +26 -0
  34. package/components/public/cms/block/CmsBlockCrossSelling.vue +15 -0
  35. package/components/public/cms/block/CmsBlockCustomForm.vue +17 -0
  36. package/components/public/cms/block/CmsBlockDefault.vue +14 -0
  37. package/components/public/cms/block/CmsBlockForm.vue +17 -0
  38. package/components/public/cms/block/CmsBlockGalleryBuybox.vue +25 -0
  39. package/components/public/cms/block/CmsBlockImage.vue +16 -0
  40. package/components/public/cms/block/CmsBlockImageBubbleRow.vue +32 -0
  41. package/components/public/cms/block/CmsBlockImageCover.vue +17 -0
  42. package/components/public/cms/block/CmsBlockImageFourColumn.vue +29 -0
  43. package/components/public/cms/block/CmsBlockImageGallery.vue +18 -0
  44. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +27 -0
  45. package/components/public/cms/block/CmsBlockImageSimpleGrid.vue +24 -0
  46. package/components/public/cms/block/CmsBlockImageSlider.vue +17 -0
  47. package/components/public/cms/block/CmsBlockImageText.vue +19 -0
  48. package/components/public/cms/block/CmsBlockImageTextBubble.vue +51 -0
  49. package/components/public/cms/block/CmsBlockImageTextCover.vue +25 -0
  50. package/components/public/cms/block/CmsBlockImageTextGallery.vue +85 -0
  51. package/components/public/cms/block/CmsBlockImageTextRow.vue +43 -0
  52. package/components/public/cms/block/CmsBlockImageThreeColumn.vue +21 -0
  53. package/components/public/cms/block/CmsBlockImageThreeCover.vue +27 -0
  54. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +25 -0
  55. package/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +15 -0
  56. package/components/public/cms/block/CmsBlockProductHeading.vue +26 -0
  57. package/components/public/cms/block/CmsBlockProductListing.vue +17 -0
  58. package/components/public/cms/block/CmsBlockProductSlider.vue +16 -0
  59. package/components/public/cms/block/CmsBlockProductThreeColumn.vue +22 -0
  60. package/components/public/cms/block/CmsBlockSidebarFilter.vue +17 -0
  61. package/components/public/cms/block/CmsBlockText.vue +15 -0
  62. package/components/public/cms/block/CmsBlockTextHero.vue +15 -0
  63. package/components/public/cms/block/CmsBlockTextOnImage.vue +20 -0
  64. package/components/public/cms/block/CmsBlockTextTeaser.vue +16 -0
  65. package/components/public/cms/block/CmsBlockTextTeaserSection.vue +21 -0
  66. package/components/public/cms/block/CmsBlockTextThreeColumn.vue +22 -0
  67. package/components/public/cms/block/CmsBlockTextTwoColumn.vue +28 -0
  68. package/components/public/cms/block/CmsBlockVimeoVideo.vue +17 -0
  69. package/components/public/cms/block/CmsBlockYoutubeVideo.vue +17 -0
  70. package/components/public/cms/element/CmsElementBuyBox.md +1 -0
  71. package/components/public/cms/element/CmsElementBuyBox.vue +190 -0
  72. package/components/public/cms/element/CmsElementCategoryNavigation.md +1 -0
  73. package/components/public/cms/element/CmsElementCategoryNavigation.vue +167 -0
  74. package/components/public/cms/element/CmsElementCrossSelling.md +1 -0
  75. package/components/public/cms/element/CmsElementCrossSelling.vue +106 -0
  76. package/components/public/cms/element/CmsElementCustomForm.md +1 -0
  77. package/components/public/cms/element/CmsElementCustomForm.vue +27 -0
  78. package/components/public/cms/element/CmsElementForm.md +1 -0
  79. package/components/public/cms/element/CmsElementForm.vue +27 -0
  80. package/components/public/cms/element/CmsElementImage.md +1 -0
  81. package/components/public/cms/element/CmsElementImage.vue +105 -0
  82. package/components/public/cms/element/CmsElementImageGallery.md +1 -0
  83. package/components/public/cms/element/CmsElementImageGallery.vue +249 -0
  84. package/components/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +53 -0
  85. package/components/public/cms/element/CmsElementImageSlider.md +1 -0
  86. package/components/public/cms/element/CmsElementImageSlider.vue +29 -0
  87. package/components/public/cms/element/CmsElementManufacturerLogo.md +1 -0
  88. package/components/public/cms/element/CmsElementManufacturerLogo.vue +11 -0
  89. package/components/public/cms/element/CmsElementProductBox.md +1 -0
  90. package/components/public/cms/element/CmsElementProductBox.vue +14 -0
  91. package/components/public/cms/element/CmsElementProductDescriptionReviews.md +1 -0
  92. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +109 -0
  93. package/components/public/cms/element/CmsElementProductListing.md +1 -0
  94. package/components/public/cms/element/CmsElementProductListing.vue +245 -0
  95. package/components/public/cms/element/CmsElementProductName.md +1 -0
  96. package/components/public/cms/element/CmsElementProductName.vue +10 -0
  97. package/components/public/cms/element/CmsElementProductSlider.md +1 -0
  98. package/components/public/cms/element/CmsElementProductSlider.vue +80 -0
  99. package/components/public/cms/element/CmsElementSidebarFilter.md +1 -0
  100. package/components/public/cms/element/CmsElementSidebarFilter.vue +12 -0
  101. package/components/public/cms/element/CmsElementText.md +1 -0
  102. package/components/public/cms/element/CmsElementText.vue +186 -0
  103. package/components/public/cms/element/CmsElementVimeoVideo.md +1 -0
  104. package/components/public/cms/element/CmsElementVimeoVideo.vue +63 -0
  105. package/components/public/cms/element/CmsElementYoutubeVideo.md +1 -0
  106. package/components/public/cms/element/CmsElementYoutubeVideo.vue +43 -0
  107. package/components/public/cms/section/CmsSectionDefault.md +3 -0
  108. package/components/public/cms/section/CmsSectionDefault.vue +21 -0
  109. package/components/public/cms/section/CmsSectionSidebar.md +3 -0
  110. package/components/public/cms/section/CmsSectionSidebar.vue +49 -0
  111. package/components/public/cms/skeleton/ProductCardSkeleton.vue +44 -0
  112. package/dist/index.d.mts +5 -0
  113. package/dist/index.d.ts +5 -0
  114. package/dist/index.mjs +31 -0
  115. package/helpers/clientOnly.ts +11 -0
  116. package/helpers/html-to-vue/ast.ts +72 -0
  117. package/helpers/html-to-vue/getOptionsFromNode.test.ts +129 -0
  118. package/helpers/html-to-vue/getOptionsFromNode.ts +52 -0
  119. package/helpers/html-to-vue/renderToHtml.ts +45 -0
  120. package/helpers/html-to-vue/renderer.ts +56 -0
  121. package/helpers/media/isSpatial.ts +8 -0
  122. package/index.cjs +7 -0
  123. package/nuxt.config.ts +21 -0
  124. 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://img.shields.io/npm/v/@shopware/cms-base-layer?color=blue&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCA0ODggNTUzIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNNDM5LjA0MSAxMjkuNTkzTDI1OC43NjkgMzEuMzA3NkMyNDQuOTE1IDIzLjc1NDEgMjI4LjExNiAyNC4wMDkzIDIxNC40OTcgMzEuOTgwMkw0Ny4yNjkgMTI5Ljg1OEMzMy40NzYzIDEzNy45MzEgMjUgMTUyLjcxMyAyNSAxNjguNjk1VjM4OC40NjZDMjUgNDA0LjczMiAzMy43Nzg1IDQxOS43MzIgNDcuOTYwMiA0MjcuNjk5TDIxNS4xNzggNTIxLjYzNkMyMjguNDUxIDUyOS4wOTIgMjQ0LjU5MyA1MjkuMzMyIDI1OC4wODIgNTIyLjI3NEw0MzguMzY0IDQyNy45MzRDNDUzLjIwMSA0MjAuMTcgNDYyLjUgNDA0LjgwOSA0NjIuNSAzODguMDYzVjE2OS4xMDJDNDYyLjUgMTUyLjYzMiA0NTMuNTAyIDEzNy40NzcgNDM5LjA0MSAxMjkuNTkzWiIgc3Ryb2tlPSJ1cmwoI3BhaW50MF9saW5lYXJfMTUzXzY5MjY1KSIgc3Ryb2tlLXdpZHRoPSI1MCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzE1M182OTI2NSIgeDE9Ii0xNi4yOTg5IiB5MT0iMTY1LjM0OSIgeDI9IjI3Ni40MTIiIHkyPSItODkuMzIzNCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjMDA4NUZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0MwRTJGNSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPg==)](https://npmjs.com/package/@shopware/cms-base-layer)
4
+ [![](https://img.shields.io/github/package-json/v/shopware/frontends?color=blue&filename=packages%2Fcms-base%2Fpackage.json&label=cms-base%40monorepo&logo=github)](https://github.com/shopware/frontends/tree/main/packages/cms-base)
5
+ [![](https://img.shields.io/github/issues/shopware/frontends/cms-base?label=cms-base%20issues&logo=github)](https://github.com/shopware/frontends/issues?q=is%3Aopen+is%3Aissue+label%3Acms-base)
6
+ [![](https://img.shields.io/github/license/shopware/frontends?color=blue)](#)
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>