@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,95 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, watch, computed, ref, toRefs } from "vue";
3
+ import type { Schemas } from "#shopware";
4
+ import type {
5
+ AssociationItemProduct,
6
+ AssociationItem,
7
+ } from "~/types/Association";
8
+
9
+ const DEFAULT_CURRENCY = "EUR";
10
+ const DEFAULT_LOCALE = "de-DE";
11
+ const ASSOCIATION_CONTEXT = "cross-selling";
12
+ const LOADING_ICON = "i-lucide-loader";
13
+ const DEFAULT_ICON = "i-lucide-plus";
14
+ const LOAD_ASSOCIATIONS_METHOD = "post";
15
+
16
+ const props = defineProps<{
17
+ product: Schemas["Product"];
18
+ }>();
19
+ const { product } = toRefs(props);
20
+ const selectedExtras = ref<AssociationItemProduct[]>([]);
21
+
22
+ const {
23
+ loadAssociations,
24
+ isLoading: isAssociationsLoading,
25
+ productAssociations,
26
+ } = useProductAssociations(product, {
27
+ associationContext: ASSOCIATION_CONTEXT,
28
+ });
29
+
30
+ const { getFormattedPrice } = usePrice({
31
+ currencyCode: DEFAULT_CURRENCY,
32
+ localeCode: DEFAULT_LOCALE,
33
+ });
34
+
35
+ function mapAssociationToItems(
36
+ associations: Schemas["CrossSellingElement"][],
37
+ ): AssociationItem[] {
38
+ return associations.map((crossSellingElement) => ({
39
+ label: crossSellingElement.crossSelling.name,
40
+ products: crossSellingElement.products.map(
41
+ (product: Schemas["Product"]) => ({
42
+ label: product.name,
43
+ value: product.id,
44
+ price: getFormattedPrice(product.calculatedPrice.unitPrice),
45
+ icon: DEFAULT_ICON,
46
+ }),
47
+ ),
48
+ }));
49
+ }
50
+
51
+ const associationItems = computed(() =>
52
+ mapAssociationToItems(productAssociations.value),
53
+ );
54
+
55
+ onMounted(() => {
56
+ loadAssociations({ method: LOAD_ASSOCIATIONS_METHOD, searchParams: {} });
57
+ });
58
+
59
+ watch(selectedExtras, () => emit("extras-selected", selectedExtras.value), {
60
+ deep: true,
61
+ });
62
+
63
+ const emit = defineEmits<{
64
+ "extras-selected": [selectedExtras: AssociationItemProduct[]];
65
+ }>();
66
+ </script>
67
+
68
+ <template>
69
+ <div v-if="!isAssociationsLoading" class="my-4">
70
+ <div
71
+ v-for="association in associationItems"
72
+ :key="association.label"
73
+ class=""
74
+ >
75
+ <div class="flex flex-row gap-2 items-center">
76
+ <div class="basis-1/3">{{ association.label }}:</div>
77
+ <UInputMenu
78
+ v-model="selectedExtras"
79
+ class="w-full"
80
+ :loading="isAssociationsLoading"
81
+ :loading-icon="LOADING_ICON"
82
+ multiple
83
+ :items="association.products"
84
+ icon="i-lucide-plus"
85
+ >
86
+ <template #item-trailing="{ item }">
87
+ <span class="">
88
+ {{ item.price }}
89
+ </span>
90
+ </template>
91
+ </UInputMenu>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </template>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+ import { toRefs, watch } from "vue";
4
+
5
+ const props = defineProps<{
6
+ product: Schemas["Product"];
7
+ }>();
8
+ const { product } = toRefs(props);
9
+
10
+ const deselected = ref([]);
11
+
12
+ const list = computed(() => {
13
+ const propsList = product.value?.properties ?? [];
14
+ return propsList
15
+ .filter(
16
+ (propertyGroupOption: Schemas["PropertyGroupOption"]) =>
17
+ propertyGroupOption.group.name === "Hauptzutaten",
18
+ )
19
+ .map(
20
+ (propertyGroupOption: Schemas["PropertyGroupOption"]) =>
21
+ propertyGroupOption.translated.name,
22
+ );
23
+ });
24
+
25
+ watch(deselected, () => emit("ingredients-deselected", deselected.value), {
26
+ deep: true,
27
+ });
28
+
29
+ const emit = defineEmits<{
30
+ "ingredients-deselected": [deselected: string[]];
31
+ }>();
32
+ </script>
33
+
34
+ <template>
35
+ <div v-if="list.length > 0" class="flex flex-row gap-2 my-6 items-center">
36
+ <div class="basis-1/3">Aber ohne:</div>
37
+ <USelect
38
+ v-model="deselected"
39
+ value-key="productId"
40
+ :items="list"
41
+ class="w-full"
42
+ icon="i-lucide-minus"
43
+ multiple
44
+ />
45
+ </div>
46
+ </template>
@@ -0,0 +1,187 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas, operations } from "#shopware";
3
+ import type { AssociationItemProduct } from "~/types/Association";
4
+ import { v5 as uuidv5 } from "uuid";
5
+ import { computed, ref, toRefs } from "vue";
6
+ import { useTrackEvent } from "#imports";
7
+
8
+ const UUID_NAMESPACE = "b098ef7e-0fa2-4073-b002-7ceec4360fbf";
9
+ const CART_SUCCESS_TITLE = "Gute Wahl!";
10
+ const LINE_ITEM_PRODUCT = "product";
11
+ const LINE_ITEM_CONTAINER = "container";
12
+
13
+ const props = defineProps<{
14
+ product: Schemas["Product"];
15
+ }>();
16
+ const { product } = toRefs(props);
17
+ const { addProducts, refreshCart } = useCart();
18
+ const toast = useToast();
19
+ const { triggerProductAdded } = useProductEvents();
20
+
21
+ const selectedExtras = ref<AssociationItemProduct[]>([]);
22
+ const deselectedIngredients = ref<string[]>([]);
23
+ const selectedQuantity = ref(1);
24
+ const selectedProduct = ref<Schemas["Product"]>(product.value);
25
+
26
+ const cartItemLabel = computed(() => {
27
+ const formatIngredientModifications = (
28
+ items: Array<{ label: string } | string>,
29
+ prefix: string,
30
+ ): string => {
31
+ if (!items.length) return "";
32
+
33
+ const labels = items.map((item) =>
34
+ typeof item === "string" ? item : item.label,
35
+ );
36
+ const separator = " " + prefix;
37
+ return " " + prefix + labels.join(separator);
38
+ };
39
+
40
+ const extrasFormatted = formatIngredientModifications(
41
+ selectedExtras.value,
42
+ "+",
43
+ );
44
+ const removedFormatted = formatIngredientModifications(
45
+ deselectedIngredients.value,
46
+ "-",
47
+ );
48
+
49
+ return `${selectedProduct.value.translated.name}${extrasFormatted}${removedFormatted}`;
50
+ });
51
+
52
+ const createExtras = () =>
53
+ selectedExtras.value.map((extra) => ({
54
+ id: extra.value,
55
+ type: LINE_ITEM_PRODUCT,
56
+ quantity: selectedQuantity.value,
57
+ }));
58
+
59
+ const generateSortedExtrasString = (extras: AssociationItemProduct[]) =>
60
+ extras
61
+ .map((extra) => extra.value)
62
+ .sort()
63
+ .join("");
64
+
65
+ const generateProductId = (baseId: string, extras: AssociationItemProduct[]) =>
66
+ extras.length ? baseId + generateSortedExtrasString(extras) : baseId;
67
+
68
+ function createCartItems(): operations["addLineItem post /checkout/cart/line-item"]["body"]["items"] {
69
+ const extras = createExtras();
70
+
71
+ // Simple product when no extras
72
+ if (extras.length === 0 && deselectedIngredients.value.length === 0) {
73
+ return [
74
+ {
75
+ id: selectedProduct.value.id,
76
+ quantity: selectedQuantity.value,
77
+ type: LINE_ITEM_PRODUCT,
78
+ label: cartItemLabel.value,
79
+ },
80
+ ];
81
+ }
82
+
83
+ // Container product when extras are selected
84
+ const generatedUuid = uuidv5(
85
+ generateProductId(selectedProduct.value.id, selectedExtras.value),
86
+ UUID_NAMESPACE,
87
+ );
88
+
89
+ return [
90
+ {
91
+ id: generatedUuid,
92
+ quantity: selectedQuantity.value,
93
+ type: LINE_ITEM_CONTAINER,
94
+ label: cartItemLabel.value,
95
+ payload: {
96
+ productNumber: selectedProduct.value.productNumber,
97
+ },
98
+ children: [
99
+ {
100
+ id: selectedProduct.value.id,
101
+ quantity: selectedQuantity.value,
102
+ type: LINE_ITEM_PRODUCT,
103
+ },
104
+ ...extras,
105
+ ],
106
+ },
107
+ ];
108
+ }
109
+
110
+ // Toast Notification
111
+ async function showSuccessToast() {
112
+ toast.add({
113
+ title: CART_SUCCESS_TITLE,
114
+ description: `${selectedProduct.value.translated.name} wurde in den Warenkorb gelegt.`,
115
+ icon: "i-lucide-shopping-cart",
116
+ color: "success",
117
+ progress: true,
118
+ color: "primary",
119
+ duration: 2000,
120
+ });
121
+ }
122
+
123
+ // Add to Cart
124
+ async function addToCart() {
125
+ const cartItems = createCartItems();
126
+ const newCart = await addProducts(cartItems);
127
+ await refreshCart(newCart);
128
+ await showSuccessToast();
129
+ emit("product-added");
130
+ triggerProductAdded(); // Clear search globally
131
+ useTrackEvent("add_to_cart", {
132
+ props: {
133
+ product_number: selectedProduct.value.productNumber,
134
+ quantity: selectedQuantity.value,
135
+ },
136
+ });
137
+ }
138
+
139
+ // Event Handlers
140
+ const onVariantSwitched = (variant: Schemas["Product"]) => {
141
+ selectedProduct.value = variant;
142
+ };
143
+
144
+ const onExtrasSelected = (extras: AssociationItemProduct[]) => {
145
+ selectedExtras.value = extras;
146
+ };
147
+
148
+ const onIngredientsDeselected = (deselected: string[]) => {
149
+ deselectedIngredients.value = deselected;
150
+ };
151
+ const emit = defineEmits(["product-added"]);
152
+ </script>
153
+
154
+ <template>
155
+ <div class="flex flex-col justify-between w-full">
156
+ <div>
157
+ <ProductConfigurator
158
+ :parent-product="product"
159
+ @variant-switched="onVariantSwitched"
160
+ />
161
+ <ProductCrossSelling
162
+ :product="selectedProduct"
163
+ @extras-selected="onExtrasSelected"
164
+ />
165
+ <ProductDeselectIngredient
166
+ :product="selectedProduct"
167
+ @ingredients-deselected="onIngredientsDeselected"
168
+ />
169
+ </div>
170
+ <div class="flex flex-row gap-4 mt-8">
171
+ <UInputNumber
172
+ v-model="selectedQuantity"
173
+ size="xl"
174
+ placeholder="Anzahl"
175
+ :min="1"
176
+ :max="100"
177
+ />
178
+ <UButton
179
+ size="xl"
180
+ label="In den Warenkorb"
181
+ icon="i-lucide-shopping-cart"
182
+ block
183
+ @click="addToCart"
184
+ />
185
+ </div>
186
+ </div>
187
+ </template>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ import { ref } from "vue";
3
+ import { onClickOutside, useDebounceFn, useFocus } from "@vueuse/core";
4
+ import type { Schemas } from "#shopware";
5
+ import { useTrackEvent } from "#imports";
6
+
7
+ withDefaults(
8
+ defineProps<{
9
+ searchInProgress?: boolean;
10
+ }>(),
11
+ {
12
+ searchInProgress: false,
13
+ },
14
+ );
15
+
16
+ const emit =
17
+ defineEmits<(e: "update:searchInProgress", value: boolean) => void>();
18
+
19
+ const { searchTerm, search, getProducts, loading } = useProductSearchSuggest();
20
+ const { onProductAdded } = useProductEvents();
21
+
22
+ const searchContainer = ref(null);
23
+ const searchInput = ref();
24
+ const minSearchLength = 1;
25
+
26
+ const active = ref(false);
27
+
28
+ // Clear search when product is added to cart
29
+ onProductAdded(() => {
30
+ typingQuery.value = "";
31
+ active.value = false;
32
+ });
33
+
34
+ watch(active, (value) => {
35
+ const { focused } = useFocus(searchInput);
36
+
37
+ focused.value = value;
38
+ });
39
+
40
+ // String the user has typed in the search field
41
+ const typingQuery = ref("");
42
+
43
+ watch(typingQuery, (value) => {
44
+ if (value.length >= minSearchLength) {
45
+ performSuggestSearch(value);
46
+ emit("update:searchInProgress", true);
47
+ } else {
48
+ emit("update:searchInProgress", false);
49
+ }
50
+ });
51
+
52
+ // Defer the search request to prevent the search from being triggered too after typing
53
+ const performSuggestSearch = useDebounceFn((value) => {
54
+ searchTerm.value = value;
55
+ search();
56
+ trackEvent(value);
57
+ }, 500);
58
+
59
+ const trackEvent = useDebounceFn((searchTerm: string) => {
60
+ const searchResult = getProducts.value.map(function (
61
+ product: Schemas["Product"],
62
+ ) {
63
+ return product.productNumber;
64
+ });
65
+ useTrackEvent("search", {
66
+ props: { search_term: searchTerm, search_result: searchResult.join(",") },
67
+ });
68
+ }, 500);
69
+
70
+ if (import.meta.client) {
71
+ onClickOutside(searchContainer, () => {
72
+ active.value = false;
73
+ });
74
+ }
75
+
76
+ const showSuggest = computed(() => {
77
+ return typingQuery.value.length >= minSearchLength && active.value;
78
+ });
79
+
80
+ defineExpose({
81
+ showSuggest,
82
+ products: getProducts,
83
+ loading,
84
+ });
85
+ </script>
86
+
87
+ <template>
88
+ <UInput
89
+ v-model="typingQuery"
90
+ name="search"
91
+ class="w-full my-2"
92
+ size="xl"
93
+ :loading="loading"
94
+ placeholder="Suche..."
95
+ :ui="{}"
96
+ @input="active = true"
97
+ >
98
+ <template v-if="typingQuery?.length" #trailing>
99
+ <UButton
100
+ color="neutral"
101
+ variant="link"
102
+ size="sm"
103
+ icon="i-lucide-circle-x"
104
+ aria-label="Clear input"
105
+ @click="typingQuery = ''"
106
+ />
107
+ </template>
108
+ </UInput>
109
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import { ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
3
+
4
+ defineProps<{
5
+ message?: string;
6
+ }>();
7
+ </script>
8
+
9
+ <template>
10
+ <div
11
+ v-if="message"
12
+ class="bg-red-100 z-50 text-blackish flex p-8 gap-4 shadow-md rounded-md my-4"
13
+ >
14
+ <ExclamationTriangleIcon class="w-14 h-14 min-w-12" />
15
+ <div>{{ message }}</div>
16
+ </div>
17
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import type { ButtonProps } from "#ui/components/Button.vue";
3
+
4
+ const { loadTopSellers } = useTopSellers();
5
+
6
+ const topSellers = await loadTopSellers();
7
+
8
+ const links = ref<ButtonProps[]>([
9
+ {
10
+ label: "Zur Speisekarte",
11
+ to: "/speisekarte",
12
+ color: "primary",
13
+ variant: "subtle",
14
+ trailingIcon: "i-lucide-arrow-right",
15
+ },
16
+ ]);
17
+ </script>
18
+
19
+ <template>
20
+ <div v-if="topSellers.length > 0">
21
+ <AnimatedSection
22
+ animation="fade-up"
23
+ duration="duration-1000"
24
+ delay="delay-100"
25
+ >
26
+ <UPageSection
27
+ headline="Bestseller"
28
+ title="Am meisten bestellt"
29
+ :links="links"
30
+ >
31
+ <UPageGrid>
32
+ <ProductCard
33
+ v-for="product in topSellers"
34
+ :key="product.productNumber"
35
+ :product="product"
36
+ :with-favorite-button="false"
37
+ :with-add-to-cart-button="false"
38
+ />
39
+ </UPageGrid>
40
+ </UPageSection>
41
+ </AnimatedSection>
42
+ </div>
43
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ const { user, userDefaultBillingAddress, userDefaultShippingAddress } =
3
+ useUser();
4
+
5
+ const fullName = computed(
6
+ () => user.value?.firstName + " " + user.value?.lastName,
7
+ );
8
+
9
+ const areAddressesDifferent = computed(() => {
10
+ if (!userDefaultBillingAddress.value || !userDefaultShippingAddress.value) {
11
+ return false; // Or true depending on your requirements
12
+ }
13
+
14
+ // Deep compare the address objects
15
+ return (
16
+ JSON.stringify(userDefaultBillingAddress.value) !==
17
+ JSON.stringify(userDefaultShippingAddress.value)
18
+ );
19
+ });
20
+ </script>
21
+
22
+ <template>
23
+ <div class="shadow-md rounded-md mb-4 p-6 bg-elevated">
24
+ <div>{{ fullName }}</div>
25
+ <div>{{ user?.email }}</div>
26
+ <USeparator
27
+ color="primary"
28
+ class="my-6"
29
+ :decorative="true"
30
+ :label="
31
+ areAddressesDifferent ? 'Lieferadresse' : 'Liefer- und Rechnungsadresse'
32
+ "
33
+ />
34
+ <AddressDetail :address="userDefaultShippingAddress" />
35
+ <USeparator
36
+ v-if="areAddressesDifferent"
37
+ class="my-6"
38
+ color="primary"
39
+ :decorative="true"
40
+ label="Rechnungsadresse"
41
+ />
42
+ <AddressDetail
43
+ v-if="areAddressesDifferent"
44
+ :address="userDefaultBillingAddress"
45
+ />
46
+ </div>
47
+ </template>
@@ -0,0 +1,105 @@
1
+ <script setup lang="ts">
2
+ import * as z from "zod";
3
+ import { useWishlist, useUser } from "@shopware/composables";
4
+ import type { FormSubmitEvent } from "@nuxt/ui";
5
+
6
+ const { isLoggedIn, login, user } = useUser();
7
+ const { mergeWishlistProducts } = useWishlist();
8
+ const toast = useToast();
9
+
10
+ const props = withDefaults(
11
+ defineProps<{
12
+ title?: string;
13
+ icon?: string;
14
+ withRegisterHint?: boolean;
15
+ }>(),
16
+ {
17
+ title: "",
18
+ icon: "",
19
+ withRegisterHint: false,
20
+ },
21
+ );
22
+
23
+ const emit = defineEmits<{
24
+ "login-success": [data: string];
25
+ }>();
26
+
27
+ const { title, icon } = toRefs(props);
28
+
29
+ if (isLoggedIn.value) {
30
+ navigateTo({ path: "/konto" });
31
+ }
32
+
33
+ const fields = [
34
+ {
35
+ name: "email",
36
+ type: "text" as const,
37
+ label: "Email",
38
+ placeholder: "Email-Adresse eingeben",
39
+ required: true,
40
+ },
41
+ {
42
+ name: "password",
43
+ label: "Passwort",
44
+ type: "password" as const,
45
+ placeholder: "Passwort eingeben",
46
+ required: true,
47
+ },
48
+ ];
49
+
50
+ const schema = z.object({
51
+ email: z.string().email("Email Adresse nicht gültig"),
52
+ password: z.string().min(8, "Mindestens 8 Zeichen"),
53
+ });
54
+
55
+ type Schema = z.output<typeof schema>;
56
+
57
+ async function onSubmit(payload: FormSubmitEvent<Schema>) {
58
+ await login({
59
+ username: payload.data.email,
60
+ password: payload.data.password,
61
+ });
62
+ toast.add({
63
+ title: "Hallo " + user.value.firstName + " " + user.value.lastName + "!",
64
+ description: "Erfolreich angemeldet.",
65
+ color: "success",
66
+ });
67
+ mergeWishlistProducts();
68
+ emit("login-success", payload.data.email);
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <UAuthForm
74
+ :schema="schema"
75
+ :title="title"
76
+ :icon="icon"
77
+ :fields="fields"
78
+ :submit="{
79
+ label: 'Anmelden',
80
+ }"
81
+ @submit="onSubmit"
82
+ >
83
+ <template v-if="withRegisterHint" #description>
84
+ Noch kein Konto erstellt?
85
+ <ULink to="registrierung" class="text-primary font-medium"
86
+ >Jetzt erstellen</ULink
87
+ >.
88
+ </template>
89
+ <template #password-hint>
90
+ <ULink
91
+ to="passwort-vergessen"
92
+ class="text-primary font-medium"
93
+ tabindex="-1"
94
+ >Passwort vergessen?</ULink
95
+ >
96
+ </template>
97
+ <template #footer>
98
+ Bei Anmeldung stimmst du unseren
99
+ <ULink to="datenschutz" class="text-primary font-medium"
100
+ >Datenschutzbestimmungen</ULink
101
+ >
102
+ zu.
103
+ </template>
104
+ </UAuthForm>
105
+ </template>