@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.
- package/.dockerignore +28 -0
- package/.env.example +11 -0
- package/.github/workflows/build.yaml +48 -0
- package/.github/workflows/ci.yaml +102 -0
- package/.prettierignore +6 -0
- package/.prettierrc +1 -0
- package/api-types/storeApiSchema.json +13863 -0
- package/api-types/storeApiTypes.d.ts +7010 -0
- package/app/app.config.ts +18 -0
- package/app/app.vue +99 -0
- package/app/assets/css/main.css +60 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-Bold.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-Italic.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/CourierPrime-Regular.ttf +0 -0
- package/app/assets/fonts/Courier_Prime/OFL.txt +93 -0
- package/app/assets/fonts/Kalam/Kalam-Bold.ttf +0 -0
- package/app/assets/fonts/Kalam/Kalam-Light.ttf +0 -0
- package/app/assets/fonts/Kalam/Kalam-Regular.ttf +0 -0
- package/app/assets/fonts/Kalam/OFL.txt +93 -0
- package/app/assets/fonts/Marcellus/Marcellus-Regular.ttf +0 -0
- package/app/assets/fonts/Marcellus/OFL.txt +93 -0
- package/app/assets/fonts/Sora/OFL.txt +93 -0
- package/app/assets/fonts/Sora/README.txt +70 -0
- package/app/assets/fonts/Sora/Sora-VariableFont_wght.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Bold.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-ExtraBold.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-ExtraLight.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Light.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Medium.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Regular.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-SemiBold.ttf +0 -0
- package/app/assets/fonts/Sora/static/Sora-Thin.ttf +0 -0
- package/app/components/AddToWishlist.vue +55 -0
- package/app/components/Address/Card.vue +32 -0
- package/app/components/Address/Detail.vue +22 -0
- package/app/components/Address/Form.vue +117 -0
- package/app/components/AnimatedSection.vue +77 -0
- package/app/components/BottomNavi.vue +63 -0
- package/app/components/Cart/Item.vue +112 -0
- package/app/components/Cart/QuickView.vue +55 -0
- package/app/components/Category/Header.vue +53 -0
- package/app/components/Category/Listing.vue +295 -0
- package/app/components/Checkout/DeliveryTimeSelect.vue +177 -0
- package/app/components/Checkout/LoginOrRegister.vue +43 -0
- package/app/components/Checkout/PaymentAndDelivery.vue +101 -0
- package/app/components/Checkout/PaymentMethod.vue +30 -0
- package/app/components/Checkout/ShippingMethod.vue +30 -0
- package/app/components/Checkout/Summary.vue +125 -0
- package/app/components/Cta.vue +34 -0
- package/app/components/Features.vue +36 -0
- package/app/components/Food/Marquee.vue +35 -0
- package/app/components/Food/MarqueeItem.vue +72 -0
- package/app/components/Footer.vue +51 -0
- package/app/components/Header.vue +160 -0
- package/app/components/Hero.vue +77 -0
- package/app/components/ImageGallery.vue +46 -0
- package/app/components/Loading.vue +29 -0
- package/app/components/Navigation/DesktopLeft.vue +51 -0
- package/app/components/Navigation/DesktopLeft2.vue +43 -0
- package/app/components/Navigation/MobileTop.vue +59 -0
- package/app/components/Navigation/MobileTop2.vue +42 -0
- package/app/components/Order/Detail.vue +84 -0
- package/app/components/Product/Card.vue +132 -0
- package/app/components/Product/Category.vue +153 -0
- package/app/components/Product/Configurator.vue +65 -0
- package/app/components/Product/CrossSelling.vue +95 -0
- package/app/components/Product/DeselectIngredient.vue +46 -0
- package/app/components/Product/Detail.vue +187 -0
- package/app/components/Product/SearchBar.vue +109 -0
- package/app/components/PublicAnnouncement.vue +17 -0
- package/app/components/Topseller.vue +43 -0
- package/app/components/User/Detail.vue +47 -0
- package/app/components/User/LoginForm.vue +105 -0
- package/app/components/User/RegistrationForm.vue +340 -0
- package/app/components/Wishlist.vue +102 -0
- package/app/composables/useDeliveryTime.ts +139 -0
- package/app/composables/useInterval.ts +15 -0
- package/app/composables/usePizzaToppings.ts +31 -0
- package/app/composables/useProductEvents.test.ts +111 -0
- package/app/composables/useProductEvents.ts +22 -0
- package/app/composables/useProductVariants.ts +61 -0
- package/app/composables/useScrollAnimation.ts +39 -0
- package/app/composables/useTopSellers.ts +34 -0
- package/app/error.vue +30 -0
- package/app/layouts/account.vue +74 -0
- package/app/layouts/default.vue +6 -0
- package/app/layouts/listing.vue +32 -0
- package/app/layouts/listing2.vue +8 -0
- package/app/middleware/trailing-slash.global.ts +19 -0
- package/app/pages/account/recover/password/index.vue +143 -0
- package/app/pages/anmelden.vue +32 -0
- package/app/pages/bestellung.vue +103 -0
- package/app/pages/c/[...all].vue +49 -0
- package/app/pages/index.vue +59 -0
- package/app/pages/konto/adressen.vue +135 -0
- package/app/pages/konto/bestellung/[id].vue +41 -0
- package/app/pages/konto/bestellungen.vue +53 -0
- package/app/pages/konto/index.vue +74 -0
- package/app/pages/konto/profil.vue +160 -0
- package/app/pages/merkliste.vue +11 -0
- package/app/pages/order/[id].vue +69 -0
- package/app/pages/passwort-vergessen.vue +103 -0
- package/app/pages/registrierung/bestaetigen.vue +44 -0
- package/app/pages/registrierung/index.vue +24 -0
- package/app/pages/speisekarte.vue +58 -0
- package/app/pages/unternehmen/[slug].vue +66 -0
- package/app/types/Association.d.ts +11 -0
- package/app/utils/businessHours.ts +119 -0
- package/app/utils/formatDate.ts +9 -0
- package/app/utils/holidays.ts +43 -0
- package/app/utils/storeHours.ts +8 -0
- package/app/utils/time.ts +20 -0
- package/app/validation/addressSchema.ts +34 -0
- package/app/validation/registrationSchema.ts +156 -0
- package/bun.dockerfile +60 -0
- package/compose.yml +17 -0
- package/container +7 -0
- package/content/index.yml +91 -0
- package/content/navigation.yml +67 -0
- package/content/unternehmen/agb.md +1 -0
- package/content/unternehmen/datenschutz.md +1 -0
- package/content/unternehmen/impressum.md +39 -0
- package/content.config.ts +134 -0
- package/eslint.config.mjs +8 -0
- package/node.dockerfile +33 -0
- package/nuxt.config.ts +153 -0
- package/package.json +70 -0
- package/public/dark/Logo.svg +32 -0
- package/public/favicon.ico +0 -0
- package/public/light/Logo.svg +32 -0
- package/renovate.json +4 -0
- package/server/tsconfig.json +3 -0
- package/shopware.d.ts +19 -0
- package/tsconfig.json +4 -0
- 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>
|