@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,55 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+ import { useTrackEvent } from "#imports";
4
+
5
+ const props = defineProps<{
6
+ product: Schemas["Product"];
7
+ }>();
8
+
9
+ const { addToWishlist, isInWishlist, removeFromWishlist } = useProductWishlist(
10
+ props.product.id,
11
+ );
12
+ const { getWishlistProducts } = useWishlist();
13
+
14
+ const trackWishlistEvent = (
15
+ action: "add_to_wishlist" | "remove_from_wishlist",
16
+ ) => {
17
+ useTrackEvent(action, {
18
+ props: { product_number: props.product.productNumber },
19
+ });
20
+ };
21
+
22
+ const toggleWishlistProduct = async () => {
23
+ try {
24
+ if (isInWishlist.value) {
25
+ await removeFromWishlist();
26
+ trackWishlistEvent("remove_from_wishlist");
27
+ await getWishlistProducts();
28
+ } else {
29
+ await addToWishlist();
30
+ trackWishlistEvent("add_to_wishlist");
31
+ await getWishlistProducts();
32
+ }
33
+ } catch (error) {
34
+ console.error("[wishlist][handleWishlistError]", error);
35
+ }
36
+ };
37
+
38
+ // Computed tooltip text
39
+ const tooltipText = computed(() =>
40
+ isInWishlist.value
41
+ ? "Von der Merkliste entfernen"
42
+ : "Auf die Merkliste setzten",
43
+ );
44
+ </script>
45
+
46
+ <template>
47
+ <UTooltip :text="tooltipText">
48
+ <UButton
49
+ icon="i-lucide-heart"
50
+ variant="ghost"
51
+ :color="isInWishlist ? 'error' : 'neutral'"
52
+ @click="toggleWishlistProduct"
53
+ />
54
+ </UTooltip>
55
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ address?: Schemas["CustomerAddress"] | null;
7
+ title?: string;
8
+ icon?: string;
9
+ }>(),
10
+ {
11
+ address: null,
12
+ title: "",
13
+ icon: "",
14
+ },
15
+ );
16
+
17
+ const { address, title, icon } = toRefs(props);
18
+ </script>
19
+
20
+ <template>
21
+ <UPageCard
22
+ v-if="address"
23
+ :icon="icon"
24
+ :title="title"
25
+ :ui="{
26
+ root: 'shadow-md rounded-md ',
27
+ footer: 'w-full',
28
+ }"
29
+ >
30
+ <AddressDetail :address="address" />
31
+ </UPageCard>
32
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+
4
+ const props = defineProps<{
5
+ address: Schemas["CustomerAddress"] | undefined | null;
6
+ }>();
7
+
8
+ const { address } = toRefs(props);
9
+ </script>
10
+
11
+ <template>
12
+ <div v-if="address">
13
+ <div>{{ address.firstName }} {{ address.lastName }}</div>
14
+ <div>{{ address.phoneNumber }}</div>
15
+ <div>{{ address.company }}</div>
16
+ <div>{{ address.department }}</div>
17
+ <div>{{ address.additionalAddressLine1 }}</div>
18
+ <div>{{ address.additionalAddressLine2 }}</div>
19
+ <div>{{ address.street }}</div>
20
+ <div>{{ address.zipcode }} {{ address.city }}</div>
21
+ </div>
22
+ </template>
@@ -0,0 +1,117 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+ import { useAddress } from "@shopware/composables";
4
+ import type { FormSubmitEvent } from "@nuxt/ui";
5
+
6
+ import { createAddressSchema } from "~/validation/addressSchema";
7
+ import type { AddressSchema } from "~/validation/addressSchema";
8
+
9
+ const props = defineProps<{
10
+ address?: Schemas["CustomerAddress"] | undefined;
11
+ }>();
12
+
13
+ const { address } = toRefs(props);
14
+
15
+ const { updateCustomerAddress, createCustomerAddress } = useAddress();
16
+
17
+ const state = reactive({
18
+ id: address.value.id,
19
+ accountType: "privat",
20
+ firstName: address.value.firstName ?? undefined,
21
+ lastName: address.value.lastName ?? undefined,
22
+ company: address.value.company ?? undefined,
23
+ department: address.value.department ?? undefined,
24
+ additionalAddressLine1: address.value.additionalAddressLine1 ?? undefined,
25
+ phoneNumber: address.value.phoneNumber ?? undefined,
26
+ street: address.value.street ?? undefined,
27
+ zipcode: address.value.zipcode ?? undefined,
28
+ city: address.value.city ?? undefined,
29
+ countryId: "018d9f162ade709b9ccc92929b44d236",
30
+ });
31
+
32
+ const schema = computed(() => createAddressSchema(state));
33
+
34
+ const toast = useToast();
35
+
36
+ async function onSubmit(event: FormSubmitEvent<AddressSchema>) {
37
+ if (event.data.id) {
38
+ await updateCustomerAddress(event.data);
39
+ } else {
40
+ await createCustomerAddress(event.data);
41
+ }
42
+
43
+ toast.add({
44
+ title: "Erfolgreich gespeichert",
45
+ color: "success",
46
+ });
47
+ emit("submit-success", event.data);
48
+ }
49
+
50
+ const accountType = ref("privat");
51
+
52
+ const emit = defineEmits<{
53
+ "submit-success": [data: AddressSchema];
54
+ }>();
55
+ </script>
56
+
57
+ <template>
58
+ <UForm
59
+ :schema="schema"
60
+ :state="state"
61
+ class="space-y-4"
62
+ @submit="onSubmit"
63
+ @error="(error) => console.log('Form validation error:', error)"
64
+ >
65
+ <UInput v-model="state.id" type="text" hidden />
66
+
67
+ <UFormField label="Vorname" name="firstName">
68
+ <UInput v-model="state.firstName" type="text" class="w-full" />
69
+ </UFormField>
70
+
71
+ <UFormField label="Nachname" name="lastName">
72
+ <UInput v-model="state.lastName" type="text" class="w-full" />
73
+ </UFormField>
74
+
75
+ <UFormField
76
+ v-if="accountType === 'commercial'"
77
+ label="Unternehmen"
78
+ name="company"
79
+ >
80
+ <UInput v-model="state.company" type="text" class="w-full" />
81
+ </UFormField>
82
+
83
+ <UFormField
84
+ v-if="accountType === 'commercial'"
85
+ label="Abteilung"
86
+ name="department"
87
+ >
88
+ <UInput v-model="state.department" type="text" class="w-full" />
89
+ </UFormField>
90
+
91
+ <UFormField label="Straße und Hausnr." name="street">
92
+ <UInput v-model="state.street" type="text" class="w-full" />
93
+ </UFormField>
94
+
95
+ <UFormField label="Zusatz" name="additionalAddressLine1">
96
+ <UInput
97
+ v-model="state.additionalAddressLine1"
98
+ type="text"
99
+ class="w-full"
100
+ />
101
+ </UFormField>
102
+
103
+ <UFormField label="Postleitzahl" name="zipcode">
104
+ <UInput v-model="state.zipcode" type="text" class="w-full" />
105
+ </UFormField>
106
+
107
+ <UFormField label="Ort" name="city">
108
+ <UInput v-model="state.city" type="text" class="w-full" />
109
+ </UFormField>
110
+
111
+ <UFormField label="Telefon" name="phoneNumber">
112
+ <UInput v-model="state.phoneNumber" type="phone" class="w-full" />
113
+ </UFormField>
114
+
115
+ <UButton type="submit" block label="Speichern" />
116
+ </UForm>
117
+ </template>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { useScrollAnimation } from "~/composables/useScrollAnimation";
3
+
4
+ interface Props {
5
+ animation?:
6
+ | "fade-up"
7
+ | "fade-down"
8
+ | "fade-left"
9
+ | "fade-right"
10
+ | "fade"
11
+ | "scale";
12
+ duration?:
13
+ | "duration-500"
14
+ | "duration-700"
15
+ | "duration-1000"
16
+ | "duration-1500";
17
+ delay?: string;
18
+ threshold?: number;
19
+ rootMargin?: string;
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ animation: "fade-up",
24
+ duration: "duration-1000",
25
+ delay: "delay-0",
26
+ threshold: 0.1,
27
+ rootMargin: "0px 0px -100px 0px",
28
+ });
29
+
30
+ const { isVisible, elementRef } = useScrollAnimation({
31
+ threshold: props.threshold,
32
+ rootMargin: props.rootMargin,
33
+ });
34
+
35
+ const animationClasses = {
36
+ "fade-up": {
37
+ initial: "opacity-0 translate-y-20",
38
+ final: "opacity-100 translate-y-0",
39
+ },
40
+ "fade-down": {
41
+ initial: "opacity-0 -translate-y-20",
42
+ final: "opacity-100 translate-y-0",
43
+ },
44
+ "fade-left": {
45
+ initial: "opacity-0 translate-x-20",
46
+ final: "opacity-100 translate-x-0",
47
+ },
48
+ "fade-right": {
49
+ initial: "opacity-0 -translate-x-20",
50
+ final: "opacity-100 translate-x-0",
51
+ },
52
+ fade: {
53
+ initial: "opacity-0",
54
+ final: "opacity-100",
55
+ },
56
+ scale: {
57
+ initial: "opacity-0 scale-95",
58
+ final: "opacity-100 scale-100",
59
+ },
60
+ };
61
+
62
+ const currentAnimation = animationClasses[props.animation];
63
+ </script>
64
+
65
+ <template>
66
+ <div
67
+ ref="elementRef"
68
+ class="transition-all ease-out"
69
+ :class="[
70
+ duration,
71
+ delay,
72
+ isVisible ? currentAnimation.final : currentAnimation.initial,
73
+ ]"
74
+ >
75
+ <slot />
76
+ </div>
77
+ </template>
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ PhoneIcon,
4
+ BookOpenIcon,
5
+ HeartIcon,
6
+ MapPinIcon,
7
+ HomeIcon,
8
+ } from "@heroicons/vue/24/outline";
9
+
10
+ const route = useRoute();
11
+ </script>
12
+
13
+ <template>
14
+ <div
15
+ class="backdrop-blur bg-white/30 fixed bottom-0 w-full px-2 md:hidden z-50"
16
+ >
17
+ <div class="flex flex-row justify-between items-center py-2 gap-1">
18
+ <NuxtLink
19
+ title="Routenplaner"
20
+ class="flex flex-col justify-center items-center bg-blackish rounded-md px-3.5 py-2.5 basis-1/4 gap-2"
21
+ to="https://www.openstreetmap.org/directions?from=&to=50.080610%2C8.863783#map=19/50.080323/8.864079"
22
+ >
23
+ <MapPinIcon class="w-5 h-5 text-white" />
24
+ <div class="text-white text-xs">Route</div>
25
+ </NuxtLink>
26
+ <NuxtLink
27
+ title="Zu deiner Merkliste"
28
+ to="/merkliste"
29
+ class="flex flex-col justify-center items-center bg-blackish rounded-md px-3.5 py-2.5 basis-1/4 gap-2"
30
+ :class="{ hidden: route.name === 'merkliste' }"
31
+ >
32
+ <HeartIcon class="w-5 h-5 text-white" />
33
+ <div class="text-white text-xs">Merkliste</div>
34
+ </NuxtLink>
35
+ <NuxtLink
36
+ title="Zur Startseite"
37
+ to="/"
38
+ class="flex flex-col justify-center items-center bg-blackish rounded-md px-3.5 py-2.5 basis-1/4 gap-2"
39
+ :class="{ hidden: route.name === 'index' }"
40
+ >
41
+ <HomeIcon class="w-5 h-5 text-white" />
42
+ <div class="text-white text-xs">Startseite</div>
43
+ </NuxtLink>
44
+ <NuxtLink
45
+ title="Zur Speisekarte"
46
+ to="/speisekarte"
47
+ class="flex flex-col justify-center items-center bg-blackish rounded-md px-3.5 py-2.5 basis-1/4 gap-2"
48
+ :class="{ hidden: route.name === 'speisekarte' }"
49
+ >
50
+ <BookOpenIcon class="w-5 h-5 text-white" />
51
+ <div class="text-white text-xs">Speisekarte</div>
52
+ </NuxtLink>
53
+ <NuxtLink
54
+ title="Anrufen"
55
+ to="tel:+49610471427"
56
+ class="flex flex-col justify-center items-center bg-blackish rounded-md px-3.5 py-2.5 basis-1/4 gap-2"
57
+ >
58
+ <PhoneIcon class="w-5 h-5 text-white" />
59
+ <div class="text-white text-xs">Anrufen</div>
60
+ </NuxtLink>
61
+ </div>
62
+ </div>
63
+ </template>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+
4
+ interface Props {
5
+ cartItem?: Schemas["LineItem"] | null;
6
+ withQuantityInput?: boolean;
7
+ withDeleteButton?: boolean;
8
+ }
9
+
10
+ const props = withDefaults(defineProps<Props>(), {
11
+ cartItem: null,
12
+ withQuantityInput: true,
13
+ withDeleteButton: true,
14
+ });
15
+
16
+ const { removeItem, changeProductQuantity } = useCart();
17
+ const { getFormattedPrice } = usePrice({
18
+ currencyCode: "EUR",
19
+ localeCode: "de-DE",
20
+ });
21
+
22
+ // Use computed for reactive quantity with proper null checks
23
+ const quantity = computed({
24
+ get: () => props.cartItem?.quantity ?? 1,
25
+ set: async (value: number) => {
26
+ if (!props.cartItem?.id) return;
27
+
28
+ await changeProductQuantity({
29
+ id: props.cartItem.id,
30
+ quantity: value,
31
+ });
32
+ },
33
+ });
34
+
35
+ const formattedPrice = computed(() => {
36
+ return props.cartItem?.price?.totalPrice
37
+ ? getFormattedPrice(props.cartItem.price.totalPrice)
38
+ : "";
39
+ });
40
+
41
+ const handleRemoveItem = () => {
42
+ if (!props.cartItem) return;
43
+ removeItem(props.cartItem);
44
+ };
45
+ </script>
46
+
47
+ <template>
48
+ <div class="cart-item">
49
+ <div v-if="cartItem" class="flex flex-col gap-4">
50
+ <!-- Main content row -->
51
+ <div class="flex flex-row justify-between items-center gap-4">
52
+ <!-- Left section: Title and variations -->
53
+ <div class="flex flex-row items-center gap-4 flex-1 min-w-0">
54
+ <div class="flex-1 min-w-0">
55
+ <h3 class="text-md">
56
+ <span v-if="!withQuantityInput" class="mr-2">
57
+ {{ quantity }}x
58
+ </span>
59
+ <span class="font-semibold">{{ cartItem.label }}</span>
60
+ </h3>
61
+
62
+ <p
63
+ v-for="option in cartItem?.payload?.options"
64
+ :key="option.group + option.option"
65
+ class="text-sm text-pretty text-toned mt-1"
66
+ >
67
+ {{ option.group }}: {{ option.option }}
68
+ </p>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Right section: Price -->
73
+ <div class="font-medium whitespace-nowrap">
74
+ {{ formattedPrice }}
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Quantity input and delete button row -->
79
+ <div v-if="withQuantityInput" class="flex flex-row items-center gap-2">
80
+ <UInputNumber
81
+ v-model="quantity"
82
+ :min="1"
83
+ :max="100"
84
+ class="max-w-46"
85
+ aria-label="Item quantity"
86
+ />
87
+ <UButton
88
+ v-if="withDeleteButton"
89
+ icon="i-lucide-trash"
90
+ variant="soft"
91
+ color="neutral"
92
+ aria-label="Remove item from cart"
93
+ @click="handleRemoveItem"
94
+ />
95
+ </div>
96
+ <!-- Delete button alone if no quantity input -->
97
+ <UButton
98
+ v-else-if="withDeleteButton"
99
+ icon="i-lucide-trash"
100
+ variant="outline"
101
+ color="error"
102
+ aria-label="Remove item from cart"
103
+ @click="handleRemoveItem"
104
+ />
105
+ </div>
106
+
107
+ <!-- Empty state -->
108
+ <div v-else class="text-center py-4">
109
+ <p class="text-toned">Warenkorb ist leer...</p>
110
+ </div>
111
+ </div>
112
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ const { cart, shippingTotal, isEmpty } = useCart();
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ withQuantityInput?: boolean;
7
+ withDeleteButton?: boolean;
8
+ withToCartButton?: boolean;
9
+ }>(),
10
+ {
11
+ withQuantityInput: true,
12
+ withDeleteButton: true,
13
+ withToCartButton: false,
14
+ },
15
+ );
16
+
17
+ const { getFormattedPrice } = usePrice({
18
+ currencyCode: "EUR",
19
+ localeCode: "de-DE",
20
+ });
21
+ const emit = defineEmits(["go-to-cart"]);
22
+ </script>
23
+
24
+ <template>
25
+ <div class="flex flex-col gap-4 h-full justify-between">
26
+ <div class="flex flex-col gap-4">
27
+ <CartItem
28
+ v-for="lineItem in cart?.lineItems ?? []"
29
+ :key="lineItem.id"
30
+ :cart-item="lineItem"
31
+ :with-quantity-input="withQuantityInput"
32
+ :with-delete-button="withDeleteButton"
33
+ />
34
+ </div>
35
+ <div class="flex flex-col gap-4">
36
+ <div class="flex flex-row justify-between">
37
+ <div>Versandkosten:</div>
38
+ <div>{{ getFormattedPrice(shippingTotal) }}</div>
39
+ </div>
40
+ <div class="flex flex-row justify-between font-bold">
41
+ <div>Summe:</div>
42
+ <div>{{ getFormattedPrice(cart?.price.totalPrice) }}</div>
43
+ </div>
44
+ <UButton
45
+ v-if="withToCartButton"
46
+ :disabled="isEmpty"
47
+ block
48
+ size="lg"
49
+ label="Bestellung aufgeben"
50
+ to="/bestellung"
51
+ @click="emit('go-to-cart')"
52
+ />
53
+ </div>
54
+ </div>
55
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+
4
+ defineProps<{
5
+ category: Schemas["Category"];
6
+ }>();
7
+ </script>
8
+
9
+ <template>
10
+ <div
11
+ v-if="category.media?.url"
12
+ class="relative mb-4 mt-8 h-40 w-full overflow-hidden rounded-lg"
13
+ >
14
+ <NuxtImg
15
+ v-if="category.media?.url"
16
+ :src="category.media.url"
17
+ class="absolute inset-0 h-full w-full object-cover"
18
+ sizes="sm:100vw md:700px"
19
+ alt="Pizza Kategorie"
20
+ placeholder
21
+ />
22
+ <div
23
+ v-if="category.media?.url"
24
+ class="absolute inset-0 bg-gradient-to-t from-black/50 to-black/10"
25
+ />
26
+
27
+ <div v-else class="absolute inset-0 bg-primary" />
28
+
29
+ <div class="relative z-10 p-4">
30
+ <h2
31
+ class="text-pretty font-semibold text-3xl md:text-4xl text-white drop-shadow"
32
+ >
33
+ {{ category.name }}
34
+ </h2>
35
+ <h3
36
+ v-if="category.description"
37
+ class="text-white/90 text-[15px] text-pretty mt-1"
38
+ >
39
+ {{ category.description }}
40
+ </h3>
41
+ </div>
42
+ </div>
43
+ <UPageCard
44
+ v-else
45
+ :title="category.translated.name ?? category.name"
46
+ :description="category.translated.description ?? category.description"
47
+ variant="soft"
48
+ class="my-4"
49
+ :ui="{
50
+ title: 'text-3xl md:text-4xl',
51
+ }"
52
+ />
53
+ </template>