@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,111 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { useProductEvents } from "./useProductEvents";
3
+ import { nextTick } from "vue";
4
+
5
+ /**
6
+ * Unit tests for useProductEvents composable
7
+ *
8
+ * This composable provides a lightweight event bus for product-related events,
9
+ * specifically designed to handle the "product added to cart" event without
10
+ * requiring prop drilling or state management libraries like Pinia.
11
+ *
12
+ * Test Coverage:
13
+ * - Basic event triggering and listening
14
+ * - Multiple triggers and listeners
15
+ * - Edge cases (no listeners, callback errors)
16
+ * - Memory management (listener cleanup)
17
+ */
18
+ describe("useProductEvents", () => {
19
+ it("should trigger and listen to product added event", async () => {
20
+ const { triggerProductAdded, onProductAdded } = useProductEvents();
21
+ const callback = vi.fn();
22
+
23
+ onProductAdded(callback);
24
+ triggerProductAdded();
25
+
26
+ await nextTick();
27
+ expect(callback).toHaveBeenCalled();
28
+ });
29
+
30
+ it("should call callback when triggered multiple times", async () => {
31
+ const { triggerProductAdded, onProductAdded } = useProductEvents();
32
+ const callback = vi.fn();
33
+
34
+ onProductAdded(callback);
35
+ triggerProductAdded();
36
+ await nextTick();
37
+
38
+ triggerProductAdded();
39
+ await nextTick();
40
+
41
+ expect(callback).toHaveBeenCalledTimes(2);
42
+ });
43
+
44
+ it("should not call callback before trigger", () => {
45
+ const { onProductAdded } = useProductEvents();
46
+ const callback = vi.fn();
47
+
48
+ onProductAdded(callback);
49
+
50
+ expect(callback).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it("should work with multiple listeners", async () => {
54
+ const { triggerProductAdded, onProductAdded } = useProductEvents();
55
+ const callback1 = vi.fn();
56
+ const callback2 = vi.fn();
57
+
58
+ onProductAdded(callback1);
59
+ onProductAdded(callback2);
60
+ triggerProductAdded();
61
+
62
+ await nextTick();
63
+ expect(callback1).toHaveBeenCalled();
64
+ expect(callback2).toHaveBeenCalled();
65
+ });
66
+
67
+ it("should work when triggered without any listeners", () => {
68
+ const { triggerProductAdded } = useProductEvents();
69
+
70
+ // Should not throw when no listeners are registered
71
+ expect(() => triggerProductAdded()).not.toThrow();
72
+ });
73
+
74
+ it("should work with shared global state across multiple instances", async () => {
75
+ const instance1 = useProductEvents();
76
+ const instance2 = useProductEvents();
77
+
78
+ const callback1 = vi.fn();
79
+ const callback2 = vi.fn();
80
+
81
+ instance1.onProductAdded(callback1);
82
+ instance2.onProductAdded(callback2);
83
+
84
+ // Trigger from instance1
85
+ instance1.triggerProductAdded();
86
+ await nextTick();
87
+
88
+ // Both callbacks should be called (global event bus behavior)
89
+ expect(callback1).toHaveBeenCalled();
90
+ expect(callback2).toHaveBeenCalled();
91
+ });
92
+
93
+ it("should call callbacks on each trigger", async () => {
94
+ const { triggerProductAdded, onProductAdded } = useProductEvents();
95
+ const callback = vi.fn();
96
+
97
+ onProductAdded(callback);
98
+
99
+ triggerProductAdded();
100
+ await nextTick();
101
+ expect(callback).toHaveBeenCalledTimes(1);
102
+
103
+ triggerProductAdded();
104
+ await nextTick();
105
+ expect(callback).toHaveBeenCalledTimes(2);
106
+
107
+ triggerProductAdded();
108
+ await nextTick();
109
+ expect(callback).toHaveBeenCalledTimes(3);
110
+ });
111
+ });
@@ -0,0 +1,22 @@
1
+ import { ref, watch } from "vue";
2
+
3
+ const productAddedTrigger = ref(0);
4
+
5
+ export const useProductEvents = () => {
6
+ const triggerProductAdded = () => {
7
+ productAddedTrigger.value++;
8
+ };
9
+
10
+ const onProductAdded = (callback: () => void) => {
11
+ watch(productAddedTrigger, () => {
12
+ if (productAddedTrigger.value > 0) {
13
+ callback();
14
+ }
15
+ });
16
+ };
17
+
18
+ return {
19
+ triggerProductAdded,
20
+ onProductAdded,
21
+ };
22
+ };
@@ -0,0 +1,61 @@
1
+ import { computed } from "vue";
2
+ import type { Schemas } from "#shopware";
3
+
4
+ // Define the SelectItem interface to match what USelect expects
5
+ interface SelectItem {
6
+ label: string;
7
+ value: string;
8
+ productId: string;
9
+ }
10
+
11
+ type VariantOptions = {
12
+ name: string;
13
+ options: SelectItem[]; // Changed to match USelect's expected format
14
+ };
15
+
16
+ type useProductVariantsReturn = {
17
+ variants: ComputedRef<Record<string, VariantOptions>>;
18
+ };
19
+
20
+ export function useProductVariants(
21
+ configuratorSettings: Ref<Schemas["ProductConfiguratorSetting"][]>,
22
+ ): useProductVariantsReturn {
23
+ const variants = computed(() => {
24
+ const configSettings = configuratorSettings.value || [];
25
+ const result: Record<string, VariantOptions> = {};
26
+
27
+ for (const option of configSettings) {
28
+ const groupId = option.option?.group?.id;
29
+ const groupName =
30
+ option.option?.group?.translated?.name || option.option?.group?.name;
31
+ const optionName = option.option?.translated?.name || option.option?.name;
32
+ const optionId = option.option?.id;
33
+
34
+ if (groupId && groupName && optionName && optionId) {
35
+ result[groupId] ??= {
36
+ name: groupName,
37
+ options: [],
38
+ };
39
+
40
+ // Check if option already exists to avoid duplicates
41
+ const existingOption = result[groupId].options.find(
42
+ (item) => item.value === optionId,
43
+ );
44
+ if (!existingOption) {
45
+ // Add option in the format USelect expects
46
+ result[groupId].options.push({
47
+ label: optionName,
48
+ value: optionId,
49
+ productId: optionId,
50
+ });
51
+ }
52
+ }
53
+ }
54
+
55
+ return result;
56
+ });
57
+
58
+ return {
59
+ variants,
60
+ };
61
+ }
@@ -0,0 +1,39 @@
1
+ import { ref, onMounted, type Ref } from "vue";
2
+
3
+ export interface ScrollAnimationOptions {
4
+ threshold?: number;
5
+ rootMargin?: string;
6
+ }
7
+
8
+ export function useScrollAnimation(options: ScrollAnimationOptions = {}) {
9
+ const { threshold = 0.1, rootMargin = "0px 0px -100px 0px" } = options;
10
+
11
+ const isVisible = ref(false);
12
+ const elementRef: Ref<HTMLElement | null> = ref(null);
13
+
14
+ onMounted(() => {
15
+ if (!elementRef.value) return;
16
+
17
+ const observer = new IntersectionObserver(
18
+ (entries) => {
19
+ entries.forEach((entry) => {
20
+ if (entry.isIntersecting) {
21
+ isVisible.value = true;
22
+ observer.unobserve(entry.target);
23
+ }
24
+ });
25
+ },
26
+ {
27
+ threshold,
28
+ rootMargin,
29
+ },
30
+ );
31
+
32
+ observer.observe(elementRef.value);
33
+ });
34
+
35
+ return {
36
+ isVisible,
37
+ elementRef,
38
+ };
39
+ }
@@ -0,0 +1,34 @@
1
+ import type { Schemas } from "#shopware";
2
+
3
+ export type useTopSellersReturn = {
4
+ loadTopSellers(): Promise<Schemas["Card"]>;
5
+ };
6
+
7
+ export function useTopSellers(): useTopSellersReturn {
8
+ const { apiClient } = useShopwareContext();
9
+ async function loadTopSellers() {
10
+ try {
11
+ const result = await apiClient.invoke("getTopSellers post /product", {
12
+ body: {
13
+ filter: [{ type: "equals", field: "markAsTopseller", value: true }],
14
+ includes: {
15
+ product: [
16
+ "productNumber",
17
+ "name",
18
+ "description",
19
+ "calculatedPrice",
20
+ ],
21
+ },
22
+ },
23
+ });
24
+ return result.data.elements;
25
+ } catch (e) {
26
+ console.error("[useTopSellers][loadTopSellers]", e);
27
+ return [];
28
+ }
29
+ }
30
+
31
+ return {
32
+ loadTopSellers,
33
+ };
34
+ }
package/app/error.vue ADDED
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { NuxtError } from "#app";
3
+
4
+ const props = defineProps<{
5
+ error: NuxtError;
6
+ }>();
7
+
8
+ console.error(props.error);
9
+
10
+ const statusCode = props.error.statusCode;
11
+
12
+ useSeoMeta({
13
+ title: `${statusCode} | Pizzeria La Fattoria`,
14
+ });
15
+ </script>
16
+
17
+ <template>
18
+ <NuxtLayout>
19
+ <div class="mb-20 flex flex-col items-center justify-center px-5 py-3">
20
+ <h2 v-if="statusCode === 404" class="text-center">
21
+ Die angeforderte Seite konnte nicht gefunden werden.
22
+ </h2>
23
+ <h2 v-else class="text-center">Irgendwas ist schief gelaufen</h2>
24
+ <h3 class="max-w-lg whitespace-pre-line pb-8 pt-5 text-3xl">
25
+ {{ error.statusCode }}
26
+ </h3>
27
+ <NuxtLink to="" class="button">Zurück zur Starseite</NuxtLink>
28
+ </div>
29
+ </NuxtLayout>
30
+ </template>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ import type { NavigationMenuItem } from "@nuxt/ui";
3
+ import { useUser } from "@shopware/composables";
4
+ const toast = useToast();
5
+ const { isLoggedIn, logout } = useUser();
6
+
7
+ onMounted(() => {
8
+ if (!isLoggedIn.value) {
9
+ navigateTo("/anmelden");
10
+ }
11
+ });
12
+
13
+ watch(isLoggedIn, (newValue) => {
14
+ if (!newValue) {
15
+ navigateTo("/anmelden");
16
+ }
17
+ });
18
+
19
+ const logoutHandler = () => {
20
+ logout();
21
+ toast.add({
22
+ title: "Tschüss!",
23
+ description: "Erfolreich abgemeldet.",
24
+ color: "success",
25
+ });
26
+ };
27
+
28
+ const items = ref<NavigationMenuItem[][]>([
29
+ [
30
+ {
31
+ label: "Übersicht",
32
+ icon: "i-lucide-grip",
33
+ to: "/konto",
34
+ },
35
+ {
36
+ label: "Bestellungen",
37
+ icon: "i-lucide-book-open",
38
+ to: "/konto/bestellungen",
39
+ },
40
+ {
41
+ label: "Persönliches Profil",
42
+ icon: "i-lucide-user",
43
+ to: "/konto/profil",
44
+ },
45
+ {
46
+ label: "Adressen",
47
+ icon: "i-lucide-house",
48
+ to: "/konto/adressen",
49
+ },
50
+ {
51
+ label: "Abmelden",
52
+ icon: "i-lucide-log-out",
53
+ onSelect: logoutHandler,
54
+ },
55
+ ],
56
+ ]);
57
+ </script>
58
+ <template>
59
+ <UContainer>
60
+ <UPage>
61
+ <template #left>
62
+ <UNavigationMenu
63
+ highlight
64
+ highlight-color="primary"
65
+ orientation="vertical"
66
+ :items="items"
67
+ class="data-[orientation=vertical] py-8"
68
+ />
69
+ </template>
70
+ <slot />
71
+ <BottomNavi />
72
+ </UPage>
73
+ </UContainer>
74
+ </template>
@@ -0,0 +1,6 @@
1
+ <script setup lang="ts"></script>
2
+ <template>
3
+ <UPage>
4
+ <slot />
5
+ </UPage>
6
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ const { count } = useWishlist();
3
+ const { refresh } = usePizzaToppings();
4
+
5
+ onMounted(() => {
6
+ refresh();
7
+ });
8
+ </script>
9
+
10
+ <template>
11
+ <div>
12
+ <UContainer>
13
+ <UPage>
14
+ <template #left>
15
+ <UPageAside>
16
+ <NavigationDesktopLeft />
17
+ </UPageAside>
18
+ </template>
19
+
20
+ <UPageBody>
21
+ <slot />
22
+ </UPageBody>
23
+
24
+ <template #right>
25
+ <UPageAside>
26
+ <Wishlist v-if="count > 0" />
27
+ </UPageAside>
28
+ </template>
29
+ </UPage>
30
+ </UContainer>
31
+ </div>
32
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts"></script>
2
+
3
+ <template>
4
+ <UPage>
5
+ <NavigationMobileTop2 />
6
+ <slot />
7
+ </UPage>
8
+ </template>
@@ -0,0 +1,19 @@
1
+ import { defineNuxtRouteMiddleware, navigateTo } from "#app";
2
+
3
+ export default defineNuxtRouteMiddleware((to) => {
4
+ // Only handle category pages
5
+ if (!to.path.startsWith("/c/")) return;
6
+
7
+ // Skip if it already has a trailing slash or it's just "/c/"
8
+ if (to.path.endsWith("/") || to.path === "/c/") return;
9
+
10
+ // Preserve query and hash while adding the trailing slash
11
+ return navigateTo(
12
+ {
13
+ path: `${to.path}/`,
14
+ query: to.query,
15
+ hash: to.hash,
16
+ },
17
+ { redirectCode: 301 },
18
+ );
19
+ });
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ import * as z from "zod";
3
+ import type { FormSubmitEvent } from "@nuxt/ui";
4
+
5
+ useHead({
6
+ meta: [
7
+ { name: "robots", content: "noindex, nofollow" },
8
+ { name: "googlebot", content: "noindex, nofollow" },
9
+ ],
10
+ });
11
+
12
+ // Constants
13
+ const MIN_PASSWORD_LENGTH = 8;
14
+ const PASSWORD_MIN_LENGTH_ERROR = "Mindestens 8 Zeichen";
15
+ const PASSWORD_MISMATCH_ERROR = "Passwörter stimmen nicht überein";
16
+ const SUCCESS_TOAST_CONFIG = {
17
+ title: "Erfolgreich zurückgesetzt!",
18
+ description: "Melden Sie sich nun mit Ihrem neuen Passwort an.",
19
+ icon: "i-lucide-check",
20
+ color: "success" as const,
21
+ duration: 0,
22
+ close: true,
23
+ };
24
+ const ERROR_TOAST_CONFIG = {
25
+ title: "Fehler beim Senden",
26
+ icon: "i-lucide-x",
27
+ description: "Bitte versuchen Sie es später erneut.",
28
+ color: "error" as const,
29
+ };
30
+
31
+ const { apiClient } = useShopwareContext();
32
+ const route = useRoute();
33
+ const toast = useToast();
34
+
35
+ // Schemas
36
+ const routeValidationSchema = z.object({
37
+ hash: z.string().min(1, "Hash parameter is required and cannot be empty"),
38
+ });
39
+
40
+ const passwordResetSchema = z
41
+ .object({
42
+ newPassword: z.string().min(MIN_PASSWORD_LENGTH, PASSWORD_MIN_LENGTH_ERROR),
43
+ newPasswordConfirm: z
44
+ .string()
45
+ .min(MIN_PASSWORD_LENGTH, PASSWORD_MIN_LENGTH_ERROR),
46
+ })
47
+ .refine((data) => data.newPassword === data.newPasswordConfirm, {
48
+ message: PASSWORD_MISMATCH_ERROR,
49
+ path: ["newPasswordConfirm"],
50
+ });
51
+
52
+ type PasswordResetSchema = z.output<typeof passwordResetSchema>;
53
+
54
+ // Extract route validation logic
55
+ function validateRouteParameters(): string {
56
+ try {
57
+ const validatedParams = routeValidationSchema.parse(route.query);
58
+ return validatedParams.hash as string;
59
+ } catch (error) {
60
+ console.error("Invalid route parameters:", error);
61
+ throw createError({
62
+ statusCode: 400,
63
+ statusMessage: "Invalid validation",
64
+ });
65
+ }
66
+ }
67
+
68
+ // Extract toast creation functions
69
+ function showSuccessToast(): void {
70
+ toast.add(SUCCESS_TOAST_CONFIG);
71
+ }
72
+
73
+ function showErrorToast(): void {
74
+ toast.add(ERROR_TOAST_CONFIG);
75
+ }
76
+
77
+ const recoveryHash = ref<string>("");
78
+ recoveryHash.value = validateRouteParameters();
79
+
80
+ const formFields = [
81
+ {
82
+ name: "newPassword",
83
+ type: "password" as const,
84
+ label: "Neues Passwort",
85
+ placeholder: "Neues Passwort eingeben",
86
+ required: true,
87
+ },
88
+ {
89
+ name: "newPasswordConfirm",
90
+ type: "password" as const,
91
+ label: "Neues Passwort wiederholen",
92
+ placeholder: "Neues Passwort wiederholen",
93
+ required: true,
94
+ },
95
+ ];
96
+
97
+ async function handlePasswordReset(
98
+ payload: FormSubmitEvent<PasswordResetSchema>,
99
+ ) {
100
+ try {
101
+ await apiClient.invoke(
102
+ "recoveryPassword post /account/recovery-password-confirm",
103
+ {
104
+ body: {
105
+ newPassword: payload.data.newPassword,
106
+ newPasswordConfirm: payload.data.newPasswordConfirm,
107
+ hash: recoveryHash.value,
108
+ },
109
+ },
110
+ );
111
+ showSuccessToast();
112
+ navigateTo("/anmelden");
113
+ } catch (error) {
114
+ showErrorToast();
115
+ console.error("Password recovery error:", error);
116
+ }
117
+ }
118
+ </script>
119
+ <template>
120
+ <UContainer class="max-w-xl mx-auto mt-18">
121
+ <UAuthForm
122
+ :schema="passwordResetSchema"
123
+ title="Password zurücksetzten"
124
+ icon="i-lucide-shield-user"
125
+ :fields="formFields"
126
+ :submit="{
127
+ label: 'Senden',
128
+ }"
129
+ @submit="handlePasswordReset"
130
+ >
131
+ <template #description>
132
+ <p>Vergeben Sie hier Ihr neues Passwort.</p>
133
+ </template>
134
+ <template #footer>
135
+ Beim absenden stimmst du unseren
136
+ <ULink to="datenschutz" class="text-primary font-medium"
137
+ >Datenschutzbestimmungen</ULink
138
+ >
139
+ zu.
140
+ </template>
141
+ </UAuthForm>
142
+ </UContainer>
143
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ const { isLoggedIn } = useUser();
3
+
4
+ onMounted(() => {
5
+ if (isLoggedIn.value) {
6
+ navigateTo("/konto");
7
+ }
8
+ });
9
+
10
+ watch(isLoggedIn, (newValue) => {
11
+ if (newValue) {
12
+ navigateTo("/konto");
13
+ }
14
+ });
15
+
16
+ function handleLoginSuccess() {
17
+ navigateTo("/speisekarte");
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <UContainer>
23
+ <div class="max-w-xl mx-auto mt-16">
24
+ <UserLoginForm
25
+ title="Anmelden"
26
+ icon="i-lucide-user"
27
+ with-register-hint
28
+ @login-success="handleLoginSuccess"
29
+ />
30
+ </div>
31
+ </UContainer>
32
+ </template>