@shopbite-de/storefront 1.1.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/.dockerignore +28 -0
- package/.env.example +11 -0
- package/.github/workflows/build.yaml +48 -0
- package/.github/workflows/ci.yaml +102 -0
- package/.prettierignore +6 -0
- package/.prettierrc +1 -0
- package/api-types/storeApiSchema.json +13863 -0
- package/api-types/storeApiTypes.d.ts +7010 -0
- package/app/app.config.ts +18 -0
- package/app/app.vue +99 -0
- package/app/assets/css/main.css +60 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-Bold.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-Italic.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-Regular.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/OFL.txt +93 -0
- package/app/assets/fonts/Kalam/Kalam-Bold.ttf +0 -0
- package/app/assets/fonts/Kalam/Kalam-Light.ttf +0 -0
- package/app/assets/fonts/Kalam/Kalam-Regular.ttf +0 -0
- package/app/assets/fonts/Kalam/OFL.txt +93 -0
- package/app/assets/fonts/Marcellus/Marcellus-Regular.ttf +0 -0
- package/app/assets/fonts/Marcellus/OFL.txt +93 -0
- package/app/assets/fonts/Sora/OFL.txt +93 -0
- package/app/assets/fonts/Sora/README.txt +70 -0
- package/app/assets/fonts/Sora/Sora-VariableFont_wght.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Bold.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-ExtraBold.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-ExtraLight.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Light.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Medium.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Regular.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-SemiBold.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Thin.ttf +0 -0
- package/app/components/AddToWishlist.vue +55 -0
- package/app/components/Address/Card.vue +32 -0
- package/app/components/Address/Detail.vue +22 -0
- package/app/components/Address/Form.vue +117 -0
- package/app/components/AnimatedSection.vue +77 -0
- package/app/components/BottomNavi.vue +63 -0
- package/app/components/Cart/Item.vue +112 -0
- package/app/components/Cart/QuickView.vue +55 -0
- package/app/components/Category/Header.vue +53 -0
- package/app/components/Category/Listing.vue +295 -0
- package/app/components/Checkout/DeliveryTimeSelect.vue +177 -0
- package/app/components/Checkout/LoginOrRegister.vue +43 -0
- package/app/components/Checkout/PaymentAndDelivery.vue +101 -0
- package/app/components/Checkout/PaymentMethod.vue +30 -0
- package/app/components/Checkout/ShippingMethod.vue +30 -0
- package/app/components/Checkout/Summary.vue +125 -0
- package/app/components/Cta.vue +34 -0
- package/app/components/Features.vue +36 -0
- package/app/components/Food/Marquee.vue +35 -0
- package/app/components/Food/MarqueeItem.vue +72 -0
- package/app/components/Footer.vue +51 -0
- package/app/components/Header.vue +160 -0
- package/app/components/Hero.vue +77 -0
- package/app/components/ImageGallery.vue +46 -0
- package/app/components/Loading.vue +29 -0
- package/app/components/Navigation/DesktopLeft.vue +51 -0
- package/app/components/Navigation/DesktopLeft2.vue +43 -0
- package/app/components/Navigation/MobileTop.vue +59 -0
- package/app/components/Navigation/MobileTop2.vue +42 -0
- package/app/components/Order/Detail.vue +84 -0
- package/app/components/Product/Card.vue +132 -0
- package/app/components/Product/Category.vue +153 -0
- package/app/components/Product/Configurator.vue +65 -0
- package/app/components/Product/CrossSelling.vue +95 -0
- package/app/components/Product/DeselectIngredient.vue +46 -0
- package/app/components/Product/Detail.vue +187 -0
- package/app/components/Product/SearchBar.vue +109 -0
- package/app/components/PublicAnnouncement.vue +17 -0
- package/app/components/Topseller.vue +43 -0
- package/app/components/User/Detail.vue +47 -0
- package/app/components/User/LoginForm.vue +105 -0
- package/app/components/User/RegistrationForm.vue +340 -0
- package/app/components/Wishlist.vue +102 -0
- package/app/composables/useDeliveryTime.ts +139 -0
- package/app/composables/useInterval.ts +15 -0
- package/app/composables/usePizzaToppings.ts +31 -0
- package/app/composables/useProductEvents.test.ts +111 -0
- package/app/composables/useProductEvents.ts +22 -0
- package/app/composables/useProductVariants.ts +61 -0
- package/app/composables/useScrollAnimation.ts +39 -0
- package/app/composables/useTopSellers.ts +34 -0
- package/app/error.vue +30 -0
- package/app/layouts/account.vue +74 -0
- package/app/layouts/default.vue +6 -0
- package/app/layouts/listing.vue +32 -0
- package/app/layouts/listing2.vue +8 -0
- package/app/middleware/trailing-slash.global.ts +19 -0
- package/app/pages/account/recover/password/index.vue +143 -0
- package/app/pages/anmelden.vue +32 -0
- package/app/pages/bestellung.vue +103 -0
- package/app/pages/c/[...all].vue +49 -0
- package/app/pages/index.vue +59 -0
- package/app/pages/konto/adressen.vue +135 -0
- package/app/pages/konto/bestellung/[id].vue +41 -0
- package/app/pages/konto/bestellungen.vue +53 -0
- package/app/pages/konto/index.vue +74 -0
- package/app/pages/konto/profil.vue +160 -0
- package/app/pages/merkliste.vue +11 -0
- package/app/pages/order/[id].vue +69 -0
- package/app/pages/passwort-vergessen.vue +103 -0
- package/app/pages/registrierung/bestaetigen.vue +44 -0
- package/app/pages/registrierung/index.vue +24 -0
- package/app/pages/speisekarte.vue +58 -0
- package/app/pages/unternehmen/[slug].vue +66 -0
- package/app/types/Association.d.ts +11 -0
- package/app/utils/businessHours.ts +119 -0
- package/app/utils/formatDate.ts +9 -0
- package/app/utils/holidays.ts +43 -0
- package/app/utils/storeHours.ts +8 -0
- package/app/utils/time.ts +20 -0
- package/app/validation/addressSchema.ts +34 -0
- package/app/validation/registrationSchema.ts +156 -0
- package/bun.dockerfile +60 -0
- package/compose.yml +17 -0
- package/container +7 -0
- package/content/index.yml +91 -0
- package/content/navigation.yml +67 -0
- package/content/unternehmen/agb.md +1 -0
- package/content/unternehmen/datenschutz.md +1 -0
- package/content/unternehmen/impressum.md +39 -0
- package/content.config.ts +134 -0
- package/eslint.config.mjs +8 -0
- package/node.dockerfile +33 -0
- package/nuxt.config.ts +153 -0
- package/package.json +70 -0
- package/public/dark/Logo.svg +32 -0
- package/public/favicon.ico +0 -0
- package/public/light/Logo.svg +32 -0
- package/renovate.json +4 -0
- package/server/tsconfig.json +3 -0
- package/shopware.d.ts +19 -0
- package/tsconfig.json +4 -0
- package/vitest.config.mts +26 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createRegistrationSchema } from "~/validation/registrationSchema";
|
|
3
|
+
import type { RegistrationSchema } from "~/validation/registrationSchema";
|
|
4
|
+
import type { FormSubmitEvent } from "@nuxt/ui";
|
|
5
|
+
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const { register, isLoggedIn } = useUser();
|
|
8
|
+
|
|
9
|
+
if (import.meta.client && isLoggedIn.value) {
|
|
10
|
+
navigateTo({ path: "/konto" });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
watch(isLoggedIn, (isLoggedIn) => {
|
|
14
|
+
if (isLoggedIn) {
|
|
15
|
+
navigateTo({ path: "/konto" });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const state = reactive({
|
|
20
|
+
accountType: "private",
|
|
21
|
+
salutationId: undefined,
|
|
22
|
+
firstName: undefined,
|
|
23
|
+
lastName: undefined,
|
|
24
|
+
email: undefined,
|
|
25
|
+
guest: true,
|
|
26
|
+
password: undefined,
|
|
27
|
+
passwordConfirm: undefined,
|
|
28
|
+
acceptedDataProtection: true,
|
|
29
|
+
isShippingAddressDifferent: false,
|
|
30
|
+
storefrontUrl: config.public.shopware.devStorefrontUrl,
|
|
31
|
+
billingAddress: {
|
|
32
|
+
company: undefined,
|
|
33
|
+
department: undefined,
|
|
34
|
+
salutationId: undefined,
|
|
35
|
+
firstName: undefined,
|
|
36
|
+
lastName: undefined,
|
|
37
|
+
phoneNumber: undefined,
|
|
38
|
+
street: undefined,
|
|
39
|
+
zipcode: undefined,
|
|
40
|
+
city: undefined,
|
|
41
|
+
countryId: config.public.site.countryId,
|
|
42
|
+
},
|
|
43
|
+
shippingAddress: {
|
|
44
|
+
company: undefined,
|
|
45
|
+
department: undefined,
|
|
46
|
+
salutationId: undefined,
|
|
47
|
+
firstName: undefined,
|
|
48
|
+
lastName: undefined,
|
|
49
|
+
phoneNumber: undefined,
|
|
50
|
+
street: undefined,
|
|
51
|
+
zipcode: undefined,
|
|
52
|
+
city: undefined,
|
|
53
|
+
countryId: config.public.site.countryId,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const schema = computed(() => createRegistrationSchema(state));
|
|
58
|
+
|
|
59
|
+
const toast = useToast();
|
|
60
|
+
|
|
61
|
+
async function onSubmit(event: FormSubmitEvent<RegistrationSchema>) {
|
|
62
|
+
const registrationData = { ...event.data };
|
|
63
|
+
|
|
64
|
+
console.log(registrationData);
|
|
65
|
+
if (
|
|
66
|
+
!registrationData.billingAddress.firstName &&
|
|
67
|
+
registrationData.firstName
|
|
68
|
+
) {
|
|
69
|
+
registrationData.billingAddress.firstName = registrationData.firstName;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!registrationData.billingAddress.lastName && registrationData.lastName) {
|
|
73
|
+
registrationData.billingAddress.lastName = registrationData.lastName;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!state.isShippingAddressDifferent) {
|
|
77
|
+
delete registrationData.shippingAddress;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await register(registrationData);
|
|
82
|
+
|
|
83
|
+
toast.add({
|
|
84
|
+
title: "Erfolgreich Kundendaten erfasst",
|
|
85
|
+
color: "success",
|
|
86
|
+
});
|
|
87
|
+
emit("registration-success", registrationData);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Registration failed:", error);
|
|
90
|
+
toast.add({
|
|
91
|
+
title: "Registrierung fehlgeschlagen",
|
|
92
|
+
description: "Bitte versuchen Sie es erneut.",
|
|
93
|
+
color: "error",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const accountTypes = ref([
|
|
99
|
+
{
|
|
100
|
+
label: "Privatkunde",
|
|
101
|
+
value: "private",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: "Geschäftskunde",
|
|
105
|
+
value: "commercial",
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const emit = defineEmits<{
|
|
110
|
+
"registration-success": [data: RegistrationSchema];
|
|
111
|
+
}>();
|
|
112
|
+
|
|
113
|
+
const allowedCities = ref(["Obertshausen", "Lämmerspiel"]);
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<UForm
|
|
118
|
+
:schema="schema"
|
|
119
|
+
:state="state"
|
|
120
|
+
class="space-y-4"
|
|
121
|
+
@submit="onSubmit"
|
|
122
|
+
@error="(error) => console.log('Form validation error:', error)"
|
|
123
|
+
>
|
|
124
|
+
<UFormField name="accountType">
|
|
125
|
+
<USelect
|
|
126
|
+
v-model="state.accountType"
|
|
127
|
+
value-key="value"
|
|
128
|
+
:items="accountTypes"
|
|
129
|
+
class="w-full"
|
|
130
|
+
/>
|
|
131
|
+
</UFormField>
|
|
132
|
+
|
|
133
|
+
<UFormField name="guest">
|
|
134
|
+
<USwitch
|
|
135
|
+
v-model="state.guest"
|
|
136
|
+
label="Als Gast bestellen"
|
|
137
|
+
class="w-full"
|
|
138
|
+
/>
|
|
139
|
+
</UFormField>
|
|
140
|
+
|
|
141
|
+
<div class="flex flex-row justify-between gap-4">
|
|
142
|
+
<UFormField label="Vorname" name="firstName" required class="w-full">
|
|
143
|
+
<UInput v-model="state.firstName" type="text" class="w-full" />
|
|
144
|
+
</UFormField>
|
|
145
|
+
|
|
146
|
+
<UFormField label="Nachname" name="lastName" required class="w-full">
|
|
147
|
+
<UInput v-model="state.lastName" type="text" class="w-full" />
|
|
148
|
+
</UFormField>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<UFormField label="Email" name="email" required>
|
|
152
|
+
<UInput v-model="state.email" class="w-full" />
|
|
153
|
+
</UFormField>
|
|
154
|
+
|
|
155
|
+
<UFormField label="Telefon" name="billingAddress.phoneNumber" required>
|
|
156
|
+
<UInput
|
|
157
|
+
v-model="state.billingAddress.phoneNumber"
|
|
158
|
+
type="text"
|
|
159
|
+
class="w-full"
|
|
160
|
+
/>
|
|
161
|
+
</UFormField>
|
|
162
|
+
|
|
163
|
+
<UFormField v-if="!state.guest" label="Password" name="password">
|
|
164
|
+
<UInput v-model="state.password" type="password" class="w-full" />
|
|
165
|
+
</UFormField>
|
|
166
|
+
|
|
167
|
+
<UFormField
|
|
168
|
+
v-if="!state.guest"
|
|
169
|
+
label="Password wiederholen"
|
|
170
|
+
name="passwordConfirm"
|
|
171
|
+
>
|
|
172
|
+
<UInput v-model="state.passwordConfirm" type="password" class="w-full" />
|
|
173
|
+
</UFormField>
|
|
174
|
+
|
|
175
|
+
<USeparator
|
|
176
|
+
color="primary"
|
|
177
|
+
:label="
|
|
178
|
+
state.isShippingAddressDifferent
|
|
179
|
+
? 'Rechnungsadresse'
|
|
180
|
+
: 'Rechnungs- und Lieferadresse'
|
|
181
|
+
"
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
<UFormField
|
|
185
|
+
v-if="state.accountType === 'commercial'"
|
|
186
|
+
label="Unternehmen"
|
|
187
|
+
name="billingAddress.company"
|
|
188
|
+
>
|
|
189
|
+
<UInput
|
|
190
|
+
v-model="state.billingAddress.company"
|
|
191
|
+
type="text"
|
|
192
|
+
class="w-full"
|
|
193
|
+
/>
|
|
194
|
+
</UFormField>
|
|
195
|
+
|
|
196
|
+
<UFormField
|
|
197
|
+
v-if="state.accountType === 'commercial'"
|
|
198
|
+
label="Abteilung"
|
|
199
|
+
name="billingAddress.department"
|
|
200
|
+
>
|
|
201
|
+
<UInput
|
|
202
|
+
v-model="state.billingAddress.department"
|
|
203
|
+
type="text"
|
|
204
|
+
class="w-full"
|
|
205
|
+
/>
|
|
206
|
+
</UFormField>
|
|
207
|
+
|
|
208
|
+
<UFormField
|
|
209
|
+
label="Straße und Hausnr."
|
|
210
|
+
name="billingAddress.street"
|
|
211
|
+
required
|
|
212
|
+
>
|
|
213
|
+
<UInput
|
|
214
|
+
v-model="state.billingAddress.street"
|
|
215
|
+
type="text"
|
|
216
|
+
class="w-full"
|
|
217
|
+
/>
|
|
218
|
+
</UFormField>
|
|
219
|
+
|
|
220
|
+
<UFormField
|
|
221
|
+
v-if="state.isShippingAddressDifferent"
|
|
222
|
+
label="Postleitzahl"
|
|
223
|
+
name="billingAddress.zipcode"
|
|
224
|
+
>
|
|
225
|
+
<UInput
|
|
226
|
+
v-model="state.billingAddress.zipcode"
|
|
227
|
+
type="text"
|
|
228
|
+
class="w-full"
|
|
229
|
+
/>
|
|
230
|
+
</UFormField>
|
|
231
|
+
|
|
232
|
+
<UFormField label="Ort" name="billingAddress.city" required>
|
|
233
|
+
<USelect
|
|
234
|
+
v-if="!state.isShippingAddressDifferent"
|
|
235
|
+
v-model="state.billingAddress.city"
|
|
236
|
+
:items="allowedCities"
|
|
237
|
+
class="w-full"
|
|
238
|
+
/>
|
|
239
|
+
<UInput
|
|
240
|
+
v-else
|
|
241
|
+
v-model="state.billingAddress.city"
|
|
242
|
+
type="text"
|
|
243
|
+
class="w-full"
|
|
244
|
+
/>
|
|
245
|
+
</UFormField>
|
|
246
|
+
|
|
247
|
+
<UFormField name="guest">
|
|
248
|
+
<UCheckbox
|
|
249
|
+
v-model="state.isShippingAddressDifferent"
|
|
250
|
+
label="Rechnungsadresse weicht von Lieferadresse ab"
|
|
251
|
+
class="w-full"
|
|
252
|
+
/>
|
|
253
|
+
</UFormField>
|
|
254
|
+
|
|
255
|
+
<USeparator
|
|
256
|
+
v-if="state.isShippingAddressDifferent"
|
|
257
|
+
color="primary"
|
|
258
|
+
label="Lieferadresse"
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
<UFormField
|
|
262
|
+
v-if="
|
|
263
|
+
state.accountType === 'commercial' && state.isShippingAddressDifferent
|
|
264
|
+
"
|
|
265
|
+
label="Unternehmen"
|
|
266
|
+
name="shippingAddress.company"
|
|
267
|
+
>
|
|
268
|
+
<UInput
|
|
269
|
+
v-model="state.shippingAddress.company"
|
|
270
|
+
type="text"
|
|
271
|
+
class="w-full"
|
|
272
|
+
/>
|
|
273
|
+
</UFormField>
|
|
274
|
+
|
|
275
|
+
<UFormField
|
|
276
|
+
v-if="
|
|
277
|
+
state.accountType === 'commercial' && state.isShippingAddressDifferent
|
|
278
|
+
"
|
|
279
|
+
label="Abteilung"
|
|
280
|
+
name="billingAddress.department"
|
|
281
|
+
>
|
|
282
|
+
<UInput
|
|
283
|
+
v-model="state.shippingAddress.department"
|
|
284
|
+
type="text"
|
|
285
|
+
class="w-full"
|
|
286
|
+
/>
|
|
287
|
+
</UFormField>
|
|
288
|
+
|
|
289
|
+
<UFormField
|
|
290
|
+
v-if="state.isShippingAddressDifferent"
|
|
291
|
+
label="Vorname"
|
|
292
|
+
name="shippingAddress.firstName"
|
|
293
|
+
required
|
|
294
|
+
>
|
|
295
|
+
<UInput
|
|
296
|
+
v-model="state.shippingAddress.firstName"
|
|
297
|
+
type="text"
|
|
298
|
+
class="w-full"
|
|
299
|
+
/>
|
|
300
|
+
</UFormField>
|
|
301
|
+
|
|
302
|
+
<UFormField
|
|
303
|
+
v-if="state.isShippingAddressDifferent"
|
|
304
|
+
label="Nachname"
|
|
305
|
+
name="shippingAddress.lastName"
|
|
306
|
+
>
|
|
307
|
+
<UInput
|
|
308
|
+
v-model="state.shippingAddress.lastName"
|
|
309
|
+
type="text"
|
|
310
|
+
class="w-full"
|
|
311
|
+
/>
|
|
312
|
+
</UFormField>
|
|
313
|
+
|
|
314
|
+
<UFormField
|
|
315
|
+
v-if="state.isShippingAddressDifferent"
|
|
316
|
+
label="Straße und Hausnr."
|
|
317
|
+
name="shippingAddress.street"
|
|
318
|
+
>
|
|
319
|
+
<UInput
|
|
320
|
+
v-model="state.shippingAddress.street"
|
|
321
|
+
type="text"
|
|
322
|
+
class="w-full"
|
|
323
|
+
/>
|
|
324
|
+
</UFormField>
|
|
325
|
+
|
|
326
|
+
<UFormField
|
|
327
|
+
v-if="state.isShippingAddressDifferent"
|
|
328
|
+
label="Ort"
|
|
329
|
+
name="shippingAddress.city"
|
|
330
|
+
>
|
|
331
|
+
<USelect
|
|
332
|
+
v-model="state.shippingAddress.city"
|
|
333
|
+
:items="allowedCities"
|
|
334
|
+
class="w-full"
|
|
335
|
+
/>
|
|
336
|
+
</UFormField>
|
|
337
|
+
|
|
338
|
+
<UButton block type="submit">Speichern</UButton>
|
|
339
|
+
</UForm>
|
|
340
|
+
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
|
|
4
|
+
const { getWishlistProducts, items, clearWishlist } = useWishlist();
|
|
5
|
+
const { apiClient } = useShopwareContext();
|
|
6
|
+
const products = ref<Schemas["Product"][]>([]);
|
|
7
|
+
const isLoading = ref(false);
|
|
8
|
+
|
|
9
|
+
const clearWishlistHandler = async () => {
|
|
10
|
+
try {
|
|
11
|
+
isLoading.value = true;
|
|
12
|
+
clearWishlist();
|
|
13
|
+
} finally {
|
|
14
|
+
isLoading.value = false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const loadProductsByItemIds = async (itemIds: string[]): Promise<void> => {
|
|
18
|
+
isLoading.value = true;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { data } = await apiClient.invoke("readProduct post /product", {
|
|
22
|
+
body: { ids: itemIds || items.value },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (data?.elements) {
|
|
26
|
+
products.value = data.elements;
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error("[wishlist][loadProductsByItemIds]", error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
isLoading.value = false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
watch(
|
|
36
|
+
items,
|
|
37
|
+
(items, oldItems) => {
|
|
38
|
+
if (items.length !== oldItems?.length) {
|
|
39
|
+
products.value = products.value.filter(({ id }: { id: string }) =>
|
|
40
|
+
items.includes(id),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!items.length) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
loadProductsByItemIds(items);
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
immediate: true,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
onMounted(async () => {
|
|
54
|
+
await getWishlistProducts();
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div>
|
|
60
|
+
<h2 class="text-2xl">Merkliste</h2>
|
|
61
|
+
<div class="flex flex-col gap-4 h-full">
|
|
62
|
+
<div
|
|
63
|
+
v-if="products.length === 0"
|
|
64
|
+
class="flex flex-row items-center gap-2"
|
|
65
|
+
>
|
|
66
|
+
<UIcon name="i-lucide-frown" class="size-5" />
|
|
67
|
+
<p>Keine Produkte</p>
|
|
68
|
+
</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
|
+
/>
|
|
77
|
+
<UButton
|
|
78
|
+
v-if="products.length > 0"
|
|
79
|
+
color="neutral"
|
|
80
|
+
variant="outline"
|
|
81
|
+
icon="i-lucide-trash"
|
|
82
|
+
@click="clearWishlistHandler"
|
|
83
|
+
>
|
|
84
|
+
Alles löschen
|
|
85
|
+
</UButton>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<style scoped>
|
|
92
|
+
@import "tailwindcss";
|
|
93
|
+
@import "@nuxt/ui";
|
|
94
|
+
|
|
95
|
+
h1 {
|
|
96
|
+
@apply text-3xl sm:text-4xl lg:text-5xl text-pretty tracking-tight font-bold text-highlighted my-14;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
h2 {
|
|
100
|
+
@apply mt-0 text-2xl sm:text-3xl lg:text-4xl text-pretty tracking-tight font-bold text-highlighted my-10;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { computed, type Ref } from "vue";
|
|
2
|
+
import { toTimeString, parseTimeString } from "~/utils/time";
|
|
3
|
+
import {
|
|
4
|
+
getEarliestSelectableTime,
|
|
5
|
+
findActiveInterval,
|
|
6
|
+
getServiceIntervals,
|
|
7
|
+
isTuesday,
|
|
8
|
+
type ServiceInterval,
|
|
9
|
+
} from "~/utils/businessHours";
|
|
10
|
+
|
|
11
|
+
function isTimeWithinBounds(
|
|
12
|
+
time: string,
|
|
13
|
+
min: string | null,
|
|
14
|
+
max: string | null,
|
|
15
|
+
): boolean {
|
|
16
|
+
if (!min || !max) return false;
|
|
17
|
+
return time >= min && time <= max;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useDeliveryTime(now: Ref<Date>) {
|
|
21
|
+
const { deliveryTime } = usePizzaToppings();
|
|
22
|
+
const earliest = computed<Date>(() =>
|
|
23
|
+
getEarliestSelectableTime(now.value, deliveryTime.value),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const activeInterval = computed<ServiceInterval | null>(() =>
|
|
27
|
+
findActiveInterval(now.value, deliveryTime.value),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const minTime = computed<string | null>(() => {
|
|
31
|
+
const interval = activeInterval.value;
|
|
32
|
+
if (!interval) return null;
|
|
33
|
+
const minDate = new Date(
|
|
34
|
+
Math.max(earliest.value.getTime(), interval.start.getTime()),
|
|
35
|
+
);
|
|
36
|
+
return toTimeString(minDate);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const maxTime = computed<string | null>(() => {
|
|
40
|
+
const interval = activeInterval.value;
|
|
41
|
+
return interval ? toTimeString(interval.end) : null;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const isClosedToday = computed<boolean>(() => activeInterval.value === null);
|
|
45
|
+
|
|
46
|
+
const helperText = computed<string>(() => {
|
|
47
|
+
const interval = activeInterval.value;
|
|
48
|
+
if (!interval) {
|
|
49
|
+
if (isTuesday(now.value)) {
|
|
50
|
+
return "Heute (Dienstag) ist Ruhetag. Bitte an einem anderen Tag bestellen.";
|
|
51
|
+
}
|
|
52
|
+
const intervals = getServiceIntervals(now.value);
|
|
53
|
+
const lastInterval = intervals.at(-1);
|
|
54
|
+
if (lastInterval && earliest.value > lastInterval.end) {
|
|
55
|
+
return "Heute sind keine Lieferzeiten mehr verfügbar.";
|
|
56
|
+
}
|
|
57
|
+
return "Aktuell keine Lieferzeit verfügbar. Bitte später erneut versuchen.";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const startStr = toTimeString(interval.start);
|
|
61
|
+
const endStr = toTimeString(interval.end);
|
|
62
|
+
const earliestStr = toTimeString(earliest.value);
|
|
63
|
+
|
|
64
|
+
return earliest.value <= interval.start
|
|
65
|
+
? `Nächster möglicher Zeitraum: ${startStr} – ${endStr}`
|
|
66
|
+
: `Frühestmöglich ab ${earliestStr} (heute), erlaubt: ${startStr} – ${endStr}`;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function clampTimeToInterval(
|
|
70
|
+
selectedTime: string,
|
|
71
|
+
min: string,
|
|
72
|
+
max: string,
|
|
73
|
+
baseDate: Date,
|
|
74
|
+
): string {
|
|
75
|
+
const selected = parseTimeString(selectedTime, baseDate);
|
|
76
|
+
const minDate = parseTimeString(min, baseDate);
|
|
77
|
+
const maxDate = parseTimeString(max, baseDate);
|
|
78
|
+
|
|
79
|
+
let clamped = selected;
|
|
80
|
+
if (selected < minDate) clamped = minDate;
|
|
81
|
+
if (selected > maxDate) clamped = maxDate;
|
|
82
|
+
|
|
83
|
+
return toTimeString(clamped);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate a selected time and return a human-readable error message,
|
|
88
|
+
* or null if the time is valid (or empty).
|
|
89
|
+
*/
|
|
90
|
+
function validate(selectedTime: string | null | undefined): string | null {
|
|
91
|
+
// No value -> no error (let required-validation happen elsewhere if needed)
|
|
92
|
+
if (!selectedTime) return null;
|
|
93
|
+
|
|
94
|
+
// Simple HH:MM check – prevents parse errors and immediate feedback
|
|
95
|
+
const isHHMM = /^\d{2}:\d{2}$/.test(selectedTime);
|
|
96
|
+
if (!isHHMM) {
|
|
97
|
+
return "Bitte eine gültige Uhrzeit im Format HH:MM eingeben.";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Closed or no selectable window
|
|
101
|
+
if (isClosedToday.value || !minTime.value || !maxTime.value) {
|
|
102
|
+
return helperText.value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Bound checks (string compare works for HH:MM)
|
|
106
|
+
if (selectedTime < minTime.value) {
|
|
107
|
+
return `Die Zeit liegt vor dem frühestmöglichen Zeitpunkt. Frühestens ab ${minTime.value}.`;
|
|
108
|
+
}
|
|
109
|
+
if (selectedTime > maxTime.value) {
|
|
110
|
+
return `Die Zeit liegt nach dem spätestmöglichen Zeitpunkt. Spätestens bis ${maxTime.value}.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Optional: enforce 5-minute steps (step="300" in input)
|
|
114
|
+
try {
|
|
115
|
+
const parsed = parseTimeString(selectedTime, now.value);
|
|
116
|
+
const minutes = parsed.getHours() * 60 + parsed.getMinutes();
|
|
117
|
+
if (minutes % 5 !== 0) {
|
|
118
|
+
return "Bitte eine Zeit in 5-Minuten-Schritten auswählen.";
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// If parsing fails for some reason, show a generic message
|
|
122
|
+
return "Bitte eine gültige Uhrzeit auswählen.";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
earliest,
|
|
130
|
+
activeInterval,
|
|
131
|
+
minTime,
|
|
132
|
+
maxTime,
|
|
133
|
+
isClosedToday,
|
|
134
|
+
helperText,
|
|
135
|
+
clampTimeToInterval,
|
|
136
|
+
isTimeWithinBounds,
|
|
137
|
+
validate,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { onMounted, onUnmounted } from "vue";
|
|
2
|
+
|
|
3
|
+
export function useInterval(callback: () => void, interval: number) {
|
|
4
|
+
let timer: NodeJS.Timeout | null = null;
|
|
5
|
+
|
|
6
|
+
onMounted(() => {
|
|
7
|
+
timer = setInterval(callback, interval);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
onUnmounted(() => {
|
|
11
|
+
if (timer !== null) {
|
|
12
|
+
clearInterval(timer);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useContext, useShopwareContext } from "#imports";
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
|
|
4
|
+
type usePizzaToppingsReturn = {
|
|
5
|
+
deliveryTime: ComputedRef<number>;
|
|
6
|
+
isCheckoutEnabled: ComputedRef<boolean>;
|
|
7
|
+
refresh(): Promise<Schemas["PizzaToppings"]>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function usePizzaToppings(): usePizzaToppingsReturn {
|
|
11
|
+
const { apiClient } = useShopwareContext();
|
|
12
|
+
|
|
13
|
+
const _deliveryTime = useContext<number>("deliveryTime");
|
|
14
|
+
const _isCheckoutEnabled = useContext<boolean>("isCheckoutActive");
|
|
15
|
+
|
|
16
|
+
async function refresh(): Promise<Schemas["PizzaToppings"]> {
|
|
17
|
+
const { data } = await apiClient.invoke(
|
|
18
|
+
"pizza-toppings.get get /pizza-toppings",
|
|
19
|
+
);
|
|
20
|
+
_deliveryTime.value = data.deliveryTime;
|
|
21
|
+
_isCheckoutEnabled.value = data.isCheckoutEnabled;
|
|
22
|
+
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
deliveryTime: computed(() => _deliveryTime.value),
|
|
28
|
+
isCheckoutEnabled: computed(() => _isCheckoutEnabled.value),
|
|
29
|
+
refresh,
|
|
30
|
+
};
|
|
31
|
+
}
|