@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,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>