@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
@@ -0,0 +1,116 @@
1
+ <script setup lang="ts">
2
+ import { useCmsTranslations } from "@shopware/composables";
3
+ import { getProductRoute } from "@shopware/helpers";
4
+ import { defu } from "defu";
5
+ import { computed, ref, unref } from "vue";
6
+ import type { ComputedRef } from "vue";
7
+ import { useRouter } from "vue-router";
8
+ import { useProductConfigurator } from "#imports";
9
+ import type { Schemas } from "#shopware";
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ allowRedirect?: boolean;
14
+ }>(),
15
+ {
16
+ allowRedirect: true,
17
+ },
18
+ );
19
+
20
+ type Translations = {
21
+ product: {
22
+ chooseA: string;
23
+ };
24
+ };
25
+
26
+ let translations: Translations = {
27
+ product: {
28
+ chooseA: "Choose a",
29
+ },
30
+ };
31
+
32
+ translations = defu(useCmsTranslations(), translations) as Translations;
33
+
34
+ const emit =
35
+ defineEmits<
36
+ (e: "change", selected: Partial<Schemas["Product"]> | undefined) => void
37
+ >();
38
+ const isLoading = ref<boolean>();
39
+ const router = useRouter();
40
+ const {
41
+ handleChange,
42
+ getOptionGroups,
43
+ getSelectedOptions,
44
+ findVariantForSelectedOptions,
45
+ } = useProductConfigurator();
46
+
47
+ const selectedOptions: ComputedRef = computed(() =>
48
+ Object.values(unref(getSelectedOptions)),
49
+ );
50
+ const isOptionSelected = (optionId: string) =>
51
+ Object.values(getSelectedOptions.value).includes(optionId);
52
+
53
+ const onHandleChange = async () => {
54
+ isLoading.value = true;
55
+ const variantFound = await findVariantForSelectedOptions(
56
+ unref(selectedOptions),
57
+ );
58
+
59
+ const selectedOptionsVariantPath = getProductRoute(variantFound);
60
+ if (props.allowRedirect && selectedOptionsVariantPath) {
61
+ try {
62
+ router.push(selectedOptionsVariantPath);
63
+ } catch {
64
+ console.error("incorrect URL", selectedOptionsVariantPath);
65
+ }
66
+ } else {
67
+ emit("change", variantFound);
68
+ }
69
+ isLoading.value = false;
70
+ };
71
+ </script>
72
+
73
+ <template>
74
+ <div class="flex flex-col">
75
+ <div
76
+ v-if="isLoading"
77
+ class="absolute inset-0 flex items-center justify-center z-10 bg-white/75"
78
+ >
79
+ <div
80
+ data-testid="loading"
81
+ class="h-15 w-15 i-carbon-progress-bar-round animate-spin c-gray-500"
82
+ />
83
+ </div>
84
+ <div
85
+ v-for="optionGroup in getOptionGroups"
86
+ :key="optionGroup.id"
87
+ class="mt-6"
88
+ >
89
+ <h3 class="text-sm text-gray-900 font-medium">{{ optionGroup.name }}</h3>
90
+ <fieldset class="mt-4 flex-1">
91
+ <legend class="sr-only">
92
+ {{ translations.product.chooseA }} {{ optionGroup.name }}
93
+ </legend>
94
+ <div class="flex gap-3">
95
+ <label
96
+ v-for="option in optionGroup.options"
97
+ :key="option.id"
98
+ data-testid="product-variant"
99
+ class="group relative border rounded-md py-3 px-4 flex items-center justify-center text-sm font-medium uppercase hover:bg-gray-50 focus:outline-none sm:flex-1 bg-white shadow-sm text-gray-900 cursor-pointer"
100
+ :class="{
101
+ 'border-3 border-indigo-600': isOptionSelected(option.id),
102
+ }"
103
+ @click="handleChange(optionGroup.name, option.id, onHandleChange)"
104
+ >
105
+ <p
106
+ :id="`${option.id}-choice-label`"
107
+ data-testid="product-variant-text"
108
+ >
109
+ {{ option.name }}
110
+ </p>
111
+ </label>
112
+ </div>
113
+ </fieldset>
114
+ </div>
115
+ </div>
116
+ </template>
@@ -0,0 +1,160 @@
1
+ <script
2
+ setup
3
+ lang="ts"
4
+ generic="
5
+ ListingFilter extends {
6
+ code: string;
7
+ min?: number;
8
+ max?: number;
9
+ label: string;
10
+ }
11
+ "
12
+ >
13
+ import { useCmsTranslations } from "@shopware/composables";
14
+ import { onClickOutside, useDebounceFn } from "@vueuse/core";
15
+ import { defu } from "defu";
16
+ import { onMounted, reactive, ref, watch } from "vue";
17
+ import type { Schemas } from "#shopware";
18
+
19
+ const emits =
20
+ defineEmits<
21
+ (e: "select-value", value: { code: string; value: unknown }) => void
22
+ >();
23
+
24
+ const props = defineProps<{
25
+ filter: ListingFilter;
26
+ selectedFilters: Schemas["ProductListingResult"]["currentFilters"];
27
+ }>();
28
+
29
+ type Translations = {
30
+ listing: {
31
+ min: string;
32
+ max: string;
33
+ };
34
+ };
35
+ let translations: Translations = {
36
+ listing: {
37
+ min: "Min",
38
+ max: "Max",
39
+ },
40
+ };
41
+ translations = defu(useCmsTranslations(), translations) as Translations;
42
+
43
+ const prices = reactive<{ min: number; max: number }>({
44
+ min: 0,
45
+ max: 0,
46
+ });
47
+
48
+ onMounted(() => {
49
+ prices.min = Math.floor(
50
+ props.selectedFilters.price.min ?? props.filter?.min ?? 0,
51
+ );
52
+ prices.max = Math.floor(
53
+ props.selectedFilters.price.max ?? props.filter?.max ?? 0,
54
+ );
55
+ });
56
+
57
+ const isFilterVisible = ref<boolean>(false);
58
+ const toggle = () => {
59
+ isFilterVisible.value = !isFilterVisible.value;
60
+ };
61
+
62
+ const dropdownElement = ref(null);
63
+ onClickOutside(dropdownElement, () => {
64
+ isFilterVisible.value = false;
65
+ });
66
+
67
+ function onMinPriceChange(newPrice: number, oldPrice: number) {
68
+ if (newPrice === oldPrice || oldPrice === 0) return;
69
+ emits("select-value", {
70
+ code: "min-price",
71
+ value: newPrice,
72
+ });
73
+ }
74
+ const debounceMinPriceUpdate = useDebounceFn(onMinPriceChange, 500);
75
+ watch(() => prices.min, debounceMinPriceUpdate);
76
+
77
+ function onMaxPriceChange(newPrice: number, oldPrice: number) {
78
+ if (newPrice === oldPrice || oldPrice === 0) return;
79
+ emits("select-value", {
80
+ code: "max-price",
81
+ value: newPrice,
82
+ });
83
+ }
84
+ const debounceMaxPriceUpdate = useDebounceFn(onMaxPriceChange, 500);
85
+ watch(() => prices.max, debounceMaxPriceUpdate);
86
+ </script>
87
+
88
+ <template>
89
+ <div class="border-b border-gray-200 py-6 px-5">
90
+ <h3 class="-my-3 flow-root">
91
+ <button
92
+ type="button"
93
+ class="flex w-full items-center justify-between bg-white py-2 text-base text-gray-400 hover:text-gray-500"
94
+ @click="toggle"
95
+ >
96
+ <span class="font-medium text-gray-900 text-left">{{
97
+ props.filter.label
98
+ }}</span>
99
+ <span class="ml-6 flex items-center">
100
+ <i
101
+ :class="[
102
+ !isFilterVisible
103
+ ? 'i-carbon-chevron-down'
104
+ : 'i-carbon-chevron-up',
105
+ ]"
106
+ />
107
+ </span>
108
+ </button>
109
+ </h3>
110
+
111
+ <transition name="fade" mode="out-in">
112
+ <div v-show="isFilterVisible" class="space-y-6 mt-5">
113
+ <div class="mt-2 flex">
114
+ <div class="w-1/2 flex rounded-md mr-4">
115
+ <span
116
+ class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm"
117
+ >
118
+ {{ translations.listing.min }}
119
+ </span>
120
+ <input
121
+ id="min-price"
122
+ v-model="prices.min"
123
+ step="1"
124
+ type="number"
125
+ name="min-price"
126
+ class="pl-2 focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border border-gray-300"
127
+ :placeholder="prices.min?.toString()"
128
+ />
129
+ </div>
130
+ <div class="w-1/2 flex rounded-md">
131
+ <span
132
+ class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm"
133
+ >
134
+ {{ translations.listing.max }}
135
+ </span>
136
+ <input
137
+ id="max-price"
138
+ v-model="prices.max"
139
+ type="number"
140
+ name="max-price"
141
+ class="pl-2 focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border border-gray-300"
142
+ :placeholder="prices.max?.toString()"
143
+ />
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </transition>
148
+ </div>
149
+ </template>
150
+ <style scoped>
151
+ .fade-enter-active,
152
+ .fade-leave-active {
153
+ transition: all 0.2s ease;
154
+ }
155
+
156
+ .fade-enter-from,
157
+ .fade-leave-to {
158
+ opacity: 0;
159
+ }
160
+ </style>
@@ -0,0 +1,123 @@
1
+ <script
2
+ setup
3
+ lang="ts"
4
+ generic="
5
+ ListingFilter extends {
6
+ code: string;
7
+ label: string;
8
+ name: string;
9
+ options: Array<Schemas['PropertyGroupOption']>;
10
+ entities: Array<Schemas['ProductManufacturer']>;
11
+ }
12
+ "
13
+ >
14
+ import { getTranslatedProperty } from "@shopware/helpers";
15
+ import { inject, ref } from "vue";
16
+ import type { Schemas } from "#shopware";
17
+
18
+ const props = defineProps<{
19
+ filter: ListingFilter;
20
+ }>();
21
+
22
+ const emits =
23
+ defineEmits<
24
+ (e: "select-value", value: { code: string; value: unknown }) => void
25
+ >();
26
+ const selectedOptionIds = inject<string[]>("selectedOptionIds");
27
+ const isFilterVisible = ref<boolean>(false);
28
+ const toggle = () => {
29
+ isFilterVisible.value = !isFilterVisible.value;
30
+ };
31
+ </script>
32
+
33
+ <template>
34
+ <div class="border-b border-gray-200 py-6 px-5">
35
+ <h2 class="-my-3 flow-root">
36
+ <button
37
+ type="button"
38
+ class="flex w-full items-center justify-between bg-white py-2 text-base text-gray-400 hover:text-gray-500"
39
+ @click="toggle"
40
+ >
41
+ <span class="font-medium text-gray-900 text-left">{{
42
+ props.filter.label
43
+ }}</span>
44
+ <span class="ml-6 flex items-center">
45
+ <i
46
+ :class="[
47
+ !isFilterVisible
48
+ ? 'i-carbon-chevron-down'
49
+ : 'i-carbon-chevron-up',
50
+ ]"
51
+ />
52
+ </span>
53
+ </button>
54
+ </h2>
55
+ <transition name="fade" mode="out-in">
56
+ <div v-show="isFilterVisible" :id="props.filter.code" class="pt-6">
57
+ <fieldset class="space-y-4">
58
+ <legend class="sr-only">{{ props.filter.name }}</legend>
59
+ <div
60
+ v-for="option in props.filter.options || props.filter.entities"
61
+ :key="`${option.id}-${selectedOptionIds?.includes(option.id)}`"
62
+ class="flex items-center"
63
+ >
64
+ <input
65
+ :id="`filter-mobile-${props.filter.code}-${option.id}`"
66
+ :checked="selectedOptionIds?.includes(option.id)"
67
+ :name="props.filter.name"
68
+ :value="option.name"
69
+ :aria-label="`${option.name} filter`"
70
+ type="checkbox"
71
+ class="h-4 w-4 border-gray-300 rounded text-indigo-600 focus:ring-indigo-500"
72
+ @change="
73
+ emits('select-value', {
74
+ code: props.filter.code,
75
+ value: option.id,
76
+ })
77
+ "
78
+ />
79
+
80
+ <div v-if="option.media?.url">
81
+ <img
82
+ loading="lazy"
83
+ class="ml-2 h-4 w-4"
84
+ :src="option.media.url"
85
+ :alt="option.media.translated.alt || ''"
86
+ :class="{
87
+ 'border-blue border-2': selectedOptionIds?.includes(
88
+ option.id,
89
+ ),
90
+ }"
91
+ />
92
+ </div>
93
+ <div
94
+ v-else-if="option.colorHexCode"
95
+ class="ml-2 h-4 w-4"
96
+ :style="`background-color: ${option.colorHexCode}`"
97
+ :class="{
98
+ 'border-blue border-2': selectedOptionIds?.includes(option.id),
99
+ }"
100
+ />
101
+ <label
102
+ :for="`filter-mobile-${props.filter.code}-${option.id}`"
103
+ class="ml-3 text-gray-600"
104
+ >
105
+ {{ getTranslatedProperty(option, "name") }}
106
+ </label>
107
+ </div>
108
+ </fieldset>
109
+ </div>
110
+ </transition>
111
+ </div>
112
+ </template>
113
+ <style scoped>
114
+ .fade-enter-active,
115
+ .fade-leave-active {
116
+ transition: all 0.2s ease;
117
+ }
118
+
119
+ .fade-enter-from,
120
+ .fade-leave-to {
121
+ opacity: 0;
122
+ }
123
+ </style>
@@ -0,0 +1,101 @@
1
+ <script
2
+ setup
3
+ lang="ts"
4
+ generic="
5
+ ListingFilter extends {
6
+ code: string;
7
+ label: string;
8
+ }
9
+ "
10
+ >
11
+ import { computed, ref } from "vue";
12
+ import type { Schemas } from "#shopware";
13
+
14
+ const emits =
15
+ defineEmits<
16
+ (e: "select-value", value: { code: string; value: unknown }) => void
17
+ >();
18
+
19
+ const props = defineProps<{
20
+ filter: ListingFilter;
21
+ selectedFilters: Schemas["ProductListingResult"]["currentFilters"];
22
+ }>();
23
+ const isHoverActive = ref(false);
24
+ const hoveredIndex = ref(0);
25
+ const displayedScore = computed(() =>
26
+ isHoverActive.value ? hoveredIndex.value : props.selectedFilters?.rating || 0,
27
+ );
28
+
29
+ const hoverRating = (key: number) => {
30
+ hoveredIndex.value = key;
31
+ isHoverActive.value = true;
32
+ };
33
+ const onChangeRating = () => {
34
+ const newValue =
35
+ props.selectedFilters?.rating !== hoveredIndex.value
36
+ ? hoveredIndex.value
37
+ : undefined;
38
+ emits("select-value", { code: props.filter?.code, value: newValue });
39
+ };
40
+
41
+ const isFilterVisible = ref<boolean>(false);
42
+ const toggle = () => {
43
+ isFilterVisible.value = !isFilterVisible.value;
44
+ };
45
+ </script>
46
+
47
+ <template>
48
+ <div class="border-b border-gray-200 py-6 px-5">
49
+ <h3 class="-my-3 flow-root">
50
+ <button
51
+ type="button"
52
+ class="flex w-full items-center justify-between bg-white py-2 text-base text-gray-400 hover:text-gray-500"
53
+ @click="toggle"
54
+ >
55
+ <span class="font-medium text-gray-900 text-left">{{
56
+ props.filter.label
57
+ }}</span>
58
+ <span class="ml-6 flex items-center">
59
+ <i
60
+ :class="[
61
+ !isFilterVisible
62
+ ? 'i-carbon-chevron-down'
63
+ : 'i-carbon-chevron-up',
64
+ ]"
65
+ />
66
+ </span>
67
+ </button>
68
+ </h3>
69
+ <transition name="fade" mode="out-in">
70
+ <div v-show="isFilterVisible">
71
+ <div class="space-y-6 mt-4">
72
+ <div class="flex">
73
+ <div
74
+ v-for="i in 5"
75
+ :key="i"
76
+ class="h-6 w-6 c-yellow-500"
77
+ :class="{
78
+ 'i-carbon-star-filled': displayedScore >= i,
79
+ 'i-carbon-star': displayedScore < i,
80
+ }"
81
+ @mouseleave="isHoverActive = false"
82
+ @click="onChangeRating()"
83
+ @mouseover="hoverRating(i)"
84
+ />
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </transition>
89
+ </div>
90
+ </template>
91
+ <style scoped>
92
+ .fade-enter-active,
93
+ .fade-leave-active {
94
+ transition: all 0.2s ease;
95
+ }
96
+
97
+ .fade-enter-from,
98
+ .fade-leave-to {
99
+ opacity: 0;
100
+ }
101
+ </style>
@@ -0,0 +1,104 @@
1
+ <script
2
+ setup
3
+ lang="ts"
4
+ generic="
5
+ ListingFilter extends {
6
+ id: string;
7
+ code: keyof Schemas['ProductListingResult']['currentFilters'];
8
+ label: string;
9
+ name: string;
10
+ }
11
+ "
12
+ >
13
+ import { onClickOutside } from "@vueuse/core";
14
+ import { computed, ref } from "vue";
15
+ import type { Schemas } from "#shopware";
16
+
17
+ const props = defineProps<{
18
+ filter: ListingFilter;
19
+ selectedFilters: Schemas["ProductListingResult"]["currentFilters"];
20
+ }>();
21
+
22
+ const emits =
23
+ defineEmits<
24
+ (e: "select-value", value: { code: string; value: unknown }) => void
25
+ >();
26
+ const currentFilterData = computed(
27
+ () => !!props.selectedFilters[props.filter?.code],
28
+ );
29
+ const onChangeOption = (): void => {
30
+ emits("select-value", {
31
+ code: props.filter?.code,
32
+ value: !currentFilterData.value,
33
+ });
34
+ };
35
+
36
+ const isFilterVisible = ref<boolean>(false);
37
+ const toggle = () => {
38
+ isFilterVisible.value = !isFilterVisible.value;
39
+ };
40
+
41
+ const dropdownElement = ref(null);
42
+ onClickOutside(dropdownElement, () => {
43
+ isFilterVisible.value = false;
44
+ });
45
+ </script>
46
+
47
+ <template>
48
+ <div class="border-b border-gray-200 py-6 px-5">
49
+ <h3 class="-my-3 flow-root">
50
+ <button
51
+ type="button"
52
+ class="flex w-full items-center justify-between bg-white py-2 text-base text-gray-400 hover:text-gray-500"
53
+ @click="toggle"
54
+ >
55
+ <span class="font-medium text-gray-900 text-left">{{
56
+ props.filter.label
57
+ }}</span>
58
+ <span class="ml-6 flex items-center">
59
+ <i
60
+ :class="[
61
+ !isFilterVisible
62
+ ? 'i-carbon-chevron-down'
63
+ : 'i-carbon-chevron-up',
64
+ ]"
65
+ />
66
+ </span>
67
+ </button>
68
+ </h3>
69
+ <transition name="fade" mode="out-in">
70
+ <div v-show="isFilterVisible" id="filter-section-0" class="pt-6">
71
+ <div class="space-y-4">
72
+ <div class="flex items-center" @click="onChangeOption()">
73
+ <input
74
+ :id="`filter-mobile-${props.filter.id || props.filter.code}`"
75
+ :checked="currentFilterData"
76
+ :name="props.filter.name"
77
+ :value="props.filter.name"
78
+ type="checkbox"
79
+ class="h-4 w-4 border-gray-300 rounded text-indigo-600 focus:ring-indigo-500"
80
+ />
81
+
82
+ <label
83
+ :for="`filter-mobile-${props.filter.id || props.filter.code}`"
84
+ class="ml-3 text-gray-600"
85
+ >
86
+ {{ props.filter.label }}
87
+ </label>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </transition>
92
+ </div>
93
+ </template>
94
+ <style scoped>
95
+ .fade-enter-active,
96
+ .fade-leave-active {
97
+ transition: all 0.2s ease;
98
+ }
99
+
100
+ .fade-enter-from,
101
+ .fade-leave-to {
102
+ opacity: 0;
103
+ }
104
+ </style>
@@ -0,0 +1,27 @@
1
+ Renders a Block type structure
2
+
3
+ Example usage:
4
+
5
+ ```vue{14-19}
6
+ <script setup lang="ts">
7
+ import type { CmsSectionDefault } from "@shopware/composables";
8
+ import { getCmsLayoutConfiguration } from "@shopware/helpers";
9
+
10
+ const props = defineProps<{
11
+ content: CmsSectionDefault;
12
+ }>();
13
+
14
+ const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(props.content);
15
+ </script>
16
+
17
+ <template>
18
+ <div class="cms-section-default" :class="cssClasses" :styles="layoutStyles">
19
+ <CmsGenericBlock
20
+ v-for="cmsBlock in content.blocks"
21
+ class="overflow-auto"
22
+ :key="cmsBlock.id"
23
+ :content="cmsBlock"
24
+ />
25
+ </div>
26
+ </template>
27
+ ```
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import { resolveCmsComponent } from "@shopware/composables";
3
+ import {
4
+ getBackgroundImageUrl,
5
+ getCmsLayoutConfiguration,
6
+ } from "@shopware/helpers";
7
+ import { h } from "vue";
8
+ import type { Schemas } from "#shopware";
9
+
10
+ const props = defineProps<{
11
+ content: Schemas["CmsBlock"];
12
+ }>();
13
+
14
+ const DynamicRender = () => {
15
+ const {
16
+ resolvedComponent,
17
+ componentName,
18
+ isResolved,
19
+ componentNameToResolve,
20
+ } = resolveCmsComponent(props.content);
21
+
22
+ if (resolvedComponent) {
23
+ if (!isResolved)
24
+ return h("div", {}, `Problem resolving component: ${componentName}`);
25
+
26
+ const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(
27
+ props.content,
28
+ );
29
+
30
+ if (layoutStyles.backgroundImage) {
31
+ layoutStyles.backgroundImage = getBackgroundImageUrl(
32
+ layoutStyles.backgroundImage,
33
+ props.content,
34
+ );
35
+ }
36
+
37
+ const containerStyles = {
38
+ backgroundColor: layoutStyles.backgroundColor,
39
+ backgroundImage: layoutStyles.backgroundImage,
40
+ };
41
+
42
+ layoutStyles.backgroundColor = null;
43
+ layoutStyles.backgroundImage = null;
44
+ return h(
45
+ "div",
46
+ {
47
+ style: containerStyles,
48
+ },
49
+ h(resolvedComponent, {
50
+ content: props.content,
51
+ style: layoutStyles,
52
+ class: cssClasses,
53
+ }),
54
+ );
55
+ }
56
+ console.error(`Component not resolve: ${componentNameToResolve}`);
57
+ return h("div", {}, "");
58
+ };
59
+ </script>
60
+
61
+ <template>
62
+ <DynamicRender />
63
+ </template>