@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,125 @@
1
+ <script setup lang="ts">
2
+ import QuickView from "~/components/Cart/QuickView.vue";
3
+ import { useIntervalFn } from "@vueuse/core";
4
+ import { useTrackEvent } from "#imports";
5
+
6
+ const { selectedPaymentMethod, selectedShippingMethod } = useCheckout();
7
+ const { createOrder } = useCheckout();
8
+ const { refreshCart } = useCart();
9
+ const { isLoggedIn, isGuestSession } = useUser();
10
+ const { isCheckoutEnabled, refresh } = usePizzaToppings();
11
+
12
+ const toast = useToast();
13
+
14
+ onMounted(() => {
15
+ refresh();
16
+ });
17
+
18
+ watch(isCheckoutEnabled, () => console.log(isCheckoutEnabled.value));
19
+
20
+ useIntervalFn(refresh, 10000);
21
+
22
+ async function handleCreateOrder() {
23
+ const order = await createOrder({
24
+ customerComment: "Wunschlieferzeit: " + selectedDeliveryTime.value,
25
+ });
26
+
27
+ useTrackEvent("checkout", {
28
+ revenue: {
29
+ currency: "EUR",
30
+ amount: order.amountTotal as number,
31
+ },
32
+ });
33
+
34
+ await refreshCart();
35
+ toast.add({
36
+ title: "Bestellung aufgegeben!",
37
+ icon: "i-lucide-shopping-cart",
38
+ color: "success",
39
+ progress: false,
40
+ });
41
+ navigateTo("/order/" + order.id);
42
+ }
43
+
44
+ const customerDataAvailable = computed<boolean>(
45
+ () => isLoggedIn.value || isGuestSession.value,
46
+ );
47
+
48
+ const isValidToProceed = computed(
49
+ () =>
50
+ customerDataAvailable.value && isCheckoutEnabled.value && isValidTime.value,
51
+ );
52
+
53
+ const selectedDeliveryTime = ref("");
54
+ const isValidTime = ref(true);
55
+
56
+ watch(selectedDeliveryTime, (newValue) => {
57
+ console.log(newValue);
58
+ });
59
+
60
+ const checkoutButtonLabel = computed<string>(() => {
61
+ if (!customerDataAvailable.value) {
62
+ return "Bitte einloggen oder Kundendaten erfassen";
63
+ }
64
+
65
+ if (!isValidTime.value) {
66
+ return "Wir haben aktuell leider geschlossen";
67
+ }
68
+
69
+ if (!isCheckoutEnabled.value) {
70
+ return "Es werden aktuell keine weiteren Bestellungen mehr aufgenommen";
71
+ }
72
+
73
+ return "Jetzt bestellen!";
74
+ });
75
+ </script>
76
+
77
+ <template>
78
+ <UContainer class="my-16">
79
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
80
+ <div class="flex flex-col gap-4">
81
+ <h3 class="text-xl font-bold">Kundendaten</h3>
82
+ <UserDetail v-if="customerDataAvailable" />
83
+ <div v-else>Bitte vorher einloggen oder Kundendaten erfassen</div>
84
+ <div class="flex flex-col gap-4">
85
+ <h3 class="text-xl font-bold">Versand- und Zahlung</h3>
86
+ <CheckoutPaymentMethod :payment-method="selectedPaymentMethod" />
87
+ <CheckoutShippingMethod :shipping-method="selectedShippingMethod" />
88
+ <CheckoutDeliveryTimeSelect
89
+ v-model:valid="isValidTime"
90
+ v-model="selectedDeliveryTime"
91
+ />
92
+ </div>
93
+ </div>
94
+ <div class="flex flex-col gap-4">
95
+ <h3 class="text-xl font-bold">Warenkorb</h3>
96
+ <div class="rounded-lg flex flex-col gap-4">
97
+ <QuickView
98
+ :with-quantity-input="false"
99
+ :with-delete-button="false"
100
+ class="p-6 bg-elevated"
101
+ />
102
+ <UButton
103
+ :icon="
104
+ isValidToProceed ? 'i-lucide-shopping-cart' : 'i-lucide-lock'
105
+ "
106
+ :disabled="!isValidToProceed"
107
+ :label="checkoutButtonLabel"
108
+ size="xl"
109
+ block
110
+ @click="handleCreateOrder"
111
+ />
112
+ <p class="font-light text-muted">
113
+ Mit Klick auf den Button "Jetzt bestellen!" erklärst du dich mit
114
+ unseren
115
+ <ULink to="/agb" class="text-primary font-medium">AGB</ULink> und
116
+ <ULink to="/datenschutz" class="text-primary font-medium"
117
+ >Datenschutzbestimmungen</ULink
118
+ >
119
+ einverstanden.
120
+ </p>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </UContainer>
125
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import type { ButtonProps } from "#ui/components/Button.vue";
3
+
4
+ const links = ref<ButtonProps[]>([
5
+ {
6
+ label: "Zur Speisekarte",
7
+ color: "primary",
8
+ to: "/speisekarte",
9
+ },
10
+ {
11
+ label: "Tisch reservieren",
12
+ variant: "outline",
13
+ trailingIcon: "i-lucide-phone",
14
+ to: "tel:+490610471427",
15
+ },
16
+ ]);
17
+ </script>
18
+
19
+ <template>
20
+ <div class="relative overflow-hidden">
21
+ <NuxtImg
22
+ src="https://shopware.shopbite.de/media/f5/19/4a/1762546880/category-pizza-header.webp"
23
+ class="absolute inset-0 z-0 w-full h-full object-cover opacity-40"
24
+ alt="CTA Background"
25
+ />
26
+ <UPageCTA
27
+ variant="soft"
28
+ title="Jetzt bestellen!"
29
+ description="Genieße die italienische Küche, frisch zubereitet und direkt zu dir geliefert oder vor Ort genießen."
30
+ :links="links"
31
+ class="relative z-10"
32
+ />
33
+ </div>
34
+ </template>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import type { PageFeatureProps } from "#ui/components/PageFeature.vue";
3
+
4
+ defineProps<{
5
+ title: string;
6
+ description: string;
7
+ headline?: string | undefined;
8
+ features: PageFeatureProps[];
9
+ }>();
10
+ </script>
11
+
12
+ <template>
13
+ <AnimatedSection
14
+ animation="fade-up"
15
+ duration="duration-1000"
16
+ delay="delay-100"
17
+ >
18
+ <UPageSection
19
+ id="features"
20
+ :description="description"
21
+ :features="features"
22
+ :headline="headline"
23
+ class="relative overflow-hidden"
24
+ >
25
+ <div
26
+ class="absolute rounded-full -left-10 top-10 size-[300px] z-10 bg-primary opacity-30 blur-[200px]"
27
+ />
28
+ <div
29
+ class="absolute rounded-full -right-10 -bottom-10 size-[300px] z-10 bg-primary opacity-30 blur-[200px]"
30
+ />
31
+ <template #title>
32
+ <MDC :value="title" class="*:leading-9" />
33
+ </template>
34
+ </UPageSection>
35
+ </AnimatedSection>
36
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import type { MarqueeItem } from "~/components/Food/MarqueeItem.vue";
3
+
4
+ defineProps<{
5
+ title: string;
6
+ description?: string | undefined;
7
+ headline?: string | undefined;
8
+ items: MarqueeItem[];
9
+ }>();
10
+ </script>
11
+
12
+ <template>
13
+ <AnimatedSection
14
+ animation="fade-up"
15
+ duration="duration-1000"
16
+ delay="delay-100"
17
+ >
18
+ <UPageSection
19
+ id="food"
20
+ :title="title"
21
+ :description="description"
22
+ :headline="headline"
23
+ :ui="{ container: 'max-w-full' }"
24
+ >
25
+ <UMarquee :repeat="6" pause-on-hover :overlay="true">
26
+ <FoodMarqueeItem
27
+ v-for="item in items"
28
+ :key="item.productId"
29
+ :product-id="item.productId"
30
+ :image="item.image"
31
+ />
32
+ </UMarquee>
33
+ </UPageSection>
34
+ </AnimatedSection>
35
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { useTrackEvent } from "#imports";
3
+ import { useProductSearch } from "@shopware/composables";
4
+ import type { Schemas } from "#shopware";
5
+
6
+ export type MarqueeItem = {
7
+ productId: string;
8
+ image: string;
9
+ };
10
+
11
+ const props = defineProps<MarqueeItem>();
12
+
13
+ const { productId, image } = toRefs(props);
14
+ const { search } = useProductSearch();
15
+
16
+ const productResponse = await search(productId.value);
17
+
18
+ // object that keeps a Product entity
19
+ const product: Schemas["Product"] = productResponse.product;
20
+
21
+ const CART_SUCCESS_TITLE = "Gute Wahl!";
22
+
23
+ const { refreshCart, addProduct } = useCart();
24
+ const toast = useToast();
25
+
26
+ const alt = computed(() => product.name + " #" + product.productNumber);
27
+
28
+ async function showSuccessToast() {
29
+ toast.add({
30
+ title: CART_SUCCESS_TITLE,
31
+ description: `${product.translated.name} wurde in den Warenkorb gelegt.`,
32
+ icon: "i-lucide-shopping-cart",
33
+ color: "success",
34
+ progress: false,
35
+ });
36
+ }
37
+
38
+ async function addToCart(productId: string) {
39
+ const newCart = await addProduct({
40
+ id: productId,
41
+ quantity: 1,
42
+ });
43
+ await refreshCart(newCart);
44
+ await showSuccessToast();
45
+ useTrackEvent("add_to_cart", {
46
+ props: {
47
+ product_number: product.productNumber,
48
+ quantity: 1,
49
+ },
50
+ });
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <div v-if="product" :id="product.id" class="relative inline-block">
56
+ <NuxtImg
57
+ :src="image"
58
+ :alt="alt"
59
+ class="rounded-lg shadow-2xl ring ring-default h-50 md:h-80"
60
+ />
61
+ <div
62
+ class="absolute bottom-0 left-0 right-0 bg-black/60 text-white px-4 py-2 rounded-b-md flex justify-between items-center"
63
+ >
64
+ <p class="text-sm font-medium truncate">{{ product.translated.name }}</p>
65
+ <UButton
66
+ icon="i-lucide-shopping-cart"
67
+ size="lg"
68
+ @click="addToCart(product.id)"
69
+ />
70
+ </div>
71
+ </div>
72
+ </template>
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ // Fetch navigation from content
3
+ const { data: navigationData } = await useAsyncData("footer-navigation", () =>
4
+ queryCollection("navigation").first(),
5
+ );
6
+
7
+ const columns = computed(() => {
8
+ if (!navigationData.value?.footer.columns) return [];
9
+ return navigationData.value.footer.columns;
10
+ });
11
+ </script>
12
+
13
+ <template>
14
+ <USeparator class="h-px" />
15
+
16
+ <UFooter :ui="{ top: 'border-b border-default' }">
17
+ <template #top>
18
+ <UContainer>
19
+ <UFooterColumns :columns="columns" />
20
+ </UContainer>
21
+ </template>
22
+
23
+ <template #left>
24
+ <NuxtLink
25
+ to="https://shopbite.de"
26
+ class="text-sm text-muted"
27
+ target="_blank"
28
+ >
29
+ Bestellsystm von ShopBite • © {{ new Date().getFullYear() }}
30
+ </NuxtLink>
31
+ </template>
32
+
33
+ <p v-if="navigationData?.footer.text" class="text-muted text-sm">
34
+ {{ navigationData.footer.text }}
35
+ </p>
36
+
37
+ <template #right>
38
+ <UColorModeButton v-if="navigationData?.footer.withColorModeSwitch" />
39
+
40
+ <UButton
41
+ v-if="navigationData?.footer.withGithubLink"
42
+ to="https://github.com/shopbite-de/storefront"
43
+ target="_blank"
44
+ icon="i-simple-icons-github"
45
+ aria-label="ShopBite on GitHub"
46
+ color="neutral"
47
+ variant="ghost"
48
+ />
49
+ </template>
50
+ </UFooter>
51
+ </template>
@@ -0,0 +1,160 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuItem, NavigationMenuItem } from "@nuxt/ui";
3
+ import { useRoute } from "vue-router";
4
+ import { useUser } from "@shopware/composables";
5
+
6
+ const route = useRoute();
7
+ const toast = useToast();
8
+
9
+ const { isLoggedIn, user, logout } = useUser();
10
+ const { isCheckoutEnabled } = usePizzaToppings();
11
+ const { count } = useCart();
12
+ const runtimeConfig = useRuntimeConfig();
13
+
14
+ // Fetch navigation from content
15
+ const { data: navigationData } = await useAsyncData("header-navigation", () =>
16
+ queryCollection("navigation").first(),
17
+ );
18
+
19
+ const siteName = computed(() => runtimeConfig.public.site?.name ?? "ShopBite");
20
+
21
+ const navi = computed<NavigationMenuItem[]>(() => {
22
+ if (!navigationData.value?.main) return [];
23
+
24
+ return navigationData.value.main.map((item) => ({
25
+ label: item.label,
26
+ icon: item.icon,
27
+ to: item.to,
28
+ target: item.target,
29
+ active:
30
+ item.to === "/"
31
+ ? route.path.length === 1
32
+ : route.path.startsWith(item.to),
33
+ }));
34
+ });
35
+
36
+ const accountHoverText = computed(() => {
37
+ return isLoggedIn.value
38
+ ? `${user.value?.firstName} ${user.value?.lastName}`
39
+ : "Hallo";
40
+ });
41
+
42
+ const logoutHandler = () => {
43
+ logout();
44
+ toast.add({
45
+ title: "Tschüss!",
46
+ description: "Erfolreich abgemeldet.",
47
+ color: "success",
48
+ });
49
+ };
50
+
51
+ const loggedInDropDown = computed<DropdownMenuItem[][]>(() => {
52
+ if (!navigationData.value?.account.loggedIn) return [];
53
+
54
+ return navigationData.value.account.loggedIn.map((group) =>
55
+ group.map((item) => ({
56
+ label: item.type === "label" ? accountHoverText.value : item.label,
57
+ type: item.type,
58
+ icon: item.icon,
59
+ to: item.to,
60
+ onSelect: item.action === "logout" ? logoutHandler : undefined,
61
+ })),
62
+ );
63
+ });
64
+
65
+ const loggedOutDropDown = computed<DropdownMenuItem[][]>(() => {
66
+ if (!navigationData.value?.account.loggedOut) return [];
67
+
68
+ return navigationData.value.account.loggedOut.map((group) =>
69
+ group.map((item) => ({
70
+ label: item.label,
71
+ type: item.type,
72
+ icon: item.icon,
73
+ to: item.to,
74
+ })),
75
+ );
76
+ });
77
+
78
+ const loginSlide = ref(false);
79
+ const cartQuickViewOpen = ref(false);
80
+ </script>
81
+
82
+ <template>
83
+ <UHeader>
84
+ <template #title>
85
+ <NuxtLink to="/" class="-m-1.5 p-1.5">
86
+ <span class="sr-only">{{ siteName }}</span>
87
+ <UColorModeImage
88
+ light="/light/Logo.svg"
89
+ dark="/dark/Logo.svg"
90
+ class="h-12 w-auto"
91
+ />
92
+ </NuxtLink>
93
+ </template>
94
+
95
+ <UNavigationMenu color="primary" variant="pill" :items="navi" />
96
+
97
+ <template #right>
98
+ <UButton
99
+ color="neutral"
100
+ variant="ghost"
101
+ to="tel:+49610471427"
102
+ target="_blank"
103
+ icon="i-lucide-phone"
104
+ aria-label="Anrufen"
105
+ />
106
+ <UDropdownMenu :items="isLoggedIn ? loggedInDropDown : loggedOutDropDown">
107
+ <UChip v-if="isLoggedIn" size="3xl" text="✓">
108
+ <UButton icon="i-lucide-user" color="neutral" variant="outline" />
109
+ </UChip>
110
+ <UButton
111
+ v-else
112
+ icon="i-lucide-user"
113
+ color="neutral"
114
+ variant="outline"
115
+ />
116
+ </UDropdownMenu>
117
+ <UDrawer
118
+ v-if="isCheckoutEnabled"
119
+ v-model:open="cartQuickViewOpen"
120
+ title="Warenkorb"
121
+ direction="right"
122
+ >
123
+ <UChip :text="count" size="3xl">
124
+ <UButton
125
+ color="neutral"
126
+ variant="outline"
127
+ icon="i-lucide-shopping-cart"
128
+ />
129
+ </UChip>
130
+ <template #body>
131
+ <CartQuickView
132
+ :with-to-cart-button="true"
133
+ class="md:min-w-90"
134
+ @go-to-cart="cartQuickViewOpen = false"
135
+ />
136
+ </template>
137
+ </UDrawer>
138
+ </template>
139
+
140
+ <template #body>
141
+ <UNavigationMenu
142
+ color="primary"
143
+ :items="navi"
144
+ orientation="vertical"
145
+ class="-mx-2.5"
146
+ />
147
+ </template>
148
+ </UHeader>
149
+ <USlideover
150
+ v-model:open="loginSlide"
151
+ title="Konto"
152
+ description="Alle Vorteile eines Kontos genießen"
153
+ >
154
+ <template #body>
155
+ <div class="h-full m-4">
156
+ <UserLoginForm />
157
+ </div>
158
+ </template>
159
+ </USlideover>
160
+ </template>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import type { ButtonProps } from "#ui/components/Button.vue";
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ title: string;
7
+ description?: string;
8
+ headline?: string;
9
+ backgroundVideo?: string;
10
+ links: ButtonProps[];
11
+ usps?: {
12
+ title?: string;
13
+ subtitle?: string;
14
+ icon?: string;
15
+ link?: string;
16
+ }[];
17
+ }>(),
18
+ {
19
+ description: undefined,
20
+ headline: undefined,
21
+ backgroundVideo: undefined,
22
+ usps: () => [],
23
+ },
24
+ );
25
+ </script>
26
+
27
+ <template>
28
+ <div class="relative">
29
+ <video
30
+ autoplay
31
+ loop
32
+ muted
33
+ playsinline
34
+ class="absolute inset-0 w-full h-full object-cover -z-10"
35
+ >
36
+ <source :src="backgroundVideo" type="video/mp4" >
37
+ </video>
38
+ <div class="bg-black/50 backdrop-blur-sm">
39
+ <UPageHero
40
+ :title="title"
41
+ :description="description"
42
+ :headline="headline"
43
+ :links="links"
44
+ orientation="vertical"
45
+ :ui="{
46
+ title: 'text-white',
47
+ description: 'text-white',
48
+ }"
49
+ >
50
+ <div v-if="usps" class="flex flex-row gap-2 md:gap-8 justify-center">
51
+ <ULink
52
+ v-for="(usp, index) in usps"
53
+ :key="index"
54
+ as="button"
55
+ :to="usp.link ?? ''"
56
+ class="flex items-center text-left gap-2"
57
+ target="_blank"
58
+ >
59
+ <UIcon
60
+ v-if="usp.icon"
61
+ :name="usp.icon"
62
+ class="text-white text-xl"
63
+ />
64
+ <div class="flex flex-col">
65
+ <span v-if="usp.title" class="text-white font-semibold">{{
66
+ usp.title
67
+ }}</span>
68
+ <span v-if="usp.subtitle" class="text-white/80 text-xs">{{
69
+ usp.subtitle
70
+ }}</span>
71
+ </div>
72
+ </ULink>
73
+ </div>
74
+ </UPageHero>
75
+ </div>
76
+ </div>
77
+ </template>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import type { CarouselItem } from "#ui/components/Carousel.vue";
3
+ import type { ButtonProps } from "#ui/components/Button.vue";
4
+
5
+ type ImageProp = {
6
+ image: string;
7
+ alt: string;
8
+ };
9
+
10
+ type ImageCarousel = CarouselItem & ImageProp;
11
+
12
+ defineProps<{
13
+ title: string;
14
+ description?: string | undefined;
15
+ headline?: string | undefined;
16
+ images: ImageCarousel[] | undefined;
17
+ links?: ButtonProps[] | undefined;
18
+ }>();
19
+ </script>
20
+
21
+ <template>
22
+ <UPageSection
23
+ :title="title"
24
+ :description="description"
25
+ :headline="headline"
26
+ :links="links"
27
+ >
28
+ <template #body>
29
+ <UCarousel
30
+ v-slot="{ item }"
31
+ :items="images"
32
+ class="mx-auto w-full"
33
+ auto-height
34
+ arrows
35
+ loop
36
+ >
37
+ <NuxtImg
38
+ :src="item.image"
39
+ :alt="item.alt"
40
+ class="rounded-lg w-full max-h-screen object-contain"
41
+ sizes="sm:100vw md:100vw lg:100vw"
42
+ />
43
+ </UCarousel>
44
+ </template>
45
+ </UPageSection>
46
+ </template>
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ size?: "sm" | "md" | "lg";
4
+ text?: string;
5
+ }
6
+
7
+ const props = withDefaults(defineProps<Props>(), {
8
+ size: "md",
9
+ text: "Loading...",
10
+ });
11
+
12
+ const sizeClasses = {
13
+ sm: "w-4 h-4",
14
+ md: "w-8 h-8",
15
+ lg: "w-12 h-12",
16
+ } as const;
17
+
18
+ const spinnerSize = computed(() => sizeClasses[props.size]);
19
+ </script>
20
+
21
+ <template>
22
+ <div class="flex flex-col items-center justify-center gap-3 p-8">
23
+ <div
24
+ class="animate-spin rounded-full border-2 border-gray-300 border-t-primary"
25
+ :class="spinnerSize"
26
+ />
27
+ <p v-if="text" class="text-gray-600">{{ text }}</p>
28
+ </div>
29
+ </template>