@shopbite-de/storefront 1.2.5 → 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.
@@ -1,19 +1,30 @@
1
1
  <script setup lang="ts">
2
2
  import type { Schemas } from "#shopware";
3
3
 
4
- const { getWishlistProducts, items, clearWishlist } = useWishlist();
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
- <h2 class="text-2xl">Merkliste</h2>
61
- <div class="flex flex-col gap-4 h-full">
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
- v-if="products.length === 0"
64
- class="flex flex-row items-center gap-2"
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
- <UIcon name="i-lucide-frown" class="size-5" />
67
- <p>Keine Produkte</p>
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
- <div class="flex flex-col gap-4">
70
- <ProductCard
71
- v-for="product in products"
72
- :key="product.id"
73
- :product="product"
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
- v-if="products.length > 0"
201
+ icon="i-lucide-trash-2"
79
202
  color="neutral"
80
- variant="outline"
81
- icon="i-lucide-trash"
203
+ variant="ghost"
204
+ size="md"
205
+ class="flex-1"
206
+ :disabled="isLoading"
82
207
  @click="clearWishlistHandler"
83
208
  >
84
- Alles löschen
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
+ }
@@ -12,6 +12,12 @@ useSeoMeta({
12
12
 
13
13
  <template>
14
14
  <UContainer>
15
- <Wishlist />
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [