@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,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
3
|
+
import type { Schemas } from "#shopware";
|
|
4
|
+
import { useTrackEvent } from "#imports";
|
|
5
|
+
|
|
6
|
+
const { loadNavigationElements, navigationElements } = useNavigation();
|
|
7
|
+
|
|
8
|
+
loadNavigationElements({ depth: 1 });
|
|
9
|
+
|
|
10
|
+
const scrollToElement = (elementId: string, margin = 0) => {
|
|
11
|
+
const element = document.getElementById(elementId);
|
|
12
|
+
if (element) {
|
|
13
|
+
const elementPosition =
|
|
14
|
+
element.getBoundingClientRect().top + window.scrollY;
|
|
15
|
+
const offsetPosition = elementPosition - margin;
|
|
16
|
+
window.scrollTo({
|
|
17
|
+
top: offsetPosition,
|
|
18
|
+
behavior: "smooth",
|
|
19
|
+
});
|
|
20
|
+
useTrackEvent("scroll_to_category", {
|
|
21
|
+
props: { category_name: elementId },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const navItems = computed<NavigationMenuItem[][]>(() => {
|
|
27
|
+
return navigationElements.value.map((item: Schemas["Category"]) => {
|
|
28
|
+
return {
|
|
29
|
+
label: item.translated?.name,
|
|
30
|
+
onSelect: () => scrollToElement(item.name ?? "#", 90),
|
|
31
|
+
children: item.children?.map((child: Schemas["Category"]) => {
|
|
32
|
+
return {
|
|
33
|
+
label: child.translated?.name,
|
|
34
|
+
onSelect: () => scrollToElement(child.name ?? "#", 90),
|
|
35
|
+
};
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div>
|
|
44
|
+
<h2>Navigation</h2>
|
|
45
|
+
<UNavigationMenu
|
|
46
|
+
orientation="vertical"
|
|
47
|
+
:items="navItems"
|
|
48
|
+
class="data-[orientation=vertical]"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
3
|
+
import type { Schemas } from "#shopware";
|
|
4
|
+
|
|
5
|
+
const { loadNavigationElements } = useNavigation();
|
|
6
|
+
|
|
7
|
+
const { data: navigationElements } = await useAsyncData(
|
|
8
|
+
`menu-navigation`,
|
|
9
|
+
async () => {
|
|
10
|
+
return await loadNavigationElements({ depth: 2 });
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const navItems = computed<NavigationMenuItem[]>(() => {
|
|
15
|
+
return navigationElements.value?.map((item: Schemas["Category"]) => {
|
|
16
|
+
return {
|
|
17
|
+
label: item.translated?.name,
|
|
18
|
+
description: `${item.translated?.name} Kategorie`,
|
|
19
|
+
to: item.seoUrl,
|
|
20
|
+
defaultOpen: true,
|
|
21
|
+
children: item.children?.map((child: Schemas["Category"]) => {
|
|
22
|
+
return {
|
|
23
|
+
label: child.translated?.name,
|
|
24
|
+
description: `${child.translated?.name} Kategorie`,
|
|
25
|
+
to: child.seoUrl,
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div>
|
|
35
|
+
<h2>Navigation</h2>
|
|
36
|
+
<UNavigationMenu
|
|
37
|
+
variant="link"
|
|
38
|
+
orientation="vertical"
|
|
39
|
+
class="data-[orientation=vertical]:w-48"
|
|
40
|
+
:items="navItems"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
3
|
+
import type { Schemas } from "#shopware";
|
|
4
|
+
import { useTrackEvent } from "#imports";
|
|
5
|
+
|
|
6
|
+
const { loadNavigationElements, navigationElements } = useNavigation();
|
|
7
|
+
|
|
8
|
+
loadNavigationElements({ depth: 1 });
|
|
9
|
+
|
|
10
|
+
const scrollToElement = (elementId: string, margin = 0) => {
|
|
11
|
+
const element = document.getElementById(elementId);
|
|
12
|
+
if (element) {
|
|
13
|
+
const elementPosition =
|
|
14
|
+
element.getBoundingClientRect().top + window.scrollY;
|
|
15
|
+
const offsetPosition = elementPosition - margin;
|
|
16
|
+
window.scrollTo({
|
|
17
|
+
top: offsetPosition,
|
|
18
|
+
behavior: "smooth",
|
|
19
|
+
});
|
|
20
|
+
useTrackEvent("scroll_to_category", {
|
|
21
|
+
props: { category_name: elementId },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const navItems = computed<NavigationMenuItem[][]>(() => {
|
|
27
|
+
return navigationElements.value.map((item: Schemas["Category"]) => {
|
|
28
|
+
return {
|
|
29
|
+
label: item.translated?.name,
|
|
30
|
+
onSelect: () => scrollToElement(item.name ?? "#", 90),
|
|
31
|
+
children: item.children?.map((child: Schemas["Category"]) => {
|
|
32
|
+
return {
|
|
33
|
+
label: child.translated?.name,
|
|
34
|
+
onSelect: () => scrollToElement(child.name ?? "#", 90),
|
|
35
|
+
};
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<UNavigationMenu
|
|
44
|
+
class="lg:hidden"
|
|
45
|
+
orientation="horizontal"
|
|
46
|
+
:items="navItems"
|
|
47
|
+
:ui="{
|
|
48
|
+
list: 'overflow-x-auto',
|
|
49
|
+
item: 'flex-shrink-0',
|
|
50
|
+
}"
|
|
51
|
+
>
|
|
52
|
+
<template #list-leading>
|
|
53
|
+
<UIcon name="i-lucide-chevron-left" class="size-8" />
|
|
54
|
+
</template>
|
|
55
|
+
<template #list-trailing>
|
|
56
|
+
<UIcon name="i-lucide-chevron-right" class="size-8" />
|
|
57
|
+
</template>
|
|
58
|
+
</UNavigationMenu>
|
|
59
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
3
|
+
import type { Schemas } from "#shopware";
|
|
4
|
+
|
|
5
|
+
const { loadNavigationElements, navigationElements } = useNavigation();
|
|
6
|
+
|
|
7
|
+
loadNavigationElements({ depth: 1 });
|
|
8
|
+
|
|
9
|
+
const navItems = computed<NavigationMenuItem[]>(() => {
|
|
10
|
+
return navigationElements.value?.map((item: Schemas["Category"]) => {
|
|
11
|
+
return {
|
|
12
|
+
label: item.translated?.name,
|
|
13
|
+
to: item.children.length === 0 ? item.seoUrl : undefined,
|
|
14
|
+
children: item.children?.map((child: Schemas["Category"]) => {
|
|
15
|
+
return {
|
|
16
|
+
label: child.translated?.name,
|
|
17
|
+
to: child.seoUrl,
|
|
18
|
+
};
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<UNavigationMenu
|
|
27
|
+
class="lg:hidden"
|
|
28
|
+
orientation="horizontal"
|
|
29
|
+
:items="navItems"
|
|
30
|
+
:ui="{
|
|
31
|
+
list: 'overflow-x-auto',
|
|
32
|
+
item: 'flex-shrink-0',
|
|
33
|
+
}"
|
|
34
|
+
>
|
|
35
|
+
<template #list-leading>
|
|
36
|
+
<UIcon name="i-lucide-chevron-left" class="size-8" />
|
|
37
|
+
</template>
|
|
38
|
+
<template #list-trailing>
|
|
39
|
+
<UIcon name="i-lucide-chevron-right" class="size-8" />
|
|
40
|
+
</template>
|
|
41
|
+
</UNavigationMenu>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
import type { TableColumn } from "#ui/components/Table.vue";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
order: Schemas["Order"];
|
|
7
|
+
status: string;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const { order } = toRefs(props);
|
|
11
|
+
|
|
12
|
+
const { getFormattedPrice } = usePrice({
|
|
13
|
+
currencyCode: "EUR",
|
|
14
|
+
localeCode: "de-DE",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const isLoadingData = ref(true);
|
|
18
|
+
|
|
19
|
+
const columns: TableColumn<Schemas["OrderLineItem"]>[] = [
|
|
20
|
+
{
|
|
21
|
+
accessorKey: "label",
|
|
22
|
+
header: "Produkt",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
accessorKey: "unitPrice",
|
|
26
|
+
header: "Einzelpreis",
|
|
27
|
+
cell: ({ row }) => {
|
|
28
|
+
return getFormattedPrice(row.getValue("unitPrice"));
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
accessorKey: "quantity",
|
|
33
|
+
header: "Anzahl",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
accessorKey: "totalPrice",
|
|
37
|
+
header: () => h("div", { class: "text-right" }, "Preis"),
|
|
38
|
+
cell: ({ row }) => {
|
|
39
|
+
const formatted = getFormattedPrice(row.getValue("totalPrice"));
|
|
40
|
+
|
|
41
|
+
return h("div", { class: "text-right" }, formatted);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
onMounted(() => {
|
|
47
|
+
isLoadingData.value = false;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const columnRows = computed(() => {
|
|
51
|
+
return order.value?.lineItems.filter(
|
|
52
|
+
(lineItem: Schemas["OrderLineItem"]) => lineItem.parentId === null,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<div class="flex flex-row justify-between">
|
|
59
|
+
<UBadge variant="outline" color="neutral" size="xl"
|
|
60
|
+
>Status: {{ status }}</UBadge
|
|
61
|
+
>
|
|
62
|
+
<UBadge variant="outline" color="neutral" size="xl"
|
|
63
|
+
>Versandart: {{ order?.deliveries[0].shippingMethod.name }}</UBadge
|
|
64
|
+
>
|
|
65
|
+
</div>
|
|
66
|
+
<UTable
|
|
67
|
+
:columns="columns"
|
|
68
|
+
:loading="isLoadingData"
|
|
69
|
+
loading-color="primary"
|
|
70
|
+
loading-animation="carousel"
|
|
71
|
+
:data="columnRows"
|
|
72
|
+
class="flex-1"
|
|
73
|
+
/>
|
|
74
|
+
<div class="flex flex-col items-end w-full pr-4">
|
|
75
|
+
<div>Lieferkosten: {{ getFormattedPrice(order?.shippingTotal) }}</div>
|
|
76
|
+
<div>Gesamtkosten Netto: {{ getFormattedPrice(order?.amountNet) }}</div>
|
|
77
|
+
<div v-for="tax in order?.price.calculatedTaxes" :key="tax.taxRate">
|
|
78
|
+
inkl. {{ tax.taxRate }}% MwSt. {{ getFormattedPrice(tax.tax) }}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="font-bold">
|
|
81
|
+
Gesamtkosten Brutto: {{ getFormattedPrice(order?.amountTotal) }}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
product: Schemas["Product"];
|
|
6
|
+
withFavoriteButton: boolean;
|
|
7
|
+
withAddToCartButton: boolean;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const { isCheckoutEnabled } = usePizzaToppings();
|
|
11
|
+
|
|
12
|
+
const { product, withFavoriteButton, withAddToCartButton } = toRefs(props);
|
|
13
|
+
|
|
14
|
+
const { getFormattedPrice } = usePrice({
|
|
15
|
+
currencyCode: "EUR",
|
|
16
|
+
localeCode: "de-DE",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const isVegi = computed<boolean>(() => {
|
|
20
|
+
if (!product.value?.properties) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return product.value?.sortedProperties?.some(
|
|
25
|
+
(propertyGroup: Schemas["PropertyGroup"]) =>
|
|
26
|
+
propertyGroup.translated.name === "Vegetarisch" &&
|
|
27
|
+
propertyGroup.options?.some(
|
|
28
|
+
(option: Schemas["PropertyGroupOption"]) =>
|
|
29
|
+
option.translated.name === "Ja",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
const openDetails = ref(false);
|
|
34
|
+
|
|
35
|
+
function toggleDetails() {
|
|
36
|
+
openDetails.value = !openDetails.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAIN_INGREDIENTS_PROPERTY_LABEL = "Hauptzutaten";
|
|
40
|
+
|
|
41
|
+
const mainIngredients = computed<Schemas["PropertyGroupOption"][]>(() => {
|
|
42
|
+
const sortedProps =
|
|
43
|
+
product.value?.sortedProperties ?? ([] as Schemas["PropertyGroup"][]);
|
|
44
|
+
const mainIngredientsProperty = sortedProps.find(
|
|
45
|
+
(propertyGroup: Schemas["PropertyGroup"]) =>
|
|
46
|
+
propertyGroup.translated.name === MAIN_INGREDIENTS_PROPERTY_LABEL,
|
|
47
|
+
);
|
|
48
|
+
return mainIngredientsProperty?.options ?? [];
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<AnimatedSection
|
|
54
|
+
animation="fade-up"
|
|
55
|
+
duration="duration-1000"
|
|
56
|
+
delay="delay-100"
|
|
57
|
+
>
|
|
58
|
+
<UPageCard
|
|
59
|
+
:orientation="product.cover?.media?.url ? 'horizontal' : 'vertical'"
|
|
60
|
+
variant="outline"
|
|
61
|
+
reverse
|
|
62
|
+
:ui="{ footer: 'w-full', root: 'shadow-lg' }"
|
|
63
|
+
>
|
|
64
|
+
<template #header>
|
|
65
|
+
<UBadge
|
|
66
|
+
v-if="isVegi"
|
|
67
|
+
icon="i-lucide-leaf"
|
|
68
|
+
color="success"
|
|
69
|
+
variant="outline"
|
|
70
|
+
size="sm"
|
|
71
|
+
label="Vegetarisch"
|
|
72
|
+
/>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<div v-if="product.cover?.media?.url">
|
|
76
|
+
<NuxtImg
|
|
77
|
+
:src="product.cover.media.url"
|
|
78
|
+
class="rounded-md h-auto max-w-full object-contain ransition-opacity duration-700"
|
|
79
|
+
sizes="(min-width: 1024px) 50vw, 100vw"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<template #title>
|
|
84
|
+
<div class="flex flex-row items-baseline gap-1">
|
|
85
|
+
<span class="text-sm text-brand-500"
|
|
86
|
+
>#{{ product.productNumber }}</span
|
|
87
|
+
>
|
|
88
|
+
|
|
89
|
+
<p class="text-base text-pretty font-semibold text-highlighted">
|
|
90
|
+
{{ product.translated.name }}
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<template #description>
|
|
96
|
+
<div class="flex flex-col gap-2">
|
|
97
|
+
<div>{{ product.description }}</div>
|
|
98
|
+
<div
|
|
99
|
+
v-if="mainIngredients.length > 0"
|
|
100
|
+
class="font-extralight text-sm text-pretty"
|
|
101
|
+
>
|
|
102
|
+
{{
|
|
103
|
+
mainIngredients
|
|
104
|
+
.map((ingredient) => ingredient.translated.name)
|
|
105
|
+
.join(", ")
|
|
106
|
+
}}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<template #footer>
|
|
112
|
+
<div class="flex flex-row justify-between content-center w-full">
|
|
113
|
+
<p>{{ getFormattedPrice(product.calculatedPrice.totalPrice) }}</p>
|
|
114
|
+
<div class="flex flex-row gap-2">
|
|
115
|
+
<AddToWishlist v-if="withFavoriteButton" :product="product" />
|
|
116
|
+
<UButton
|
|
117
|
+
v-if="withAddToCartButton && isCheckoutEnabled"
|
|
118
|
+
icon="i-lucide-shopping-cart"
|
|
119
|
+
variant="subtle"
|
|
120
|
+
@click="toggleDetails"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<UCollapsible v-model:open="openDetails" class="flex flex-col gap-2">
|
|
125
|
+
<template #content>
|
|
126
|
+
<ProductDetail :product="product" @product-added="toggleDetails" />
|
|
127
|
+
</template>
|
|
128
|
+
</UCollapsible>
|
|
129
|
+
</template>
|
|
130
|
+
</UPageCard>
|
|
131
|
+
</AnimatedSection>
|
|
132
|
+
</template>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
import ProductCard from "~/components/Product/Card.vue";
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
category: {
|
|
7
|
+
type: Object as () => Schemas["Category"],
|
|
8
|
+
required: true,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const category = toRef(props.category);
|
|
13
|
+
const { apiClient } = useShopwareContext();
|
|
14
|
+
const categoryProducts = ref<Schemas["Product"][]>([]);
|
|
15
|
+
const isLoading = ref(true);
|
|
16
|
+
|
|
17
|
+
const fetchCategoryProducts = async () => {
|
|
18
|
+
isLoading.value = true;
|
|
19
|
+
try {
|
|
20
|
+
const { data } = await apiClient.invoke("readProduct post /product", {
|
|
21
|
+
body: {
|
|
22
|
+
filter: [
|
|
23
|
+
{
|
|
24
|
+
type: "equals",
|
|
25
|
+
field: "categoryTree",
|
|
26
|
+
value: category.value.id,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "equals",
|
|
30
|
+
field: "parentId",
|
|
31
|
+
value: null,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
includes: {
|
|
35
|
+
product: [
|
|
36
|
+
"id",
|
|
37
|
+
"productNumber",
|
|
38
|
+
"name",
|
|
39
|
+
"description",
|
|
40
|
+
"calculatedPrice",
|
|
41
|
+
"translated",
|
|
42
|
+
"categories",
|
|
43
|
+
"properties",
|
|
44
|
+
"propertyIds",
|
|
45
|
+
"options",
|
|
46
|
+
"optionIds",
|
|
47
|
+
"configuratorSettings",
|
|
48
|
+
"children",
|
|
49
|
+
"parentId",
|
|
50
|
+
"sortedProperties",
|
|
51
|
+
"cover",
|
|
52
|
+
],
|
|
53
|
+
property: ["id", "name", "translated", "options"],
|
|
54
|
+
property_group_option: ["id", "name", "translated", "group"],
|
|
55
|
+
product_configurator_setting: [
|
|
56
|
+
"id",
|
|
57
|
+
"optionId",
|
|
58
|
+
"option",
|
|
59
|
+
"productId",
|
|
60
|
+
],
|
|
61
|
+
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
62
|
+
},
|
|
63
|
+
sort: [
|
|
64
|
+
{
|
|
65
|
+
field: "productNumber",
|
|
66
|
+
order: "ASC",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
associations: {
|
|
70
|
+
cover: {
|
|
71
|
+
associations: {
|
|
72
|
+
media: {},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
categories: {},
|
|
76
|
+
properties: {
|
|
77
|
+
associations: {
|
|
78
|
+
group: {},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
options: {
|
|
82
|
+
associations: {
|
|
83
|
+
group: {},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
configuratorSettings: {
|
|
87
|
+
associations: {
|
|
88
|
+
option: {
|
|
89
|
+
associations: {
|
|
90
|
+
group: {},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
children: {
|
|
96
|
+
associations: {
|
|
97
|
+
properties: {
|
|
98
|
+
associations: {
|
|
99
|
+
group: {},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
options: {
|
|
103
|
+
associations: {
|
|
104
|
+
group: {},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (data?.elements) {
|
|
114
|
+
categoryProducts.value = data.elements;
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`[category-products][${category.value.id}]`, error);
|
|
118
|
+
} finally {
|
|
119
|
+
isLoading.value = false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Fetch products when the component is mounted
|
|
124
|
+
onMounted(() => {
|
|
125
|
+
fetchCategoryProducts();
|
|
126
|
+
});
|
|
127
|
+
</script>
|
|
128
|
+
<template>
|
|
129
|
+
<div
|
|
130
|
+
v-if="!isLoading && categoryProducts.length > 0"
|
|
131
|
+
:id="category.name ?? ''"
|
|
132
|
+
class="flex flex-col"
|
|
133
|
+
>
|
|
134
|
+
<CategoryHeader :category="category" />
|
|
135
|
+
<div v-if="category.childCount > 0" class="grid grid-cols-1 gap-4">
|
|
136
|
+
<ProductCategory
|
|
137
|
+
v-for="child in category.children"
|
|
138
|
+
:key="child.id"
|
|
139
|
+
:category="child"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
<div v-else class="grid grid-cols-1 gap-4">
|
|
143
|
+
<ProductCard
|
|
144
|
+
v-for="product in categoryProducts"
|
|
145
|
+
:key="product.id"
|
|
146
|
+
:product="product"
|
|
147
|
+
:with-favorite-button="true"
|
|
148
|
+
:with-add-to-cart-button="true"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div v-else-if="isLoading" class="flex flex-col" />
|
|
153
|
+
</template>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
const props = defineProps<{
|
|
4
|
+
parentProduct: Schemas["Product"];
|
|
5
|
+
}>();
|
|
6
|
+
const { parentProduct } = toRefs(props);
|
|
7
|
+
const { product, changeVariant, configurator } = useProduct(
|
|
8
|
+
parentProduct.value?.children?.length > 0
|
|
9
|
+
? parentProduct.value.children[0]
|
|
10
|
+
: parentProduct.value,
|
|
11
|
+
parentProduct.value?.configuratorSettings || [],
|
|
12
|
+
);
|
|
13
|
+
const { findVariantForSelectedOptions } = useProductConfigurator();
|
|
14
|
+
const { variants: selectableOptions } = useProductVariants(configurator);
|
|
15
|
+
const selectedOptions = ref<Record<string, string>>({});
|
|
16
|
+
function initialOptions(variant: Ref<Schemas["Product"]>) {
|
|
17
|
+
const options = variant.value.options as Schemas["PropertyGroupOption"][];
|
|
18
|
+
for (const option of options) {
|
|
19
|
+
if (option.group && option.id) {
|
|
20
|
+
selectedOptions.value[option.group.id] = option.id;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
onMounted(() => {
|
|
25
|
+
initialOptions(product);
|
|
26
|
+
});
|
|
27
|
+
watch(
|
|
28
|
+
selectedOptions.value,
|
|
29
|
+
async () => {
|
|
30
|
+
const foundVariant = await findVariantForSelectedOptions(
|
|
31
|
+
selectedOptions.value,
|
|
32
|
+
);
|
|
33
|
+
const variant = parentProduct.value.children?.find(
|
|
34
|
+
(child: Schemas["Product"]) => child.id === foundVariant?.id,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (variant) {
|
|
38
|
+
changeVariant(variant);
|
|
39
|
+
emit("variant-switched", variant);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{ deep: true },
|
|
43
|
+
);
|
|
44
|
+
const emit = defineEmits<{
|
|
45
|
+
"variant-switched": [variant: Schemas["Product"]];
|
|
46
|
+
}>();
|
|
47
|
+
</script>
|
|
48
|
+
<template>
|
|
49
|
+
<div
|
|
50
|
+
v-for="(variantGroup, propertyGroupId) in selectableOptions"
|
|
51
|
+
:key="propertyGroupId"
|
|
52
|
+
class="my-6"
|
|
53
|
+
>
|
|
54
|
+
<div class="flex flex-row gap-2 items-center">
|
|
55
|
+
<div class="basis-1/3">{{ variantGroup.name }}:</div>
|
|
56
|
+
<USelect
|
|
57
|
+
v-model="selectedOptions[propertyGroupId]"
|
|
58
|
+
value-key="productId"
|
|
59
|
+
:items="variantGroup.options"
|
|
60
|
+
class="w-full"
|
|
61
|
+
icon="i-lucide-square-stack"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|