@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,69 @@
1
+ <script setup lang="ts">
2
+ import { useOrderDetails } from "@shopware/composables";
3
+ import { formatDate } from "~/utils/formatDate";
4
+
5
+ import type { RouteParams } from "vue-router";
6
+ import type { ButtonProps } from "#ui/components/Button.vue";
7
+
8
+ interface OrderRouteParams extends RouteParams {
9
+ id: string;
10
+ }
11
+
12
+ const route = useRoute();
13
+ const { id } = route.params as OrderRouteParams;
14
+ const { order, loadOrderDetails, status } = useOrderDetails(id);
15
+
16
+ const isLoadingData = ref(true);
17
+
18
+ onMounted(async () => {
19
+ isLoadingData.value = true;
20
+
21
+ await loadOrderDetails();
22
+
23
+ isLoadingData.value = false;
24
+ });
25
+ const links = ref<ButtonProps[]>([
26
+ {
27
+ label: "Zurück zur Startseite",
28
+ to: "/",
29
+ color: "error",
30
+ variant: "subtle",
31
+ },
32
+ ]);
33
+ </script>
34
+
35
+ <template>
36
+ <!-- Show loading spinner while fetching order data -->
37
+ <UContainer v-if="isLoadingData">
38
+ <UPageHeader headline="BESTELLUNG" title="Lädt..." />
39
+ <UPageBody>
40
+ <div class="flex flex-col items-center justify-center py-16 gap-4">
41
+ <UIcon
42
+ name="i-lucide-loader-circle"
43
+ class="w-12 h-12 animate-spin text-primary"
44
+ />
45
+ <p class="text-lg text-muted">Bestellung wird geladen...</p>
46
+ </div>
47
+ </UPageBody>
48
+ </UContainer>
49
+
50
+ <UContainer v-else-if="order">
51
+ <UPageHeader
52
+ headline="BESTELLUNG"
53
+ :title="order?.orderNumber"
54
+ :description="formatDate(order?.createdAt)"
55
+ />
56
+ <UPageBody>
57
+ <OrderDetail :order="order" :status="status ?? 'laden...'" />
58
+ </UPageBody>
59
+ </UContainer>
60
+
61
+ <UContainer v-else>
62
+ <UPageSection
63
+ headline="FEHLER"
64
+ title="Irgendwas ist schief gelaufen"
65
+ description="Sie können Speisen und Getränke abholen, liefern lassen, oder vor Ort in unserem Restaurant genießen."
66
+ :links="links"
67
+ />
68
+ </UContainer>
69
+ </template>
@@ -0,0 +1,103 @@
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 SUCCESS_TOAST_CONFIG = {
14
+ title: "Erfolgreich abgesendet!",
15
+ description: "Bitte überprüfen Sie Ihre Postfach.",
16
+ icon: "i-lucide-check",
17
+ color: "success" as const,
18
+ duration: 0,
19
+ close: true,
20
+ };
21
+
22
+ const ERROR_TOAST_CONFIG = {
23
+ title: "Fehler beim Senden",
24
+ icon: "i-lucide-x",
25
+ description: "Bitte versuchen Sie es später erneut.",
26
+ color: "error" as const,
27
+ };
28
+
29
+ const { apiClient } = useShopwareContext();
30
+ const toast = useToast();
31
+
32
+ const schema = z.object({
33
+ email: z.string().email("Ungültige E-Mail-Adresse"),
34
+ });
35
+
36
+ type Schema = z.output<typeof schema>;
37
+
38
+ const fields = [
39
+ {
40
+ name: "email",
41
+ type: "text" as const,
42
+ label: "Email",
43
+ placeholder: "Email-Adresse eingeben",
44
+ required: true,
45
+ },
46
+ ];
47
+
48
+ // Extract toast creation functions
49
+ function showSuccessToast(): void {
50
+ toast.add(SUCCESS_TOAST_CONFIG);
51
+ }
52
+
53
+ function showErrorToast(): void {
54
+ toast.add(ERROR_TOAST_CONFIG);
55
+ }
56
+
57
+ async function handlePasswordRecovery(payload: FormSubmitEvent<Schema>) {
58
+ try {
59
+ await apiClient.invoke("sendRecoveryMail post /account/recovery-password", {
60
+ body: {
61
+ email: payload.data.email,
62
+ storefrontUrl: useRuntimeConfig().public.storeUrl,
63
+ },
64
+ });
65
+ showSuccessToast();
66
+ } catch (error) {
67
+ showErrorToast();
68
+ console.error("Password recovery error:", error);
69
+ }
70
+ }
71
+ </script>
72
+ <template>
73
+ <UContainer class="max-w-xl mx-auto mt-18">
74
+ <UAuthForm
75
+ :schema="schema"
76
+ title="Password vergessen"
77
+ icon="i-lucide-shield-user"
78
+ :fields="fields"
79
+ :submit="{
80
+ label: 'Senden',
81
+ }"
82
+ @submit="handlePasswordRecovery"
83
+ >
84
+ <template #description>
85
+ <p>
86
+ Geben Sie Ihre E-Mail-Adresse ein um Ihr Passwort zurückzusetzten.
87
+ </p>
88
+ <p>
89
+ Wenn Sie bei uns ein Kundenkonto mit dieser Adresse angelegt haben
90
+ bekommen Sie in den nächsten Minuten eine E-Mail zugesendet mit
91
+ weiteren Anweisungen.
92
+ </p>
93
+ </template>
94
+ <template #footer>
95
+ Beim absenden stimmst du unseren
96
+ <ULink to="datenschutz" class="text-primary font-medium"
97
+ >Datenschutzbestimmungen</ULink
98
+ >
99
+ zu.
100
+ </template>
101
+ </UAuthForm>
102
+ </UContainer>
103
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ const route = useRoute();
3
+ const toast = useToast();
4
+
5
+ const { apiClient } = useShopwareContext();
6
+
7
+ const emParameter = computed(() => route.query.em as string | undefined);
8
+ const hashParameter = computed(() => route.query.hash as string | undefined);
9
+
10
+ const { mergeWishlistProducts } = useWishlist();
11
+
12
+ await useAsyncData("register-confirm", async () => {
13
+ try {
14
+ if (!emParameter.value || !hashParameter.value) {
15
+ throw new Error("Missing required parameters");
16
+ }
17
+
18
+ return await apiClient.invoke(
19
+ "registerConfirm post /account/register-confirm",
20
+ {
21
+ body: {
22
+ em: emParameter.value,
23
+ hash: hashParameter.value,
24
+ },
25
+ },
26
+ );
27
+ } catch (e) {
28
+ console.error("Registration confirmation failed:", e);
29
+ return null;
30
+ }
31
+ });
32
+
33
+ mergeWishlistProducts();
34
+ toast.add({
35
+ title: "Bestätigung erfolgreich!",
36
+ description: "Jetzt anmelden und los legen.",
37
+ color: "success",
38
+ });
39
+ navigateTo({ path: "/konto" });
40
+ </script>
41
+
42
+ <template>
43
+ <UContainer> Etwas ist schief gelaufen </UContainer>
44
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ const { isLoggedIn } = useUser();
3
+
4
+ if (import.meta.client && isLoggedIn.value) {
5
+ navigateTo({ path: "/konto" });
6
+ }
7
+
8
+ watch(isLoggedIn, (isLoggedIn) => {
9
+ if (isLoggedIn) {
10
+ navigateTo({ path: "/konto" });
11
+ }
12
+ });
13
+ </script>
14
+
15
+ <template>
16
+ <UPageSection
17
+ class="max-w-2xl mx-auto"
18
+ headline="KONTO"
19
+ title="Registrieren"
20
+ description="Erstelle dein Kundenkonto."
21
+ >
22
+ <UserRegistrationForm @registration-success="navigateTo('/konto')" />
23
+ </UPageSection>
24
+ </template>
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+
4
+ useSeoMeta({
5
+ title: "Speisekarte | Pizzeria La Fattoria",
6
+ });
7
+
8
+ definePageMeta({
9
+ layout: "listing",
10
+ });
11
+
12
+ const { loadNavigationElements, navigationElements } = useNavigation();
13
+ await loadNavigationElements({ depth: 1 });
14
+
15
+ const searchBarRef = ref<{
16
+ showSuggest: boolean;
17
+ loading: boolean;
18
+ products: Schemas["Product"];
19
+ } | null>(null);
20
+
21
+ const searchInProgress = ref(false);
22
+ </script>
23
+
24
+ <template>
25
+ <div v-if="navigationElements">
26
+ <div
27
+ class="sticky top-16 left-0 z-20 w-full px-4 md:px-6 lg:px-8 backdrop-blur-md rounded-md"
28
+ >
29
+ <NavigationMobileTop v-if="!searchInProgress" class="" />
30
+ <ProductSearchBar
31
+ ref="searchBarRef"
32
+ v-model:search-in-progress="searchInProgress"
33
+ />
34
+ </div>
35
+ <div
36
+ v-if="searchBarRef?.showSuggest"
37
+ class="flex flex-col gap-4 mt-4 px-4 md:px-6 lg:px-8"
38
+ >
39
+ <div v-if="!searchBarRef?.loading" class="flex flex-col gap-4">
40
+ <ProductCard
41
+ v-for="product in searchBarRef?.products"
42
+ :key="product.id"
43
+ :product="product"
44
+ :with-favorite-button="true"
45
+ :with-add-to-cart-button="true"
46
+ />
47
+ </div>
48
+ </div>
49
+ <div v-else class="px-4 md:px-6 lg:px-8">
50
+ <ProductCategory
51
+ v-for="category in navigationElements"
52
+ :key="category.id"
53
+ :category="category"
54
+ />
55
+ </div>
56
+ </div>
57
+ <div v-else>Loading...</div>
58
+ </template>
@@ -0,0 +1,66 @@
1
+ <script setup>
2
+ const slug = useRoute().params.slug;
3
+ const { data } = await useAsyncData(`unternehmen-${slug}`, () => {
4
+ return queryCollection("unternehmen").path(`/unternehmen/${slug}`).first();
5
+ });
6
+ </script>
7
+
8
+ <template>
9
+ <UContainer>
10
+ <ContentRenderer :value="data" class="content my-8" />
11
+ </UContainer>
12
+ </template>
13
+
14
+ <style scoped>
15
+ @import "tailwindcss";
16
+ @import "@nuxt/ui";
17
+
18
+ :deep(.content h1) {
19
+ @apply text-4xl font-bold mb-8 text-gray-900 dark:text-gray-100;
20
+ }
21
+
22
+ :deep(.content h2) {
23
+ @apply text-2xl font-semibold mt-8 mb-4 text-gray-800 dark:text-gray-200;
24
+ }
25
+
26
+ :deep(.content h3) {
27
+ @apply text-xl font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200;
28
+ }
29
+
30
+ :deep(.content p) {
31
+ @apply mb-4 text-gray-700 dark:text-gray-300 leading-relaxed;
32
+ }
33
+
34
+ :deep(.content ul),
35
+ :deep(.content ol) {
36
+ @apply mb-4 ml-6 space-y-2;
37
+ }
38
+
39
+ :deep(.content li) {
40
+ @apply text-gray-700 dark:text-gray-300 leading-relaxed;
41
+ }
42
+
43
+ :deep(.content ul li) {
44
+ @apply list-disc;
45
+ }
46
+
47
+ :deep(.content ol li) {
48
+ @apply list-decimal;
49
+ }
50
+
51
+ :deep(.content strong) {
52
+ @apply font-semibold text-gray-900 dark:text-gray-100;
53
+ }
54
+
55
+ /* Unstyle links by default, keep subtle hover */
56
+ :deep(.content a) {
57
+ @apply no-underline text-inherit transition-colors;
58
+ }
59
+ :deep(.content a:hover) {
60
+ @apply text-primary;
61
+ }
62
+
63
+ :deep(.content section) {
64
+ @apply mb-8;
65
+ }
66
+ </style>
@@ -0,0 +1,11 @@
1
+ export type AssociationItemProduct = {
2
+ label: string;
3
+ value: string;
4
+ price: string;
5
+ icon?: string;
6
+ };
7
+
8
+ export type AssociationItem = {
9
+ label: string;
10
+ products: AssociationItemProduct[];
11
+ };
@@ -0,0 +1,119 @@
1
+ import { setTime } from "./time";
2
+ import { isClosedHoliday } from "~/utils/holidays";
3
+
4
+ export type ServiceInterval = { start: Date; end: Date };
5
+
6
+ export function isTuesday(date: Date): boolean {
7
+ return date.getDay() === 2;
8
+ }
9
+
10
+ export function isSaturday(date: Date): boolean {
11
+ return date.getDay() === 6;
12
+ }
13
+
14
+ export function getServiceIntervals(date: Date): Array<ServiceInterval> {
15
+ if (isTuesday(date)) return [];
16
+
17
+ const lunchStart = setTime(date, 11, 30);
18
+ const lunchEnd = setTime(date, 14, 30);
19
+ const dinnerStart = setTime(date, 17, 30);
20
+ const dinnerEnd = isSaturday(date)
21
+ ? setTime(date, 23, 30)
22
+ : setTime(date, 23, 0);
23
+
24
+ if (isSaturday(date)) {
25
+ // Saturday: only dinner, no lunch
26
+ return [{ start: dinnerStart, end: dinnerEnd }];
27
+ }
28
+
29
+ return [
30
+ { start: lunchStart, end: lunchEnd },
31
+ { start: dinnerStart, end: dinnerEnd },
32
+ ];
33
+ }
34
+
35
+ export function getEarliestSelectableTime(
36
+ currentTime: Date,
37
+ currentDeliveryTime: number | null,
38
+ ): Date {
39
+ const earliest = new Date(currentTime);
40
+ earliest.setMinutes(earliest.getMinutes() + (currentDeliveryTime ?? 30));
41
+ earliest.setSeconds(0, 0);
42
+ return earliest;
43
+ }
44
+
45
+ export function getNextOpeningTime(now: Ref<Date>): string | null {
46
+ const currentDate = now.value;
47
+
48
+ // Check if closed for holiday
49
+ if (isClosedHoliday(currentDate)) {
50
+ return "13.08."; // Based on your existing code
51
+ }
52
+
53
+ // Check today's intervals
54
+ const todayIntervals = getServiceIntervals(currentDate);
55
+ const currentTime = currentDate.getTime();
56
+
57
+ // Find next opening today
58
+ for (const interval of todayIntervals) {
59
+ if (interval.start.getTime() > currentTime) {
60
+ const hours = interval.start.getHours().toString().padStart(2, "0");
61
+ const minutes = interval.start.getMinutes().toString().padStart(2, "0");
62
+ return `${hours}:${minutes} Uhr`;
63
+ }
64
+ }
65
+
66
+ // Check tomorrow
67
+ const tomorrow = new Date(currentDate);
68
+ tomorrow.setDate(tomorrow.getDate() + 1);
69
+ tomorrow.setHours(0, 0, 0, 0);
70
+
71
+ // Try up to 7 days ahead to find next opening
72
+ for (let i = 0; i < 7; i++) {
73
+ const checkDate = new Date(tomorrow);
74
+ checkDate.setDate(checkDate.getDate() + i);
75
+
76
+ const intervals = getServiceIntervals(checkDate);
77
+ if (intervals.length > 0) {
78
+ const nextOpen = intervals[0].start;
79
+ const dayName = [
80
+ "Sonntag",
81
+ "Montag",
82
+ "Dienstag",
83
+ "Mittwoch",
84
+ "Donnerstag",
85
+ "Freitag",
86
+ "Samstag",
87
+ ][nextOpen.getDay()];
88
+ const hours = nextOpen.getHours().toString().padStart(2, "0");
89
+ const minutes = nextOpen.getMinutes().toString().padStart(2, "0");
90
+
91
+ if (i === 0) {
92
+ return `morgen um ${hours}:${minutes} Uhr`;
93
+ }
94
+ return `${dayName} um ${hours}:${minutes} Uhr`;
95
+ }
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ export function findActiveInterval(
102
+ currentTime: Date,
103
+ currentDeliveryTime: number | null,
104
+ ): ServiceInterval | null {
105
+ const intervals = getServiceIntervals(currentTime);
106
+ const earliest = getEarliestSelectableTime(
107
+ currentTime,
108
+ currentDeliveryTime ?? 30,
109
+ );
110
+
111
+ if (intervals.length === 0) return null;
112
+
113
+ const current = intervals.find(
114
+ (interval) => earliest >= interval.start && earliest <= interval.end,
115
+ );
116
+ if (current) return current;
117
+
118
+ return intervals.find((interval) => interval.start > earliest) ?? null;
119
+ }
@@ -0,0 +1,9 @@
1
+ export function formatDate(date: string) {
2
+ return new Date(date).toLocaleString("de-DE", {
3
+ year: "numeric",
4
+ month: "2-digit",
5
+ day: "2-digit",
6
+ hour: "2-digit",
7
+ minute: "2-digit",
8
+ });
9
+ }
@@ -0,0 +1,43 @@
1
+ export function isClosedHoliday(date: Date): boolean {
2
+ // Format date as YYYY-MM-DD for comparison
3
+ const formattedDate = formatDateYYYYMMDD(date);
4
+
5
+ // List of holidays (YYYY-MM-DD format)
6
+ const holidays = [
7
+ "2025-07-21",
8
+ "2025-07-22",
9
+ "2025-07-23",
10
+ "2025-07-24",
11
+ "2025-07-25",
12
+ "2025-07-26",
13
+ "2025-07-27",
14
+ "2025-07-28",
15
+ "2025-07-29",
16
+ "2025-07-30",
17
+ "2025-07-31",
18
+ "2025-08-01",
19
+ "2025-08-02",
20
+ "2025-08-03",
21
+ "2025-08-04",
22
+ "2025-08-05",
23
+ "2025-08-06",
24
+ "2025-08-07",
25
+ "2025-08-08",
26
+ "2025-08-09",
27
+ ];
28
+
29
+ return holidays.includes(formattedDate);
30
+ }
31
+
32
+ /**
33
+ * Formats a date as YYYY-MM-DD
34
+ * @param date The date to format
35
+ * @returns The formatted date string
36
+ */
37
+ function formatDateYYYYMMDD(date: Date): string {
38
+ const year = date.getFullYear();
39
+ const month = String(date.getMonth() + 1).padStart(2, "0");
40
+ const day = String(date.getDate()).padStart(2, "0");
41
+
42
+ return `${year}-${month}-${day}`;
43
+ }
@@ -0,0 +1,8 @@
1
+ // utils/storeHours.ts
2
+ import { getServiceIntervals } from "~/utils/businessHours";
3
+
4
+ export function isStoreOpen(date: Date = new Date()): boolean {
5
+ const intervals = getServiceIntervals(date);
6
+ if (intervals.length === 0) return false;
7
+ return intervals.some(({ start, end }) => date >= start && date <= end);
8
+ }
@@ -0,0 +1,20 @@
1
+ // app/utils/time.ts
2
+
3
+ export function pad(n: number): string {
4
+ return n.toString().padStart(2, "0");
5
+ }
6
+
7
+ export function toTimeString(date: Date): string {
8
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
9
+ }
10
+
11
+ export function setTime(date: Date, h: number, m: number): Date {
12
+ const d = new Date(date);
13
+ d.setHours(h, m, 0, 0);
14
+ return d;
15
+ }
16
+
17
+ export function parseTimeString(timeStr: string, baseDate: Date): Date {
18
+ const [hours, minutes] = timeStr.split(":").map((n) => Number.parseInt(n));
19
+ return setTime(baseDate, hours as number, minutes as number);
20
+ }
@@ -0,0 +1,34 @@
1
+ // schemas/registrationSchema.ts
2
+ import * as z from "zod";
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ export const createAddressSchema = (state: any) =>
6
+ z.object({
7
+ id: z.string().optional(),
8
+ firstName: z.string().min(1).optional(),
9
+ lastName: z.string().min(1).optional(),
10
+ accountType: z.string().min(1).optional(),
11
+ company: z
12
+ .string()
13
+ .optional()
14
+ .refine(
15
+ (val) => {
16
+ return (
17
+ state.accountType !== "commercial" ||
18
+ (val !== undefined && val !== "")
19
+ );
20
+ },
21
+ {
22
+ message: "Als Geschäftskunde ist dieses Feld pflicht.",
23
+ },
24
+ ),
25
+ department: z.string().optional(),
26
+ street: z.string().min(1),
27
+ zipcode: z.string().min(1),
28
+ city: z.string().min(1),
29
+ countryId: z.string().min(1),
30
+ phoneNumber: z.string().min(7),
31
+ additionalAddressLine1: z.string().optional(),
32
+ });
33
+
34
+ export type AddressSchema = z.infer<ReturnType<typeof createAddressSchema>>;