@shopbite-de/storefront 1.18.0 → 1.18.2

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/app.vue CHANGED
@@ -7,11 +7,11 @@ const TOAST_CONFIG = {
7
7
  title: "Wir haben geöffnet!",
8
8
  color: "primary" as const,
9
9
  progress: false,
10
- duration: 0,
10
+ duration: 5000,
11
11
  icon: "i-lucide-party-popper",
12
12
  actions: [
13
13
  {
14
- icon: "i-lucide-pizza",
14
+ icon: "i-lucide-utensils-crossed",
15
15
  label: "Zur Speisekarte",
16
16
  color: "neutral" as const,
17
17
  variant: "outline" as const,
@@ -64,7 +64,7 @@ const handleRemoveItem = () => {
64
64
  ? cartItem?.children?.[0]?.payload?.options
65
65
  : cartItem?.payload?.options"
66
66
  :key="option.group + option.option"
67
- class="text-sm text-pretty text-toned mt-1"
67
+ class="text-sm text-pretty text-muted mt-1"
68
68
  >
69
69
  {{ option.group }}: {{ option.option }}
70
70
  </p>
@@ -84,14 +84,14 @@ const handleRemoveItem = () => {
84
84
  :min="1"
85
85
  :max="100"
86
86
  class="max-w-46"
87
- aria-label="Item quantity"
87
+ aria-label="Menge"
88
88
  />
89
89
  <UButton
90
90
  v-if="withDeleteButton"
91
91
  icon="i-lucide-trash"
92
92
  variant="soft"
93
93
  color="neutral"
94
- aria-label="Remove item from cart"
94
+ aria-label="Artikel entfernen"
95
95
  @click="handleRemoveItem"
96
96
  />
97
97
  </div>
@@ -101,14 +101,14 @@ const handleRemoveItem = () => {
101
101
  icon="i-lucide-trash"
102
102
  variant="outline"
103
103
  color="error"
104
- aria-label="Remove item from cart"
104
+ aria-label="Artikel entfernen"
105
105
  @click="handleRemoveItem"
106
106
  />
107
107
  </div>
108
108
 
109
109
  <!-- Empty state -->
110
110
  <div v-else class="text-center py-4">
111
- <p class="text-toned">Warenkorb ist leer...</p>
111
+ <p class="text-muted">Warenkorb ist leer...</p>
112
112
  </div>
113
113
  </div>
114
114
  </template>
@@ -46,12 +46,14 @@ const {
46
46
  loading,
47
47
  search,
48
48
  getElements,
49
+ getCurrentListing,
49
50
  getCurrentSortingOrder,
50
51
  getSortingOrders,
51
52
  changeCurrentSortingOrder,
52
53
  getAvailableFilters,
53
54
  getCurrentFilters,
54
55
  setCurrentFilters,
56
+ setInitialListing,
55
57
  } = useListing({
56
58
  listingType: "categoryListing",
57
59
  categoryId: props.id,
@@ -85,9 +87,27 @@ const selectedListingFilters = computed<ShortcutFilterParam[]>(() => {
85
87
  ];
86
88
  });
87
89
 
88
- await useAsyncData(`listing${categoryId.value}`, async () => {
89
- await search(searchCriteria);
90
- });
90
+ const nuxtApp = useNuxtApp();
91
+ const { data: listingPayload, pending } = await useAsyncData(
92
+ `listing${categoryId.value}`,
93
+ async () => {
94
+ await search(searchCriteria);
95
+ // Return the result so it gets serialized into the SSR payload.
96
+ // On the client, useAsyncData will restore this without re-running search().
97
+ return getCurrentListing.value;
98
+ },
99
+ );
100
+
101
+ // Populate useListing state from the SSR payload on the client.
102
+ // useListing uses plain refs (not useState), so its state is not automatically
103
+ // hydrated — we restore it via setInitialListing.
104
+ if (listingPayload.value) {
105
+ await setInitialListing(listingPayload.value);
106
+ }
107
+
108
+ // During SSR hydration, pending may briefly be true before the payload cache is applied.
109
+ // Suppress the skeleton in that window to prevent a hydration mismatch.
110
+ const showSkeleton = computed(() => pending.value && !nuxtApp.isHydrating);
91
111
 
92
112
  watch(selectedListingFilters, (newFilters, oldFilters) => {
93
113
  if (newFilters[0]?.value === oldFilters?.[0]?.value) {
@@ -196,9 +216,20 @@ const moreThanOneFilterAndOption = computed<boolean>(
196
216
  </UDrawer>
197
217
  </div>
198
218
 
199
- <Loading v-if="loading" text="Produkte werden geladen..." size="lg" />
219
+ <div
220
+ v-if="showSkeleton"
221
+ class="flex flex-col gap-4"
222
+ aria-busy="true"
223
+ aria-label="Produkte werden geladen"
224
+ >
225
+ <LazyProductCardSkeleton v-for="i in 6" :key="i" />
226
+ </div>
200
227
 
201
- <div v-else class="flex flex-col gap-4">
228
+ <div
229
+ v-else
230
+ class="flex flex-col gap-4 transition-opacity duration-200"
231
+ :class="{ 'opacity-40 pointer-events-none': loading }"
232
+ >
202
233
  <ProductCard
203
234
  v-for="product in getElements"
204
235
  :key="product.id"
@@ -136,24 +136,26 @@ function handleTimeInput(event: Event): void {
136
136
  <template>
137
137
  <div v-if="isClosedHoliday(now) === false" class="flex flex-col gap-2 mt-4">
138
138
  <div class="flex flex-row items-center justify-between gap-4">
139
- <div>Wunschlieferung- oder Abholzeit ab:</div>
139
+ <label for="delivery-time" class="flex-1">
140
+ Wunschlieferung- oder Abholzeit ab:
141
+ </label>
140
142
  <client-only>
141
- <input
143
+ <UInput
144
+ id="delivery-time"
142
145
  type="time"
143
146
  :min="minTime ?? undefined"
144
147
  :max="maxTime ?? undefined"
145
148
  :disabled="isClosedToday || isClosedHoliday(now) === true"
146
- :value="selected"
149
+ :model-value="selected"
147
150
  step="300"
148
- class="border rounded px-2 py-1"
149
151
  @input="handleTimeInput"
150
152
  />
151
153
  </client-only>
152
154
  </div>
153
- <p v-if="validationError" class="text-sm text-red-600">
155
+ <p v-if="validationError" class="text-sm text-error">
154
156
  {{ validationError }}
155
157
  </p>
156
- <p v-else class="text-sm text-gray-500">{{ helperText }}</p>
158
+ <p v-else class="text-sm text-muted">{{ helperText }}</p>
157
159
  <UBadge
158
160
  :label="deliveryTimeInfo"
159
161
  icon="i-lucide-clock"
@@ -82,42 +82,40 @@ watch(
82
82
  </script>
83
83
 
84
84
  <template>
85
- <UContainer>
86
- <div class="flex flex-col md:flex-row justify-between gap-4">
87
- <div class="basis-1/2">
88
- <div class="flex flex-row items-center gap-4">
89
- <UIcon name="i-lucide-badge-euro" class="size-8" />
90
- <h2 class="text-2xl text-blackish my-8">Zahlungsarten</h2>
91
- <UButton
92
- to="/zahlung-und-versand"
93
- size="md"
94
- variant="ghost"
95
- icon="i-lucide-circle-question-mark"
96
- />
97
- </div>
98
- <URadioGroup
99
- v-model="selectedPaymentMethodId"
100
- :items="selectablePaymentMethods"
101
- variant="card"
85
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
86
+ <div class="flex flex-col gap-4">
87
+ <div class="flex items-center gap-2">
88
+ <UIcon name="i-lucide-badge-euro" class="size-5 text-muted" />
89
+ <h2 class="text-lg font-semibold">Zahlungsarten</h2>
90
+ <UButton
91
+ to="/zahlung-und-versand"
92
+ size="sm"
93
+ variant="ghost"
94
+ icon="i-lucide-circle-help"
102
95
  />
103
96
  </div>
104
- <div class="basis-1/2">
105
- <div class="flex flex-row items-center gap-4">
106
- <UIcon name="i-lucide-car" class="size-8" />
107
- <h2 class="text-2xl text-blackish my-8">Versandarten</h2>
108
- <UButton
109
- to="/zahlung-und-versand"
110
- size="md"
111
- variant="ghost"
112
- icon="i-lucide-circle-question-mark"
113
- />
114
- </div>
115
- <URadioGroup
116
- v-model="selectedShippingMethodId"
117
- :items="selectableShippingMethods"
118
- variant="card"
97
+ <URadioGroup
98
+ v-model="selectedPaymentMethodId"
99
+ :items="selectablePaymentMethods"
100
+ variant="card"
101
+ />
102
+ </div>
103
+ <div class="flex flex-col gap-4">
104
+ <div class="flex items-center gap-2">
105
+ <UIcon name="i-lucide-car" class="size-5 text-muted" />
106
+ <h2 class="text-lg font-semibold">Versandarten</h2>
107
+ <UButton
108
+ to="/zahlung-und-versand"
109
+ size="sm"
110
+ variant="ghost"
111
+ icon="i-lucide-circle-help"
119
112
  />
120
113
  </div>
114
+ <URadioGroup
115
+ v-model="selectedShippingMethodId"
116
+ :items="selectableShippingMethods"
117
+ variant="card"
118
+ />
121
119
  </div>
122
- </UContainer>
120
+ </div>
123
121
  </template>
@@ -22,7 +22,7 @@ const { paymentMethod } = toRefs(props);
22
22
  <h3 class="text-base text-pretty font-semibold text-highlighted">
23
23
  Zahlart
24
24
  </h3>
25
- <p class="text-[15px] text-pretty text-toned mt-1">
25
+ <p class="text-[15px] text-pretty text-muted mt-1">
26
26
  {{ paymentMethod.distinguishableName }}
27
27
  </p>
28
28
  </div>
@@ -22,7 +22,7 @@ const { shippingMethod } = toRefs(props);
22
22
  <h3 class="text-base text-pretty font-semibold text-highlighted">
23
23
  Versandart
24
24
  </h3>
25
- <p class="text-[15px] text-pretty text-toned mt-1">
25
+ <p class="text-[15px] text-pretty text-muted mt-1">
26
26
  {{ shippingMethod.name }}
27
27
  </p>
28
28
  </div>
@@ -23,8 +23,6 @@ onMounted(() => {
23
23
  });
24
24
  });
25
25
 
26
- watch(isCheckoutEnabled, () => console.log(isCheckoutEnabled.value));
27
-
28
26
  useIntervalFn(refresh, 10000);
29
27
 
30
28
  async function handleCreateOrder() {
@@ -63,10 +61,6 @@ const isValidToProceed = computed(
63
61
  const selectedDeliveryTime = ref("");
64
62
  const isValidTime = ref(true);
65
63
 
66
- watch(selectedDeliveryTime, (newValue) => {
67
- console.log(newValue);
68
- });
69
-
70
64
  const checkoutButtonLabel = computed<string>(() => {
71
65
  if (!customerDataAvailable.value) {
72
66
  return "Bitte einloggen oder Kundendaten erfassen";
@@ -85,52 +79,47 @@ const checkoutButtonLabel = computed<string>(() => {
85
79
  </script>
86
80
 
87
81
  <template>
88
- <UContainer class="my-16">
89
- <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
82
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8 py-8">
83
+ <div class="flex flex-col gap-6">
90
84
  <div class="flex flex-col gap-4">
91
- <h3 class="text-xl font-bold">Kundendaten</h3>
85
+ <h3 class="text-lg font-semibold">Kundendaten</h3>
92
86
  <UserDetail v-if="customerDataAvailable" />
93
- <div v-else>Bitte vorher einloggen oder Kundendaten erfassen</div>
94
- <div class="flex flex-col gap-4">
95
- <h3 class="text-xl font-bold">Versand- und Zahlung</h3>
96
- <CheckoutPaymentMethod :payment-method="selectedPaymentMethod" />
97
- <CheckoutShippingMethod :shipping-method="selectedShippingMethod" />
98
- <CheckoutDeliveryTimeSelect
99
- v-model:valid="isValidTime"
100
- v-model="selectedDeliveryTime"
101
- />
102
- </div>
87
+ <p v-else class="text-muted">
88
+ Bitte vorher einloggen oder Kundendaten erfassen
89
+ </p>
103
90
  </div>
104
91
  <div class="flex flex-col gap-4">
105
- <h3 class="text-xl font-bold">Warenkorb</h3>
106
- <div class="rounded-lg flex flex-col gap-4">
107
- <QuickView
108
- :with-quantity-input="false"
109
- :with-delete-button="false"
110
- class="p-6 bg-elevated"
111
- />
112
- <CheckoutVoucherInput />
113
- <UButton
114
- :icon="
115
- isValidToProceed ? 'i-lucide-shopping-cart' : 'i-lucide-lock'
116
- "
117
- :disabled="!isValidToProceed"
118
- :label="checkoutButtonLabel"
119
- size="xl"
120
- block
121
- @click="handleCreateOrder"
122
- />
123
- <p class="font-light text-muted">
124
- Mit Klick auf den Button "Jetzt bestellen!" erklärst du dich mit
125
- unseren
126
- <ULink to="/agb" class="text-primary font-medium">AGB</ULink> und
127
- <ULink to="/datenschutz" class="text-primary font-medium"
128
- >Datenschutzbestimmungen</ULink
129
- >
130
- einverstanden.
131
- </p>
132
- </div>
92
+ <h3 class="text-lg font-semibold">Versand & Zahlung</h3>
93
+ <CheckoutPaymentMethod :payment-method="selectedPaymentMethod" />
94
+ <CheckoutShippingMethod :shipping-method="selectedShippingMethod" />
95
+ <CheckoutDeliveryTimeSelect
96
+ v-model:valid="isValidTime"
97
+ v-model="selectedDeliveryTime"
98
+ />
133
99
  </div>
134
100
  </div>
135
- </UContainer>
101
+ <div class="flex flex-col gap-4">
102
+ <h3 class="text-lg font-semibold">Warenkorb</h3>
103
+ <UCard>
104
+ <QuickView :with-quantity-input="false" :with-delete-button="false" />
105
+ </UCard>
106
+ <CheckoutVoucherInput />
107
+ <UButton
108
+ :icon="isValidToProceed ? 'i-lucide-shopping-cart' : 'i-lucide-lock'"
109
+ :disabled="!isValidToProceed"
110
+ :label="checkoutButtonLabel"
111
+ size="xl"
112
+ block
113
+ @click="handleCreateOrder"
114
+ />
115
+ <p class="text-sm text-muted">
116
+ Mit Klick auf "Jetzt bestellen!" erklärst du dich mit unseren
117
+ <ULink to="/agb" class="text-primary font-medium">AGB</ULink> und
118
+ <ULink to="/datenschutz" class="text-primary font-medium"
119
+ >Datenschutzbestimmungen</ULink
120
+ >
121
+ einverstanden.
122
+ </p>
123
+ </div>
124
+ </div>
136
125
  </template>
@@ -1,14 +1,21 @@
1
1
  <script setup lang="ts">
2
- import { useNavigation } from "~/composables/useNavigation";
2
+ import { useUser } from "@shopware/composables";
3
3
 
4
- const config = useRuntimeConfig();
4
+ const { mainMenu } = useNavigation(false);
5
+ const multiChannelEnabled =
6
+ useRuntimeConfig().public.shopBite.feature.multiChannel;
5
7
 
6
- const multiChannelEnabled = computed(
7
- () => config.public.shopBite.feature.multiChannel === "true",
8
- );
8
+ const { isLoggedIn, isGuestSession, logout } = useUser();
9
+ const toast = useToast();
9
10
 
10
- console.log(multiChannelEnabled.value);
11
- const { mainMenu } = useNavigation(false);
11
+ const logoutHandler = () => {
12
+ logout();
13
+ toast.add({
14
+ title: "Tschüss!",
15
+ description: "Erfolgreich abgemeldet.",
16
+ color: "success",
17
+ });
18
+ };
12
19
  </script>
13
20
 
14
21
  <template>
@@ -18,7 +25,58 @@ const { mainMenu } = useNavigation(false);
18
25
  orientation="vertical"
19
26
  class="-mx-2.5"
20
27
  />
21
- <div v-if="multiChannelEnabled" class="my-4">
28
+
29
+ <USeparator class="my-4" />
30
+
31
+ <div class="flex flex-col gap-1">
32
+ <template v-if="isLoggedIn || isGuestSession">
33
+ <UButton
34
+ color="neutral"
35
+ variant="ghost"
36
+ to="/konto"
37
+ icon="i-lucide-user"
38
+ label="Mein Konto"
39
+ class="justify-start"
40
+ />
41
+ <UButton
42
+ color="neutral"
43
+ variant="ghost"
44
+ to="/konto/bestellungen"
45
+ icon="i-lucide-pizza"
46
+ label="Bestellungen"
47
+ class="justify-start"
48
+ />
49
+ <UButton
50
+ color="neutral"
51
+ variant="ghost"
52
+ icon="i-lucide-log-out"
53
+ label="Abmelden"
54
+ class="justify-start"
55
+ @click="logoutHandler"
56
+ />
57
+ </template>
58
+ <template v-else>
59
+ <UButton
60
+ color="neutral"
61
+ variant="ghost"
62
+ to="/anmelden"
63
+ icon="i-lucide-user"
64
+ label="Anmelden"
65
+ class="justify-start"
66
+ />
67
+ <UButton
68
+ color="neutral"
69
+ variant="ghost"
70
+ to="/registrierung"
71
+ icon="i-lucide-user-plus"
72
+ label="Registrieren"
73
+ class="justify-start"
74
+ />
75
+ </template>
76
+ </div>
77
+
78
+ <div v-if="multiChannelEnabled" class="mt-4">
79
+ <USeparator class="mb-4" />
22
80
  <SalesChannelSwitch />
23
81
  </div>
24
82
  </template>
@@ -134,7 +134,7 @@ const dropDownMenu = computed<DropdownMenuItem[][]>(() => {
134
134
  </div>
135
135
  </template>
136
136
  <template #body>
137
- <CartQuickView
137
+ <LazyCartQuickView
138
138
  :with-to-cart-button="true"
139
139
  class="md:min-w-90"
140
140
  @go-to-cart="cartQuickViewOpen = false"
@@ -1,16 +1,9 @@
1
- <script setup lang="ts">
2
- const config = useRuntimeConfig();
3
- const siteName = computed(() => config.public.site.name);
4
- </script>
5
-
1
+ <script setup lang="ts"></script>
6
2
  <template>
7
- <NuxtLink to="/" class="-m-1.5 p-1.5">
8
- <span class="sr-only">{{ siteName }}</span>
9
- <UColorModeImage
10
- alt="Logo"
11
- light="/light/Logo.png"
12
- dark="/dark/Logo.png"
13
- class="h-12 w-auto"
14
- />
15
- </NuxtLink>
3
+ <UColorModeImage
4
+ alt="Logo"
5
+ light="/light/Logo.png"
6
+ dark="/dark/Logo.png"
7
+ width="150"
8
+ />
16
9
  </template>
@@ -1,8 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { useNavigation } from "~/composables/useNavigation";
3
-
4
2
  const { mainMenu } = useNavigation(false);
5
-
6
3
  const loginSlide = ref(false);
7
4
  </script>
8
5
 
@@ -13,13 +10,14 @@ const loginSlide = ref(false);
13
10
  </template>
14
11
 
15
12
  <UNavigationMenu color="primary" variant="pill" :items="mainMenu" />
13
+ <SalesChannelSwitch />
16
14
 
17
15
  <template #right>
18
16
  <HeaderRight />
19
17
  </template>
20
18
 
21
19
  <template #body>
22
- <HeaderBody />
20
+ <LazyHeaderBody />
23
21
  </template>
24
22
  </UHeader>
25
23
 
@@ -30,7 +28,7 @@ const loginSlide = ref(false);
30
28
  >
31
29
  <template #body>
32
30
  <div class="h-full m-4">
33
- <UserLoginForm />
31
+ <LazyUserLoginForm />
34
32
  </div>
35
33
  </template>
36
34
  </USlideover>
@@ -81,7 +81,7 @@ function onVariantSelected(variant: Schemas["Product"]) {
81
81
  :ui="{ footer: 'w-full', root: 'shadow-lg' }"
82
82
  >
83
83
  <template #header>
84
- <UBadge
84
+ <LazyUBadge
85
85
  v-if="isVegi"
86
86
  icon="i-lucide-leaf"
87
87
  color="success"
@@ -92,7 +92,7 @@ function onVariantSelected(variant: Schemas["Product"]) {
92
92
  </template>
93
93
 
94
94
  <div v-if="product.cover?.media?.url">
95
- <NuxtImg
95
+ <LazyNuxtImg
96
96
  :src="product.cover.media.url"
97
97
  class="rounded-md h-auto max-w-full object-contain ransition-opacity duration-700"
98
98
  sizes="(min-width: 1024px) 50vw, 100vw"
@@ -138,15 +138,17 @@ function onVariantSelected(variant: Schemas["Product"]) {
138
138
  />
139
139
  </div>
140
140
  </div>
141
- <UCollapsible v-model:open="openDetails" class="flex flex-col gap-2">
142
- <template #content>
143
- <ProductDetail2
144
- :product-id="product.id"
145
- @product-added="toggleDetails"
146
- @variant-selected="onVariantSelected"
147
- />
148
- </template>
149
- </UCollapsible>
141
+ <ClientOnly>
142
+ <UCollapsible v-model:open="openDetails" class="flex flex-col gap-2">
143
+ <template #content>
144
+ <LazyProductDetail
145
+ :product-id="product.id"
146
+ @product-added="toggleDetails"
147
+ @variant-selected="onVariantSelected"
148
+ />
149
+ </template>
150
+ </UCollapsible>
151
+ </ClientOnly>
150
152
  </template>
151
153
  </UPageCard>
152
154
  </AnimatedSection>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <div
3
+ class="ring-1 ring-accented rounded-xl shadow-lg p-4 flex flex-row gap-4"
4
+ >
5
+ <div class="flex flex-col gap-3 flex-1 min-w-0">
6
+ <USkeleton class="h-5 w-20 rounded-full" />
7
+ <div class="flex items-baseline gap-2">
8
+ <USkeleton class="h-3.5 w-7" />
9
+ <USkeleton class="h-3.5 w-36" />
10
+ </div>
11
+ <div class="flex flex-col gap-2">
12
+ <USkeleton class="h-3 w-full" />
13
+ <USkeleton class="h-3 w-5/6" />
14
+ <USkeleton class="h-3 w-1/2" />
15
+ </div>
16
+ <div class="flex justify-between items-center mt-auto pt-2">
17
+ <USkeleton class="h-4 w-14" />
18
+ <div class="flex gap-2">
19
+ <USkeleton class="h-8 w-8 rounded-md" />
20
+ <USkeleton class="h-8 w-8 rounded-md" />
21
+ </div>
22
+ </div>
23
+ </div>
24
+ <USkeleton class="size-28 rounded-md shrink-0" />
25
+ </div>
26
+ </template>
@@ -142,7 +142,7 @@ watch(productDetails, () => {
142
142
  <template>
143
143
  <div v-if="!pending">
144
144
  <div v-if="productDetails?.configurator">
145
- <ProductConfigurator2
145
+ <ProductConfigurator
146
146
  v-if="productDetails?.configurator"
147
147
  :p="productDetails.product"
148
148
  :c="productDetails.configurator"
@@ -8,9 +8,7 @@ const { apiClient } = useShopwareContext();
8
8
 
9
9
  const config = useRuntimeConfig();
10
10
 
11
- const isMultiChannel = computed(
12
- () => config.public.shopBite.feature.multiChannel === "true",
13
- );
11
+ const isMultiChannel = useRuntimeConfig().public.shopBite.feature.multiChannel;
14
12
 
15
13
  const storeUrl = computed(() => config.public.storeUrl);
16
14
 
@@ -47,7 +47,7 @@ onMounted(() => {
47
47
  </script>
48
48
 
49
49
  <template>
50
- <div class="shadow-md rounded-md mb-4 p-6 bg-elevated">
50
+ <UCard class="mb-4">
51
51
  <div>{{ fullName }}</div>
52
52
  <div>{{ user?.email }}</div>
53
53
  <USeparator
@@ -76,5 +76,5 @@ onMounted(() => {
76
76
  :with-edit-button="withEditButton"
77
77
  @update:address="handleAddressUpdate"
78
78
  />
79
- </div>
79
+ </UCard>
80
80
  </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ const { isLoggedIn, isGuestSession } = useUser();
3
+ const { setStep } = useCheckoutStore();
4
+
5
+ if (!isLoggedIn.value && !isGuestSession.value) {
6
+ await navigateTo("/bestellung/warenkorb");
7
+ }
8
+
9
+ setStep(2);
10
+ </script>
11
+
12
+ <template>
13
+ <CheckoutSummary />
14
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import LoginOrRegister from "~/components/Checkout/LoginOrRegister.vue";
3
+
4
+ const { isLoggedIn, isGuestSession } = useUser();
5
+ const { isEmpty } = useCart();
6
+ const { setStep } = useCheckoutStore();
7
+
8
+ setStep(0);
9
+
10
+ const isCustomerAvailable = computed<boolean>(() => {
11
+ return isLoggedIn.value || isGuestSession.value;
12
+ });
13
+ </script>
14
+
15
+ <template>
16
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 py-8">
17
+ <div class="flex flex-col gap-4">
18
+ <h2 class="text-lg font-semibold">
19
+ {{ isCustomerAvailable ? "Deine Daten" : "Anmelden oder registrieren" }}
20
+ </h2>
21
+ <LoginOrRegister v-if="!isLoggedIn && !isGuestSession" />
22
+ <UserDetail v-else :with-edit-button="true" />
23
+ </div>
24
+
25
+ <div class="flex flex-col gap-4">
26
+ <h2 class="text-lg font-semibold">Warenkorb</h2>
27
+ <UCard>
28
+ <CartQuickView />
29
+ </UCard>
30
+ <UButton
31
+ v-if="!isEmpty && isCustomerAvailable"
32
+ label="Zahlungs- und Versandart auswählen"
33
+ size="xl"
34
+ block
35
+ trailing-icon="i-lucide-arrow-right"
36
+ to="/bestellung/zahlung-versand"
37
+ />
38
+ <UButton
39
+ v-if="isEmpty"
40
+ label="Zur Speisekarte"
41
+ size="xl"
42
+ block
43
+ variant="outline"
44
+ icon="i-lucide-arrow-left"
45
+ to="/speisekarte"
46
+ />
47
+ </div>
48
+ </div>
49
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ const { isLoggedIn, isGuestSession } = useUser();
3
+ const { setStep } = useCheckoutStore();
4
+
5
+ if (!isLoggedIn.value && !isGuestSession.value) {
6
+ await navigateTo("/bestellung/warenkorb");
7
+ }
8
+
9
+ setStep(1);
10
+ </script>
11
+
12
+ <template>
13
+ <div class="flex flex-col gap-8 py-8">
14
+ <CheckoutPaymentAndDelivery />
15
+ <UButton
16
+ label="Weiter zu Prüfen & Bestellen"
17
+ trailing-icon="i-lucide-arrow-right"
18
+ size="xl"
19
+ block
20
+ to="/bestellung/bestaetigen"
21
+ />
22
+ </div>
23
+ </template>
@@ -1,95 +1,47 @@
1
1
  <script setup lang="ts">
2
- import LoginOrRegister from "~/components/Checkout/LoginOrRegister.vue";
3
2
  import type { StepperItem } from "@nuxt/ui";
4
3
 
5
- const { isLoggedIn, isGuestSession } = useUser();
6
- const { isEmpty } = useCart();
4
+ const checkoutStore = useCheckoutStore();
5
+ const { step } = storeToRefs(checkoutStore);
7
6
 
8
- const items = [
9
- {
10
- slot: "user" as const,
11
- title: "Deine Daten",
12
- icon: "i-lucide-user",
13
- },
14
- {
15
- slot: "shipping" as const,
16
- title: "Zahlung & Versand",
17
- icon: "i-lucide-truck",
18
- },
19
- {
20
- slot: "checkout" as const,
21
- title: "Prüfen & Bestellen",
22
- icon: "i-lucide-check",
23
- },
24
- ] satisfies StepperItem[];
7
+ const stepRoutes = [
8
+ "/bestellung/warenkorb",
9
+ "/bestellung/zahlung-versand",
10
+ "/bestellung/bestaetigen",
11
+ ] as const;
25
12
 
26
- const activeStep = ref(0);
13
+ const items = computed(
14
+ () =>
15
+ [
16
+ {
17
+ title: "Warenkorb",
18
+ icon: "i-lucide-shopping-cart",
19
+ disabled: false,
20
+ },
21
+ {
22
+ title: "Zahlung & Versand",
23
+ icon: "i-lucide-truck",
24
+ disabled: step.value < 1,
25
+ },
26
+ {
27
+ title: "Prüfen & Bestellen",
28
+ icon: "i-lucide-check",
29
+ disabled: step.value < 2,
30
+ },
31
+ ] satisfies StepperItem[],
32
+ );
27
33
 
28
- const isCustomerAvailable = computed<boolean>(() => {
29
- return isLoggedIn.value || isGuestSession.value;
34
+ watch(step, (newStep) => {
35
+ navigateTo(stepRoutes[newStep]);
30
36
  });
31
37
  </script>
32
38
 
33
39
  <template>
34
- <UContainer>
35
- <UPageHeader title="Bestellung aufgeben" />
36
- <UPageBody>
37
- <UStepper v-model="activeStep" :items="items">
38
- <template #user>
39
- <div class="flex flex-col md:flex-row w-full gap-18 my-16">
40
- <LoginOrRegister
41
- v-if="!isLoggedIn && !isGuestSession"
42
- class="basis-1/2"
43
- />
44
- <div v-else class="basis-1/2">
45
- <UserDetail :with-edit-button="true" />
46
- </div>
47
- <div class="basis-1/2">
48
- <h3 class="text-2xl mb-6 font-semibold">Warenkorb</h3>
49
- <CartQuickView />
50
- <UButton
51
- v-if="!isEmpty && isCustomerAvailable"
52
- :disabled="!isLoggedIn && !isGuestSession"
53
- label="Zahlungs- und Versandart auswählen"
54
- size="xl"
55
- block
56
- trailing-icon="i-lucide-arrow-right"
57
- class="my-4"
58
- @click="activeStep = 1"
59
- />
60
- <UButton
61
- v-if="isEmpty"
62
- label="Zur Speisekarte"
63
- size="xl"
64
- block
65
- icon="i-lucide-arrow-left"
66
- class="my-4"
67
- to="/speisekarte"
68
- />
69
- </div>
70
- </div>
71
- </template>
72
-
73
- <template #shipping>
74
- <div class="my-14">
75
- <CheckoutPaymentAndDelivery />
76
- <div class="w-full flex justify-end">
77
- <UButton
78
- label="Weiter zu Prüfen & Bestellen"
79
- trailing-icon="i-lucide-arrow-right"
80
- class="m-8 md:max-w-96"
81
- size="xl"
82
- block
83
- @click="activeStep = 2"
84
- />
85
- </div>
86
- </div>
87
- </template>
88
-
89
- <template #checkout>
90
- <CheckoutSummary />
91
- </template>
92
- </UStepper>
93
- </UPageBody>
94
- </UContainer>
40
+ <UPageSection>
41
+ <UStepper ref="stepper" v-model="step" :items="items" size="lg">
42
+ <template #content>
43
+ <NuxtPage />
44
+ </template>
45
+ </UStepper>
46
+ </UPageSection>
95
47
  </template>
@@ -47,7 +47,7 @@ useSeoMeta({
47
47
 
48
48
  <USeparator :ui="{ border: 'border-primary/30' }" />
49
49
 
50
- <FoodMarquee
50
+ <LazyFoodMarquee
51
51
  v-if="page.marquee.items?.length > 0"
52
52
  :title="page.marquee.title"
53
53
  :description="page.marquee.description"
@@ -13,7 +13,7 @@ if (config.public.shopBite.feature.contactForm !== true) {
13
13
 
14
14
  <template>
15
15
  <UPageSection
16
- title="Sie haben eine Frage oder Anregung?Wir freuen uns auf Ihre Nachricht."
16
+ title="Sie haben eine Frage oder Anregung? Wir freuen uns auf Ihre Nachricht."
17
17
  description="Bitte beachten Sie, dass wir Tischreservierung oder Tischstornierung nur telefonisch entgegennehmen können."
18
18
  icon="i-lucide-mail"
19
19
  orientation="horizontal"
@@ -1,3 +1,4 @@
1
+ H
1
2
  <script setup lang="ts">
2
3
  import type { Schemas } from "#shopware";
3
4
 
@@ -0,0 +1,22 @@
1
+ const stepRoutes = [
2
+ "/bestellung/warenkorb",
3
+ "/bestellung/zahlung-versand",
4
+ "/bestellung/bestaetigen",
5
+ ] as const;
6
+
7
+ export const useCheckoutStore = defineStore("checkout", () => {
8
+ const step = ref(0);
9
+
10
+ function setStep(index: number) {
11
+ step.value = index;
12
+ }
13
+
14
+ async function navigateToStep(index: number) {
15
+ if (index < 0 || index >= stepRoutes.length) return;
16
+ if (index <= step.value) {
17
+ await navigateTo(stepRoutes[index]);
18
+ }
19
+ }
20
+
21
+ return { step, setStep, navigateToStep };
22
+ });
package/nuxt.config.ts CHANGED
@@ -57,7 +57,7 @@ export default defineNuxtConfig({
57
57
  shopBite: {
58
58
  menuCategoryId: "",
59
59
  feature: {
60
- multiChannel: "",
60
+ multiChannel: false,
61
61
  secureKey: "",
62
62
  contactForm: false,
63
63
  },
@@ -78,6 +78,9 @@ export default defineNuxtConfig({
78
78
  "/registrierung/bestaetigen": {
79
79
  ssr: false,
80
80
  },
81
+ "/bestellung": {
82
+ redirect: "/bestellung/warenkorb",
83
+ },
81
84
  },
82
85
 
83
86
  css: ["~/assets/css/main.css"],
@@ -100,6 +103,8 @@ export default defineNuxtConfig({
100
103
  "@nuxt/scripts",
101
104
  "nuxt-vitalizer",
102
105
  "@nuxt/eslint",
106
+ "@pinia/nuxt",
107
+ "@nuxt/hints",
103
108
  ],
104
109
 
105
110
  content: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.18.0",
3
+ "version": "1.18.2",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [
@@ -23,10 +23,12 @@
23
23
  "@iconify-json/lucide": "^1.2.97",
24
24
  "@iconify-json/simple-icons": "^1.2.73",
25
25
  "@nuxt/content": "3.12.0",
26
+ "@nuxt/hints": "1.0.2",
26
27
  "@nuxt/image": "^2.0.0",
27
28
  "@nuxt/scripts": "0.13.2",
28
29
  "@nuxt/ui": "^4.5.1",
29
30
  "@nuxtjs/robots": "^6.0.0",
31
+ "@pinia/nuxt": "0.11.3",
30
32
  "@sentry/nuxt": "^10.43.0",
31
33
  "@shopware/api-client": "^1.4.0",
32
34
  "@shopware/api-gen": "^1.4.0",
@@ -39,7 +39,7 @@ async function clearCart(page: Page) {
39
39
  await expect(page.getByRole("heading", { name: /warenkorb/i })).toBeVisible();
40
40
 
41
41
  const deleteButtons = page.getByRole("button", {
42
- name: "Remove item from cart",
42
+ name: "Artikel entfernen",
43
43
  });
44
44
  const itemCount = await deleteButtons.count();
45
45
 
@@ -92,7 +92,7 @@ async function selectProductAndAddToCart(
92
92
  }
93
93
 
94
94
  async function proceedToCheckoutAndLogin(page: Page) {
95
- await page.goto("/bestellung", { waitUntil: "load" });
95
+ await page.goto("/bestellung/warenkorb", { waitUntil: "load" });
96
96
 
97
97
  const loginTab = page.getByRole("tab", { name: "Einloggen" });
98
98
  await expect(loginTab).toBeVisible();
@@ -129,7 +129,7 @@ async function proceedToCheckoutAndLogin(page: Page) {
129
129
 
130
130
  // Wait for login to complete and navigate back to checkout
131
131
  await page.waitForTimeout(2000);
132
- await page.goto("/bestellung", { waitUntil: "load" });
132
+ await page.goto("/bestellung/zahlung-versand", { waitUntil: "load" });
133
133
  await page.waitForTimeout(1000);
134
134
 
135
135
  // Skip the rest of this function since we already logged in
@@ -159,15 +159,15 @@ async function proceedToCheckoutAndLogin(page: Page) {
159
159
 
160
160
  async function verifyCheckoutQuantity(page: Page) {
161
161
  // Ensure we're on the checkout page
162
- if (!page.url().includes("/bestellung")) {
163
- await page.goto("/bestellung", { waitUntil: "load" });
162
+ if (!page.url().includes("/bestellung/warenkorb")) {
163
+ await page.goto("/bestellung/warenkorb", { waitUntil: "load" });
164
164
  await page.waitForTimeout(1000);
165
165
  }
166
166
 
167
167
  // Try to find the quantity input with multiple strategies
168
168
  const checkoutQuantityInput = page
169
169
  .getByRole("spinbutton", {
170
- name: /item quantity/i,
170
+ name: /menge/i,
171
171
  })
172
172
  .or(page.getByRole("spinbutton"))
173
173
  .or(page.locator("input[type='number']"));
@@ -178,7 +178,7 @@ async function verifyCheckoutQuantity(page: Page) {
178
178
  }
179
179
 
180
180
  async function selectPaymentAndShipping(page: Page) {
181
- const nextStepButton = page.getByRole("button", {
181
+ const nextStepButton = page.getByRole("link", {
182
182
  name: "Zahlungs- und Versandart auswählen",
183
183
  });
184
184
  await expect(nextStepButton).toBeVisible({ timeout: 10000 });
@@ -197,7 +197,7 @@ async function selectPaymentAndShipping(page: Page) {
197
197
  }
198
198
 
199
199
  async function proceedToOrderReview(page: Page) {
200
- const lastStepButton = page.getByRole("button", {
200
+ const lastStepButton = page.getByRole("link", {
201
201
  name: "Weiter zu Prüfen & Bestellen",
202
202
  });
203
203
  await expect(lastStepButton).toBeVisible({ timeout: 10000 });
@@ -1,42 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
3
- import HeaderTitle from "~/components/Header/Title.vue";
4
-
5
- const { mockRuntimeConfig } = vi.hoisted(() => ({
6
- mockRuntimeConfig: {
7
- app: {
8
- baseURL: "/",
9
- },
10
- public: {
11
- site: {
12
- name: "ShopBite Test Store",
13
- },
14
- },
15
- },
16
- }));
17
-
18
- mockNuxtImport("useRuntimeConfig", () => () => mockRuntimeConfig);
19
-
20
- describe("HeaderTitle", () => {
21
- it("renders the site name for screen readers", async () => {
22
- const component = await mountSuspended(HeaderTitle);
23
- const srOnly = component.find(".sr-only");
24
- expect(srOnly.exists()).toBe(true);
25
- expect(srOnly.text()).toBe("ShopBite Test Store");
26
- });
27
-
28
- it("renders a link to the home page", async () => {
29
- const component = await mountSuspended(HeaderTitle);
30
- const link = component.findComponent({ name: "NuxtLink" });
31
- expect(link.exists()).toBe(true);
32
- expect(link.props("to")).toBe("/");
33
- });
34
-
35
- it("renders the logo image", async () => {
36
- const component = await mountSuspended(HeaderTitle);
37
- const image = component.findComponent({ name: "UColorModeImage" });
38
- expect(image.exists()).toBe(true);
39
- expect(image.props("light")).toBe("/light/Logo.png");
40
- expect(image.props("dark")).toBe("/dark/Logo.png");
41
- });
42
- });