@shopbite-de/storefront 1.2.4 → 1.2.6
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/assets/css/main.css +3 -0
- package/app/components/AnimatedSection.vue +1 -1
- package/app/components/Category/Header.vue +16 -9
- package/app/components/Category/Listing.vue +10 -64
- package/app/components/Features.vue +1 -0
- package/app/components/Header.vue +11 -0
- package/app/components/Navigation/DesktopLeft2.vue +1 -1
- package/app/components/Product/Card.vue +5 -2
- package/app/components/Product/Detail2.vue +72 -4
- package/app/components/Wishlist.vue +166 -29
- package/app/composables/useAddToCart.ts +1 -1
- package/app/composables/useWishlistActions.ts +160 -0
- package/app/pages/merkliste.vue +7 -1
- package/package.json +1 -1
package/app/assets/css/main.css
CHANGED
|
@@ -24,7 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
24
24
|
duration: "duration-1000",
|
|
25
25
|
delay: "delay-0",
|
|
26
26
|
threshold: 0.1,
|
|
27
|
-
rootMargin: "0px 0px
|
|
27
|
+
rootMargin: "0px 0px 100px 0px",
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const { isVisible, elementRef } = useScrollAnimation({
|
|
@@ -27,17 +27,12 @@ defineProps<{
|
|
|
27
27
|
<div v-else class="absolute inset-0 bg-primary" />
|
|
28
28
|
|
|
29
29
|
<div class="relative p-4">
|
|
30
|
-
<
|
|
31
|
-
class="text-pretty font-semibold text-3xl md:text-4xl text-white drop-shadow"
|
|
32
|
-
>
|
|
30
|
+
<h1 class="text-white font-semibold">
|
|
33
31
|
{{ category.name }}
|
|
34
|
-
</
|
|
35
|
-
<
|
|
36
|
-
v-if="category.description"
|
|
37
|
-
class="text-white/90 text-[15px] text-pretty mt-1"
|
|
38
|
-
>
|
|
32
|
+
</h1>
|
|
33
|
+
<p v-if="category.description">
|
|
39
34
|
{{ category.description }}
|
|
40
|
-
</
|
|
35
|
+
</p>
|
|
41
36
|
</div>
|
|
42
37
|
</div>
|
|
43
38
|
<UPageCard
|
|
@@ -51,3 +46,15 @@ defineProps<{
|
|
|
51
46
|
}"
|
|
52
47
|
/>
|
|
53
48
|
</template>
|
|
49
|
+
|
|
50
|
+
<style scoped>
|
|
51
|
+
@import "tailwindcss";
|
|
52
|
+
|
|
53
|
+
h1 {
|
|
54
|
+
@apply text-white text-4xl md:text-5xl lg:text-6xl font-extrabold leading-none tracking-tighter mb-3;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
p {
|
|
58
|
+
@apply text-white/90 text-[16px] text-pretty mt-1;
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
@@ -16,21 +16,13 @@ const searchCriteria = {
|
|
|
16
16
|
"description",
|
|
17
17
|
"calculatedPrice",
|
|
18
18
|
"translated",
|
|
19
|
-
"categories",
|
|
20
19
|
"properties",
|
|
21
20
|
"propertyIds",
|
|
22
|
-
"options",
|
|
23
|
-
"optionIds",
|
|
24
|
-
"configuratorSettings",
|
|
25
|
-
"children",
|
|
26
|
-
"parentId",
|
|
27
21
|
"sortedProperties",
|
|
28
22
|
"cover",
|
|
29
|
-
"parentId",
|
|
30
23
|
],
|
|
31
24
|
property: ["id", "name", "translated", "options"],
|
|
32
25
|
property_group_option: ["id", "name", "translated", "group"],
|
|
33
|
-
product_configurator_setting: ["id", "optionId", "option", "productId"],
|
|
34
26
|
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
35
27
|
},
|
|
36
28
|
associations: {
|
|
@@ -39,57 +31,14 @@ const searchCriteria = {
|
|
|
39
31
|
media: {},
|
|
40
32
|
},
|
|
41
33
|
},
|
|
42
|
-
categories: {},
|
|
43
34
|
properties: {
|
|
44
35
|
associations: {
|
|
45
36
|
group: {},
|
|
46
37
|
},
|
|
47
38
|
},
|
|
48
|
-
options: {
|
|
49
|
-
associations: {
|
|
50
|
-
group: {},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
configuratorSettings: {
|
|
54
|
-
associations: {
|
|
55
|
-
option: {
|
|
56
|
-
associations: {
|
|
57
|
-
group: {},
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
children: {
|
|
63
|
-
associations: {
|
|
64
|
-
properties: {
|
|
65
|
-
associations: {
|
|
66
|
-
group: {},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
options: {
|
|
70
|
-
associations: {
|
|
71
|
-
group: {},
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
39
|
},
|
|
77
40
|
} as operations["searchPage post /search"]["body"];
|
|
78
41
|
|
|
79
|
-
const defaultSorting = {
|
|
80
|
-
sort: [
|
|
81
|
-
{
|
|
82
|
-
field: "productNumber",
|
|
83
|
-
order: "ASC",
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const searchCriteriaWithSorting = {
|
|
89
|
-
...searchCriteria,
|
|
90
|
-
...defaultSorting,
|
|
91
|
-
} as operations["searchPage post /search"]["body"];
|
|
92
|
-
|
|
93
42
|
const {
|
|
94
43
|
resetFilters,
|
|
95
44
|
loading,
|
|
@@ -149,30 +98,27 @@ const selectedListingFilters = computed<ShortcutFilterParam[]>(() => {
|
|
|
149
98
|
});
|
|
150
99
|
|
|
151
100
|
await useAsyncData(`listing${categoryId.value}`, async () => {
|
|
152
|
-
await search(
|
|
101
|
+
await search(searchCriteria);
|
|
153
102
|
});
|
|
154
103
|
|
|
155
|
-
watch(selectedListingFilters, () => {
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
} else {
|
|
159
|
-
setCurrentFilters(selectedListingFilters.value);
|
|
104
|
+
watch(selectedListingFilters, (newFilters, oldFilters) => {
|
|
105
|
+
if (newFilters[0]?.value === oldFilters?.[0]?.value) {
|
|
106
|
+
return;
|
|
160
107
|
}
|
|
108
|
+
setCurrentFilters(newFilters);
|
|
109
|
+
currentSorting.value = "Sortieren";
|
|
161
110
|
});
|
|
162
111
|
|
|
163
|
-
watch(currentSorting, () => {
|
|
164
|
-
const
|
|
112
|
+
watch(currentSorting, async () => {
|
|
113
|
+
const sortingQuery = {
|
|
165
114
|
query: getCurrentFilters.value?.search,
|
|
166
115
|
properties: getCurrentFilters.value?.properties?.join("|"),
|
|
167
116
|
};
|
|
168
|
-
|
|
169
|
-
changeCurrentSortingOrder(currentSorting.value as string, q);
|
|
117
|
+
await changeCurrentSortingOrder(currentSorting.value as string, sortingQuery);
|
|
170
118
|
});
|
|
171
119
|
|
|
172
120
|
async function handleFilterRest() {
|
|
173
121
|
await resetFilters();
|
|
174
|
-
selectedPropertyFilters.value = [];
|
|
175
|
-
currentSorting.value = "number-asc";
|
|
176
122
|
}
|
|
177
123
|
|
|
178
124
|
const moreThanOneFilterAndOption = computed<boolean>(
|
|
@@ -278,7 +224,7 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
278
224
|
<template #right>
|
|
279
225
|
<UPageAside>
|
|
280
226
|
<div v-if="moreThanOneFilterAndOption" class="flex flex-col gap-4">
|
|
281
|
-
<
|
|
227
|
+
<h2 class="text-3xl md:text-4xl mt-8 mb-3 pb-2">Filter</h2>
|
|
282
228
|
<div
|
|
283
229
|
v-for="filter in propertyFilters"
|
|
284
230
|
:key="filter.id"
|
|
@@ -127,6 +127,17 @@ const cartQuickViewOpen = ref(false);
|
|
|
127
127
|
icon="i-lucide-shopping-cart"
|
|
128
128
|
/>
|
|
129
129
|
</UChip>
|
|
130
|
+
|
|
131
|
+
<template #header>
|
|
132
|
+
<h2 class="text-3xl md:text-4xl mt-8 mb-3 pb-2">
|
|
133
|
+
<UIcon
|
|
134
|
+
name="i-lucide-shopping-cart"
|
|
135
|
+
class="size-8"
|
|
136
|
+
color="primary"
|
|
137
|
+
/>
|
|
138
|
+
Warenkorb
|
|
139
|
+
</h2>
|
|
140
|
+
</template>
|
|
130
141
|
<template #body>
|
|
131
142
|
<CartQuickView
|
|
132
143
|
:with-to-cart-button="true"
|
|
@@ -87,7 +87,7 @@ const mainIngredients = computed<Schemas["PropertyGroupOption"][]>(() => {
|
|
|
87
87
|
>
|
|
88
88
|
|
|
89
89
|
<p class="text-base text-pretty font-semibold text-highlighted">
|
|
90
|
-
{{ product.translated.name }}
|
|
90
|
+
{{ product.translated.name ?? product.name }}
|
|
91
91
|
</p>
|
|
92
92
|
</div>
|
|
93
93
|
</template>
|
|
@@ -123,7 +123,10 @@ const mainIngredients = computed<Schemas["PropertyGroupOption"][]>(() => {
|
|
|
123
123
|
</div>
|
|
124
124
|
<UCollapsible v-model:open="openDetails" class="flex flex-col gap-2">
|
|
125
125
|
<template #content>
|
|
126
|
-
<ProductDetail2
|
|
126
|
+
<ProductDetail2
|
|
127
|
+
:product-id="product.id"
|
|
128
|
+
@product-added="toggleDetails"
|
|
129
|
+
/>
|
|
127
130
|
</template>
|
|
128
131
|
</UCollapsible>
|
|
129
132
|
</template>
|
|
@@ -1,14 +1,81 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Schemas } from "#shopware";
|
|
2
|
+
import type { operations, Schemas } from "#shopware";
|
|
3
3
|
import type { AssociationItemProduct } from "~/types/Association";
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{
|
|
6
|
-
|
|
6
|
+
productId: string;
|
|
7
7
|
}>();
|
|
8
8
|
|
|
9
9
|
const { apiClient } = useShopwareContext();
|
|
10
10
|
|
|
11
|
-
const productId = toRef(props.
|
|
11
|
+
const productId = toRef(props.productId);
|
|
12
|
+
|
|
13
|
+
const searchCriteria = {
|
|
14
|
+
includes: {
|
|
15
|
+
product: [
|
|
16
|
+
"id",
|
|
17
|
+
"productNumber",
|
|
18
|
+
"name",
|
|
19
|
+
"description",
|
|
20
|
+
"calculatedPrice",
|
|
21
|
+
"translated",
|
|
22
|
+
"properties",
|
|
23
|
+
"propertyIds",
|
|
24
|
+
"options",
|
|
25
|
+
"optionIds",
|
|
26
|
+
"configuratorSettings",
|
|
27
|
+
"children",
|
|
28
|
+
"parentId",
|
|
29
|
+
"sortedProperties",
|
|
30
|
+
"cover",
|
|
31
|
+
],
|
|
32
|
+
property: ["id", "name", "translated", "options"],
|
|
33
|
+
property_group_option: ["id", "name", "translated", "group"],
|
|
34
|
+
product_configurator_setting: ["id", "optionId", "option", "productId"],
|
|
35
|
+
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
36
|
+
},
|
|
37
|
+
associations: {
|
|
38
|
+
cover: {
|
|
39
|
+
associations: {
|
|
40
|
+
media: {},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
properties: {
|
|
44
|
+
associations: {
|
|
45
|
+
group: {},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
options: {
|
|
49
|
+
associations: {
|
|
50
|
+
group: {},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
configuratorSettings: {
|
|
54
|
+
associations: {
|
|
55
|
+
option: {
|
|
56
|
+
associations: {
|
|
57
|
+
group: {},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
children: {
|
|
63
|
+
associations: {
|
|
64
|
+
properties: {
|
|
65
|
+
associations: {
|
|
66
|
+
group: {},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
options: {
|
|
70
|
+
associations: {
|
|
71
|
+
group: {},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
} as operations["searchPage post /search"]["body"];
|
|
78
|
+
|
|
12
79
|
const { data: productDetails, pending } = useAsyncData(
|
|
13
80
|
() => `product-${productId.value ?? "none"}`,
|
|
14
81
|
async () => {
|
|
@@ -17,6 +84,7 @@ const { data: productDetails, pending } = useAsyncData(
|
|
|
17
84
|
"readProductDetail post /product/{productId}",
|
|
18
85
|
{
|
|
19
86
|
pathParams: { productId: productId.value },
|
|
87
|
+
body: searchCriteria,
|
|
20
88
|
},
|
|
21
89
|
);
|
|
22
90
|
return response.data;
|
|
@@ -65,7 +133,7 @@ const emit = defineEmits(["product-added"]);
|
|
|
65
133
|
<div v-if="productDetails?.configurator">
|
|
66
134
|
<ProductConfigurator2
|
|
67
135
|
v-if="productDetails?.configurator"
|
|
68
|
-
:p="product"
|
|
136
|
+
:p="productDetails.product"
|
|
69
137
|
:c="productDetails.configurator"
|
|
70
138
|
@variant-switched="onVariantSwitched"
|
|
71
139
|
/>
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Schemas } from "#shopware";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
showMenuButton?: boolean;
|
|
7
|
+
useGridLayout?: boolean;
|
|
8
|
+
}>(),
|
|
9
|
+
{
|
|
10
|
+
showMenuButton: false,
|
|
11
|
+
useGridLayout: false,
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const { getWishlistProducts, items } = useWishlist();
|
|
5
16
|
const { apiClient } = useShopwareContext();
|
|
17
|
+
const {
|
|
18
|
+
isAddingToCart,
|
|
19
|
+
addingItemId,
|
|
20
|
+
isLoading,
|
|
21
|
+
clearWishlistHandler,
|
|
22
|
+
addSingleItemToCart,
|
|
23
|
+
addAllItemsToCart,
|
|
24
|
+
} = useWishlistActions();
|
|
25
|
+
|
|
6
26
|
const products = ref<Schemas["Product"][]>([]);
|
|
7
|
-
const isLoading = ref(false);
|
|
8
27
|
|
|
9
|
-
const clearWishlistHandler = async () => {
|
|
10
|
-
try {
|
|
11
|
-
isLoading.value = true;
|
|
12
|
-
clearWishlist();
|
|
13
|
-
} finally {
|
|
14
|
-
isLoading.value = false;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
28
|
const loadProductsByItemIds = async (itemIds: string[]): Promise<void> => {
|
|
18
29
|
isLoading.value = true;
|
|
19
30
|
|
|
@@ -56,33 +67,159 @@ onMounted(async () => {
|
|
|
56
67
|
</script>
|
|
57
68
|
|
|
58
69
|
<template>
|
|
59
|
-
<div>
|
|
60
|
-
|
|
61
|
-
<div
|
|
70
|
+
<div class="flex flex-col gap-6">
|
|
71
|
+
<!-- Primary Action - Add All to Cart -->
|
|
72
|
+
<div v-if="products.length > 0">
|
|
73
|
+
<UButton
|
|
74
|
+
icon="i-lucide-shopping-cart"
|
|
75
|
+
color="primary"
|
|
76
|
+
variant="solid"
|
|
77
|
+
size="xl"
|
|
78
|
+
block
|
|
79
|
+
:loading="isAddingToCart"
|
|
80
|
+
class="font-semibold shadow-md"
|
|
81
|
+
@click="addAllItemsToCart(products)"
|
|
82
|
+
>
|
|
83
|
+
Alle in den Warenkorb ({{ products.length }})
|
|
84
|
+
</UButton>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Empty State -->
|
|
88
|
+
<UPageCard v-if="products.length === 0 && !isLoading" class="text-center">
|
|
89
|
+
<div class="flex flex-col items-center justify-center gap-4 py-8">
|
|
90
|
+
<UIcon name="i-lucide-heart-off" class="size-16 text-neutral-400" />
|
|
91
|
+
<div class="flex flex-col gap-2">
|
|
92
|
+
<h3 class="text-lg font-semibold text-highlighted">
|
|
93
|
+
Keine Produkte auf der Merkliste
|
|
94
|
+
</h3>
|
|
95
|
+
<p class="text-sm text-neutral-500">
|
|
96
|
+
Füge deine Lieblingsprodukte zur Merkliste hinzu, um sie später
|
|
97
|
+
schnell wiederzufinden.
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
<NuxtLink v-if="showMenuButton" to="/speisekarte">
|
|
101
|
+
<UButton
|
|
102
|
+
icon="i-lucide-utensils"
|
|
103
|
+
color="primary"
|
|
104
|
+
variant="solid"
|
|
105
|
+
size="lg"
|
|
106
|
+
>
|
|
107
|
+
Zur Speisekarte
|
|
108
|
+
</UButton>
|
|
109
|
+
</NuxtLink>
|
|
110
|
+
</div>
|
|
111
|
+
</UPageCard>
|
|
112
|
+
|
|
113
|
+
<!-- Loading State -->
|
|
114
|
+
<div v-if="isLoading" class="flex flex-col gap-4">
|
|
115
|
+
<USkeleton class="h-32 w-full" />
|
|
116
|
+
<USkeleton class="h-32 w-full" />
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<!-- Products List -->
|
|
120
|
+
<div v-if="products.length > 0" class="flex flex-col gap-4">
|
|
121
|
+
<!-- Product Items -->
|
|
62
122
|
<div
|
|
63
|
-
|
|
64
|
-
|
|
123
|
+
:class="
|
|
124
|
+
props.useGridLayout
|
|
125
|
+
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
|
|
126
|
+
: 'flex flex-col gap-4'
|
|
127
|
+
"
|
|
65
128
|
>
|
|
66
|
-
<
|
|
67
|
-
|
|
129
|
+
<div v-for="product in products" :key="product.id">
|
|
130
|
+
<div class="relative">
|
|
131
|
+
<UPageCard
|
|
132
|
+
variant="outline"
|
|
133
|
+
:ui="{ root: 'shadow-sm hover:shadow-md transition-shadow' }"
|
|
134
|
+
>
|
|
135
|
+
<div class="flex flex-col gap-3">
|
|
136
|
+
<!-- Product Info with Heart Button -->
|
|
137
|
+
<div class="flex flex-row gap-3 items-start justify-between">
|
|
138
|
+
<div class="flex flex-row gap-3 items-start flex-1 min-w-0">
|
|
139
|
+
<div
|
|
140
|
+
v-if="product.cover?.media?.url"
|
|
141
|
+
class="flex-shrink-0 w-16 h-16"
|
|
142
|
+
>
|
|
143
|
+
<NuxtImg
|
|
144
|
+
:src="product.cover.media.url"
|
|
145
|
+
class="rounded-md w-full h-full object-cover"
|
|
146
|
+
sizes="64px"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="flex-1 min-w-0">
|
|
150
|
+
<div class="flex flex-col gap-1">
|
|
151
|
+
<span class="text-xs text-brand-500 font-medium">
|
|
152
|
+
#{{ product.productNumber }}
|
|
153
|
+
</span>
|
|
154
|
+
<h3
|
|
155
|
+
class="text-sm font-semibold text-highlighted line-clamp-2 leading-tight"
|
|
156
|
+
>
|
|
157
|
+
{{ product.translated.name }}
|
|
158
|
+
</h3>
|
|
159
|
+
<p class="text-base font-bold text-primary-600">
|
|
160
|
+
{{
|
|
161
|
+
usePrice({
|
|
162
|
+
currencyCode: "EUR",
|
|
163
|
+
localeCode: "de-DE",
|
|
164
|
+
}).getFormattedPrice(
|
|
165
|
+
product.calculatedPrice.totalPrice,
|
|
166
|
+
)
|
|
167
|
+
}}
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<!-- Heart Button -->
|
|
173
|
+
<div class="flex-shrink-0">
|
|
174
|
+
<AddToWishlist :product="product" />
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Add to Cart Button -->
|
|
179
|
+
<UButton
|
|
180
|
+
icon="i-lucide-shopping-cart"
|
|
181
|
+
color="primary"
|
|
182
|
+
variant="solid"
|
|
183
|
+
size="md"
|
|
184
|
+
block
|
|
185
|
+
:loading="addingItemId === product.id"
|
|
186
|
+
@click="addSingleItemToCart(product)"
|
|
187
|
+
>
|
|
188
|
+
In den Warenkorb
|
|
189
|
+
</UButton>
|
|
190
|
+
</div>
|
|
191
|
+
</UPageCard>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
68
194
|
</div>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
:with-favorite-button="true"
|
|
75
|
-
:with-add-to-cart-button="false"
|
|
76
|
-
/>
|
|
195
|
+
|
|
196
|
+
<!-- Secondary Actions -->
|
|
197
|
+
<div
|
|
198
|
+
class="pt-4 border-t border-gray-200 dark:border-gray-800 flex flex-col sm:flex-row gap-2"
|
|
199
|
+
>
|
|
77
200
|
<UButton
|
|
78
|
-
|
|
201
|
+
icon="i-lucide-trash-2"
|
|
79
202
|
color="neutral"
|
|
80
|
-
variant="
|
|
81
|
-
|
|
203
|
+
variant="ghost"
|
|
204
|
+
size="md"
|
|
205
|
+
class="flex-1"
|
|
206
|
+
:disabled="isLoading"
|
|
82
207
|
@click="clearWishlistHandler"
|
|
83
208
|
>
|
|
84
|
-
|
|
209
|
+
Merkliste leeren
|
|
85
210
|
</UButton>
|
|
211
|
+
|
|
212
|
+
<NuxtLink v-if="showMenuButton" to="/speisekarte" class="flex-1">
|
|
213
|
+
<UButton
|
|
214
|
+
icon="i-lucide-utensils"
|
|
215
|
+
color="neutral"
|
|
216
|
+
variant="outline"
|
|
217
|
+
size="md"
|
|
218
|
+
block
|
|
219
|
+
>
|
|
220
|
+
Zur Speisekarte
|
|
221
|
+
</UButton>
|
|
222
|
+
</NuxtLink>
|
|
86
223
|
</div>
|
|
87
224
|
</div>
|
|
88
225
|
</div>
|
|
@@ -79,9 +79,9 @@ export function useAddToCart() {
|
|
|
79
79
|
if (extras.length === 0 && deselectedIngredients.value.length === 0) {
|
|
80
80
|
return [
|
|
81
81
|
{
|
|
82
|
-
id: selectedProduct.value.id,
|
|
83
82
|
quantity: selectedQuantity.value,
|
|
84
83
|
type: LINE_ITEM_PRODUCT,
|
|
84
|
+
referencedId: selectedProduct.value.id,
|
|
85
85
|
},
|
|
86
86
|
];
|
|
87
87
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { Schemas } from "#shopware";
|
|
2
|
+
|
|
3
|
+
export function useWishlistActions() {
|
|
4
|
+
const { addProducts, refreshCart } = useCart();
|
|
5
|
+
const toast = useToast();
|
|
6
|
+
const { triggerProductAdded } = useProductEvents();
|
|
7
|
+
const { clearWishlist } = useWishlist();
|
|
8
|
+
|
|
9
|
+
const isAddingToCart = ref(false);
|
|
10
|
+
const addingItemId = ref<string | null>(null);
|
|
11
|
+
const isLoading = ref(false);
|
|
12
|
+
|
|
13
|
+
const clearWishlistHandler = async () => {
|
|
14
|
+
try {
|
|
15
|
+
isLoading.value = true;
|
|
16
|
+
clearWishlist();
|
|
17
|
+
toast.add({
|
|
18
|
+
title: "Merkliste geleert",
|
|
19
|
+
description: "Alle Produkte wurden von der Merkliste entfernt.",
|
|
20
|
+
icon: "i-lucide-trash",
|
|
21
|
+
color: "neutral",
|
|
22
|
+
});
|
|
23
|
+
} finally {
|
|
24
|
+
isLoading.value = false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const addSingleItemToCart = async (product: Schemas["Product"]) => {
|
|
29
|
+
try {
|
|
30
|
+
addingItemId.value = product.id;
|
|
31
|
+
|
|
32
|
+
// Check if this is a base product with variants
|
|
33
|
+
const isBaseProduct = product.childCount && product.childCount > 0;
|
|
34
|
+
if (isBaseProduct) {
|
|
35
|
+
toast.add({
|
|
36
|
+
title: "Variante erforderlich",
|
|
37
|
+
description: `${product.translated.name} hat Varianten. Bitte wähle eine spezifische Variante aus.`,
|
|
38
|
+
icon: "i-lucide-alert-circle",
|
|
39
|
+
color: "warning",
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cartItems = [
|
|
45
|
+
{
|
|
46
|
+
id: product.id,
|
|
47
|
+
quantity: 1,
|
|
48
|
+
type: "product" as const,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const newCart = await addProducts(cartItems);
|
|
53
|
+
await refreshCart(newCart);
|
|
54
|
+
|
|
55
|
+
triggerProductAdded();
|
|
56
|
+
|
|
57
|
+
toast.add({
|
|
58
|
+
title: "In den Warenkorb gelegt",
|
|
59
|
+
description: `${product.translated.name} wurde hinzugefügt.`,
|
|
60
|
+
icon: "i-lucide-shopping-cart",
|
|
61
|
+
color: "primary",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
useTrackEvent("add_to_cart", {
|
|
65
|
+
props: {
|
|
66
|
+
product_number: product.productNumber,
|
|
67
|
+
quantity: 1,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("[wishlist][addSingleItemToCart] Error details:", error);
|
|
72
|
+
toast.add({
|
|
73
|
+
title: "Fehler",
|
|
74
|
+
description: "Produkt konnte nicht hinzugefügt werden.",
|
|
75
|
+
icon: "i-lucide-alert-circle",
|
|
76
|
+
color: "error",
|
|
77
|
+
});
|
|
78
|
+
} finally {
|
|
79
|
+
addingItemId.value = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const addAllItemsToCart = async (products: Schemas["Product"][]) => {
|
|
84
|
+
if (products.length === 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
isAddingToCart.value = true;
|
|
90
|
+
|
|
91
|
+
// Filter out base products (parent products with childCount > 0)
|
|
92
|
+
// Only add actual variants or simple products
|
|
93
|
+
const addableProducts = products.filter((product) => {
|
|
94
|
+
const isBaseProduct = product.childCount && product.childCount > 0;
|
|
95
|
+
return !isBaseProduct;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (addableProducts.length === 0) {
|
|
99
|
+
toast.add({
|
|
100
|
+
title: "Keine Produkte hinzugefügt",
|
|
101
|
+
description: "Bitte wähle zuerst Varianten für deine Produkte aus.",
|
|
102
|
+
icon: "i-lucide-alert-circle",
|
|
103
|
+
color: "warning",
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const cartItems = addableProducts.map((product) => ({
|
|
109
|
+
id: product.id,
|
|
110
|
+
quantity: 1,
|
|
111
|
+
type: "product" as const,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
const newCart = await addProducts(cartItems);
|
|
115
|
+
await refreshCart(newCart);
|
|
116
|
+
|
|
117
|
+
triggerProductAdded();
|
|
118
|
+
|
|
119
|
+
const skippedCount = products.length - addableProducts.length;
|
|
120
|
+
const successMessage =
|
|
121
|
+
skippedCount > 0
|
|
122
|
+
? `${addableProducts.length} Produkte hinzugefügt. ${skippedCount} Produkt(e) übersprungen (Varianten müssen einzeln ausgewählt werden).`
|
|
123
|
+
: `${addableProducts.length} Produkte wurden in den Warenkorb gelegt.`;
|
|
124
|
+
|
|
125
|
+
toast.add({
|
|
126
|
+
title: "Produkte hinzugefügt",
|
|
127
|
+
description: successMessage,
|
|
128
|
+
icon: "i-lucide-shopping-cart",
|
|
129
|
+
color: "primary",
|
|
130
|
+
});
|
|
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
|
+
} catch (error) {
|
|
140
|
+
console.error("[wishlist][addAllItemsToCart] Error:", error);
|
|
141
|
+
toast.add({
|
|
142
|
+
title: "Fehler",
|
|
143
|
+
description: "Produkte konnten nicht hinzugefügt werden.",
|
|
144
|
+
icon: "i-lucide-alert-circle",
|
|
145
|
+
color: "error",
|
|
146
|
+
});
|
|
147
|
+
} finally {
|
|
148
|
+
isAddingToCart.value = false;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
isAddingToCart,
|
|
154
|
+
addingItemId,
|
|
155
|
+
isLoading,
|
|
156
|
+
clearWishlistHandler,
|
|
157
|
+
addSingleItemToCart,
|
|
158
|
+
addAllItemsToCart,
|
|
159
|
+
};
|
|
160
|
+
}
|
package/app/pages/merkliste.vue
CHANGED
|
@@ -12,6 +12,12 @@ useSeoMeta({
|
|
|
12
12
|
|
|
13
13
|
<template>
|
|
14
14
|
<UContainer>
|
|
15
|
-
<
|
|
15
|
+
<UPageSection
|
|
16
|
+
title="Merkliste"
|
|
17
|
+
description="Verwalte deine Lieblingsprodukte."
|
|
18
|
+
icon="i-lucide-heart"
|
|
19
|
+
>
|
|
20
|
+
<Wishlist :show-menu-button="true" :use-grid-layout="true" />
|
|
21
|
+
</UPageSection>
|
|
16
22
|
</UContainer>
|
|
17
23
|
</template>
|