@shopbite-de/storefront 1.7.9 → 1.8.0
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/app/components/AddToWishlist.vue +2 -11
- package/app/components/Checkout/Summary.vue +2 -7
- package/app/components/Food/MarqueeItem.vue +0 -7
- package/app/components/Navigation/DesktopLeft.vue +0 -4
- package/app/components/Navigation/MobileTop.vue +0 -4
- package/app/components/Product/Detail2.vue +9 -0
- package/app/components/Product/SearchBar.vue +0 -14
- package/app/composables/useAddToCart.ts +2 -7
- package/app/composables/useTrackEvent.ts +62 -0
- package/app/composables/useWishlistActions.ts +2 -15
- package/content/index.yml +2 -2
- package/content/navigation.yml +2 -6
- package/nuxt.config.ts +0 -6
- package/package.json +2 -3
- package/test/nuxt/useAddToCart.test.ts +7 -6
- package/test/nuxt/useWishlistActions.test.ts +4 -5
- package/app/components/Product/Configurator.vue +0 -65
- package/app/components/Product/Detail.vue +0 -187
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Schemas } from "#shopware";
|
|
3
|
-
import { useTrackEvent } from "#imports";
|
|
4
3
|
|
|
5
4
|
const props = defineProps<{
|
|
6
5
|
product: Schemas["Product"];
|
|
@@ -10,25 +9,17 @@ const { addToWishlist, isInWishlist, removeFromWishlist } = useProductWishlist(
|
|
|
10
9
|
props.product.id,
|
|
11
10
|
);
|
|
12
11
|
const { getWishlistProducts } = useWishlist();
|
|
13
|
-
|
|
14
|
-
const trackWishlistEvent = (
|
|
15
|
-
action: "add_to_wishlist" | "remove_from_wishlist",
|
|
16
|
-
) => {
|
|
17
|
-
useTrackEvent(action, {
|
|
18
|
-
props: { product_number: props.product.productNumber },
|
|
19
|
-
});
|
|
20
|
-
};
|
|
12
|
+
const { trackAddToWishlist } = useTrackEvent();
|
|
21
13
|
|
|
22
14
|
const toggleWishlistProduct = async () => {
|
|
23
15
|
try {
|
|
24
16
|
if (isInWishlist.value) {
|
|
25
17
|
await removeFromWishlist();
|
|
26
|
-
trackWishlistEvent("remove_from_wishlist");
|
|
27
18
|
await getWishlistProducts();
|
|
28
19
|
} else {
|
|
29
20
|
await addToWishlist();
|
|
30
|
-
trackWishlistEvent("add_to_wishlist");
|
|
31
21
|
await getWishlistProducts();
|
|
22
|
+
trackAddToWishlist(props.product);
|
|
32
23
|
}
|
|
33
24
|
} catch (error) {
|
|
34
25
|
console.error("[wishlist][handleWishlistError]", error);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import QuickView from "~/components/Cart/QuickView.vue";
|
|
3
3
|
import { useIntervalFn } from "@vueuse/core";
|
|
4
|
-
import { useTrackEvent } from "#imports";
|
|
5
4
|
|
|
6
5
|
const { selectedPaymentMethod, selectedShippingMethod } = useCheckout();
|
|
7
6
|
const { createOrder } = useCheckout();
|
|
8
7
|
const { refreshCart } = useCart();
|
|
9
8
|
const { isLoggedIn, isGuestSession } = useUser();
|
|
10
9
|
const { isCheckoutEnabled, refresh } = useShopBiteConfig();
|
|
10
|
+
const { trackOrder } = useTrackEvent();
|
|
11
11
|
|
|
12
12
|
const toast = useToast();
|
|
13
13
|
|
|
@@ -24,12 +24,7 @@ async function handleCreateOrder() {
|
|
|
24
24
|
customerComment: "Wunschlieferzeit: " + selectedDeliveryTime.value,
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
revenue: {
|
|
29
|
-
currency: "EUR",
|
|
30
|
-
amount: order.amountTotal as number,
|
|
31
|
-
},
|
|
32
|
-
});
|
|
27
|
+
trackOrder(order);
|
|
33
28
|
|
|
34
29
|
await refreshCart();
|
|
35
30
|
toast.add({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { useTrackEvent } from "#imports";
|
|
3
2
|
import { useProductSearch } from "@shopware/composables";
|
|
4
3
|
import type { Schemas } from "#shopware";
|
|
5
4
|
|
|
@@ -42,12 +41,6 @@ async function addToCart(productId: string) {
|
|
|
42
41
|
});
|
|
43
42
|
await refreshCart(newCart);
|
|
44
43
|
await showSuccessToast();
|
|
45
|
-
useTrackEvent("add_to_cart", {
|
|
46
|
-
props: {
|
|
47
|
-
product_number: product.productNumber,
|
|
48
|
-
quantity: 1,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
44
|
}
|
|
52
45
|
</script>
|
|
53
46
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
3
3
|
import type { Schemas } from "#shopware";
|
|
4
|
-
import { useTrackEvent } from "#imports";
|
|
5
4
|
|
|
6
5
|
const { loadNavigationElements, navigationElements } = useNavigation();
|
|
7
6
|
|
|
@@ -17,9 +16,6 @@ const scrollToElement = (elementId: string, margin = 0) => {
|
|
|
17
16
|
top: offsetPosition,
|
|
18
17
|
behavior: "smooth",
|
|
19
18
|
});
|
|
20
|
-
useTrackEvent("scroll_to_category", {
|
|
21
|
-
props: { category_name: elementId },
|
|
22
|
-
});
|
|
23
19
|
}
|
|
24
20
|
};
|
|
25
21
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
3
3
|
import type { Schemas } from "#shopware";
|
|
4
|
-
import { useTrackEvent } from "#imports";
|
|
5
4
|
|
|
6
5
|
const { loadNavigationElements, navigationElements } = useNavigation();
|
|
7
6
|
|
|
@@ -17,9 +16,6 @@ const scrollToElement = (elementId: string, margin = 0) => {
|
|
|
17
16
|
top: offsetPosition,
|
|
18
17
|
behavior: "smooth",
|
|
19
18
|
});
|
|
20
|
-
useTrackEvent("scroll_to_category", {
|
|
21
|
-
props: { category_name: elementId },
|
|
22
|
-
});
|
|
23
19
|
}
|
|
24
20
|
};
|
|
25
21
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { operations, Schemas } from "#shopware";
|
|
3
3
|
import type { AssociationItemProduct } from "~/types/Association";
|
|
4
|
+
import { useTrackEvent } from "~/composables/useTrackEvent";
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
productId: string;
|
|
7
8
|
}>();
|
|
8
9
|
|
|
9
10
|
const { apiClient } = useShopwareContext();
|
|
11
|
+
const { trackProductView } = useTrackEvent();
|
|
10
12
|
|
|
11
13
|
const productId = toRef(props.productId);
|
|
12
14
|
|
|
@@ -23,6 +25,7 @@ const searchCriteria = {
|
|
|
23
25
|
"propertyIds",
|
|
24
26
|
"options",
|
|
25
27
|
"optionIds",
|
|
28
|
+
"seoCategory",
|
|
26
29
|
"configuratorSettings",
|
|
27
30
|
"children",
|
|
28
31
|
"parentId",
|
|
@@ -33,6 +36,7 @@ const searchCriteria = {
|
|
|
33
36
|
property_group_option: ["id", "name", "translated", "group"],
|
|
34
37
|
product_configurator_setting: ["id", "optionId", "option", "productId"],
|
|
35
38
|
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
39
|
+
category: ["id", "name", "translated"],
|
|
36
40
|
},
|
|
37
41
|
associations: {
|
|
38
42
|
cover: {
|
|
@@ -128,6 +132,11 @@ const onIngredientsDeselected = (deselected: string[]) => {
|
|
|
128
132
|
const onAddToCart = () => emit("product-added");
|
|
129
133
|
|
|
130
134
|
const emit = defineEmits(["product-added", "variant-selected"]);
|
|
135
|
+
|
|
136
|
+
watch(productDetails, () => {
|
|
137
|
+
if (!productDetails.value) return;
|
|
138
|
+
trackProductView(productDetails.value.product);
|
|
139
|
+
});
|
|
131
140
|
</script>
|
|
132
141
|
|
|
133
142
|
<template>
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from "vue";
|
|
3
3
|
import { onClickOutside, useDebounceFn, useFocus } from "@vueuse/core";
|
|
4
|
-
import type { Schemas } from "#shopware";
|
|
5
|
-
import { useTrackEvent } from "#imports";
|
|
6
4
|
|
|
7
5
|
withDefaults(
|
|
8
6
|
defineProps<{
|
|
@@ -53,18 +51,6 @@ watch(typingQuery, (value) => {
|
|
|
53
51
|
const performSuggestSearch = useDebounceFn((value) => {
|
|
54
52
|
searchTerm.value = value;
|
|
55
53
|
search();
|
|
56
|
-
trackEvent(value);
|
|
57
|
-
}, 500);
|
|
58
|
-
|
|
59
|
-
const trackEvent = useDebounceFn((searchTerm: string) => {
|
|
60
|
-
const searchResult = getProducts.value.map(function (
|
|
61
|
-
product: Schemas["Product"],
|
|
62
|
-
) {
|
|
63
|
-
return product.productNumber;
|
|
64
|
-
});
|
|
65
|
-
useTrackEvent("search", {
|
|
66
|
-
props: { search_term: searchTerm, search_result: searchResult.join(",") },
|
|
67
|
-
});
|
|
68
54
|
}, 500);
|
|
69
55
|
|
|
70
56
|
if (import.meta.client) {
|
|
@@ -12,6 +12,7 @@ export function useAddToCart() {
|
|
|
12
12
|
const { addProducts, refreshCart } = useCart();
|
|
13
13
|
const toast = useToast();
|
|
14
14
|
const { triggerProductAdded } = useProductEvents();
|
|
15
|
+
const { trackAddToCart: trackAddToCartEvent } = useTrackEvent();
|
|
15
16
|
|
|
16
17
|
const selectedExtras = ref<AssociationItemProduct[]>([]);
|
|
17
18
|
const deselectedIngredients = ref<string[]>([]);
|
|
@@ -140,13 +141,7 @@ export function useAddToCart() {
|
|
|
140
141
|
await showSuccessToast();
|
|
141
142
|
|
|
142
143
|
triggerProductAdded();
|
|
143
|
-
|
|
144
|
-
useTrackEvent("add_to_cart", {
|
|
145
|
-
props: {
|
|
146
|
-
product_number: selectedProduct.value.productNumber,
|
|
147
|
-
quantity: selectedQuantity.value,
|
|
148
|
-
},
|
|
149
|
-
});
|
|
144
|
+
trackAddToCartEvent(selectedProduct.value, selectedQuantity.value);
|
|
150
145
|
|
|
151
146
|
isLoading.value = false;
|
|
152
147
|
if (onSuccess) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Schemas } from "#shopware";
|
|
2
|
+
|
|
3
|
+
export function useTrackEvent() {
|
|
4
|
+
const { proxy } = useScriptMatomoAnalytics();
|
|
5
|
+
|
|
6
|
+
function trackProductView(product: Schemas["Product"]) {
|
|
7
|
+
proxy._paq.push([
|
|
8
|
+
"setEcommerceView",
|
|
9
|
+
product.productNumber,
|
|
10
|
+
product.translated.name ?? product.name,
|
|
11
|
+
product.seoCategory.name,
|
|
12
|
+
product.calculatedPrice.unitPrice,
|
|
13
|
+
]);
|
|
14
|
+
proxy._paq.push(["trackPageView", product.productNumber]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function trackOrder(order: Schemas["Order"]) {
|
|
18
|
+
order.lineItems?.forEach((item) => {
|
|
19
|
+
if (item.type === "container") return;
|
|
20
|
+
proxy._paq.push([
|
|
21
|
+
"addEcommerceItem",
|
|
22
|
+
item.product?.productNumber ?? item.id,
|
|
23
|
+
item.label,
|
|
24
|
+
item.product?.seoCategory.name ?? "Default",
|
|
25
|
+
item.unitPrice,
|
|
26
|
+
item.quantity,
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
proxy._paq.push([
|
|
31
|
+
"trackEcommerceOrder",
|
|
32
|
+
order.orderNumber,
|
|
33
|
+
order.price.totalPrice,
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function trackAddToWishlist(product: Schemas["Product"]) {
|
|
38
|
+
proxy._paq.push([
|
|
39
|
+
"trackEvent",
|
|
40
|
+
"Product",
|
|
41
|
+
"AddToWishlist",
|
|
42
|
+
product.productNumber,
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function trackAddToCart(product: Schemas["Product"], quantity: number) {
|
|
47
|
+
proxy._paq.push([
|
|
48
|
+
"trackEvent",
|
|
49
|
+
"Cart",
|
|
50
|
+
"AddToCart",
|
|
51
|
+
product.productNumber,
|
|
52
|
+
quantity,
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
trackProductView,
|
|
58
|
+
trackOrder,
|
|
59
|
+
trackAddToWishlist,
|
|
60
|
+
trackAddToCart,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -5,6 +5,7 @@ export function useWishlistActions() {
|
|
|
5
5
|
const toast = useToast();
|
|
6
6
|
const { triggerProductAdded } = useProductEvents();
|
|
7
7
|
const { clearWishlist } = useWishlist();
|
|
8
|
+
const { trackAddToCart: trackAddToCartEvent } = useTrackEvent();
|
|
8
9
|
|
|
9
10
|
const isAddingToCart = ref(false);
|
|
10
11
|
const addingItemId = ref<string | null>(null);
|
|
@@ -53,6 +54,7 @@ export function useWishlistActions() {
|
|
|
53
54
|
await refreshCart(newCart);
|
|
54
55
|
|
|
55
56
|
triggerProductAdded();
|
|
57
|
+
trackAddToCartEvent(product, 1);
|
|
56
58
|
|
|
57
59
|
toast.add({
|
|
58
60
|
title: "In den Warenkorb gelegt",
|
|
@@ -60,13 +62,6 @@ export function useWishlistActions() {
|
|
|
60
62
|
icon: "i-lucide-shopping-cart",
|
|
61
63
|
color: "primary",
|
|
62
64
|
});
|
|
63
|
-
|
|
64
|
-
useTrackEvent("add_to_cart", {
|
|
65
|
-
props: {
|
|
66
|
-
product_number: product.productNumber,
|
|
67
|
-
quantity: 1,
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
65
|
} catch (error) {
|
|
71
66
|
console.error("[wishlist][addSingleItemToCart] Error details:", error);
|
|
72
67
|
toast.add({
|
|
@@ -128,14 +123,6 @@ export function useWishlistActions() {
|
|
|
128
123
|
icon: "i-lucide-shopping-cart",
|
|
129
124
|
color: "primary",
|
|
130
125
|
});
|
|
131
|
-
|
|
132
|
-
useTrackEvent("add_to_cart", {
|
|
133
|
-
props: {
|
|
134
|
-
product_count: addableProducts.length,
|
|
135
|
-
skipped_count: products.length - addableProducts.length,
|
|
136
|
-
source: "wishlist_bulk",
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
126
|
} catch (error) {
|
|
140
127
|
console.error("[wishlist][addAllItemsToCart] Error:", error);
|
|
141
128
|
toast.add({
|
package/content/index.yml
CHANGED
|
@@ -23,7 +23,7 @@ hero:
|
|
|
23
23
|
icon: i-lucide-arrow-right
|
|
24
24
|
trailing: true
|
|
25
25
|
color: primary
|
|
26
|
-
to: /
|
|
26
|
+
to: /menu/
|
|
27
27
|
size: xl
|
|
28
28
|
- label: Tisch reservieren
|
|
29
29
|
icon: i-lucide-phone
|
|
@@ -87,7 +87,7 @@ cta:
|
|
|
87
87
|
backgroundImage: https://shopware.shopbite.de/media/f5/19/4a/1762546880/category-pizza-header.webp
|
|
88
88
|
links:
|
|
89
89
|
- label: Zur Speisekarte
|
|
90
|
-
to: /
|
|
90
|
+
to: /menu/
|
|
91
91
|
color: primary
|
|
92
92
|
- label: Tisch reservieren
|
|
93
93
|
to: tel:+49610471427
|
package/content/navigation.yml
CHANGED
|
@@ -48,12 +48,8 @@ footer:
|
|
|
48
48
|
to: /agb
|
|
49
49
|
- label: Top Kategorien
|
|
50
50
|
children:
|
|
51
|
-
- label:
|
|
52
|
-
to: /
|
|
53
|
-
- label: Nudeln
|
|
54
|
-
to: /c/Nudeln/
|
|
55
|
-
- label: Fleischgerichte
|
|
56
|
-
to: /c/Fleischgerichte/
|
|
51
|
+
- label: Burger
|
|
52
|
+
to: /menu/burger/
|
|
57
53
|
- label: Unternehmen
|
|
58
54
|
children:
|
|
59
55
|
- label: 'Tel: 06104 71427'
|
package/nuxt.config.ts
CHANGED
|
@@ -97,14 +97,9 @@ export default defineNuxtConfig({
|
|
|
97
97
|
"@sentry/nuxt/module",
|
|
98
98
|
"@nuxt/ui",
|
|
99
99
|
"@nuxt/scripts",
|
|
100
|
-
"@nuxtjs/plausible",
|
|
101
100
|
"nuxt-vitalizer",
|
|
102
101
|
],
|
|
103
102
|
|
|
104
|
-
plausible: {
|
|
105
|
-
ignoredHostnames: ["localhost"],
|
|
106
|
-
},
|
|
107
|
-
|
|
108
103
|
content: {
|
|
109
104
|
experimental: { sqliteConnector: "native" },
|
|
110
105
|
},
|
|
@@ -184,7 +179,6 @@ export default defineNuxtConfig({
|
|
|
184
179
|
"@sentry/nuxt/module",
|
|
185
180
|
"@nuxt/ui",
|
|
186
181
|
"@nuxt/scripts",
|
|
187
|
-
"@nuxtjs/plausible",
|
|
188
182
|
"@nuxt/test-utils/module",
|
|
189
183
|
"@nuxt/eslint",
|
|
190
184
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopbite-de/storefront",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"main": "nuxt.config.ts",
|
|
5
5
|
"description": "Shopware storefront for food delivery shops",
|
|
6
6
|
"keywords": [
|
|
@@ -18,11 +18,10 @@
|
|
|
18
18
|
"@heroicons/vue": "^2.2.0",
|
|
19
19
|
"@iconify-json/lucide": "^1.2.73",
|
|
20
20
|
"@iconify-json/simple-icons": "^1.2.59",
|
|
21
|
-
"@nuxt/content": "
|
|
21
|
+
"@nuxt/content": "3.8.2",
|
|
22
22
|
"@nuxt/image": "^2.0.0",
|
|
23
23
|
"@nuxt/scripts": "0.13.2",
|
|
24
24
|
"@nuxt/ui": "^4.1.0",
|
|
25
|
-
"@nuxtjs/plausible": "2.0.1",
|
|
26
25
|
"@nuxtjs/robots": "^5.5.6",
|
|
27
26
|
"@sentry/nuxt": "^10.25.0",
|
|
28
27
|
"@shopware/api-client": "^1.3.0",
|
|
@@ -39,7 +39,9 @@ mockNuxtImport("useProductEvents", () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
mockNuxtImport("useTrackEvent", () => {
|
|
42
|
-
return
|
|
42
|
+
return () => ({
|
|
43
|
+
trackAddToCart: mockTrackEvent,
|
|
44
|
+
});
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
// Provide the mocks globally or in a way that they are picked up
|
|
@@ -53,7 +55,9 @@ vi.stubGlobal("useToast", () => ({
|
|
|
53
55
|
vi.stubGlobal("useProductEvents", () => ({
|
|
54
56
|
triggerProductAdded: mockTriggerProductAdded,
|
|
55
57
|
}));
|
|
56
|
-
vi.stubGlobal("useTrackEvent",
|
|
58
|
+
vi.stubGlobal("useTrackEvent", () => ({
|
|
59
|
+
trackAddToCart: mockTrackEvent,
|
|
60
|
+
}));
|
|
57
61
|
|
|
58
62
|
describe("useAddToCart", () => {
|
|
59
63
|
const mockProduct = {
|
|
@@ -131,10 +135,7 @@ describe("useAddToCart", () => {
|
|
|
131
135
|
expect(mockRefreshCart).toHaveBeenCalled();
|
|
132
136
|
expect(mockToastAdd).toHaveBeenCalled();
|
|
133
137
|
expect(mockTriggerProductAdded).toHaveBeenCalled();
|
|
134
|
-
expect(mockTrackEvent).toHaveBeenCalledWith(
|
|
135
|
-
"add_to_cart",
|
|
136
|
-
expect.any(Object),
|
|
137
|
-
);
|
|
138
|
+
expect(mockTrackEvent).toHaveBeenCalledWith(mockProduct, 1);
|
|
138
139
|
});
|
|
139
140
|
|
|
140
141
|
it("should add product with extras to cart as container", async () => {
|
|
@@ -35,7 +35,9 @@ mockNuxtImport("useWishlist", () => () => ({
|
|
|
35
35
|
clearWishlist: mockClearWishlist,
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
|
-
mockNuxtImport("useTrackEvent", () =>
|
|
38
|
+
mockNuxtImport("useTrackEvent", () => () => ({
|
|
39
|
+
trackAddToCart: mockTrackEvent,
|
|
40
|
+
}));
|
|
39
41
|
|
|
40
42
|
describe("useWishlistActions", () => {
|
|
41
43
|
const mockProduct = {
|
|
@@ -72,10 +74,7 @@ describe("useWishlistActions", () => {
|
|
|
72
74
|
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
73
75
|
expect.objectContaining({ title: "In den Warenkorb gelegt" }),
|
|
74
76
|
);
|
|
75
|
-
expect(mockTrackEvent).toHaveBeenCalledWith(
|
|
76
|
-
"add_to_cart",
|
|
77
|
-
expect.any(Object),
|
|
78
|
-
);
|
|
77
|
+
expect(mockTrackEvent).toHaveBeenCalledWith(mockProduct, 1);
|
|
79
78
|
});
|
|
80
79
|
|
|
81
80
|
it("should warn when adding a base product with variants", async () => {
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Schemas } from "#shopware";
|
|
3
|
-
const props = defineProps<{
|
|
4
|
-
parentProduct: Schemas["Product"];
|
|
5
|
-
}>();
|
|
6
|
-
const { parentProduct } = toRefs(props);
|
|
7
|
-
const { product, changeVariant, configurator } = useProduct(
|
|
8
|
-
parentProduct.value?.children?.length > 0
|
|
9
|
-
? parentProduct.value.children[0]
|
|
10
|
-
: parentProduct.value,
|
|
11
|
-
parentProduct.value?.configuratorSettings || [],
|
|
12
|
-
);
|
|
13
|
-
const { findVariantForSelectedOptions } = useProductConfigurator();
|
|
14
|
-
const { variants: selectableOptions } = useProductVariants(configurator);
|
|
15
|
-
const selectedOptions = ref<Record<string, string>>({});
|
|
16
|
-
function initialOptions(variant: Ref<Schemas["Product"]>) {
|
|
17
|
-
const options = variant.value.options as Schemas["PropertyGroupOption"][];
|
|
18
|
-
for (const option of options) {
|
|
19
|
-
if (option.group && option.id) {
|
|
20
|
-
selectedOptions.value[option.group.id] = option.id;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
onMounted(() => {
|
|
25
|
-
initialOptions(product);
|
|
26
|
-
});
|
|
27
|
-
watch(
|
|
28
|
-
selectedOptions.value,
|
|
29
|
-
async () => {
|
|
30
|
-
const foundVariant = await findVariantForSelectedOptions(
|
|
31
|
-
selectedOptions.value,
|
|
32
|
-
);
|
|
33
|
-
const variant = parentProduct.value.children?.find(
|
|
34
|
-
(child: Schemas["Product"]) => child.id === foundVariant?.id,
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
if (variant) {
|
|
38
|
-
changeVariant(variant);
|
|
39
|
-
emit("variant-switched", variant);
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
{ deep: true },
|
|
43
|
-
);
|
|
44
|
-
const emit = defineEmits<{
|
|
45
|
-
"variant-switched": [variant: Schemas["Product"]];
|
|
46
|
-
}>();
|
|
47
|
-
</script>
|
|
48
|
-
<template>
|
|
49
|
-
<div
|
|
50
|
-
v-for="(variantGroup, propertyGroupId) in selectableOptions"
|
|
51
|
-
:key="propertyGroupId"
|
|
52
|
-
class="my-6"
|
|
53
|
-
>
|
|
54
|
-
<div class="flex flex-row gap-2 items-center">
|
|
55
|
-
<div class="basis-1/3">{{ variantGroup.name }}:</div>
|
|
56
|
-
<USelect
|
|
57
|
-
v-model="selectedOptions[propertyGroupId]"
|
|
58
|
-
value-key="productId"
|
|
59
|
-
:items="variantGroup.options"
|
|
60
|
-
class="w-full"
|
|
61
|
-
icon="i-lucide-square-stack"
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</template>
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Schemas, operations } from "#shopware";
|
|
3
|
-
import type { AssociationItemProduct } from "~/types/Association";
|
|
4
|
-
import { v5 as uuidv5 } from "uuid";
|
|
5
|
-
import { computed, ref, toRefs } from "vue";
|
|
6
|
-
import { useTrackEvent } from "#imports";
|
|
7
|
-
|
|
8
|
-
const UUID_NAMESPACE = "b098ef7e-0fa2-4073-b002-7ceec4360fbf";
|
|
9
|
-
const CART_SUCCESS_TITLE = "Gute Wahl!";
|
|
10
|
-
const LINE_ITEM_PRODUCT = "product";
|
|
11
|
-
const LINE_ITEM_CONTAINER = "container";
|
|
12
|
-
|
|
13
|
-
const props = defineProps<{
|
|
14
|
-
product: Schemas["Product"];
|
|
15
|
-
}>();
|
|
16
|
-
const { product } = toRefs(props);
|
|
17
|
-
const { addProducts, refreshCart } = useCart();
|
|
18
|
-
const toast = useToast();
|
|
19
|
-
const { triggerProductAdded } = useProductEvents();
|
|
20
|
-
|
|
21
|
-
const selectedExtras = ref<AssociationItemProduct[]>([]);
|
|
22
|
-
const deselectedIngredients = ref<string[]>([]);
|
|
23
|
-
const selectedQuantity = ref(1);
|
|
24
|
-
const selectedProduct = ref<Schemas["Product"]>(product.value);
|
|
25
|
-
|
|
26
|
-
const cartItemLabel = computed(() => {
|
|
27
|
-
const formatIngredientModifications = (
|
|
28
|
-
items: Array<{ label: string } | string>,
|
|
29
|
-
prefix: string,
|
|
30
|
-
): string => {
|
|
31
|
-
if (!items.length) return "";
|
|
32
|
-
|
|
33
|
-
const labels = items.map((item) =>
|
|
34
|
-
typeof item === "string" ? item : item.label,
|
|
35
|
-
);
|
|
36
|
-
const separator = " " + prefix;
|
|
37
|
-
return " " + prefix + labels.join(separator);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const extrasFormatted = formatIngredientModifications(
|
|
41
|
-
selectedExtras.value,
|
|
42
|
-
"+",
|
|
43
|
-
);
|
|
44
|
-
const removedFormatted = formatIngredientModifications(
|
|
45
|
-
deselectedIngredients.value,
|
|
46
|
-
"-",
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
return `${selectedProduct.value.translated.name}${extrasFormatted}${removedFormatted}`;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const createExtras = () =>
|
|
53
|
-
selectedExtras.value.map((extra) => ({
|
|
54
|
-
id: extra.value,
|
|
55
|
-
type: LINE_ITEM_PRODUCT,
|
|
56
|
-
quantity: selectedQuantity.value,
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
const generateSortedExtrasString = (extras: AssociationItemProduct[]) =>
|
|
60
|
-
extras
|
|
61
|
-
.map((extra) => extra.value)
|
|
62
|
-
.sort()
|
|
63
|
-
.join("");
|
|
64
|
-
|
|
65
|
-
const generateProductId = (baseId: string, extras: AssociationItemProduct[]) =>
|
|
66
|
-
extras.length ? baseId + generateSortedExtrasString(extras) : baseId;
|
|
67
|
-
|
|
68
|
-
function createCartItems(): operations["addLineItem post /checkout/cart/line-item"]["body"]["items"] {
|
|
69
|
-
const extras = createExtras();
|
|
70
|
-
|
|
71
|
-
// Simple product when no extras
|
|
72
|
-
if (extras.length === 0 && deselectedIngredients.value.length === 0) {
|
|
73
|
-
return [
|
|
74
|
-
{
|
|
75
|
-
id: selectedProduct.value.id,
|
|
76
|
-
quantity: selectedQuantity.value,
|
|
77
|
-
type: LINE_ITEM_PRODUCT,
|
|
78
|
-
label: cartItemLabel.value,
|
|
79
|
-
},
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Container product when extras are selected
|
|
84
|
-
const generatedUuid = uuidv5(
|
|
85
|
-
generateProductId(selectedProduct.value.id, selectedExtras.value),
|
|
86
|
-
UUID_NAMESPACE,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
return [
|
|
90
|
-
{
|
|
91
|
-
id: generatedUuid,
|
|
92
|
-
quantity: selectedQuantity.value,
|
|
93
|
-
type: LINE_ITEM_CONTAINER,
|
|
94
|
-
label: cartItemLabel.value,
|
|
95
|
-
payload: {
|
|
96
|
-
productNumber: selectedProduct.value.productNumber,
|
|
97
|
-
},
|
|
98
|
-
children: [
|
|
99
|
-
{
|
|
100
|
-
id: selectedProduct.value.id,
|
|
101
|
-
quantity: selectedQuantity.value,
|
|
102
|
-
type: LINE_ITEM_PRODUCT,
|
|
103
|
-
},
|
|
104
|
-
...extras,
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Toast Notification
|
|
111
|
-
async function showSuccessToast() {
|
|
112
|
-
toast.add({
|
|
113
|
-
title: CART_SUCCESS_TITLE,
|
|
114
|
-
description: `${selectedProduct.value.translated.name} wurde in den Warenkorb gelegt.`,
|
|
115
|
-
icon: "i-lucide-shopping-cart",
|
|
116
|
-
color: "success",
|
|
117
|
-
progress: true,
|
|
118
|
-
color: "primary",
|
|
119
|
-
duration: 2000,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Add to Cart
|
|
124
|
-
async function addToCart() {
|
|
125
|
-
const cartItems = createCartItems();
|
|
126
|
-
const newCart = await addProducts(cartItems);
|
|
127
|
-
await refreshCart(newCart);
|
|
128
|
-
await showSuccessToast();
|
|
129
|
-
emit("product-added");
|
|
130
|
-
triggerProductAdded(); // Clear search globally
|
|
131
|
-
useTrackEvent("add_to_cart", {
|
|
132
|
-
props: {
|
|
133
|
-
product_number: selectedProduct.value.productNumber,
|
|
134
|
-
quantity: selectedQuantity.value,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Event Handlers
|
|
140
|
-
const onVariantSwitched = (variant: Schemas["Product"]) => {
|
|
141
|
-
selectedProduct.value = variant;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const onExtrasSelected = (extras: AssociationItemProduct[]) => {
|
|
145
|
-
selectedExtras.value = extras;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const onIngredientsDeselected = (deselected: string[]) => {
|
|
149
|
-
deselectedIngredients.value = deselected;
|
|
150
|
-
};
|
|
151
|
-
const emit = defineEmits(["product-added"]);
|
|
152
|
-
</script>
|
|
153
|
-
|
|
154
|
-
<template>
|
|
155
|
-
<div class="flex flex-col justify-between w-full">
|
|
156
|
-
<div>
|
|
157
|
-
<ProductConfigurator
|
|
158
|
-
:parent-product="product"
|
|
159
|
-
@variant-switched="onVariantSwitched"
|
|
160
|
-
/>
|
|
161
|
-
<ProductCrossSelling
|
|
162
|
-
:product="selectedProduct"
|
|
163
|
-
@extras-selected="onExtrasSelected"
|
|
164
|
-
/>
|
|
165
|
-
<ProductDeselectIngredient
|
|
166
|
-
:product="selectedProduct"
|
|
167
|
-
@ingredients-deselected="onIngredientsDeselected"
|
|
168
|
-
/>
|
|
169
|
-
</div>
|
|
170
|
-
<div class="flex flex-row gap-4 mt-8">
|
|
171
|
-
<UInputNumber
|
|
172
|
-
v-model="selectedQuantity"
|
|
173
|
-
size="xl"
|
|
174
|
-
placeholder="Anzahl"
|
|
175
|
-
:min="1"
|
|
176
|
-
:max="100"
|
|
177
|
-
/>
|
|
178
|
-
<UButton
|
|
179
|
-
size="xl"
|
|
180
|
-
label="In den Warenkorb"
|
|
181
|
-
icon="i-lucide-shopping-cart"
|
|
182
|
-
block
|
|
183
|
-
@click="addToCart"
|
|
184
|
-
/>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
</template>
|