@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.
Files changed (136) hide show
  1. package/.dockerignore +28 -0
  2. package/.env.example +11 -0
  3. package/.github/workflows/build.yaml +48 -0
  4. package/.github/workflows/ci.yaml +102 -0
  5. package/.prettierignore +6 -0
  6. package/.prettierrc +1 -0
  7. package/api-types/storeApiSchema.json +13863 -0
  8. package/api-types/storeApiTypes.d.ts +7010 -0
  9. package/app/app.config.ts +18 -0
  10. package/app/app.vue +99 -0
  11. package/app/assets/css/main.css +60 -0
  12. package/app/assets/fonts/Courier_Prime/CourierPrime-Bold.ttf +0 -0
  13. package/app/assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf +0 -0
  14. package/app/assets/fonts/Courier_Prime/CourierPrime-Italic.ttf +0 -0
  15. package/app/assets/fonts/Courier_Prime/CourierPrime-Regular.ttf +0 -0
  16. package/app/assets/fonts/Courier_Prime/OFL.txt +93 -0
  17. package/app/assets/fonts/Kalam/Kalam-Bold.ttf +0 -0
  18. package/app/assets/fonts/Kalam/Kalam-Light.ttf +0 -0
  19. package/app/assets/fonts/Kalam/Kalam-Regular.ttf +0 -0
  20. package/app/assets/fonts/Kalam/OFL.txt +93 -0
  21. package/app/assets/fonts/Marcellus/Marcellus-Regular.ttf +0 -0
  22. package/app/assets/fonts/Marcellus/OFL.txt +93 -0
  23. package/app/assets/fonts/Sora/OFL.txt +93 -0
  24. package/app/assets/fonts/Sora/README.txt +70 -0
  25. package/app/assets/fonts/Sora/Sora-VariableFont_wght.ttf +0 -0
  26. package/app/assets/fonts/Sora/static/Sora-Bold.ttf +0 -0
  27. package/app/assets/fonts/Sora/static/Sora-ExtraBold.ttf +0 -0
  28. package/app/assets/fonts/Sora/static/Sora-ExtraLight.ttf +0 -0
  29. package/app/assets/fonts/Sora/static/Sora-Light.ttf +0 -0
  30. package/app/assets/fonts/Sora/static/Sora-Medium.ttf +0 -0
  31. package/app/assets/fonts/Sora/static/Sora-Regular.ttf +0 -0
  32. package/app/assets/fonts/Sora/static/Sora-SemiBold.ttf +0 -0
  33. package/app/assets/fonts/Sora/static/Sora-Thin.ttf +0 -0
  34. package/app/components/AddToWishlist.vue +55 -0
  35. package/app/components/Address/Card.vue +32 -0
  36. package/app/components/Address/Detail.vue +22 -0
  37. package/app/components/Address/Form.vue +117 -0
  38. package/app/components/AnimatedSection.vue +77 -0
  39. package/app/components/BottomNavi.vue +63 -0
  40. package/app/components/Cart/Item.vue +112 -0
  41. package/app/components/Cart/QuickView.vue +55 -0
  42. package/app/components/Category/Header.vue +53 -0
  43. package/app/components/Category/Listing.vue +295 -0
  44. package/app/components/Checkout/DeliveryTimeSelect.vue +177 -0
  45. package/app/components/Checkout/LoginOrRegister.vue +43 -0
  46. package/app/components/Checkout/PaymentAndDelivery.vue +101 -0
  47. package/app/components/Checkout/PaymentMethod.vue +30 -0
  48. package/app/components/Checkout/ShippingMethod.vue +30 -0
  49. package/app/components/Checkout/Summary.vue +125 -0
  50. package/app/components/Cta.vue +34 -0
  51. package/app/components/Features.vue +36 -0
  52. package/app/components/Food/Marquee.vue +35 -0
  53. package/app/components/Food/MarqueeItem.vue +72 -0
  54. package/app/components/Footer.vue +51 -0
  55. package/app/components/Header.vue +160 -0
  56. package/app/components/Hero.vue +77 -0
  57. package/app/components/ImageGallery.vue +46 -0
  58. package/app/components/Loading.vue +29 -0
  59. package/app/components/Navigation/DesktopLeft.vue +51 -0
  60. package/app/components/Navigation/DesktopLeft2.vue +43 -0
  61. package/app/components/Navigation/MobileTop.vue +59 -0
  62. package/app/components/Navigation/MobileTop2.vue +42 -0
  63. package/app/components/Order/Detail.vue +84 -0
  64. package/app/components/Product/Card.vue +132 -0
  65. package/app/components/Product/Category.vue +153 -0
  66. package/app/components/Product/Configurator.vue +65 -0
  67. package/app/components/Product/CrossSelling.vue +95 -0
  68. package/app/components/Product/DeselectIngredient.vue +46 -0
  69. package/app/components/Product/Detail.vue +187 -0
  70. package/app/components/Product/SearchBar.vue +109 -0
  71. package/app/components/PublicAnnouncement.vue +17 -0
  72. package/app/components/Topseller.vue +43 -0
  73. package/app/components/User/Detail.vue +47 -0
  74. package/app/components/User/LoginForm.vue +105 -0
  75. package/app/components/User/RegistrationForm.vue +340 -0
  76. package/app/components/Wishlist.vue +102 -0
  77. package/app/composables/useDeliveryTime.ts +139 -0
  78. package/app/composables/useInterval.ts +15 -0
  79. package/app/composables/usePizzaToppings.ts +31 -0
  80. package/app/composables/useProductEvents.test.ts +111 -0
  81. package/app/composables/useProductEvents.ts +22 -0
  82. package/app/composables/useProductVariants.ts +61 -0
  83. package/app/composables/useScrollAnimation.ts +39 -0
  84. package/app/composables/useTopSellers.ts +34 -0
  85. package/app/error.vue +30 -0
  86. package/app/layouts/account.vue +74 -0
  87. package/app/layouts/default.vue +6 -0
  88. package/app/layouts/listing.vue +32 -0
  89. package/app/layouts/listing2.vue +8 -0
  90. package/app/middleware/trailing-slash.global.ts +19 -0
  91. package/app/pages/account/recover/password/index.vue +143 -0
  92. package/app/pages/anmelden.vue +32 -0
  93. package/app/pages/bestellung.vue +103 -0
  94. package/app/pages/c/[...all].vue +49 -0
  95. package/app/pages/index.vue +59 -0
  96. package/app/pages/konto/adressen.vue +135 -0
  97. package/app/pages/konto/bestellung/[id].vue +41 -0
  98. package/app/pages/konto/bestellungen.vue +53 -0
  99. package/app/pages/konto/index.vue +74 -0
  100. package/app/pages/konto/profil.vue +160 -0
  101. package/app/pages/merkliste.vue +11 -0
  102. package/app/pages/order/[id].vue +69 -0
  103. package/app/pages/passwort-vergessen.vue +103 -0
  104. package/app/pages/registrierung/bestaetigen.vue +44 -0
  105. package/app/pages/registrierung/index.vue +24 -0
  106. package/app/pages/speisekarte.vue +58 -0
  107. package/app/pages/unternehmen/[slug].vue +66 -0
  108. package/app/types/Association.d.ts +11 -0
  109. package/app/utils/businessHours.ts +119 -0
  110. package/app/utils/formatDate.ts +9 -0
  111. package/app/utils/holidays.ts +43 -0
  112. package/app/utils/storeHours.ts +8 -0
  113. package/app/utils/time.ts +20 -0
  114. package/app/validation/addressSchema.ts +34 -0
  115. package/app/validation/registrationSchema.ts +156 -0
  116. package/bun.dockerfile +60 -0
  117. package/compose.yml +17 -0
  118. package/container +7 -0
  119. package/content/index.yml +91 -0
  120. package/content/navigation.yml +67 -0
  121. package/content/unternehmen/agb.md +1 -0
  122. package/content/unternehmen/datenschutz.md +1 -0
  123. package/content/unternehmen/impressum.md +39 -0
  124. package/content.config.ts +134 -0
  125. package/eslint.config.mjs +8 -0
  126. package/node.dockerfile +33 -0
  127. package/nuxt.config.ts +153 -0
  128. package/package.json +70 -0
  129. package/public/dark/Logo.svg +32 -0
  130. package/public/favicon.ico +0 -0
  131. package/public/light/Logo.svg +32 -0
  132. package/renovate.json +4 -0
  133. package/server/tsconfig.json +3 -0
  134. package/shopware.d.ts +19 -0
  135. package/tsconfig.json +4 -0
  136. package/vitest.config.mts +26 -0
@@ -0,0 +1,295 @@
1
+ <script setup lang="ts">
2
+ import type { operations, Schemas } from "#shopware";
3
+
4
+ const props = defineProps<{
5
+ id: string;
6
+ }>();
7
+
8
+ const { id: categoryId } = toRefs(props);
9
+
10
+ const {
11
+ resetFilters,
12
+ loading,
13
+ search,
14
+ getElements,
15
+ getCurrentSortingOrder,
16
+ getSortingOrders,
17
+ changeCurrentSortingOrder,
18
+ getAvailableFilters,
19
+ getCurrentFilters,
20
+ setCurrentFilters,
21
+ } = useListing({
22
+ listingType: "categoryListing",
23
+ categoryId: props.id,
24
+ });
25
+
26
+ const { search: categorySearch } = useCategorySearch();
27
+
28
+ const { data: category } = await useAsyncData(
29
+ `category${categoryId.value}`,
30
+ async () => {
31
+ return await categorySearch(categoryId.value);
32
+ },
33
+ );
34
+
35
+ const currentSorting = ref(getCurrentSortingOrder.value ?? "Sortieren");
36
+
37
+ const propertyFilters = computed<Schemas["PropertyGroup"][]>(() =>
38
+ getAvailableFilters.value?.filter(
39
+ (availableFilter) => availableFilter.code === "properties",
40
+ ),
41
+ );
42
+
43
+ const selectedPropertyFilters = ref(getCurrentFilters.value?.properties ?? []);
44
+ const selectedPropertyFiltersString = computed(() =>
45
+ selectedPropertyFilters.value?.join("|"),
46
+ );
47
+
48
+ const selectedListingFilters = computed<ShortcutFilterParam[]>(() => {
49
+ return [
50
+ {
51
+ code: "properties",
52
+ value: selectedPropertyFiltersString.value,
53
+ },
54
+ ];
55
+ });
56
+
57
+ const query = {
58
+ includes: {
59
+ product: [
60
+ "id",
61
+ "productNumber",
62
+ "name",
63
+ "description",
64
+ "calculatedPrice",
65
+ "translated",
66
+ "categories",
67
+ "properties",
68
+ "propertyIds",
69
+ "options",
70
+ "optionIds",
71
+ "configuratorSettings",
72
+ "children",
73
+ "parentId",
74
+ "sortedProperties",
75
+ "cover",
76
+ "parentId",
77
+ ],
78
+ property: ["id", "name", "translated", "options"],
79
+ property_group_option: ["id", "name", "translated", "group"],
80
+ product_configurator_setting: ["id", "optionId", "option", "productId"],
81
+ product_option: ["id", "groupId", "name", "translated", "group"],
82
+ },
83
+ sort: [
84
+ {
85
+ field: "productNumber",
86
+ order: "ASC",
87
+ },
88
+ ],
89
+ associations: {
90
+ cover: {
91
+ associations: {
92
+ media: {},
93
+ },
94
+ },
95
+ categories: {},
96
+ properties: {
97
+ associations: {
98
+ group: {},
99
+ },
100
+ },
101
+ options: {
102
+ associations: {
103
+ group: {},
104
+ },
105
+ },
106
+ configuratorSettings: {
107
+ associations: {
108
+ option: {
109
+ associations: {
110
+ group: {},
111
+ },
112
+ },
113
+ },
114
+ },
115
+ children: {
116
+ associations: {
117
+ properties: {
118
+ associations: {
119
+ group: {},
120
+ },
121
+ },
122
+ options: {
123
+ associations: {
124
+ group: {},
125
+ },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ } as operations["searchPage post /search"]["body"];
131
+
132
+ await useAsyncData(`listing${categoryId.value}`, async () => {
133
+ await search(query);
134
+ });
135
+
136
+ watch(selectedListingFilters, () => {
137
+ setCurrentFilters(selectedListingFilters.value);
138
+ });
139
+
140
+ watch(currentSorting, () => {
141
+ changeCurrentSortingOrder(currentSorting.value as string);
142
+ });
143
+
144
+ async function handleFilterRest() {
145
+ await resetFilters();
146
+ selectedPropertyFilters.value = [];
147
+ }
148
+
149
+ const moreThanOneFilterAndOption = computed<boolean>(
150
+ () => propertyFilters.value.length > 0,
151
+ );
152
+ </script>
153
+
154
+ <template>
155
+ <UContainer>
156
+ <UPage>
157
+ <template #left>
158
+ <UPageAside>
159
+ <NavigationDesktopLeft2 />
160
+ </UPageAside>
161
+ </template>
162
+
163
+ <UPageBody>
164
+ <div>
165
+ <CategoryHeader v-if="category" :category="category" />
166
+ <div class="flex flex-row justify-between gap-4 mb-4">
167
+ <UBadge
168
+ variant="subtle"
169
+ :label="`${getElements.length} Produkte`"
170
+ />
171
+ <USelect
172
+ v-model="currentSorting"
173
+ icon="i-lucide-arrow-down-wide-narrow"
174
+ value-key="key"
175
+ :items="getSortingOrders"
176
+ placeholder="Sortierung"
177
+ />
178
+ <UDrawer
179
+ v-if="moreThanOneFilterAndOption"
180
+ class="lg:hidden"
181
+ title="Filter"
182
+ direction="right"
183
+ >
184
+ <UButton
185
+ label="Filter"
186
+ icon="i-lucide-sliders-horizontal"
187
+ color="neutral"
188
+ variant="subtle"
189
+ />
190
+
191
+ <template #body>
192
+ <div class="flex flex-col gap-4">
193
+ <div
194
+ v-for="filter in propertyFilters"
195
+ :key="filter.id"
196
+ class="flex flex-col gap-4"
197
+ >
198
+ <UCollapsible
199
+ class="flex flex-col gap-2 w-48"
200
+ :default-open="true"
201
+ >
202
+ <UButton
203
+ :label="filter.translated.name"
204
+ color="neutral"
205
+ variant="subtle"
206
+ trailing-icon="i-lucide-chevron-down"
207
+ block
208
+ :ui="{
209
+ trailingIcon:
210
+ 'group-data-[state=open]:rotate-180 transition-transform duration-200',
211
+ }"
212
+ />
213
+
214
+ <template #content>
215
+ <UCheckboxGroup
216
+ v-model="selectedPropertyFilters"
217
+ :items="filter.options"
218
+ value-key="id"
219
+ label-key="translated.name"
220
+ />
221
+ </template>
222
+ </UCollapsible>
223
+ </div>
224
+ <UButton
225
+ label="Zurücksetzten"
226
+ variant="outline"
227
+ block
228
+ @click="handleFilterRest"
229
+ />
230
+ </div>
231
+ </template>
232
+ </UDrawer>
233
+ </div>
234
+
235
+ <Loading v-if="loading" text="Produkte werden geladen..." size="lg" />
236
+
237
+ <div v-else class="flex flex-col gap-4">
238
+ <ProductCard
239
+ v-for="product in getElements"
240
+ :key="product.id"
241
+ :product="product"
242
+ :with-favorite-button="true"
243
+ :with-add-to-cart-button="true"
244
+ />
245
+ </div>
246
+ </div>
247
+ </UPageBody>
248
+
249
+ <template #right>
250
+ <UPageAside>
251
+ <div v-if="moreThanOneFilterAndOption" class="flex flex-col gap-4">
252
+ <h3>Filter</h3>
253
+ <div
254
+ v-for="filter in propertyFilters"
255
+ :key="filter.id"
256
+ class="flex flex-col gap-4"
257
+ >
258
+ <UCollapsible
259
+ class="flex flex-col gap-2 w-48"
260
+ :default-open="true"
261
+ >
262
+ <UButton
263
+ :label="filter.translated.name"
264
+ color="neutral"
265
+ variant="subtle"
266
+ trailing-icon="i-lucide-chevron-down"
267
+ block
268
+ :ui="{
269
+ trailingIcon:
270
+ 'group-data-[state=open]:rotate-180 transition-transform duration-200',
271
+ }"
272
+ />
273
+
274
+ <template #content>
275
+ <UCheckboxGroup
276
+ v-model="selectedPropertyFilters"
277
+ :items="filter.options"
278
+ value-key="id"
279
+ label-key="translated.name"
280
+ />
281
+ </template>
282
+ </UCollapsible>
283
+ </div>
284
+ <UButton
285
+ label="Zurücksetzten"
286
+ variant="outline"
287
+ block
288
+ @click="handleFilterRest"
289
+ />
290
+ </div>
291
+ </UPageAside>
292
+ </template>
293
+ </UPage>
294
+ </UContainer>
295
+ </template>
@@ -0,0 +1,177 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onMounted, onUnmounted, computed } from "vue";
3
+ import { useDeliveryTime } from "~/composables/useDeliveryTime";
4
+ import { isClosedHoliday } from "~/utils/holidays";
5
+
6
+ const props = defineProps<{
7
+ modelValue?: string;
8
+ valid?: boolean;
9
+ }>();
10
+
11
+ const emit = defineEmits<{
12
+ (e: "update:modelValue", value: string | null): void;
13
+ (e: "update:valid", value: boolean): void;
14
+ }>();
15
+
16
+ const { deliveryTime } = usePizzaToppings();
17
+
18
+ const selected = ref<string>(props.modelValue ?? "");
19
+ const now = ref<Date>(new Date());
20
+
21
+ function refreshCurrentTime(): void {
22
+ now.value = new Date();
23
+ }
24
+
25
+ let timeUpdateInterval: ReturnType<typeof setInterval> | undefined;
26
+
27
+ onMounted(() => {
28
+ timeUpdateInterval = globalThis.setInterval(refreshCurrentTime, 60_000);
29
+ });
30
+
31
+ onUnmounted(() => {
32
+ if (timeUpdateInterval) {
33
+ globalThis.clearInterval(timeUpdateInterval);
34
+ }
35
+ });
36
+
37
+ const {
38
+ minTime,
39
+ maxTime,
40
+ isClosedToday,
41
+ helperText,
42
+ clampTimeToInterval,
43
+ isTimeWithinBounds,
44
+ validate,
45
+ } = useDeliveryTime(now);
46
+
47
+ const validationError = computed<string | null>(() =>
48
+ selected.value ? validate(selected.value) : null,
49
+ );
50
+
51
+ const isValid = computed<boolean>(() => {
52
+ if (!selected.value) return false;
53
+ if (isClosedHoliday(now.value)) return false;
54
+ return validate(selected.value) === null;
55
+ });
56
+
57
+ const deliveryTimeInfo = computed(() => {
58
+ return "Geschätzte Lieferzeit beträgt " + deliveryTime.value + " min.";
59
+ });
60
+
61
+ watch(
62
+ isValid,
63
+ (v) => {
64
+ emit("update:valid", v);
65
+ },
66
+ { immediate: true },
67
+ );
68
+
69
+ watch(
70
+ () => props.modelValue,
71
+ (newValue) => {
72
+ if (typeof newValue === "string" && newValue !== selected.value) {
73
+ selected.value = newValue;
74
+ }
75
+ },
76
+ );
77
+
78
+ function roundToNext5MinInterval(timeString: string | null): string {
79
+ if (!timeString) return "";
80
+ const [hours, minutes] = timeString.split(":").map(Number);
81
+ const totalMinutes = hours * 60 + minutes;
82
+ const roundedMinutes = Math.ceil(totalMinutes / 5) * 5;
83
+ const newHours = Math.floor(roundedMinutes / 60) % 24;
84
+ const newMinutes = roundedMinutes % 60;
85
+ return `${newHours.toString().padStart(2, "0")}:${newMinutes.toString().padStart(2, "0")}`;
86
+ }
87
+
88
+ // Move initialization to a proper place
89
+ watch(
90
+ [minTime, maxTime],
91
+ () => {
92
+ if (!minTime.value || !maxTime.value) {
93
+ if (selected.value) {
94
+ selected.value = "";
95
+ emit("update:modelValue", null);
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (!selected.value) {
101
+ selected.value = roundToNext5MinInterval(minTime.value);
102
+ emit("update:modelValue", selected.value);
103
+ return;
104
+ }
105
+
106
+ if (
107
+ selected.value &&
108
+ !isTimeWithinBounds(selected.value, minTime.value, maxTime.value)
109
+ ) {
110
+ const clampedTime = clampTimeToInterval(
111
+ selected.value,
112
+ minTime.value,
113
+ maxTime.value,
114
+ now.value,
115
+ );
116
+ if (clampedTime !== selected.value) {
117
+ selected.value = clampedTime;
118
+ emit("update:modelValue", clampedTime);
119
+ }
120
+ }
121
+ },
122
+ { immediate: true },
123
+ );
124
+
125
+ function handleTimeInput(event: Event): void {
126
+ const value = (event.target as HTMLInputElement).value;
127
+ selected.value = value;
128
+ emit("update:modelValue", value || null);
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <div v-if="!isClosedHoliday(now)" class="flex flex-col gap-2 mt-4">
134
+ <div class="flex flex-row items-center justify-between gap-4">
135
+ <div>Wunschlieferung- oder Abholzeit ab:</div>
136
+ <client-only>
137
+ <input
138
+ type="time"
139
+ :min="minTime ?? undefined"
140
+ :max="maxTime ?? undefined"
141
+ :disabled="isClosedToday || isClosedHoliday(now)"
142
+ :value="selected"
143
+ step="300"
144
+ class="border rounded px-2 py-1"
145
+ @input="handleTimeInput"
146
+ >
147
+ </client-only>
148
+ </div>
149
+ <p v-if="validationError" class="text-sm text-red-600">
150
+ {{ validationError }}
151
+ </p>
152
+ <p v-else class="text-sm text-gray-500">{{ helperText }}</p>
153
+ <UBadge
154
+ :label="deliveryTimeInfo"
155
+ icon="i-lucide-clock"
156
+ size="xl"
157
+ color="neutral"
158
+ variant="subtle"
159
+ />
160
+ <UBadge
161
+ label="Lieferzeiten können zu Stoßzeiten variieren."
162
+ icon="i-lucide-info"
163
+ size="xl"
164
+ color="warning"
165
+ variant="subtle"
166
+ />
167
+ </div>
168
+ <div v-else>
169
+ <UBadge
170
+ variant="subtle"
171
+ class="w-full"
172
+ icon="i-lucide-info"
173
+ color="error"
174
+ label="Geschlossen wegen Betriebsferien"
175
+ />
176
+ </div>
177
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import type { TabsItem } from "@nuxt/ui";
3
+
4
+ const { refreshUser } = useUser();
5
+ const items = [
6
+ {
7
+ label: "Daten erfassen",
8
+ slot: "register" as const,
9
+ },
10
+ {
11
+ label: "Einloggen",
12
+ slot: "login" as const,
13
+ },
14
+ ] satisfies TabsItem[];
15
+
16
+ const toast = useToast();
17
+
18
+ const { mergeWishlistProducts } = useWishlist();
19
+
20
+ async function handleLoginSuccess() {
21
+ await refreshUser();
22
+ mergeWishlistProducts();
23
+ toast.add({
24
+ title: "Wilkommen!",
25
+ color: "success",
26
+ progress: false,
27
+ });
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <div>
33
+ <UTabs :items="items" :ui="{ trigger: 'grow' }" class="gap-4 w-full">
34
+ <template #register>
35
+ <UserRegistrationForm @registration-success="handleLoginSuccess" />
36
+ </template>
37
+
38
+ <template #login>
39
+ <UserLoginForm />
40
+ </template>
41
+ </UTabs>
42
+ </div>
43
+ </template>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ RadioGroupItem,
4
+ RadioGroupValue,
5
+ } from "#ui/components/RadioGroup.vue";
6
+ import type { Schemas } from "#shopware";
7
+
8
+ const {
9
+ paymentMethods,
10
+ getPaymentMethods,
11
+ selectedPaymentMethod,
12
+ setPaymentMethod,
13
+ shippingMethods,
14
+ getShippingMethods,
15
+ selectedShippingMethod,
16
+ setShippingMethod,
17
+ } = useCheckout();
18
+
19
+ const { refreshCart } = useCart();
20
+
21
+ const toast = useToast();
22
+
23
+ onMounted(() => {
24
+ getPaymentMethods();
25
+ getShippingMethods();
26
+ });
27
+
28
+ const selectablePaymentMethods = computed<RadioGroupItem[]>(() => {
29
+ return paymentMethods.value?.map((method: Schemas["PaymentMethod"]) => ({
30
+ label: method.distinguishableName,
31
+ description: method.description,
32
+ value: method.id,
33
+ }));
34
+ });
35
+
36
+ const selectableShippingMethods = computed<RadioGroupItem[]>(() => {
37
+ return shippingMethods.value?.map((method: Schemas["ShippingMethod"]) => ({
38
+ label: method.name,
39
+ description: method.description,
40
+ value: method.id,
41
+ }));
42
+ });
43
+
44
+ const selectedPaymentMethodId = ref<RadioGroupValue>(
45
+ selectedPaymentMethod.value.id,
46
+ );
47
+ const selectedShippingMethodId = ref<RadioGroupValue>(
48
+ selectedShippingMethod.value.id,
49
+ );
50
+
51
+ watch(selectedPaymentMethodId, async (newValue: RadioGroupValue) => {
52
+ await setPaymentMethod({ id: newValue as string });
53
+ toast.add({
54
+ title: "Zahlart geändert",
55
+ description:
56
+ selectedPaymentMethod.value.distinguishableName + " ausgewählt",
57
+ color: "success",
58
+ progress: false,
59
+ });
60
+ });
61
+
62
+ watch(selectedShippingMethodId, async (newValue: RadioGroupValue) => {
63
+ await setShippingMethod({ id: newValue as string });
64
+ await refreshCart();
65
+ toast.add({
66
+ title: "Versandart geändert",
67
+ description: selectedShippingMethod.value.name + " ausgewählt",
68
+ color: "success",
69
+ progress: false,
70
+ });
71
+ });
72
+ </script>
73
+
74
+ <template>
75
+ <UContainer>
76
+ <div class="flex flex-col md:flex-row justify-between">
77
+ <div>
78
+ <div class="flex flex-row items-center gap-4">
79
+ <UIcon name="i-lucide-badge-euro" class="size-8" />
80
+ <h2 class="text-2xl text-blackish my-8">Zahlungsarten</h2>
81
+ </div>
82
+ <URadioGroup
83
+ v-model="selectedPaymentMethodId"
84
+ :items="selectablePaymentMethods"
85
+ variant="card"
86
+ />
87
+ </div>
88
+ <div>
89
+ <div class="flex flex-row items-center gap-4">
90
+ <UIcon name="i-lucide-car" class="size-8" />
91
+ <h2 class="text-2xl text-blackish my-8">Versandarten</h2>
92
+ </div>
93
+ <URadioGroup
94
+ v-model="selectedShippingMethodId"
95
+ :items="selectableShippingMethods"
96
+ variant="card"
97
+ />
98
+ </div>
99
+ </div>
100
+ </UContainer>
101
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+
4
+ const props = defineProps<{
5
+ paymentMethod: Schemas["PaymentMethod"] | null;
6
+ }>();
7
+
8
+ const { paymentMethod } = toRefs(props);
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ v-if="paymentMethod !== null"
14
+ class="flex flex-row gap-8 p-6 border rounded-lg border-primary"
15
+ >
16
+ <NuxtImg
17
+ v-if="paymentMethod.media?.url"
18
+ :src="paymentMethod.media?.url"
19
+ width="50"
20
+ />
21
+ <div>
22
+ <h3 class="text-base text-pretty font-semibold text-highlighted">
23
+ Zahlart
24
+ </h3>
25
+ <p class="text-[15px] text-pretty text-toned mt-1">
26
+ {{ paymentMethod.distinguishableName }}
27
+ </p>
28
+ </div>
29
+ </div>
30
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+
4
+ const props = defineProps<{
5
+ shippingMethod: Schemas["ShippingMethod"] | null;
6
+ }>();
7
+
8
+ const { shippingMethod } = toRefs(props);
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ v-if="shippingMethod !== null"
14
+ class="flex flex-row gap-8 p-6 border rounded-lg border-primary"
15
+ >
16
+ <NuxtImg
17
+ v-if="shippingMethod.media?.url"
18
+ :src="shippingMethod.media.url"
19
+ width="50"
20
+ />
21
+ <div>
22
+ <h3 class="text-base text-pretty font-semibold text-highlighted">
23
+ Versandart
24
+ </h3>
25
+ <p class="text-[15px] text-pretty text-toned mt-1">
26
+ {{ shippingMethod.name }}
27
+ </p>
28
+ </div>
29
+ </div>
30
+ </template>